Skip to main content

Overview

mitmproxy integration is what enables bet365-re-js to intercept HTTP responses, identify obfuscated JavaScript, deobfuscate it on-the-fly, and inject the transformed code back into the browser. This creates a seamless debugging experience where the browser executes readable JavaScript instead of obfuscated code.

What is mitmproxy?

mitmproxy is an interactive HTTPS proxy that allows you to intercept, inspect, and modify HTTP traffic. bet365-re-js uses mitmproxy’s Python scripting capabilities to:
  1. Intercept responses containing obfuscated JavaScript
  2. Save the obfuscated code to disk
  3. Run the Node.js deobfuscation pipeline
  4. Replace the obfuscated code with deobfuscated code
  5. Inject pre/post-transform instrumentation code
  6. Return the modified response to the browser

Architecture

Starting mitmproxy

The project includes a simple shell script to start mitmproxy with the custom addon:
#!/usr/bin/env bash

mitmproxy -s mitmproxy/src/python/main/download-payload.py
Run it with:
./mitmproxy.sh
This starts mitmproxy on port 8080 by default, loading the JavascriptExtractor addon.

Browser Configuration

Configure your browser to use mitmproxy as a proxy server:
open -a "Google Chrome" --args \
  --proxy-server=http://localhost:8080 \
  --enable-logging \
  --v=1 \
  --user-data-dir=$(pwd)/chrome-profile
Using a separate Chrome profile (--user-data-dir) ensures your main browsing profile isn’t affected by the proxy configuration.

The JavascriptExtractor Addon

The core of the integration is the JavascriptExtractor class in download-payload.py:
class JavascriptExtractor:
    current_directory = Path(__file__).parent.absolute()
    project_root_directory = current_directory.joinpath("../../../..").absolute()
    output_base_path = str((project_root_directory / "output").absolute()) + "/"
    javascript_base_path = str((project_root_directory / "mitmproxy/src/javascript").absolute()) + "/"
    file_delimiter = b'\x03'b'\x06'b'\x05'
    obfuscated_start_string = "(function(){ var _0x123a="
    pre_transform_code_file_name = "pre-transform-code.js"
    post_transform_code_file_name = "post-transform-code.js"
    refactor_script_on_fly = True

Configuration

The addon configures itself on startup:
def configure(self, updated):
    # Filter for specific endpoint containing JavaScript
    self.filter = flowfilter.parse("~u '/Api/1/Blob'")
    
    # Find node executable
    self.node_executable_file = subprocess.run(
        ["which", "node"], 
        capture_output=True, 
        text=True
    ).stdout.strip()
    
    # Load any pre-transformed code mappings
    transformed_files = Path(self.javascript_base_path).glob('**/transformed-*.js')
    for transformed_file in transformed_files:
        identifier = transformed_file.name.removeprefix("transformed-").removesuffix(".js")
        post_transform_file = Path(self.javascript_base_path + f'/post-transform-{identifier}.js')
        if post_transform_file.is_file():
            transformed_file_contents = transformed_file.read_text()
            post_transform_file_contents = post_transform_file.read_text()
            self.replace_contents[jsmin(transformed_file_contents)] = jsmin(post_transform_file_contents)

Request Interception

The addon modifies outgoing requests to hide automation:
def request(self, flow: http.HTTPFlow):
    # Remove "Headless" from User-Agent to avoid detection
    flow.request.headers["User-Agent"] = self.__strip_headless(
        flow.request.headers["User-Agent"]
    )

Response Transformation

