iniwex 发表于 2026-4-25 18:09:50

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

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

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

!(https://cdn.nodeimage.com/i/Zv7cRBnBFu9T85GrZB3RH1DFP7VyegJL.png)


```
// ==UserScript==
// @name         o2 Prepaid Force E_SIM
// @namespace    local.o2.force-esim
// @version      0.3
// @descriptionForce 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',
      'color:#0a84ff;font-weight:bold',
      ...args
      );
    }
}

function warn(...args) {
    if (DEBUG && W.console) {
      W.console.warn('', ...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;

      // simType / defaultSimType / selectedSimType / currentSimType ...
      if (/simType/i.test(key) && value === FROM) {
      obj = 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 = 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 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 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;
            const init = args;
            const url = getUrl(input);

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

            if (result.changed) {
                args = 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);
})();

```

iniwex 发表于 2026-4-26 22:36:04

testboy93 发表于 2026-4-26 22:35
一个帐号下是不是只能有一张这个卡?谢谢

是的,换邮箱

testboy93 发表于 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张?

liman 发表于 2026-4-30 12:39:23

xeeii 发表于 2026-4-30 12:01
已经gg了!下的号全被ban了,没信号了

我的正常也是跟着这个方法下的。用的原生手机。你这什么情况啊

huangsiting 发表于 2026-4-25 18:13:31

感谢分享,有人说审核会拒绝,现在有人能下卡吗?

iniwex 发表于 2026-4-25 18:27:55

huangsiting 发表于 2026-4-25 18:13
感谢分享,有人说审核会拒绝,现在有人能下卡吗?

没有激活满4张的可以下卡

教授小秘书 发表于 2026-4-25 18:28:40

感谢分享

amuae 发表于 2026-4-25 20:20:02

感谢分享

roryork 发表于 2026-4-25 20:26:47

赶上了,撸一个,谢谢分享哈

ddsxd 发表于 2026-4-25 20:43:03

鲁一个就够了

Zoeng 发表于 2026-4-25 20:52:30

🐮

shun26 发表于 2026-4-25 21:14:27

劲啊,今晚回家试试

donyo12360 发表于 2026-4-25 22:15:04

显示没有esim卡了,只有实体卡了

dmkgb 发表于 2026-4-25 22:17:32

一个够了,要这么多干嘛

iniwex 发表于 2026-4-25 23:06:19

donyo12360 发表于 2026-4-25 22:15
显示没有esim卡了,只有实体卡了

安装脚本后,清除浏览器缓存

xxx363516 发表于 2026-4-26 10:37:12

请问这个怎么用啊

孤影 发表于 2026-4-26 10:53:48

太有石粒了

usernameisxxx 发表于 2026-4-26 10:59:32

申请了,也收到esim下单成功的邮件了,但是一直没有esim的二维码{tieba6}

iniwex 发表于 2026-4-26 13:11:26

usernameisxxx 发表于 2026-4-26 10:59
申请了,也收到esim下单成功的邮件了,但是一直没有esim的二维码

需要登录账号去查看

xxx363516 发表于 2026-4-26 14:43:20

为什么用了还是实体卡啊😂😂

iniwex 发表于 2026-4-26 14:45:02

xxx363516 发表于 2026-4-26 14:43
为什么用了还是实体卡啊😂😂

自己浏览器缓存问题,无痕模式,或者换浏览器

testboy93 发表于 2026-4-26 22:35:20

iniwex 发表于 2026-4-26 14:45
自己浏览器缓存问题,无痕模式,或者换浏览器

一个帐号下是不是只能有一张这个卡?谢谢
页: [1] 2
查看完整版本: 【O2 DE ESIM脚本】关于O2 DE ESIM入口被下掉的研究,成功踹开大门