每日签到奶昔超市积分商城奶昔访达
12下一页
返回列表 发布新帖

[eSIM] 【O2 DE ESIM脚本】关于O2 DE ESIM入口被下掉的研究,成功踹开大门

1171 30
发表于 2026-4-25 18:09:50 | 查看全部 阅读模式

登录后免广告,享受更多奶昔会员权益!

您需要 登录 才可以下载或查看,没有账号?注册

×
本帖最后由 iniwex 于 2026-4-26 14:45 编辑

其实只是前端被下掉,后端没改,我做了个油猴脚本,可以继续下单ESIM
大家接着奏乐接着舞
下单地址: https://www.o2online.de/mobilfunk/prepaid/esim/
添加脚本后,在无痕模式打开页面,并下单,出现ESIM就成功

image

// ==UserScript==
// @name         o2 Prepaid Force E_SIM
// @namespace    local.o2.force-esim
// @version      0.3
// @description  Force o2 simType from SIM_CARD to E_SIM in page state and cart requests.
// @match        https://www.o2online.de/*
// @match        https://o2online.de/*
// @match        https://*.o2online.de/*
// @match        https://*.o9.de/*
// @run-at       document-start
// @grant        unsafeWindow
// ==/UserScript==

(function () {
  'use strict';

  const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

  const FROM = 'SIM_CARD';
  const TO = 'E_SIM';
  const DEBUG = true;

  const NativeJSONParse = W.JSON.parse.bind(W.JSON);
  const NativeJSONStringify = W.JSON.stringify.bind(W.JSON);

  const NativeRequest = W.Request;
  const NativeResponse = W.Response;
  const NativeXHR = W.XMLHttpRequest;

  function log(...args) {
    if (DEBUG && W.console) {
      W.console.log(
        '%c[o2 force E_SIM]',
        'color:#0a84ff;font-weight:bold',
        ...args
      );
    }
  }

  function warn(...args) {
    if (DEBUG && W.console) {
      W.console.warn('[o2 force E_SIM]', ...args);
    }
  }

  function isObject(value) {
    return value !== null && typeof value === 'object';
  }

  function getUrl(input) {
    try {
      if (typeof input === 'string') {
        return new W.URL(input, W.location.href);
      }

      if (input && typeof input.url === 'string') {
        return new W.URL(input.url, W.location.href);
      }
    } catch (_) {}

    return null;
  }

  function isTargetUrl(url) {
    if (!url) return true;

    const host = String(url.hostname || '').toLowerCase();

    return (
      host === String(W.location.hostname || '').toLowerCase() ||
      /(^|\.)o2online\.de$/.test(host) ||
      /(^|\.)o9\.de$/.test(host) ||
      host.includes('telefonica')
    );
  }

  /**
   * 递归修改对象里的 simType / defaultSimType / selectedSimType 等字段。
   * 注意:不会把 supportedSimType: ["E_SIM", "SIM_CARD"] 这种数组里的 SIM_CARD 删除。
   */
  function patchObject(obj, seen) {
    if (!isObject(obj)) return false;

    const WeakSetCtor = W.WeakSet || WeakSet;
    seen = seen || new WeakSetCtor();

    if (seen.has(obj)) return false;
    seen.add(obj);

    let changed = false;

    if (W.Array.isArray(obj)) {
      for (const item of obj) {
        if (isObject(item)) {
          changed = patchObject(item, seen) || changed;
        }
      }
      return changed;
    }

    for (const key of Object.keys(obj)) {
      const value = obj[key];

      // simType / defaultSimType / selectedSimType / currentSimType ...
      if (/simType/i.test(key) && value === FROM) {
        obj[key] = TO;
        changed = true;
      }

      // 兼容 { key: "simType", value: "SIM_CARD" } 这种结构
      if ((key === 'value' || key === 'selectedValue') && value === FROM) {
        const marker = [
          obj.key,
          obj.name,
          obj.id,
          obj.type,
          obj.code
        ].filter(Boolean).join(' ');

        if (/simType/i.test(marker)) {
          obj[key] = TO;
          changed = true;
        }
      }

      if (isObject(value)) {
        changed = patchObject(value, seen) || changed;
      }
    }

    return changed;
  }

  function patchStringBody(text) {
    if (typeof text !== 'string') {
      return { body: text, changed: false };
    }

    if (!text.includes(FROM) || !/simType/i.test(text)) {
      return { body: text, changed: false };
    }

    const trimmed = text.trim();

    // JSON body
    if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
      try {
        const data = NativeJSONParse(text);
        if (patchObject(data)) {
          return {
            body: NativeJSONStringify(data),
            changed: true
          };
        }
      } catch (_) {}
    }

    // 普通字符串 / fallback
    const patched = text
      .replace(/("([^"]*simType[^"]*)"\s*:\s*")SIM_CARD(")/gi, '$1E_SIM$3')
      .replace(/('([^']*simType[^']*)'\s*:\s*')SIM_CARD(')/gi, '$1E_SIM$3')
      .replace(/(\b[\w.-]*simType[\w.-]*=)SIM_CARD\b/gi, '$1E_SIM');

    return {
      body: patched,
      changed: patched !== text
    };
  }

  function patchBodySync(body) {
    if (!body) {
      return { body, changed: false };
    }

    if (typeof body === 'string') {
      return patchStringBody(body);
    }

    // URLSearchParams: simType=SIM_CARD
    try {
      if (W.URLSearchParams && body instanceof W.URLSearchParams) {
        let changed = false;
        const next = new W.URLSearchParams(body.toString());

        for (const [key, value] of next.entries()) {
          if (/simType/i.test(key) && value === FROM) {
            next.set(key, TO);
            changed = true;
          }
        }

        return changed
          ? { body: next, changed: true }
          : { body, changed: false };
      }
    } catch (_) {}

    // FormData
    try {
      if (W.FormData && body instanceof W.FormData) {
        let changed = false;

        for (const [key, value] of body.entries()) {
          if (/simType/i.test(key) && value === FROM) {
            body.set(key, TO);
            changed = true;
          }
        }

        return { body, changed };
      }
    } catch (_) {}

    // 普通对象,少见,但兼容一下
    try {
      if (isObject(body)) {
        const changed = patchObject(body);
        return { body, changed };
      }
    } catch (_) {}

    return { body, changed: false };
  }

  function maybePatchBody(body, url, source) {
    if (!isTargetUrl(url)) {
      return { body, changed: false };
    }

    const result = patchBodySync(body);

    if (result.changed) {
      log(`${source}: ${FROM} -> ${TO}`, url ? url.href : '');
    }

    return result;
  }

  /**
   * 1. 拦截 JSON.parse
   * 页面源码里很多 MFE 配置是 JSON.parse('...') 读进去的。
   */
  try {
    W.JSON.parse = function patchedJSONParse(text, reviver) {
      const data = NativeJSONParse(text, reviver);

      try {
        if (
          typeof text === 'string' &&
          text.includes(FROM) &&
          /simType/i.test(text)
        ) {
          if (patchObject(data)) {
            log(`JSON.parse: ${FROM} -> ${TO}`);
          }
        }
      } catch (_) {}

      return data;
    };

    log('JSON.parse patched');
  } catch (e) {
    warn('JSON.parse patch failed', e);
  }

  /**
   * 2. 拦截 JSON.stringify
   * 很多 add 请求的 body 是 JSON.stringify(payload) 之后发出去的。
   */
  try {
    W.JSON.stringify = function patchedJSONStringify(value, replacer, space) {
      const text = NativeJSONStringify(value, replacer, space);

      try {
        const result = patchStringBody(text);
        if (result.changed) {
          log(`JSON.stringify: ${FROM} -> ${TO}`);
          return result.body;
        }
      } catch (_) {}

      return text;
    };

    log('JSON.stringify patched');
  } catch (e) {
    warn('JSON.stringify patch failed', e);
  }

  /**
   * 3. 拦截 Response.json
   * 如果 item?offerIds=... 返回了 defaultSimType/simType=SIM_CARD,
   * 这里会在前端读取响应时改成 E_SIM。
   */
  try {
    if (NativeResponse && NativeResponse.prototype && NativeResponse.prototype.json) {
      const nativeResponseJson = NativeResponse.prototype.json;

      NativeResponse.prototype.json = function patchedResponseJson(...args) {
        const responseUrl = this && this.url ? this.url : '';

        return nativeResponseJson.apply(this, args).then((data) => {
          try {
            const url = getUrl(responseUrl);

            if (isTargetUrl(url) && patchObject(data)) {
              log(`Response.json: ${FROM} -> ${TO}`, responseUrl);
            }
          } catch (_) {}

          return data;
        });
      };

      log('Response.json patched');
    }
  } catch (e) {
    warn('Response.json patch failed', e);
  }

  /**
   * 4. 拦截 Request 构造器
   * 兼容 new Request(url, { body: ... }) 这种写法。
   */
  try {
    if (typeof NativeRequest === 'function' && typeof Proxy === 'function') {
      W.Request = new Proxy(NativeRequest, {
        construct(target, args, newTarget) {
          try {
            const input = args[0];
            const init = args[1];
            const url = getUrl(input);

            if (init && Object.prototype.hasOwnProperty.call(init, 'body')) {
              const result = maybePatchBody(init.body, url, 'new Request');

              if (result.changed) {
                args[1] = Object.assign({}, init, {
                  body: result.body
                });
              }
            }
          } catch (e) {
            warn('Request constructor patch error', e);
          }

          return Reflect.construct(target, args, newTarget);
        }
      });

      W.Request.prototype = NativeRequest.prototype;

      log('Request constructor patched');
    }
  } catch (e) {
    warn('Request constructor patch failed', e);
  }

  /**
   * 5. 拦截 fetch
   */
  try {
    if (typeof W.fetch === 'function') {
      const nativeFetch = W.fetch;

      W.fetch = async function patchedFetch(input, init) {
        let nextInput = input;
        let nextInit = init;
        const url = getUrl(input);

        try {
          // fetch(url, { body: ... })
          if (init && Object.prototype.hasOwnProperty.call(init, 'body')) {
            const result = maybePatchBody(init.body, url, 'fetch init.body');

            if (result.changed) {
              nextInit = Object.assign({}, init, {
                body: result.body
              });
            }
          }

          // fetch(new Request(...))
          else if (
            NativeRequest &&
            input instanceof NativeRequest &&
            !/^(GET|HEAD)$/i.test(input.method || 'GET')
          ) {
            const cloned = input.clone();
            const text = await cloned.text();
            const result = maybePatchBody(text, url, 'fetch Request.body');

            if (result.changed) {
              nextInput = new NativeRequest(input, {
                body: result.body
              });
            }
          }
        } catch (e) {
          warn('fetch patch error', e);
        }

        return nativeFetch.call(this, nextInput, nextInit);
      };

      log('fetch patched');
    }
  } catch (e) {
    warn('fetch patch failed', e);
  }

  /**
   * 6. 拦截 XMLHttpRequest
   */
  try {
    if (NativeXHR && NativeXHR.prototype) {
      const nativeOpen = NativeXHR.prototype.open;
      const nativeSend = NativeXHR.prototype.send;

      NativeXHR.prototype.open = function patchedOpen(method, url, ...rest) {
        try {
          this.__o2ForceESimUrl = getUrl(url);
          this.__o2ForceESimMethod = method;
        } catch (_) {}

        return nativeOpen.call(this, method, url, ...rest);
      };

      NativeXHR.prototype.send = function patchedSend(body) {
        try {
          const result = maybePatchBody(
            body,
            this.__o2ForceESimUrl,
            'XMLHttpRequest.send'
          );

          if (result.changed) {
            body = result.body;
          }
        } catch (e) {
          warn('XHR patch error', e);
        }

        return nativeSend.call(this, body);
      };

      log('XMLHttpRequest patched');
    }
  } catch (e) {
    warn('XMLHttpRequest patch failed', e);
  }

  W.__o2ForceESimPatch = {
    enabled: true,
    from: FROM,
    to: TO,
    version: '0.3'
  };

  log('installed', W.__o2ForceESimPatch);
})();
爱生活,爱奶昔~
回复