The response method is where the magic happens:
def response(self, flow: http.HTTPFlow) -> None:
    if flowfilter.match(self.filter, flow):
        file_identifier = str(time.time())
        split_content_bytes = flow.response.content.split(self.file_delimiter)
        sent_bytes_array = []
        
        for index, received_file_content_bytes in enumerate(split_content_bytes):
            sent_bytes = received_file_content_bytes
            start_byte = received_file_content_bytes[0:1]
            content_bytes = received_file_content_bytes[1:]
            content_start_string = content_bytes[:len(self.obfuscated_start_string)].decode()
            is_obfuscated_content = content_start_string.startswith(self.obfuscated_start_string)
            
            if is_obfuscated_content:
                logging.info("Intercepting response: " + flow.request.url)
                
                # Save obfuscated file
                received_file_name = self.__get_file_name(
                    self.output_base_path, 
                    received_file_content_bytes, 
                    file_identifier, 
                    "received", 
                    index
                )
                received_file = self.__create_file(received_file_name, "received")
                if received_file is not None:
                    received_file.write(content_bytes)
                    received_file.close()
                
                # Run deobfuscation on the fly
                if self.refactor_script_on_fly:
                    refactor_file = Path(self.javascript_base_path + "/refactor-obfuscated-code-jscodeshift.js")
                    deobfuscated_file_name = self.__get_file_name(
                        self.output_base_path,
                        received_file_content_bytes,
                        file_identifier,
                        "deobfuscated",
                        index
                    )
                    
                    # Execute Node.js deobfuscation
                    subprocess.run([
                        self.node_executable_file,
                        refactor_file,
                        received_file_name,
                        deobfuscated_file_name
                    ], stdout=subprocess.PIPE)
                    
                    # Build complete file with instrumentation
                    pre_transform_code_content_string = Path(
                        self.javascript_base_path + self.pre_transform_code_file_name
                    ).read_text()
                    deobfuscated_file_content_minified_string = jsmin(
                        Path(deobfuscated_file_name).read_text()
                    )
                    post_transform_code_content_string = Path(
                        self.javascript_base_path + self.post_transform_code_file_name
                    ).read_text()
                    
                    # Apply any additional replacements
                    for replace_content in self.replace_contents:
                        deobfuscated_file_content_minified_string = \
                            deobfuscated_file_content_minified_string.replace(
                                replace_content,
                                self.replace_contents[replace_content]
                            )
                    
                    complete_file_content_string = (
                        pre_transform_code_content_string + 
                        deobfuscated_file_content_minified_string + 
                        post_transform_code_content_string
                    )
                else:
                    # Use pre-deobfuscated file
                    deobfuscated_file = Path(self.javascript_base_path + "/deobfuscated.js")
                    complete_file_content_string = deobfuscated_file.read_text()
                
                # Save sent file
                sent_file_name = self.__get_file_name(
                    self.output_base_path,
                    received_file_content_bytes,
                    file_identifier,
                    "sent",
                    index
                )
                sent_file = self.__create_file(sent_file_name, "sent")
                if sent_file is not None:
                    pretty_js_string = jsbeautifier.beautify(complete_file_content_string)
                    sent_file.write(pretty_js_string.encode())
                    sent_file.close()
                
                sent_bytes = start_byte + complete_file_content_string.encode()
            
            sent_bytes_array.append(sent_bytes)
        
        # Replace response content
        flow.response.content = self.file_delimiter.join(sent_bytes_array)

File Structure

The addon creates several files in the output/ directory:
output/
├── 1741234567.123-received-14.js   # Original obfuscated code
├── 1741234567.123-deobfuscated-14.js  # Deobfuscated code
└── 1741234567.123-sent-14.js       # Final code sent to browser
Filenames contain:
  • Timestamp: Unix timestamp with microseconds
  • Type: received, deobfuscated, or sent
  • Index: File index in the response

Instrumentation Code

Pre-transform Code

The pre-transform-code.js file is injected before the deobfuscated code. It provides:
  • Logging utilities
  • State tracking functions
  • Debug helpers
var consoleLogger = function(string) {
    if (console) {
        console.log(string);
    }
};

var print = function(label, input) {
    if (Array.isArray(input)) {
        var object = Object.assign({}, input);
        Object.keys(object).forEach((key, value) => {
            try {
                object[key] = object[key];
            } catch (exception) {
                consoleLogger(exception)
            }
        });
        consoleLogger(label + ": " + toJsonString(object));
    } else if(typeof input === "object") {
        consoleLogger(label + ": " + toJsonString(input));
    } else {
        consoleLogger(label + ": " + input);
    }
};

