├── .gitattributes ├── .github └── workflows │ └── xgo.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── example └── server.py ├── hazetunnel ├── api │ ├── cert.go │ ├── cffi.go │ ├── config.go │ ├── http.go │ ├── injector.go │ ├── profiles.go │ └── proxy.go ├── go.mod ├── go.sum └── main.go └── python-bindings ├── README.md ├── hazetunnel ├── __init__.py ├── __main__.py ├── __version__.py ├── bin │ └── __init__.py ├── cffi.py └── control.py └── pyproject.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py linguist-detectable=false -------------------------------------------------------------------------------- /.github/workflows/xgo.yml: -------------------------------------------------------------------------------- 1 | name: Release Tags 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Fetch Version from Tag 15 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 16 | 17 | # Build and release CFFI artifacts 18 | 19 | - name: Build CFFI 20 | uses: crazy-max/ghaction-xgo@v3 21 | with: 22 | xgo_version: latest 23 | go_version: latest 24 | dest: cffi_dist 25 | prefix: hazetunnel-api-${{ env.VERSION }} 26 | targets: "*/*" 27 | v: true 28 | race: false 29 | ldflags: -s -w 30 | buildmode: c-shared 31 | trimpath: true 32 | working_dir: hazetunnel 33 | 34 | - name: Upload CFFI Build Artifacts to Workflow 35 | uses: actions/upload-artifact@v3 36 | with: 37 | name: cffi-build-artifacts-${{ env.VERSION }} 38 | path: hazetunnel/cffi_dist/** 39 | 40 | - name: Upload Release Assets with action-gh-release 41 | uses: softprops/action-gh-release@v1 42 | with: 43 | files: hazetunnel/cffi_dist/* 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | 48 | permissions: 49 | contents: write 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.dll~ 9 | *.so 10 | *.dylib 11 | *.h 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | 25 | # Python 26 | __pycache__ 27 | dist 28 | *.pyc 29 | 30 | *.pem 31 | credentials -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 daijro, rosahaj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hazetunnel 3 |

4 | 5 |

6 | 🔮 Vindicate non-organic web traffic 7 |