使用道具 举报

AIfengyue

评论30

iniwex楼主Lv.8 发表于 2026-4-26 22:36:04 | 查看全部
testboy93 发表于 2026-4-26 22:35
一个帐号下是不是只能有一张这个卡?谢谢

是的,换邮箱
爱生活,爱奶昔~
回复 支持 0 反对 1

使用道具 举报

testboy93Lv.2 发表于 2026-4-26 08:35:26 | 查看全部
Dein vorheriger Tarif wurde durch deine neue Auswahl ersetzt, da nur eine SIM-Karte pro Bestellung möglich ist.

提示这个?是一个帐号只能有一张么?同一个证件最多4张?
爱生活,爱奶昔~
limanLv.2 发表于 2026-4-30 12:39:23 来自手机 | 查看全部
xeeii 发表于 2026-4-30 12:01
已经gg了!下的号全被ban了,没信号了

我的正常也是跟着这个方法下的。用的原生手机。你这什么情况啊
爱生活,爱奶昔~
huangsitingLv.4 发表于 2026-4-25 18:13:31 | 查看全部
感谢分享,有人说审核会拒绝,现在有人能下卡吗?
爱生活,爱奶昔~
iniwex楼主Lv.8 发表于 2026-4-25 18:27:55 | 查看全部
huangsiting 发表于 2026-4-25 18:13
感谢分享,有人说审核会拒绝,现在有人能下卡吗?

