Scramjet Documentation

repository·main·Indexed 19 days ago

https://github.com/mercuryworkshop/scramjet

An experimental interception-based web proxy designed to bypass censorship, circumvent CORS restrictions, and sandbox web content. Scramjet enables browser-based instrumentation and debugging of arbitrary websites through rewriting and sandboxing techniques. It includes a Controller for managing frames and plugins, a bootstrap system for service worker registration, and support for multiple transport layers including epoxy and libcurl.

Tokens
26.8K
Snippets
92
Records
106
Agent score
66%

What's inside Scramjet

  1. What is Scramjet

    main

    Scramjet is an experimental interception-based web proxy designed to evade internet censorship and bypass arbitrary browser restrictions. It enables users to:

    • Sandbox arbitrary web content.
    • Bypass CORS (Cross-Origin Resource Sharing) restrictions when loading websites.
    • Instrument and debug websites directly inside the browser.

    It achieves these capabilities through a combination of interception, rewriting, and sandboxing techniques.

  2. Run Scramjet locally in development mode

    main

    To run the Scramjet development server, execute the following command from the repository root. The demo page will be available at http://localhost:4141 and will automatically rebuild when files change (note: the rewriter is excluded from automatic rebuilds).

    pnpm dev
  3. Build Scramjet from source

    main

    To build Scramjet from the repository, ensure you have the necessary dependencies installed, then follow these steps:

    1. Clone the repository recursively: git clone --recursive https://github.com/MercuryWorkshop/scramjet
    2. Install dependencies using pnpm: pnpm i
    3. Navigate to the core package: cd packages/core
    4. Build the rewriter: pnpm rewriter:build
    5. Return to the root and build Scramjet: pnpm build
    git clone --recursive https://github.com/MercuryWorkshop/scramjet
    pnpm i
    cd packages/core
    pnpm rewriter:build
    pnpm build
  4. Understand Scramjet build configurations and output formats

    main

    Scramjet uses Rspack to generate several different build targets depending on the consumer's environment. The build system produces both IIFE (Immediately Invoked Function Expression) bundles for global script injection and ESM (ES Modules) for modern module-based environments.

    Key build variations include:

    • IIFE Bundles: Assigns the library to a global variable (e.g., self.$scramjet). These can be either unbundled (requiring external assets) or bundled (including the WASM rewriter as a Base64 string).
    • ESM Modules: Uses libraryTarget: 'module' for standard imports. These can also be unbundled or bundled with WASM.
    • Controller & Utils: Specialized builds for the Scramjet controller and utility packages, often including version checks to ensure compatibility between the client and controller.
    • Bootstrap: Node.js-targeted builds for server-side or proxy-side logic.
    • CLI Tools: Builds like create-proxy-app are configured as Node.js executables with a shebang banner.
  5. Configure plugin execution order with TapOrder

    main

    When tapping into a hook, you can provide a TapOrder object to control when your callback executes relative to other plugins. This is useful for ensuring dependencies are met or that transformations happen in a specific sequence.

    • before: An array of plugin names. Your callback will run before these plugins.
    • after: An array of plugin names. Your callback will run after these plugins.

    If no order is provided to the .tap() method, the plugin's own tapOrder (defined in its constructor) is used.

    const plugin = new Plugin("my-plugin", { 
      before: ["auth-plugin"] 
    });
    
    // This specific tap will run after 'logger-plugin'
    plugin.tap(hooks.someHook, callback, { after: ["logger-plugin"] });
  6. Use ExternalStubPlugin for zero-size ESM imports

    main

    The ExternalStubPlugin is used to facilitate a pattern where consumers can import Scramjet via ESM (e.g., import { ... } from 'scramjet') while the actual implementation is provided by a global variable (e.g., self.$scramjet).

    When this plugin is applied, it automatically discovers the exports in the primary bundle and generates a thin .mjs stub file. This stub simply destructures the required members from the global object, ensuring that consumers pay zero additional source size for the import.

    // Example of what the generated stub looks like:
    // AUTO-GENERATED by ExternalStubPlugin — do not edit.
    // Re-exports of globalThis.$scramjet (set by the IIFE bundle).
    const __external = /** @type {any} */ (globalThis).$scramjet;
    export const { 
    	exportName1,
    	exportName2,
    } = __external;
  7. How the Tap system and plugins work together

    main

    The Tap system is a plugin architecture used to intercept and modify behavior via hooks.

    1. Hooks: A hook is a named entry point (created via Tap.create()) that holds a collection of callbacks.
    2. Plugins: A Plugin instance provides a namespace and a default tapOrder. Plugins use the .tap() method to register callbacks to specific hooks.
    3. Callbacks: When a hook is dispatched via Tap.dispatch(), all registered callbacks are executed in an order determined by their TapOrder requirements.
    4. Ordering: You can control execution order using before and after arrays, which reference other plugin names. This allows you to ensure your plugin runs specifically before or after a known plugin.
    // 1. Define the hook structure
    interface MyHooks extends Record<string, { context?: object; props?: object }> {
      onData: { context?: object; props?: object };
    }
    
    // 2. Create the Tap instance
    const hooks = Tap.create<MyHooks>();
    
    // 3. Register a plugin
    const myPlugin = new Plugin("my-plugin");
    myPlugin.tap(hooks.onData, (context, props) => {
      console.log("Intercepted!", context);
    }, { before: ["other-plugin"] });
    
    // 4. Dispatch the hook
    await Tap.dispatch(hooks.onData, { some: "context" }, { some: "props" });
  8. Understand the RpcHelper wire format

    main

    The RpcHelper uses a specific JSON-compatible object structure for communication, namespaced by the id provided during construction.

    Request Packet

    Sent via call(). Used to trigger a method on the remote side.

    {
      "[id]": {
        "$type": "request",
        "$method": "methodName",
        "$args": { ... },
        "$token": 123
      }
    }

    Response Packet

    Sent by the receiver after a method execution. It includes either the result or an error.

    {
      "[id]": {
        "$type": "response",
        "$token": 123,
        "$data": "resultValue",
        "$error": "Error message if applicable"
      }
    }

    Note: $token is used to match responses back to the original call promise.

  9. Use Scramjet plugins to extend frame behavior

    main

    Scramjet frames support a plugin system via the plugins array passed to createFrame. Common plugins available via the $scramjetUtils global object include:

    • $scramjetUtils.HttpCachePlugin: Enables HTTP caching for the proxied session.
    • $scramjetUtils.UrlWatcherPlugin(callback): Executes a callback whenever the URL in the frame changes. The callback receives the new URL.
    • $scramjetUtils.CatchEscapedLinksPlugin(callback): Intercepts links within the proxied content. The callback receives the original URL and should return a new URL (e.g., a rewritten link) to be used instead.
    const urlWatcher = new $scramjetUtils.UrlWatcherPlugin((url) => {
      console.log("The frame navigated to:", url);
    });
    
    const catchEscapedLinks = new $scramjetUtils.CatchEscapedLinksPlugin(
      (url) => new URL(`/?goto=${encodeURIComponent(url.href)}`, location.origin)
    );
    
    // Apply them during frame creation
    const frame = controller.createFrame(frameElement, {
      plugins: [urlWatcher, catchEscapedLinks],
    });
  10. How Scramjet handles cookie synchronization

    main

    Scramjet synchronizes cookies between the main page, the Controller, and the Service Worker to maintain session state across sandboxed environments.

    1. Persistence: The Controller uses IndexedDB (under the name __scramjet_controller) to persist the CookieJar state.
    2. Propagation: When a fetch request within a Frame modifies cookies, the Frame calls sendSetCookie on the controller. The controller then updates its local CookieJar, persists the state to IndexedDB, and propagates the change to the Service Worker via RPC.
    3. Syncing: A BroadcastChannel (__scramjet_controller_channel) is used to notify other tabs/contexts when the cookie state has been updated, triggering a reload of the persisted state.
  11. Configure Scramjet transport types

    main

    Scramjet supports different transport mechanisms via the transport option in BootstrapOptions. Currently, the following are supported:

    • epoxy: Uses the EpoxyClient. Requires epoxyClientPath to be provided in the configuration.
    • libcurl: Uses the LibcurlClient. Requires libcurlClientPath to be provided in the configuration.
    • bare: Not yet implemented; attempting to use this will throw an error.

    When using epoxy or libcurl, the client is initialized with a wisp URL constructed from the wispPath provided in the config and the current window's protocol and host.

  12. Understand ScramjetFetchParsed metadata

    main

    The ScramjetFetchParsed interface provides enriched metadata about a request after it has been processed by Scramjet. This is used by hooks to make decisions based on the request's context.

    Key fields:

    • url: The parsed _URL of the request.
    • destination: The request destination (can be overridden by $dest).
    • fetchSiteState: Tracks the worst-case Sec-Fetch-Site classification ("same-origin" | "same-site" | "cross-site") through redirects.
    • isIframe: Indicates if the request was made by a Scramjet-defined iframe.
    • meta: Contains URLMeta for advanced URL analysis.
    • fetchCredentialsInclude: Boolean indicating if credentials=include was used.