8 | 9 | --- 10 | 11 | Hazetunnel is an MITM proxy that attempts to legitimize [BrowserForge](https://github.com/daijro/browserforge/)'s injected-browser web traffic by hijacking the TLS fingerprint to mirror the passed User-Agent. 12 | 13 | Additionally, it can inject a Javascript payload into the web page to defend against [worker fingerprinting](https://github.com/apify/fingerprint-suite/issues/64). 14 | 15 |
16 | 17 | ### Features ✨ 18 | 19 | - Anti TLS fingerprinting 🪪 20 | 21 | - Emulate the ClientHello of browsers based on the passed User-Agent (e.g. Chrome/120) 22 | - Bypasses TLS fingerprinting checks 23 | 24 | - Javascript payload injection 💉 25 | 26 | - Prepends payload to all Javascript responses, including the web page Service/Shared worker scope. 27 | - Injects payload into embedded base64 encoded JavaScript within HTML responses ([see here](https://github.com/apify/fingerprint-suite/issues/64#issuecomment-1282877696)) 28 | 29 | This project was built on [tlsproxy](https://github.com/rosahaj/tlsproxy), please leave them a star! 30 | 31 | --- 32 | 33 | # Usage 34 | 35 | This package can be installed and used through Python. It is avaliable on PyPi: 36 | 37 | ```bash 38 | pip install hazetunnel 39 | ``` 40 | 41 | You can also use it as a standalone Go executable by building the tool with the [guide](https://github.com/daijro/hazetunnel?tab=readme-ov-file#building) below. 42 | 43 | ## CLI Usage 44 | 45 | After installing Hazetunnel through PyPi, it can be used as a standalone CLI application. 46 | 47 | This example will inject `alert('Hello world');` before all Javascript responses: 48 | 49 | ```bash 50 | hazetunnel run --payload "alert('Hello world');" --port 8080 51 | ``` 52 | 53 |
54 | 55 | 56 | CLI parameters 57 | 58 | 59 | ``` 60 | $ hazetunnel run --help 61 | Usage: hazetunnel run [OPTIONS] 62 | 63 | Run the MITM proxy 64 | 65 | Options: 66 | -p, --port TEXT Port to use. Default: 8080. 67 | --user_agent TEXT Override User-Agent headers. 68 | --payload TEXT Payload to inject into responses. 69 | --upstream_proxy TEXT Forward requests to an upstream proxy. 70 | --cert TEXT Path to the certificate file. 71 | --key TEXT Path to the key file. 72 | -v, --verbose Enable verbose output. 73 | --help Show this message and exit. 74 | ``` 75 | 76 |
77 | 78 | More info on other CLI commands are avaliable [here](https://github.com/daijro/hazetunnel/tree/main/python-bindings#python-usage). 79 | 80 | ### Payload Injection 81 | 82 | #### Javascript responses 83 | 84 | This [example server](https://github.com/daijro/hazetunnel/blob/main/example/server.py) will return `console.log('Original JavaScript executed.')` when called: 85 | 86 | **Original response:** 87 | 88 | ```bash 89 | $ curl http://localhost:5000/js 90 | console.log('Original JavaScript executed.'); 91 | ``` 92 | 93 | **With Hazetunnel:** 94 | 95 | ```bash 96 | $ curl http://localhost:5000/js --proxy http://localhost:8080 --cacert cert.pem 97 | alert('Hello world');console.log('Original JavaScript executed.'); 98 | ``` 99 | 100 | #### HTML responses 101 | 102 | Additionally, Hazetunnel can inject payloads into HTML responses: 103 | 104 | ```bash 105 | $ curl http://localhost:5000/html --proxy http://localhost:8080 --cacert cert.pem 106 | 107 | 108 |

Base64 JavaScript Testing Page

109 |

This page includes an embedded base64 encoded JavaScript for testing.

110 | 111 | 112 | 113 | ``` 114 | 115 | The embedded base64-encoded script will now decode to this: 116 | 117 | ``` 118 | alert('Hello world');console.log('Original JavaScript executed.'); 119 | ``` 120 | 121 | ### TLS Spoofing 122 | 123 | Hazetunnel will spoof the TLS fingerprint to match the User-Agent passed in the request. 124 | 125 | Here is an example of a request to [tls.peet.ws](https://tls.peet.ws/api/clean) through Hazetunnel with a Chrome/121 User-Agent: 126 | 127 | ```bash 128 | $ curl https://tls.peet.ws/api/clean -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" --proxy http://localhost:8080 --cacert cert.pem 129 | { 130 | "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,16-45-65281-35-5-10-23-0-27-13-65037-11-18-43-17513-51,29-23-24,0", 131 | "ja3_hash": "1a5edeab8308886ca9716ef14eecd4f3", 132 | "akamai": "2:0,4:4194304,6:10485760|1073741824|0|a,m,p,s", 133 | "akamai_hash": "55541b174e8a8adc32544ca36c6fd053", 134 | "peetprint": "GREASE-772-771|2-1.1|GREASE-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17513-18-23-27-35-43-45-5-51-65037-65281-GREASE-GREASE", 135 | "peetprint_hash": "8ad9325e12f531d2983b78860de7b0ec" 136 | } 137 | ``` 138 | 139 | Hazetunnel interprets from the User-Agent that the response is meant to mimic Chrome 121, and sends a ClientHello that mimics Chrome 121 browsers' cipher suites, GREASE functionality, elliptic curves, etc. 140 | 141 | It also generates an Akamai HTTP/2 fingerprint. 142 | 143 | This supports User-Agents from **Firefox, Chrome, iOS, Android, Edge (legacy), Safari, 360Browser, QQBrowser, etc.** 144 | 145 |
146 | 147 | ## Python API 148 | 149 | Hazetunnel can also be used as a Python library. 150 | 151 | #### Simple usage 152 | 153 | ```py 154 | from hazetunnel import HazeTunnel 155 | from browserforge.headers import HeaderGenerator 156 | ... 157 | # Initialize the proxy 158 | proxy = HazeTunnel(port='8080', payload='alert("Hello World!");') 159 | proxy.launch() 160 | # Send the request 161 | requests.get( 162 | url='https://example.com', 163 | headers=HeaderGenerator().generate(browser='chrome'), 164 | proxies={'https': proxy.url}, 165 | verify=proxy.cert 166 | ).text 167 | # Stop the proxy 168 | proxy.stop() 169 | ``` 170 | 171 |
172 | 173 | 174 | HazeTunnel parameters 175 | 176 | 177 | ``` 178 | Parameters: 179 | port (Optional[str]): Specify a port to listen on. Default is random. 180 | payload (Optional[str]): Payload to inject into responses 181 | user_agent (Optional[str]): Optionally override all User-Agent headers 182 | upstream_proxy (Optional[str]): Optionally forward requests to an upstream proxy 183 | ``` 184 | 185 |
186 | 187 | #### Using a context manager 188 | 189 | A context manager will automatically close the server when not needed anymore. 190 | 191 | ```py 192 | with HazeTunnel(port='8080', payload='alert("Hello World!");') as proxy: 193 | # Send the request 194 | requests.get( 195 | url='https://example.com', 196 | headers=HeaderGenerator().generate(browser='chrome'), 197 | proxies={'https': proxy.url}, 198 | verify=proxy.cert 199 | ).text 200 | ``` 201 | 202 |
203 | 204 | ## Building 205 | 206 | ### CFFI 207 | 208 | Pre-built C shared library binaries provided in [Releases](https://github.com/daijro/hazetunnel/releases). 209 | 210 | Otherwise, you can build these yourself using the `build.bat` file provided. 211 | 212 | ### CLI 213 | 214 | #### Building from source 215 | 216 | ```bash 217 | git clone https://github.com/daijro/hazetunnel 218 | cd hazetunnel 219 | go build 220 | ``` 221 | 222 | #### Usage 223 | 224 | ``` 225 | Usage of hazetunnel: 226 | -addr string 227 | Proxy listen address 228 | -cert string 229 | TLS CA certificate (generated automatically if not present) (default "cert.pem") 230 | -key string 231 | TLS CA key (generated automatically if not present) (default "key.pem") 232 | -port string 233 | Proxy listen port (default "8080") 234 | -user_agent string 235 | Override the User-Agent header for incoming requests. Optional. 236 | -verbose 237 | Enable verbose logging 238 | ``` 239 | 240 | --- 241 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0 -------------------------------------------------------------------------------- /example/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example server to test the capabilities of hazetunnel. 3 | 4 | Example command for testing HTML injection, 5 | Assuming hazetunnel is running on 8080, and this server on 5000: 6 | 7 | curl --proxy http://localhost:8080 \ 8 | --insecure http://localhost:5000/html \ 9 | -H "x-mitm-payload: alert('Hello world');" \ 10 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" 11 | 12 | Example command for testing JavaScript injection: 13 | 14 | curl --proxy http://localhost:8080 15 | --insecure http://localhost:5000/js \ 16 | -H "x-mitm-payload: alert('Hello world');" \ 17 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" 18 | """ 19 | 20 | from base64 import b64encode 21 | 22 | from flask import Flask, Response 23 | 24 | app = Flask(__name__) 25 | 26 | # JavaScript script to be served 27 | js_script = "console.log('Original JavaScript executed.');" 28 | # Encode the JavaScript script in base64 29 | encoded_js_script = b64encode(js_script.encode()).decode('utf-8') 30 | 31 | # HTML content with an embedded base64 JavaScript blob 32 | html_content = f""" 33 | 34 | 35 | 36 | Testing Page 37 | 38 | 39 |

Base64 JavaScript Testing Page

40 |

This page includes an embedded base64 encoded JavaScript for testing.

41 | 42 | 43 | 44 | 45 | """ 46 | 47 | 48 | @app.route('/html') 49 | def home(): 50 | return html_content 51 | 52 | 53 | @app.route('/js') 54 | def serve_js(): 55 | return Response(js_script, mimetype='application/javascript') 56 | 57 | 58 | if __name__ == '__main__': 59 | app.run(debug=True) 60 | -------------------------------------------------------------------------------- /hazetunnel/api/cert.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "log" 7 | "os" 8 | "sync" 9 | 10 | "github.com/elazarl/goproxy" 11 | 12 | cfsr "github.com/cloudflare/cfssl/csr" 13 | "github.com/cloudflare/cfssl/initca" 14 | ) 15 | 16 | var ( 17 | caLoaded = false 18 | caLoadMux sync.Mutex 19 | ) 20 | 21 | func fileExists(filename string) bool { 22 | _, err := os.Stat(filename) 23 | return !os.IsNotExist(err) 24 | } 25 | 26 | func setGoproxyCA(tlsCert tls.Certificate) { 27 | var err error 28 | if tlsCert.Leaf, err = x509.ParseCertificate(tlsCert.Certificate[0]); err != nil { 29 | log.Fatal("Unable to parse ca", err) 30 | } 31 | 32 | goproxy.GoproxyCa = tlsCert 33 | goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 34 | goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 35 | goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 36 | goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 37 | caLoaded = true 38 | } 39 | 40 | func loadCA() { 41 | if caLoaded { 42 | return // Skip if cert already loaded 43 | } 44 | caLoadMux.Lock() 45 | defer caLoadMux.Unlock() 46 | 47 | // Set default values of Config.Cert and Config.Key 48 | if Config.Cert == "" { 49 | Config.Cert = "cert.pem" 50 | } 51 | if Config.Key == "" { 52 | Config.Key = "key.pem" 53 | } 54 | 55 | // If both Config.Cert and Config.Key exist, load them 56 | if fileExists(Config.Cert) && fileExists(Config.Key) { 57 | tlsCert, err := tls.LoadX509KeyPair(Config.Cert, Config.Key) 58 | if err != nil { 59 | log.Fatal("Unable to load CA certificate and key", err) 60 | } 61 | setGoproxyCA(tlsCert) 62 | return 63 | } 64 | 65 | // If only only file exists, warn the user 66 | if fileExists(Config.Cert) { 67 | log.Fatalf("CA certificate exists, but found no corresponding key at %s", Config.Key) 68 | } else if fileExists(Config.Key) { 69 | log.Fatalf("CA key exists, but found no corresponding certificate at %s", Config.Cert) 70 | } 71 | 72 | // Generate new CA files 73 | log.Println("No CA found, generating certificate and key") 74 | tlsCert, err := generateCA() 75 | if err != nil { 76 | log.Fatal("Unable to generate CA certificate and key", err) 77 | } 78 | setGoproxyCA(tlsCert) 79 | } 80 | 81 | func generateCA() (tls.Certificate, error) { 82 | csr := cfsr.CertificateRequest{ 83 | CN: "tlsproxy CA", 84 | KeyRequest: cfsr.NewKeyRequest(), 85 | } 86 | 87 | certPEM, _, keyPEM, err := initca.New(&csr) 88 | if err != nil { 89 | return tls.Certificate{}, err 90 | } 91 | 92 | caOut, err := os.Create(Config.Cert) 93 | if err != nil { 94 | return tls.Certificate{}, err 95 | } 96 | defer caOut.Close() 97 | _, err = caOut.Write(certPEM) 98 | if err != nil { 99 | return tls.Certificate{}, err 100 | } 101 | 102 | keyOut, err := os.OpenFile(Config.Key, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 103 | if err != nil { 104 | return tls.Certificate{}, err 105 | } 106 | defer keyOut.Close() 107 | 108 | _, err = keyOut.Write(keyPEM) 109 | if err != nil { 110 | return tls.Certificate{}, err 111 | } 112 | 113 | return tls.X509KeyPair(certPEM, keyPEM) 114 | } 115 | -------------------------------------------------------------------------------- /hazetunnel/api/cffi.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | /* 4 | #include 5 | */ 6 | 7 | import ( 8 | "C" 9 | "log" 10 | 11 | json "github.com/goccy/go-json" 12 | ) 13 | import ( 14 | "context" 15 | ) 16 | 17 | /* 18 | CFFI exposed methods 19 | */ 20 | 21 | //export StartServer 22 | func StartServer(data string) { 23 | // Launch server from cffi 24 | var Flags ProxySetup 25 | err := json.Unmarshal([]byte(data), &Flags) 26 | if err != nil { 27 | log.Fatal(err) 28 | return 29 | } 30 | UpdateVerbosity() 31 | go Launch(&Flags) 32 | } 33 | 34 | //export SetVerbose 35 | func SetVerbose(data string) { 36 | // Set the verbose option from cffi 37 | var verbose VerbositySetting 38 | err := json.Unmarshal([]byte(data), &verbose) 39 | if err != nil { 40 | log.Fatal(err) 41 | return 42 | } 43 | // Update verbose level 44 | Config.Verbose = verbose.Verbose 45 | UpdateVerbosity() // Change immediately 46 | } 47 | 48 | //export SetKeyPair 49 | func SetKeyPair(data string) { 50 | // Set the x509 key pair from cffi 51 | var keypair KeyPairSetting 52 | err := json.Unmarshal([]byte(data), &keypair) 53 | if err != nil { 54 | log.Fatal(err) 55 | return 56 | } 57 | // Update the x509 key pair paths 58 | Config.Cert = keypair.Cert 59 | Config.Key = keypair.Key 60 | // Flag as unloaded 61 | caLoaded = false 62 | loadCA() 63 | } 64 | 65 | //export ShutdownServer 66 | func ShutdownServer(id string) { 67 | // Kill server from cffi 68 | serverMux.Lock() 69 | defer serverMux.Unlock() 70 | 71 | // Check if id is in proxyInstanceMap 72 | if _, ok := proxyInstanceMap[id]; !ok { 73 | // say id wasnt found 74 | log.Printf("Error: %v is not a running instance", id) 75 | return 76 | } 77 | 78 | if proxyInstanceMap[id].Server == nil { 79 | log.Println("Error: Server not found") 80 | delete(proxyInstanceMap, id) 81 | return 82 | } 83 | 84 | // Announce server shutdown to verbose logs 85 | if Config.Verbose { 86 | log.Println("Shutting down the server...") 87 | } 88 | proxyInstanceMap[id].Cancel() // Cancel the context, which should trigger graceful shutdown 89 | 90 | if err := proxyInstanceMap[id].Server.Shutdown(context.Background()); err != nil { 91 | log.Printf("Failed to shutdown the server gracefully: %v", err) 92 | } 93 | delete(proxyInstanceMap, id) 94 | } 95 | -------------------------------------------------------------------------------- /hazetunnel/api/config.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | cflog "github.com/cloudflare/cfssl/log" 5 | "github.com/elazarl/goproxy" 6 | utls "github.com/refraction-networking/utls" 7 | ) 8 | 9 | type ConfigFlags struct { 10 | // Global flags 11 | Cert string `json:"cert,omitempty"` 12 | Key string `json:"key,omitempty"` 13 | Verbose bool `json:"verbose,omitempty"` 14 | } 15 | 16 | type ProxySetup struct { 17 | // Per proxy instance 18 | Addr string `json:"addr,omitempty"` 19 | Port string `json:"port"` 20 | UserAgent string `json:"user_agent,omitempty"` 21 | Payload string `json:"payload,omitempty"` 22 | UpstreamProxy string `json:"upstreamproxy,omitempty"` 23 | Id string `json:"id"` 24 | } 25 | 26 | var ( 27 | Config ConfigFlags 28 | ) 29 | 30 | type VerbositySetting struct { 31 | Verbose bool `json:"verbose"` 32 | } 33 | 34 | type KeyPairSetting struct { 35 | Cert string `json:"cert"` 36 | Key string `json:"key"` 37 | } 38 | 39 | func UpdateVerbosity() { 40 | // Update the verbose level 41 | if Config.Verbose { 42 | cflog.Level = cflog.LevelInfo 43 | } else { 44 | cflog.Level = cflog.LevelError 45 | } 46 | } 47 | 48 | func getClientHelloID(uagent string, ctx *goproxy.ProxyCtx) (utls.ClientHelloID, error) { 49 | browser, version, err := uagentToUtls(uagent) 50 | ctx.Logf("Client: %s, UTLS Version: %s", browser, version) 51 | 52 | if err != nil { 53 | return utls.ClientHelloID{}, err 54 | } 55 | 56 | return utls.ClientHelloID{ 57 | Client: browser, 58 | Version: version, 59 | Seed: nil, 60 | Weights: nil, 61 | }, nil 62 | } 63 | -------------------------------------------------------------------------------- /hazetunnel/api/http.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/elazarl/goproxy" 7 | ) 8 | 9 | func invalidUpstreamProxyResponse( 10 | req *http.Request, 11 | ctx *goproxy.ProxyCtx, 12 | upstreamProxy string, 13 | ) *http.Response { 14 | ctx.Warnf("CRITICAL: Client specified invalid upstream proxy: %s", upstreamProxy) 15 | return goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadRequest, "HAZETUNNEL ERROR: Invalid upstream proxy: "+upstreamProxy) 16 | } 17 | 18 | func missingParameterResponse( 19 | req *http.Request, 20 | ctx *goproxy.ProxyCtx, 21 | header string, 22 | ) *http.Response { 23 | ctx.Warnf("CRITICAL: Missing header: %s", header) 24 | return goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadRequest, "HAZETUNNEL ERROR: Missing header: "+header) 25 | } 26 | -------------------------------------------------------------------------------- /hazetunnel/api/injector.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/cristalhq/base64" 10 | 11 | "github.com/elazarl/goproxy" 12 | ) 13 | 14 | func PayloadInjector(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 15 | if resp == nil || resp.Body == nil { 16 | return resp 17 | } 18 | 19 | // Retrieve the payload code from the request's context 20 | payload, ok := ctx.Req.Context().Value(payloadKey).(string) 21 | if !ok { 22 | ctx.Warnf("Error was returned. Skipping payload injection...") 23 | return resp 24 | } 25 | if payload == "" { 26 | ctx.Logf("No payload was passed") 27 | return resp 28 | } 29 | 30 | contentType := resp.Header.Get("Content-Type") 31 | ctx.Logf("Content-Type: %s", contentType) 32 | 33 | if strings.HasPrefix(contentType, "text/html") { 34 | // Inject into base64 encoded parts 35 | body, err := io.ReadAll(resp.Body) 36 | if err != nil { 37 | ctx.Warnf("Failed to read response body: %v", err) 38 | return resp 39 | } 40 | resp.Body.Close() 41 | 42 | html := string(body) 43 | html = injectPayloadIntoHTML(html, payload, ctx) 44 | 45 | resp.Body = io.NopCloser(strings.NewReader(html)) 46 | } else if strings.HasPrefix(contentType, "application/javascript") || strings.HasPrefix(contentType, "text/javascript") { 47 | body, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | ctx.Warnf("Failed to read response body: %v", err) 50 | return resp 51 | } 52 | resp.Body.Close() 53 | 54 | script := payload + string(body) 55 | resp.Body = io.NopCloser(strings.NewReader(script)) 56 | } 57 | 58 | return resp 59 | } 60 | 61 | func injectPayloadIntoHTML(html string, payload string, ctx *goproxy.ProxyCtx) string { 62 | // Inject the payload code into embedded base64 scripts within the page 63 | pattern := regexp.MustCompile(`data:(?:application|text)/javascript;base64,([\w+/=]+)`) 64 | ctx.Logf("Scanning for embedded scripts") 65 | return pattern.ReplaceAllStringFunc(html, func(match string) string { 66 | ctx.Logf("Match found!") 67 | prefix := match[:strings.Index(match, "base64,")+len("base64,")] 68 | encodedScript := match[len(prefix):] // Extract the base64 encoded script 69 | decodedScript, err := base64.StdEncoding.DecodeString(encodedScript) 70 | if err != nil { 71 | ctx.Warnf("Failed to decode base64 script: %v", err) 72 | return match // Return the original match if there's an error in decoding 73 | } 74 | // Prepend the payload code to the decoded script 75 | decodedScript = []byte(payload + string(decodedScript)) 76 | // Re-encode the modified script to base64 77 | encodedScript = base64.StdEncoding.EncodeToString(decodedScript) 78 | // Return the modified script 79 | return prefix + encodedScript 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /hazetunnel/api/profiles.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/mileusna/useragent" 9 | ) 10 | 11 | // Predefined dictionary with browser versions and their corresponding utls values. 12 | // Updated as of utls v1.6.6. 13 | // Extracted from here: https://github.com/refraction-networking/utls/blob/master/u_common.go#L573 14 | var utlsDict = map[string]map[int]string{ 15 | "Firefox": { 16 | -1: "55", 17 | 56: "56", 18 | 63: "63", 19 | 65: "65", 20 | 99: "99", 21 | 102: "102", 22 | 105: "105", 23 | 120: "120", 24 | }, 25 | "Chrome": { 26 | -1: "58", 27 | 62: "62", 28 | 70: "70", 29 | 72: "72", 30 | 83: "83", 31 | 87: "87", 32 | 96: "96", 33 | 100: "100", 34 | 102: "102", 35 | 106: "106", 36 | 112: "112_PSK", 37 | 114: "114_PSK", 38 | 120: "120", 39 | }, 40 | "iOS": { 41 | -1: "111", 42 | 12: "12.1", 43 | 13: "13", 44 | 14: "14", 45 | }, 46 | "Android": { 47 | -1: "11", 48 | }, 49 | "Edge": { 50 | -1: "85", 51 | // 106: "106", incompatible with utls 52 | }, 53 | "Safari": { 54 | -1: "16.0", 55 | }, 56 | "360Browser": { 57 | -1: "7.5", 58 | // 11: "11.0", incompatible with utls 59 | }, 60 | "QQBrowser": { 61 | -1: "11.1", 62 | }, 63 | } 64 | 65 | func uagentToUtls(uagent string) (string, string, error) { 66 | ua := useragent.Parse(uagent) 67 | utlsVersion, err := utlsVersion(ua.Name, ua.Version) 68 | if err != nil { 69 | return "", "", err 70 | } 71 | return ua.Name, utlsVersion, nil 72 | } 73 | 74 | func utlsVersion(browserName, browserVersion string) (string, error) { 75 | if versions, ok := utlsDict[browserName]; ok { 76 | // Extract the major version number from the browser version string 77 | majorVersionStr := strings.Split(browserVersion, ".")[0] 78 | majorVersion, err := strconv.Atoi(majorVersionStr) 79 | if err != nil { 80 | return "", fmt.Errorf("error parsing major version number from browser version: %v", err) 81 | } 82 | 83 | // Find the highest version that is less than or equal to the browser version 84 | var selectedVersion int = -1 85 | for version := range versions { 86 | if version <= majorVersion && version > selectedVersion { 87 | selectedVersion = version 88 | } 89 | } 90 | 91 | if utls, ok := versions[selectedVersion]; ok { 92 | return utls, nil 93 | } else { 94 | return "", fmt.Errorf("no UTLS value found for browser '%s' with version '%s'", browserName, browserVersion) 95 | } 96 | } 97 | return "", fmt.Errorf("browser '%s' not found in UTLS dictionary", browserName) 98 | } 99 | -------------------------------------------------------------------------------- /hazetunnel/api/proxy.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "sync" 9 | 10 | "github.com/elazarl/goproxy" 11 | utls "github.com/refraction-networking/utls" 12 | sf "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/utls" 13 | ) 14 | 15 | type contextKey string 16 | 17 | const payloadKey contextKey = "payload" 18 | 19 | type ProxyInstance struct { 20 | Server *http.Server 21 | Cancel context.CancelFunc 22 | } 23 | 24 | // Globals 25 | var ( 26 | serverMux sync.Mutex 27 | proxyInstanceMap = make(map[string]*ProxyInstance) 28 | ) 29 | 30 | func initServer(Flags *ProxySetup) *http.Server { 31 | serverMux.Lock() 32 | defer serverMux.Unlock() 33 | 34 | // Load CA if not already loaded 35 | loadCA() 36 | 37 | // Setup the proxy instance 38 | proxy := goproxy.NewProxyHttpServer() 39 | proxy.Verbose = Config.Verbose 40 | setupProxy(proxy, Flags) 41 | 42 | // Create the server 43 | server := &http.Server{ 44 | Addr: Flags.Addr + ":" + Flags.Port, 45 | Handler: proxy, 46 | } 47 | _, cancel := context.WithCancel(context.Background()) 48 | 49 | // Add proxy instance to the map 50 | proxyInstanceMap[Flags.Id] = &ProxyInstance{ 51 | Server: server, 52 | Cancel: cancel, 53 | } 54 | return server 55 | } 56 | 57 | func setupProxy(proxy *goproxy.ProxyHttpServer, Flags *ProxySetup) { 58 | proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) 59 | 60 | proxy.OnRequest().DoFunc( 61 | func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 62 | var upstreamProxy *url.URL 63 | 64 | // Override the User-Agent header if specified 65 | // If one wasn't specified, verify a User-Agent is in the request 66 | if len(Flags.UserAgent) != 0 { 67 | req.Header["User-Agent"] = []string{Flags.UserAgent} 68 | } else if len(req.Header["User-Agent"]) == 0 { 69 | return req, missingParameterResponse(req, ctx, "User-Agent") 70 | } 71 | 72 | // Set the ClientHello from the User-Agent header 73 | ua := req.Header["User-Agent"][0] 74 | clientHelloId, err := getClientHelloID(ua, ctx) 75 | if err != nil { 76 | // Use the latest Chrome when the User-Agent header cannot be recognized 77 | ctx.Logf("Error parsing User-Agent: %s", err) 78 | clientHelloId = utls.HelloChrome_Auto 79 | ctx.Logf("Continuing with Chrome %v ClientHello", clientHelloId.Version) 80 | } 81 | 82 | // Store the payload code in the request's context 83 | ctx.Req = req.WithContext( 84 | context.WithValue( 85 | ctx.Req.Context(), 86 | payloadKey, 87 | Flags.Payload, 88 | ), 89 | ) 90 | 91 | // If a proxy header was passed, set it to upstreamProxy 92 | if len(Flags.UpstreamProxy) != 0 { 93 | proxyUrl, err := url.Parse(Flags.UpstreamProxy) 94 | if err != nil { 95 | return req, invalidUpstreamProxyResponse(req, ctx, Flags.UpstreamProxy) 96 | } 97 | upstreamProxy = proxyUrl 98 | } 99 | 100 | // Skip TLS handshake if scheme is HTTP 101 | ctx.Logf("Scheme: %s", req.URL.Scheme) 102 | if req.URL.Scheme == "http" { 103 | ctx.Logf("Skipping TLS for HTTP request") 104 | return req, nil 105 | } 106 | 107 | // Build round tripper 108 | roundTripper := sf.NewUTLSHTTPRoundTripperWithProxy(clientHelloId, &utls.Config{ 109 | InsecureSkipVerify: true, 110 | OmitEmptyPsk: true, 111 | }, http.DefaultTransport, false, upstreamProxy) 112 | 113 | ctx.RoundTripper = goproxy.RoundTripperFunc( 114 | func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Response, error) { 115 | return roundTripper.RoundTrip(req) 116 | }) 117 | 118 | return req, nil 119 | }, 120 | ) 121 | 122 | // Inject payload code into responses 123 | proxy.OnResponse().DoFunc(PayloadInjector) 124 | } 125 | 126 | // Launches the server 127 | func Launch(Flags *ProxySetup) { 128 | server := initServer(Flags) 129 | 130 | // Print server startup message if from CLI or verbose CFFI 131 | if Flags.Id == "cli" || Config.Verbose { 132 | log.Println("Hazetunnel listening at", server.Addr) 133 | } 134 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 135 | log.Fatalf("HTTP server ListenAndServe: %v", err) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /hazetunnel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/daijro/hazetunnel/hazetunnel 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/cloudflare/cfssl v1.6.5 7 | github.com/cristalhq/base64 v0.1.2 8 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 9 | github.com/goccy/go-json v0.10.3 10 | github.com/mileusna/useragent v1.3.4 11 | github.com/refraction-networking/utls v1.6.6 12 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.9.2 13 | ) 14 | 15 | require ( 16 | github.com/andybalholm/brotli v1.1.0 // indirect 17 | github.com/cloudflare/circl v1.3.8 // indirect 18 | github.com/go-logr/logr v1.4.2 // indirect 19 | github.com/google/certificate-transparency-go v1.2.1 // indirect 20 | github.com/jmoiron/sqlx v1.4.0 // indirect 21 | github.com/klauspost/compress v1.17.8 // indirect 22 | github.com/pelletier/go-toml v1.9.5 // indirect 23 | github.com/weppos/publicsuffix-go v0.30.2 // indirect 24 | github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db // indirect 25 | github.com/zmap/zlint/v3 v3.6.2 // indirect 26 | golang.org/x/crypto v0.23.0 // indirect 27 | golang.org/x/net v0.25.0 // indirect 28 | golang.org/x/sys v0.20.0 // indirect 29 | golang.org/x/text v0.15.0 // indirect 30 | google.golang.org/protobuf v1.34.1 // indirect 31 | k8s.io/klog/v2 v2.120.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /hazetunnel/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 4 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 5 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 6 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 7 | github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= 8 | github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= 9 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 10 | github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= 11 | github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 12 | github.com/cristalhq/base64 v0.1.2 h1:edsefYyYDiac7Ytdh2xdaiiSSJzcI2f0yIkdGEf1qY0= 13 | github.com/cristalhq/base64 v0.1.2/go.mod h1:sy4+2Hale2KbtSqkzpdMeYTP/IrB+HCvxVHWsh2VSYk= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= 18 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 19 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= 20 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 24 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 25 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 26 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 29 | github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= 30 | github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE= 31 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 36 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 37 | github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= 38 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 39 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 40 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 41 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 42 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 43 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 44 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 45 | github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= 46 | github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 47 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 48 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 49 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 54 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 55 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 56 | github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk= 57 | github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= 58 | github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 59 | github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 60 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 61 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 62 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= 66 | github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 67 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 68 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 69 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 70 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 71 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= 72 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 73 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= 74 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 78 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 79 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 81 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 82 | github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= 83 | github.com/weppos/publicsuffix-go v0.30.2 h1:Np18yzfMR90jNampWFs7iSh2sw/qCZkhL41/ffyihCU= 84 | github.com/weppos/publicsuffix-go v0.30.2/go.mod h1:/hGscit36Yt+wammfBBwdMdxBT8btsTt6KvwO9OvMyM= 85 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 86 | github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= 87 | github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= 88 | github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= 89 | github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= 90 | github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= 91 | github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= 92 | github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db h1:IfONOhyZlf4qPt3ENPU+27mBbPjzTQ+swKpj7MJva9I= 93 | github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db/go.mod h1:mo/07mo6reDaiz6BzveCuYBWb1d+aX8Pf8Nh+Q57y2g= 94 | github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= 95 | github.com/zmap/zlint/v3 v3.6.2 h1:IK1Ida6HFLgBrczrCGZa8VVRpksO5iVhYw7WSDl+Irs= 96 | github.com/zmap/zlint/v3 v3.6.2/go.mod h1:NVgiIWssgzp0bNl8P4Gz94NHV2ep/4Jyj9V69uTmZyg= 97 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.9.2 h1:LXgSnJmGdsBFZdPnb1xinPTP9E3uv8g/4TT99lga33o= 98 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.9.2/go.mod h1:pJJtS5nhVfZL/4sD4nx14me4vfV/O+AwdSDxSkF22MA= 99 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 103 | golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 104 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 105 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 106 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 107 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 108 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 109 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 110 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 111 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 112 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 113 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 114 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 115 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 116 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 117 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 118 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 119 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 120 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 121 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 122 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 123 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 124 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 125 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 126 | golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= 127 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 147 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 148 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 149 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 150 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 151 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 152 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 153 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 154 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 155 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 156 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 157 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 159 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 160 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 161 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 162 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 163 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 164 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 165 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 166 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 167 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 168 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 169 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 170 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 171 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 172 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 173 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 174 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 175 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 177 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 178 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 179 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 180 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 181 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 182 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 183 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 184 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 185 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 186 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 187 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 189 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 190 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 191 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 192 | -------------------------------------------------------------------------------- /hazetunnel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/daijro/hazetunnel/hazetunnel/api" 7 | ) 8 | 9 | /* 10 | Launch from CLI 11 | */ 12 | 13 | func main() { 14 | // Parse flags 15 | var Flags api.ProxySetup 16 | flag.StringVar(&Flags.Addr, "addr", "", "Proxy listen address") 17 | flag.StringVar(&Flags.Port, "port", "8080", "Proxy listen port") 18 | flag.StringVar(&Flags.UserAgent, "user_agent", "", "Override the User-Agent header for incoming requests. Optional.") 19 | flag.StringVar(&api.Config.Cert, "cert", "cert.pem", "TLS CA certificate (generated automatically if not present)") 20 | flag.StringVar(&api.Config.Key, "key", "key.pem", "TLS CA key (generated automatically if not present)") 21 | flag.BoolVar(&api.Config.Verbose, "verbose", false, "Enable verbose logging") 22 | flag.Parse() 23 | // Set ID 24 | Flags.Id = "cli" 25 | // Set verbose level 26 | api.UpdateVerbosity() 27 | // Launch proxy server 28 | api.Launch(&Flags) 29 | } 30 | -------------------------------------------------------------------------------- /python-bindings/README.md: -------------------------------------------------------------------------------- 1 | # Python Usage 2 | 3 | ## Usage 4 | 5 | ```py 6 | from hazetunnel import HazeTunnel 7 | from browserforge.headers import HeaderGenerator 8 | ... 9 | # Initialize the proxy 10 | proxy = HazeTunnel(port='8080', payload='alert("Hello World!");') 11 | proxy.launch() 12 | # Send the request 13 | requests.get( 14 | url='https://example.com', 15 | headers=HeaderGenerator().generate(browser='chrome'), 16 | proxies={'https': proxy.url}, 17 | verify=proxy.cert 18 | ).text 19 | # Stop the proxy 20 | proxy.stop() 21 | ``` 22 | 23 |
24 | 25 | 26 | HazeTunnel parameters 27 | 28 | 29 | ``` 30 | Parameters: 31 | port (Optional[str]): Specify a port to listen on. Default is random. 32 | payload (Optional[str]): Payload to inject into responses 33 | user_agent (Optional[str]): Optionally override all User-Agent headers 34 | upstream_proxy (Optional[str]): Optionally forward requests to an upstream proxy 35 | ``` 36 | 37 |
38 | 39 | ### Using a context manager 40 | 41 | A context manager will automatically close the server when not needed anymore. 42 | 43 | ```py 44 | with HazeTunnel(port='8080', payload='alert("Hello World!");') as proxy: 45 | # Send the request 46 | requests.get( 47 | url='https://example.com', 48 | headers=HeaderGenerator().generate(browser='chrome'), 49 | proxies={'https': proxy.url}, 50 | verify=proxy.cert 51 | ).text 52 | ``` 53 | 54 |
55 | 56 | ## CLI 57 | 58 | Download the latest version of the API: 59 | 60 | ```sh 61 | python -m hazetunnel fetch 62 | ``` 63 | 64 | Remove all files before uninstalling 65 | 66 | ```sh 67 | python -m hazetunnel remove 68 | ``` 69 | 70 | Run the MITM proxy: 71 | 72 | ```sh 73 | python -m hazetunnel run -p 8080 --verbose 74 | ``` 75 | 76 | ### All commands 77 | 78 | ```sh 79 | Usage: python -m hazetunnel [OPTIONS] COMMAND [ARGS]... 80 | 81 | Options: 82 | --help Show this message and exit. 83 | 84 | Commands: 85 | fetch Fetch the latest version of hazetunnel-api 86 | remove Remove all library files 87 | run Run the MITM proxy 88 | version Display the current version 89 | ``` 90 | 91 | ### See [here](https://github.com/daijro/hazetunnel) for more information and examples. 92 | 93 | --- 94 | -------------------------------------------------------------------------------- /python-bindings/hazetunnel/__init__.py: -------------------------------------------------------------------------------- 1 | from .control import HazeTunnel, cert, key, set_key_pair, set_verbose, verbose 2 | 3 | __all__ = ['HazeTunnel', 'cert', 'key', 'set_key_pair', 'set_verbose', 'verbose'] 4 | -------------------------------------------------------------------------------- /python-bindings/hazetunnel/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Binary CLI manager for hazetunnel-api. 3 | 4 | Adapted from https://github.com/daijro/hrequests/blob/main/hrequests/__main__.py 5 | """ 6 | 7 | import os 8 | import re 9 | import time 10 | from dataclasses import dataclass 11 | from functools import total_ordering 12 | from importlib.metadata import version as pkg_version 13 | from pathlib import Path 14 | from typing import Optional 15 | 16 | import click 17 | from hazetunnel.__version__ import BRIDGE_VERSION 18 | from hazetunnel.cffi import LibraryManager, root_dir 19 | 20 | from hazetunnel import HazeTunnel, set_key_pair, set_verbose 21 | 22 | 23 | def rprint(*a, **k): 24 | click.secho(*a, **k, bold=True) 25 | 26 | 27 | @total_ordering 28 | @dataclass 29 | class Version: 30 | version: str 31 | 32 | def __post_init__(self) -> None: 33 | self.sort_version = tuple(int(x) for x in self.version.split('.')) 34 | 35 | def __eq__(self, other) -> bool: 36 | return self.sort_version == other.sort_version 37 | 38 | def __lt__(self, other) -> bool: 39 | return self.sort_version < other.sort_version 40 | 41 | def __str__(self) -> str: 42 | return self.version 43 | 44 | @staticmethod 45 | def get_version(name) -> 'Version': 46 | ver: Optional[re.Match] = LibraryUpdate.FILE_NAME.search(name) 47 | if not ver: 48 | raise ValueError(f'Could not find version in {name}') 49 | return Version(ver[1]) 50 | 51 | 52 | @dataclass 53 | class Asset: 54 | url: str 55 | name: str 56 | 57 | def __post_init__(self) -> None: 58 | self.version: Version = Version.get_version(self.name) 59 | 60 | 61 | class LibraryUpdate(LibraryManager): 62 | """ 63 | Checks if an update is available for hazetunnel-api library 64 | """ 65 | 66 | FILE_NAME: re.Pattern = re.compile(r'^hazetunnel-api-v([\d\.]+)') 67 | 68 | def __init__(self) -> None: 69 | self.parent_path: Path = root_dir / 'bin' 70 | self.file_cont, self.file_ext = self.get_name() 71 | self.file_pref = f'hazetunnel-api-v{BRIDGE_VERSION}' 72 | 73 | @property 74 | def path(self) -> Optional[str]: 75 | if paths := self.get_files(): 76 | return paths[0] 77 | 78 | @property 79 | def full_path(self) -> Optional[str]: 80 | if path := self.path: 81 | return os.path.join(self.parent_path, path) 82 | 83 | def latest_asset(self) -> Asset: 84 | """ 85 | Find the latest Asset for the hazetunnel-api library 86 | """ 87 | releases = self.get_releases() 88 | for release in releases: 89 | if asset := self.check_assets(release['assets']): 90 | url, name = asset 91 | return Asset(url, name) 92 | raise ValueError('No assets found for hazetunnel-api') 93 | 94 | def install(self) -> None: 95 | filename = super().check_library() 96 | ver: Version = Version.get_version(filename) 97 | 98 | rprint(f"Successfully downloaded hazetunnel-api v{ver}!", fg="green") 99 | 100 | def update(self) -> None: 101 | """ 102 | Updates the library if needed 103 | """ 104 | path = self.path 105 | if not path: 106 | # install the library if it doesn't exist 107 | return self.install() 108 | 109 | # get the version 110 | current_ver: Version = Version.get_version(path) 111 | 112 | # check if the version is the same as the latest available version 113 | asset: Asset = self.latest_asset() 114 | if current_ver >= asset.version: 115 | rprint("hazetunnel-api library up to date!", fg="green") 116 | rprint(f"Current version: v{current_ver}", fg="green") 117 | return 118 | 119 | # download updated file 120 | rprint( 121 | f"Updating hazetunnel-api library from v{current_ver} => v{asset.version}", fg="yellow" 122 | ) 123 | # remove old, download new 124 | self.download_file(os.path.join(self.parent_path, asset.name), asset.url) 125 | try: 126 | os.remove(os.path.join(self.parent_path, path)) 127 | except OSError: 128 | rprint("WARNING: Could not remove outdated library files.", fg="yellow") 129 | 130 | 131 | @click.group() 132 | def cli() -> None: 133 | pass 134 | 135 | 136 | @cli.command(name='fetch') 137 | def fetch(): 138 | """ 139 | Fetch the latest version of hazetunnel-api 140 | """ 141 | LibraryUpdate().update() 142 | 143 | 144 | @cli.command(name='remove') 145 | def remove() -> None: 146 | """ 147 | Remove all library files 148 | """ 149 | path = str(LibraryUpdate().full_path) 150 | # remove all .pem files 151 | for file in (root_dir / 'bin').glob('*.pem'): 152 | rprint(f"Removed {file}", fg="green") 153 | file.unlink() 154 | # remove library 155 | if not os.path.exists(path): 156 | rprint("Library is not downloaded.", fg="yellow") 157 | return 158 | try: 159 | os.remove(path) 160 | except OSError as e: 161 | rprint(f"WARNING: Could not remove {path}: {e}", fg="red") 162 | else: 163 | rprint(f"Removed {path}", fg="green") 164 | rprint("Library files have been removed.", fg="yellow") 165 | 166 | 167 | @cli.command(name='version') 168 | def version() -> None: 169 | """ 170 | Display the current version 171 | """ 172 | # python package version 173 | rprint(f"Pip package:\tv{pkg_version('hazetunnel')}", fg="green") 174 | 175 | # library path 176 | libup = LibraryUpdate() 177 | path = libup.path 178 | # if the library is not installed 179 | if not path: 180 | rprint("hazetunnel-api:\tNot downloaded!", fg="red") 181 | return 182 | # library version 183 | lib_ver = Version.get_version(path) 184 | rprint(f"hazetunnel-api:\tv{lib_ver} ", fg="green", nl=False) 185 | 186 | # check for library updates 187 | latest_ver = libup.latest_asset().version 188 | if latest_ver == lib_ver: 189 | rprint("(Up to date!)", fg="yellow") 190 | else: 191 | rprint(f"(Latest: v{latest_ver})", fg="red") 192 | 193 | 194 | @cli.command(name='run') 195 | @click.option('-p', '--port', type=str, default='8080', help="Port to use. Default: 8080.") 196 | @click.option('--user_agent', type=str, default=None, help="Override User-Agent headers.") 197 | @click.option('--payload', type=str, default=None, help="Payload to inject into responses.") 198 | @click.option( 199 | '--upstream_proxy', type=str, default=None, help="Forward requests to an upstream proxy." 200 | ) 201 | @click.option('--cert', type=str, default=None, help="Path to the certificate file.") 202 | @click.option('--key', type=str, default=None, help="Path to the key file.") 203 | @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output.") 204 | def run( 205 | port: str, 206 | user_agent: str, 207 | payload: str, 208 | upstream_proxy: str, 209 | cert: str, 210 | key: str, 211 | verbose: bool, 212 | ) -> None: 213 | """ 214 | Run the MITM proxy 215 | """ 216 | if cert or key: 217 | set_key_pair(cert, key) 218 | if verbose: 219 | set_verbose(True) 220 | server = HazeTunnel( 221 | port=port, payload=payload, user_agent=user_agent, upstream_proxy=upstream_proxy 222 | ) 223 | # wait forever until keyboard interrupt 224 | server.launch() 225 | print('Server has started! Press Ctrl-C to quit.') 226 | try: 227 | time.sleep(1e6) 228 | except KeyboardInterrupt: 229 | pass 230 | finally: 231 | server.stop() 232 | 233 | 234 | if __name__ == '__main__': 235 | cli() 236 | -------------------------------------------------------------------------------- /python-bindings/hazetunnel/__version__.py: -------------------------------------------------------------------------------- 1 | # Supported binary version 2 | BRIDGE_VERSION = '2.' 3 | -------------------------------------------------------------------------------- /python-bindings/hazetunnel/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daijro/hazetunnel/01a924340d1999de3214f730c0709f1553a16f0c/python-bindings/hazetunnel/bin/__init__.py -------------------------------------------------------------------------------- /python-bindings/hazetunnel/cffi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Binary auto-downloader and CFFI bindings for hazetunnel-api. 3 | 4 | Adapted from https://github.com/daijro/hrequests/blob/main/hrequests/cffi.py 5 | """ 6 | 7 | import ctypes 8 | import json 9 | import os 10 | import socket 11 | from contextlib import closing 12 | from pathlib import Path 13 | from platform import machine 14 | from sys import platform 15 | from typing import Dict, Optional, Tuple 16 | 17 | import click 18 | from httpx import get, stream 19 | 20 | from .__version__ import BRIDGE_VERSION 21 | 22 | root_dir: Path = Path(os.path.abspath(os.path.dirname(__file__))) 23 | 24 | 25 | # Map machine architecture to hazetunnel-api binary name 26 | arch_map = { 27 | 'amd64': 'amd64', 28 | 'x86_64': 'amd64', 29 | 'x86': '386', 30 | 'i686': '386', 31 | 'i386': '386', 32 | 'arm64': 'arm64', 33 | 'aarch64': 'arm64', 34 | 'armv5l': 'arm-5', 35 | 'armv6l': 'arm-6', 36 | 'armv7l': 'arm-7', 37 | 'ppc64le': 'ppc64le', 38 | 'riscv64': 'riscv64', 39 | 's390x': 's390x', 40 | } 41 | 42 | 43 | class LibraryManager: 44 | def __init__(self) -> None: 45 | self.parent_path: Path = root_dir / 'bin' 46 | self.file_cont, self.file_ext = self.get_name() 47 | self.file_pref = f'hazetunnel-api-v{BRIDGE_VERSION}' 48 | filename = self.check_library() 49 | self.full_path: str = str(self.parent_path / filename) 50 | 51 | @staticmethod 52 | def get_name() -> Tuple[str, str]: 53 | try: 54 | arch = arch_map[machine().lower()] 55 | except KeyError as e: 56 | raise OSError('Your machine architecture is not supported.') from e 57 | if platform == 'darwin': 58 | return f'darwin-{arch}', '.dylib' 59 | elif platform in ('win32', 'cygwin'): 60 | return f'windows-{arch}', '.dll' 61 | return f'linux-{arch}', '.so' 62 | 63 | def get_files(self) -> list: 64 | files: list = [file.name for file in self.parent_path.glob('hazetunnel-api-*')] 65 | return sorted(files, reverse=True) 66 | 67 | def check_library(self) -> str: 68 | files: list = self.get_files() 69 | for file in files: 70 | if not file.endswith(self.file_ext): 71 | continue 72 | if file.startswith(self.file_pref): 73 | return file 74 | # delete residual files from previous versions 75 | os.remove(self.parent_path / file) 76 | self.download_library() 77 | return self.check_library() 78 | 79 | def check_assets(self, assets) -> Optional[Tuple[str, str]]: 80 | for asset in assets: 81 | if ( 82 | # filter via version 83 | asset['name'].startswith(self.file_pref) 84 | # filter via os 85 | and self.file_cont in asset['name'] 86 | # filter via file extension 87 | and asset['name'].endswith(self.file_ext) 88 | ): 89 | return asset['browser_download_url'], asset['name'] 90 | 91 | def get_releases(self) -> dict: 92 | # pull release assets from github daijro/hazetunnel 93 | resp = get('https://api.github.com/repos/daijro/hazetunnel/releases') 94 | if resp.status_code != 200: 95 | raise ConnectionError(f'Could not connect to GitHub: {resp.text}') 96 | return resp.json() 97 | 98 | def download_library(self): 99 | releases = self.get_releases() 100 | for release in releases: 101 | asset = self.check_assets(release['assets']) 102 | if asset: 103 | url, name = asset 104 | break 105 | else: 106 | raise IOError('Could not find a matching binary for your system.') 107 | # download file 108 | file = self.parent_path / name 109 | self.download_file(file, url) 110 | 111 | def download_file(self, file, url): 112 | # handle download_exec 113 | try: 114 | with open(file, 'wb') as fstream: 115 | self.download_exec(fstream, url) 116 | except KeyboardInterrupt as e: 117 | print('Cancelled.') 118 | os.remove(file) 119 | raise e 120 | 121 | @staticmethod 122 | def download_exec(fstream, url): 123 | # file downloader with progress bar 124 | with stream('GET', url, follow_redirects=True) as resp: 125 | total = int(resp.headers['Content-Length']) 126 | with click.progressbar( 127 | length=total, 128 | label='Downloading hazetunnel-api from daijro/hazetunnel: ', 129 | fill_char='*', 130 | show_percent=True, 131 | ) as bar: 132 | for chunk in resp.iter_bytes(chunk_size=4096): 133 | fstream.write(chunk) 134 | bar.update(len(chunk)) 135 | 136 | @staticmethod 137 | def load_library() -> ctypes.CDLL: 138 | libman: LibraryManager = LibraryManager() 139 | return ctypes.cdll.LoadLibrary(libman.full_path) 140 | 141 | 142 | class GoString(ctypes.Structure): 143 | # wrapper around Go's string type 144 | _fields_ = [("p", ctypes.c_char_p), ("n", ctypes.c_longlong)] 145 | 146 | 147 | def gostring(s: str) -> GoString: 148 | # create a string buffer and keep a reference to it 149 | port_buf = ctypes.create_string_buffer(s.encode('utf-8')) 150 | # pass the buffer to GoString 151 | go_str = GoString(ctypes.cast(port_buf, ctypes.c_char_p), len(s)) 152 | # attach the buffer to the GoString instance to keep it alive 153 | go_str._keep_alive = port_buf 154 | return go_str 155 | 156 | 157 | class Library: 158 | def __init__(self) -> None: 159 | # Load the shared package 160 | self.library: ctypes.CDLL = LibraryManager.load_library() 161 | 162 | # Global config data 163 | self._key_pair: Tuple[str, str] 164 | self._verbose: bool = False 165 | 166 | # Extract the exposed functions 167 | self.library.StartServer.argtypes = [GoString] 168 | self.library.ShutdownServer.argtypes = [GoString] 169 | self.library.SetVerbose.argtypes = [GoString] 170 | self.library.SetKeyPair.argtypes = [GoString] 171 | 172 | # Set the default key pair paths 173 | bin_path = root_dir / "bin" 174 | self.key_pair = (str(bin_path / "key.pem"), str(bin_path / "cert.pem")) 175 | 176 | @staticmethod 177 | def get_open_port() -> int: 178 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 179 | s.bind(('', 0)) 180 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 181 | return s.getsockname()[1] 182 | 183 | def start_server(self, options: Dict[str, str]): 184 | # Launch the server 185 | ref: GoString = gostring(json.dumps(options)) 186 | self.library.StartServer(ref) 187 | 188 | def stop_server(self, id: str): 189 | # Stop the server 190 | ref: GoString = gostring(id) 191 | self.library.ShutdownServer(ref) 192 | 193 | """ 194 | Global config data 195 | """ 196 | 197 | @property 198 | def verbose(self): 199 | return self._verbose 200 | 201 | @verbose.setter 202 | def verbose(self, verbose: bool): 203 | # Set the verbose level 204 | self._verbose = verbose 205 | ref: GoString = gostring(json.dumps({"verbose": verbose})) 206 | self.library.SetVerbose(ref) 207 | 208 | @property 209 | def key_pair(self): 210 | return self._key_pair 211 | 212 | @key_pair.setter 213 | def key_pair(self, key_pair: Tuple[str, str]): 214 | # Set the cert and key pair 215 | cert, key = self._key_pair = key_pair 216 | ref: GoString = gostring(json.dumps({"cert": cert, "key": key})) 217 | self.library.SetKeyPair(ref) 218 | 219 | 220 | # Maintain a universal library instance 221 | _library: Optional[Library] = None 222 | 223 | 224 | def get_library() -> Library: 225 | global _library 226 | if _library is None: 227 | _library = Library() 228 | return _library 229 | -------------------------------------------------------------------------------- /python-bindings/hazetunnel/control.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional, Union 3 | from uuid import uuid4 4 | 5 | from .cffi import get_library 6 | 7 | """ 8 | Hazetunnel may also run in a context manager. 9 | 10 | from hazetunnel import HazeTunnel 11 | ... 12 | with HazeTunnel(port='8080', payload='alert("Hello World!");') as proxy: 13 | requests.get( 14 | url='https://tls.peet.ws/api/clean', 15 | headers=HeaderGenerator().generate(browser='chrome'), 16 | proxies={'https': proxy.url}, 17 | verify=proxy.cert 18 | ).text 19 | ... 20 | """ 21 | 22 | 23 | class HazeTunnel: 24 | def __init__( 25 | self, 26 | port: Optional[str] = None, 27 | payload: Optional[str] = None, 28 | user_agent: Optional[str] = None, 29 | upstream_proxy: Optional[str] = None, 30 | ) -> None: 31 | """ 32 | HazeTunnel constructor 33 | 34 | Parameters: 35 | port (Optional[str]): Specify a port to listen on. Default is random. 36 | payload (Optional[str]): Payload to inject into responses 37 | user_agent (Optional[str]): Override user agent 38 | upstream_proxy (Optional[str]): Optionally forward requests to an upstream proxy 39 | """ 40 | # Generate a ID 41 | self.id = str(uuid4()) 42 | 43 | # Set options 44 | self.options = { 45 | "port": port or '', 46 | "payload": payload or '', 47 | "user_agent": user_agent or '', 48 | "upstream_proxy": upstream_proxy or '', 49 | "id": self.id, 50 | } 51 | 52 | self.lib = get_library() 53 | self.is_running = False 54 | 55 | """ 56 | Start/stopping the server 57 | """ 58 | 59 | def launch(self) -> None: 60 | """ 61 | Launch the server 62 | """ 63 | # Raise error if running already 64 | if self.is_running: 65 | raise RuntimeError("Server is already running.") 66 | # Generate a port if one wasn't passed 67 | if not self.options['port']: 68 | self.options['port'] = str(self.lib.get_open_port()) 69 | 70 | self.lib.start_server(self.options) 71 | self.is_running = True 72 | 73 | def stop(self) -> None: 74 | """ 75 | Stop the server 76 | """ 77 | if not self.is_running: 78 | raise RuntimeError("Server is not running.") 79 | self.lib.stop_server(self.id) 80 | self.is_running = False 81 | 82 | """ 83 | Configuration 84 | """ 85 | 86 | @property 87 | def url(self) -> str: 88 | """ 89 | Returns the URL of the server 90 | """ 91 | # Raise error if not running 92 | if not self.is_running: 93 | raise RuntimeError("Server is not running.") 94 | return f"http://127.0.0.1:{self.options['port']}" 95 | 96 | @property 97 | def cert(self) -> str: 98 | """ 99 | Returns the path to the server's certificate 100 | """ 101 | return self.lib.key_pair[0] 102 | 103 | @cert.setter 104 | def cert(self, _) -> None: 105 | raise NotImplementedError( 106 | "Setting the certificate path is not supported. " 107 | "This must be done with the hazetunnel.set_curt(path) method" 108 | ) 109 | 110 | @property 111 | def key(self) -> str: 112 | """ 113 | Returns the path to the server's key 114 | """ 115 | return self.lib.key_pair[1] 116 | 117 | @key.setter 118 | def key(self, _) -> None: 119 | raise NotImplementedError( 120 | "Setting the certificate path is not supported. " 121 | "This must be done with the hazetunnel.set_key(path) method" 122 | ) 123 | 124 | @property 125 | def verbose(self) -> bool: 126 | """ 127 | Returns the verbosity of the server logs 128 | """ 129 | return self.lib.verbose 130 | 131 | @verbose.setter 132 | def verbose(self, option: bool) -> None: 133 | """ 134 | Set the verbosity of the logs 135 | """ 136 | self.lib.verbose = option 137 | 138 | """ 139 | Context manager methods 140 | """ 141 | 142 | def __enter__(self) -> "HazeTunnel": 143 | self.launch() 144 | return self 145 | 146 | def __exit__(self, *_) -> None: 147 | self.stop() 148 | 149 | 150 | """ 151 | Global config setters 152 | Note: These MUST be done before starting the HazeTunnel instance. 153 | """ 154 | 155 | 156 | def set_key_pair( 157 | cert_path: Optional[Union[str, Path]], key_path: Optional[Union[str, Path]] = '' 158 | ) -> None: 159 | """ 160 | Set the certificate path 161 | """ 162 | if not (cert_path or key_path): 163 | raise ValueError("Either cert and key must be set") 164 | lib = get_library() 165 | lib.key_pair = (str(cert_path), str(key_path)) 166 | 167 | 168 | def set_verbose(option: bool) -> None: 169 | """ 170 | Set the logging level to verbose 171 | """ 172 | lib = get_library() 173 | lib.verbose = option 174 | 175 | 176 | """ 177 | Global config getters 178 | """ 179 | 180 | cert = lambda: get_library().key_pair[0] 181 | key = lambda: get_library().key_pair[1] 182 | verbose = lambda: get_library().verbose 183 | -------------------------------------------------------------------------------- /python-bindings/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "hazetunnel" 7 | version = "2.0.1" 8 | description = "Mitm proxy that defends against TLS and JS worker fingerprinting." 9 | authors = ["daijro "] 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/daijro/hazetunnel" 13 | keywords = [ 14 | "tls", 15 | "golang", 16 | "networking", 17 | "proxy", 18 | "mitm", 19 | "injector", 20 | "playwright", 21 | ] 22 | classifiers = [ 23 | "Topic :: Security", 24 | "Topic :: Internet :: WWW/HTTP", 25 | "Topic :: Internet :: Proxy Servers", 26 | "Topic :: System :: Networking :: Monitoring", 27 | "Topic :: Software Development :: Testing", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | ] 30 | 31 | [tool.poetry.dependencies] 32 | python = "^3.8" 33 | click = "*" 34 | httpx = "*" 35 | 36 | [tool.poetry.scripts] 37 | hazetunnel = "hazetunnel.__main__:cli" 38 | --------------------------------------------------------------------------------