没有激活满4张的可以下卡
爱生活,爱奶昔~
教授小秘书Lv.4 发表于 2026-4-25 18:28:40 | 查看全部
感谢分享
爱生活,爱奶昔~
amuaeLv.1 发表于 2026-4-25 20:20:02 来自手机 | 查看全部
感谢分享
爱生活,爱奶昔~
roryorkLv.2 发表于 2026-4-25 20:26:47 来自手机 | 查看全部
赶上了,撸一个,谢谢分享哈
爱生活,爱奶昔~
ddsxdLv.2 发表于 2026-4-25 20:43:03 来自手机 | 查看全部
鲁一个就够了
爱生活,爱奶昔~
ZoengLv.2 发表于 2026-4-25 20:52:30 来自手机 | 查看全部
🐮
爱生活,爱奶昔~
回复

使用道具 举报

shun26Lv.1 发表于 2026-4-25 21:14:27 | 查看全部
劲啊,今晚回家试试
爱生活,爱奶昔~
donyo12360Lv.2 发表于 2026-4-25 22:15:04 | 查看全部
显示没有esim卡了,只有实体卡了
爱生活,爱奶昔~
dmkgbLv.2 发表于 2026-4-25 22:17:32 来自手机 | 查看全部
一个够了,要这么多干嘛
爱生活,爱奶昔~
iniwex楼主Lv.8 发表于 2026-4-25 23:06:19 | 查看全部
donyo12360 发表于 2026-4-25 22:15
显示没有esim卡了,只有实体卡了