var toJsonString = function(input) {
    return JSON.stringify(input, circularObjectReferenceToString);
};

var beforeFunctionState = function(functionName, globalState) {
    var stateContext = new Map([
        ["functionName", functionName],
        ["before", globalState.slice()]
    ]);
    return stateContext;
};

var afterFunctionState = function(stateContext, globalState) {
    var beforeFunctionGlobalState = stateContext.get("before");
    var changes = getGlobalStateChanges(beforeFunctionGlobalState, globalState);
    if (changes) {
        stateContext.delete("before");
        stateContext.set("change", changes);
        states.push(stateContext);
    }
};

var tapeKeywords = {};

Post-transform Code

The post-transform-code.js file is injected after the deobfuscated code for final setup:
var consoleLogger = function(string) {
    if (console) {
        console.log(string);
    }
};

Detecting Obfuscated Code

The addon identifies obfuscated JavaScript by checking if the content starts with a specific pattern:
obfuscated_start_string = "(function(){ var _0x123a="

# Check if content is obfuscated
content_start_string = content_bytes[:len(self.obfuscated_start_string)].decode()
is_obfuscated_content = content_start_string.startswith(self.obfuscated_start_string)
This detection pattern may need updating if bet365 changes their obfuscation wrapper. Monitor the received-*.js files to identify new patterns.

Handling Multiple Files

bet365 sends multiple JavaScript and CSS files in a single response, delimited by a special byte sequence:
file_delimiter = b'\x03'b'\x06'b'\x05'

# Split response into individual files
split_content_bytes = flow.response.content.split(self.file_delimiter)
Each file has a type byte prefix:
  • 4: JavaScript file
  • 5: CSS file
@staticmethod
def __is_js(file_content):
    return bool(file_content) and 4 == file_content[0]

@staticmethod
def __is_css(file_content):
    return bool(file_content) and 5 == file_content[0]

Performance Considerations

On-the-fly vs Pre-deobfuscated

The addon supports two modes:
Pros:
  • Always uses latest deobfuscation logic
  • Handles new obfuscation patterns immediately
Cons:
  • Slower response time (Node.js process overhead)
  • Can block browser if deobfuscation is slow

Debugging

View Console Output

When Chrome is started with --enable-logging --v=1, console output is written to:
<user-data-dir>/chrome_debug.log
View just the JSON output:
tail -f <user-data-dir>/chrome_debug.log | \
  sed -En "s/.*inside.*\]: (.*)\", source\:  \(3\)/\1/p"

mitmproxy Web Interface

mitmproxy also provides a web interface at http://localhost:8081 (default) where you can:
  • Inspect all intercepted requests and responses
  • View request/response headers and bodies
  • Replay requests
  • Filter traffic

Recommendations

1

Use Dedicated Chrome Profile

Always use a separate Chrome profile for mitmproxy to avoid affecting your main browsing session.
2

Install Clear Cache Extension

Install a clear cache extension to quickly clear the cache without opening DevTools.
3

Monitor Output Directory

Watch the output/ directory to see what files are being intercepted and transformed.
4

Check Logs

Monitor mitmproxy logs for errors or issues with the deobfuscation process.
Using obfuscated-code-logger.js (if enabled) might add significant noise to console debugging output.

Troubleshooting

No Files in Output Directory

  • Verify mitmproxy is running
  • Check browser proxy settings
  • Ensure you’re visiting bet365.com pages that load the obfuscated JavaScript
  • Check the URL filter in configure() method

Deobfuscation Fails

  • Check Node.js is installed and in PATH
  • Verify the deobfuscation script path is correct
  • Look for errors in mitmproxy logs
  • Check if the obfuscation pattern has changed

Browser Shows Certificate Error

Next Steps

Build docs developers (and LLMs) love