安装脚本后,清除浏览器缓存
爱生活,爱奶昔~
xxx363516Lv.2 发表于 2026-4-26 10:37:12 来自手机 | 查看全部
请问这个怎么用啊
爱生活,爱奶昔~
孤影Lv.3 发表于 2026-4-26 10:53:48 来自手机 | 查看全部
太有石粒了
爱生活,爱奶昔~
usernameisxxxLv.4 发表于 2026-4-26 10:59:32 | 查看全部
申请了,也收到esim下单成功的邮件了,但是一直没有esim的二维码
爱生活,爱奶昔~
iniwex楼主Lv.8 发表于 2026-4-26 13:11:26 | 查看全部
usernameisxxx 发表于 2026-4-26 10:59
申请了,也收到esim下单成功的邮件了,但是一直没有esim的二维码

需要登录账号去查看
爱生活,爱奶昔~
xxx363516Lv.2 发表于 2026-4-26 14:43:20 来自手机 | 查看全部
为什么用了还是实体卡啊😂😂
爱生活,爱奶昔~
iniwex楼主Lv.8 发表于 2026-4-26 14:45:02 | 查看全部
xxx363516 发表于 2026-4-26 14:43
为什么用了还是实体卡啊😂😂

自己浏览器缓存问题,无痕模式,或者换浏览器
爱生活,爱奶昔~
testboy93Lv.2 发表于 2026-4-26 22:35:20 | 查看全部
iniwex 发表于 2026-4-26 14:45
自己浏览器缓存问题,无痕模式,或者换浏览器

一个帐号下是不是只能有一张这个卡?谢谢
爱生活,爱奶昔~

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则

© 2026 Naixi Networks. 沪ICP备13020230号-1|沪公网安备 31010702007642号手机版小黑屋RSS
返回顶部 关灯 在本版发帖
快速回复 返回顶部 返回列表