├── .github ├── FUNDING.yml ├── close_issue.py ├── dependabot.yml └── workflows │ ├── CI.yml │ ├── CloseIssue.yml │ └── Release.yml ├── .gitignore ├── .zed └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── README_zh.md ├── build.sh ├── crates ├── mitm │ ├── Cargo.toml │ └── src │ │ ├── cagen.rs │ │ ├── lib.rs │ │ └── proxy │ │ ├── ca.rs │ │ ├── error.rs │ │ ├── handler.rs │ │ ├── http_client.rs │ │ ├── mitm.rs │ │ ├── mod.rs │ │ └── sni_reader.rs ├── openai │ ├── Cargo.toml │ ├── build.rs │ ├── frontend │ │ ├── auth.htm │ │ ├── har │ │ │ ├── error.html │ │ │ ├── login.html │ │ │ ├── success.html │ │ │ └── upload.html │ │ ├── login.htm │ │ ├── service-worker.js │ │ └── static │ │ │ ├── arkose │ │ │ └── cdn │ │ │ │ └── fc │ │ │ │ └── assets │ │ │ │ └── ec-game-core │ │ │ │ └── bootstrap │ │ │ │ └── 1.18.0 │ │ │ │ └── standard │ │ │ │ ├── game_core_bootstrap.js │ │ │ │ └── sri.json │ │ │ ├── fonts │ │ │ └── colfax │ │ │ │ ├── ColfaxAIBold.woff │ │ │ │ ├── ColfaxAIBold.woff2 │ │ │ │ ├── ColfaxAIBoldItalic.woff │ │ │ │ ├── ColfaxAIBoldItalic.woff2 │ │ │ │ ├── ColfaxAIRegular.woff │ │ │ │ ├── ColfaxAIRegular.woff2 │ │ │ │ ├── ColfaxAIRegularItalic.woff │ │ │ │ └── ColfaxAIRegularItalic.woff2 │ │ │ ├── resources │ │ │ ├── apple-touch-icon.png │ │ │ ├── clipboard.min.js │ │ │ ├── corporate-ui-dashboard.css │ │ │ ├── dall-e.webp │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.png │ │ │ ├── jquery.min.js │ │ │ └── manifest.json │ │ │ ├── sweetalert2 │ │ │ ├── bulma.min.css │ │ │ └── sweetalert2.all.min-bc15590d.js │ │ │ └── ulp │ │ │ └── react-components │ │ │ └── 1.66.5 │ │ │ └── css │ │ │ └── main.cdn.min.css │ └── src │ │ ├── arkose │ │ ├── blob.rs │ │ ├── crypto.rs │ │ ├── error.rs │ │ ├── funcaptcha │ │ │ ├── breaker.rs │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ └── solver.rs │ │ ├── mod.rs │ │ └── murmur.rs │ │ ├── auth │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ └── provide │ │ │ ├── apple.rs │ │ │ ├── mod.rs │ │ │ ├── platform.rs │ │ │ └── web.rs │ │ ├── chatgpt │ │ ├── api.rs │ │ ├── mod.rs │ │ └── model │ │ │ ├── mod.rs │ │ │ ├── req.rs │ │ │ └── resp.rs │ │ ├── client.rs │ │ ├── constant.rs │ │ ├── context │ │ ├── args.rs │ │ ├── arkose │ │ │ ├── har.rs │ │ │ ├── mod.rs │ │ │ └── version.rs │ │ ├── init.rs │ │ ├── mod.rs │ │ └── preauth.rs │ │ ├── dns │ │ ├── fast.rs │ │ └── mod.rs │ │ ├── eventsource │ │ ├── error.rs │ │ ├── event_source.rs │ │ ├── mod.rs │ │ ├── reqwest_ext.rs │ │ └── retry.rs │ │ ├── gpt_model.rs │ │ ├── homedir.rs │ │ ├── lib.rs │ │ ├── log.rs │ │ ├── platform │ │ ├── mod.rs │ │ └── v1 │ │ │ ├── api.rs │ │ │ ├── endpoints │ │ │ ├── chat.rs │ │ │ ├── completions.rs │ │ │ ├── mod.rs │ │ │ └── models.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ ├── models.rs │ │ │ └── resources │ │ │ ├── chat_completion.rs │ │ │ ├── chat_completion_stream.rs │ │ │ ├── completion.rs │ │ │ ├── completion_stream.rs │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ └── shared.rs │ │ ├── proxy.rs │ │ ├── serve │ │ ├── error.rs │ │ ├── middleware │ │ │ ├── auth.rs │ │ │ ├── csrf.rs │ │ │ ├── limit.rs │ │ │ ├── mod.rs │ │ │ └── tokenbucket.rs │ │ ├── mod.rs │ │ ├── preauth.rs │ │ ├── proxy │ │ │ ├── ext.rs │ │ │ ├── mod.rs │ │ │ ├── req.rs │ │ │ ├── resp.rs │ │ │ └── toapi │ │ │ │ ├── mod.rs │ │ │ │ ├── model.rs │ │ │ │ └── stream.rs │ │ ├── puid.rs │ │ ├── router │ │ │ ├── chat │ │ │ │ ├── cookier.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── props.rs │ │ │ │ └── session │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── session.rs │ │ │ ├── files.rs │ │ │ ├── har │ │ │ │ ├── mod.rs │ │ │ │ └── token.rs │ │ │ └── mod.rs │ │ ├── signal.rs │ │ ├── turnstile.rs │ │ └── whitelist.rs │ │ ├── token │ │ ├── mod.rs │ │ └── model.rs │ │ ├── unescape.rs │ │ ├── urldecoding.rs │ │ └── uuid.rs └── self_update │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ └── src │ ├── backends │ ├── github.rs │ └── mod.rs │ ├── errors.rs │ ├── lib.rs │ ├── macros.rs │ ├── update.rs │ └── version.rs ├── doc ├── authorization.md ├── python-requests.ipynb └── rest.http ├── docker ├── Dockerfile ├── build.sh └── warp │ ├── Cargo.toml │ ├── Dockerfile │ ├── boot.sh │ └── src │ └── main.rs ├── examples ├── auth.rs ├── chatgpt.rs ├── crypto.rs ├── funcaptcha.rs ├── har.rs ├── pow.rs ├── print_image.rs └── upgrade.rs └── src ├── alloc.rs ├── args.rs ├── daemon.rs ├── inter ├── authorize │ ├── auth.rs │ ├── mod.rs │ └── oauth.rs ├── config.rs ├── context.rs ├── conversation │ ├── api.rs │ ├── chatgpt.rs │ └── mod.rs ├── dashboard.rs ├── mod.rs ├── standard.rs └── valid.rs ├── main.rs ├── parse.rs ├── store ├── account.rs ├── conf.rs └── mod.rs ├── update.rs └── utils ├── mod.rs └── unix.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://github.com/gngpp/gngpp/blob/main/SPONSOR.md#sponsor-my-open-source-works'] 14 | -------------------------------------------------------------------------------- /.github/close_issue.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | issue_labels = ['no respect'] 5 | github_repo = 'gngpp/ninja' 6 | github_token = os.getenv("GITHUB_TOKEN") 7 | headers = { 8 | 'Authorization': 'Bearer ' + github_token, 9 | 'Accept': 'application/vnd.github+json', 10 | 'X-GitHub-Api-Version': '2022-11-28', 11 | } 12 | 13 | def get_stargazers(repo): 14 | page = 1 15 | _stargazers = {} 16 | while True: 17 | queries = { 18 | 'per_page': 100, 19 | 'page': page, 20 | } 21 | url = 'https://api.github.com/repos/{}/stargazers?'.format(repo) 22 | 23 | resp = requests.get(url, headers=headers, params=queries) 24 | if resp.status_code != 200: 25 | raise Exception('Error get stargazers: ' + resp.text) 26 | 27 | data = resp.json() 28 | if not data: 29 | break 30 | 31 | for stargazer in data: 32 | _stargazers[stargazer['login']] = True 33 | page += 1 34 | 35 | print('list stargazers done, total: ' + str(len(_stargazers))) 36 | return _stargazers 37 | 38 | 39 | def get_issues(repo): 40 | page = 1 41 | _issues = [] 42 | while True: 43 | queries = { 44 | 'state': 'open', 45 | 'sort': 'created', 46 | 'direction': 'desc', 47 | 'per_page': 100, 48 | 'page': page, 49 | } 50 | url = 'https://api.github.com/repos/{}/issues?'.format(repo) 51 | 52 | resp = requests.get(url, headers=headers, params=queries) 53 | if resp.status_code != 200: 54 | raise Exception('Error get issues: ' + resp.text) 55 | 56 | data = resp.json() 57 | if not data: 58 | break 59 | 60 | _issues += data 61 | page += 1 62 | 63 | print('list issues done, total: ' + str(len(_issues))) 64 | return _issues 65 | 66 | 67 | def close_issue(repo, issue_number): 68 | url = 'https://api.github.com/repos/{}/issues/{}'.format(repo, issue_number) 69 | data = { 70 | 'state': 'closed', 71 | 'state_reason': 'not_planned', 72 | 'labels': issue_labels, 73 | } 74 | resp = requests.patch(url, headers=headers, json=data) 75 | if resp.status_code != 200: 76 | raise Exception('Error close issue: ' + resp.text) 77 | 78 | print('issue: {} closed'.format(issue_number)) 79 | 80 | 81 | def lock_issue(repo, issue_number): 82 | url = 'https://api.github.com/repos/{}/issues/{}/lock'.format(repo, issue_number) 83 | data = { 84 | 'lock_reason': 'spam', 85 | } 86 | resp = requests.put(url, headers=headers, json=data) 87 | if resp.status_code != 204: 88 | raise Exception('Error lock issue: ' + resp.text) 89 | 90 | print('issue: {} locked'.format(issue_number)) 91 | 92 | 93 | if '__main__' == __name__: 94 | stargazers = get_stargazers(github_repo) 95 | 96 | issues = get_issues(github_repo) 97 | for issue in issues: 98 | login = issue['user']['login'] 99 | if login not in stargazers: 100 | print('issue: {}, login: {} not in stargazers'.format(issue['number'], login)) 101 | close_issue(github_repo, issue['number']) 102 | lock_issue(github_repo, issue['number']) 103 | 104 | print('done') -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: CI 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | check: 15 | name: Check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: dtolnay/rust-toolchain@stable 20 | - run: cargo check --all 21 | fmt: 22 | name: Rustfmt 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: dtolnay/rust-toolchain@stable 27 | with: 28 | components: rustfmt 29 | - run: cargo fmt --all -- --check 30 | clippy: 31 | name: Clippy 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: dtolnay/rust-toolchain@stable 36 | with: 37 | components: clippy 38 | - run: cargo clippy 39 | -------------------------------------------------------------------------------- /.github/workflows/CloseIssue.yml: -------------------------------------------------------------------------------- 1 | name: CloseIssue 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | run-python-script: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.10" 15 | 16 | - name: Install Dependencies 17 | run: pip install requests 18 | 19 | - name: Run close_issue.py Script 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.CR_PAT }} 22 | run: python .github/close_issue.py 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | .idea 17 | 18 | # Added by cargo 19 | 20 | /target 21 | 22 | .ninja-access_tokens 23 | .vscode 24 | thunder-tests 25 | .env 26 | ./arkose 27 | ./ninja 28 | uploads 29 | ./vendor 30 | upgrade 31 | ca 32 | *.har 33 | har/*.har 34 | patch/.patch 35 | *.patch 36 | *.gz 37 | *.csv 38 | bx/ 39 | images/ 40 | serve.toml 41 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings 5 | { 6 | "lsp": { 7 | "rust-analyzer": { 8 | "initialization_options": { 9 | "inlayHints": { 10 | "maxLength": null, 11 | "lifetimeElisionHints": { 12 | "useParameterNames": true, 13 | "enable": "skip_trivial" 14 | }, 15 | "closureReturnTypeHints": { 16 | "enable": "always" 17 | } 18 | } 19 | } 20 | } 21 | }, 22 | "inlay_hints": { 23 | "enabled": true, 24 | "show_type_hints": true, 25 | "show_parameter_hints": true, 26 | "show_other_hints": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ninja" 3 | version = "0.9.37" 4 | edition = "2021" 5 | description = "Reverse engineered ChatGPT proxy" 6 | license = "GPL3.0" 7 | homepage = "https://github.com/gngpp/ninja" 8 | repository = "https://github.com/gngpp/ninja.git" 9 | readme = "README.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | anyhow = "1.0.75" 15 | clap = { version = "4.4.3", features = ["derive", "env"] } 16 | serde = {version = "1.0.188", features = ["derive"] } 17 | openai = { path = "./crates/openai" } 18 | mitm = { path = "./crates/mitm", optional = true } 19 | cidr = "0.2.2" 20 | toml = "0.8.0" 21 | url = "2.4.1" 22 | 23 | tokio = { version = "1.35.1", default-features = false, features = ["rt"], optional = true } 24 | serde_json = { version = "1.0.108", optional = true } 25 | inquire = { version = "0.6.2", optional = true } 26 | colored_json = { version = "4.0.0", optional = true } 27 | indicatif = { version = "0.17.7", optional = true } 28 | json_to_table = { version = "0.7.0", optional = true } 29 | tabled = { version = "0.12.2", optional = true } 30 | self_update = { path = "./crates/self_update", default-features = false, features = ["archive-tar", "compression-flate2"] } 31 | reqwest = { package = "reqwest-impersonate", version ="0.11.49", default-features = false, features = ["impersonate"] } 32 | 33 | # self update 34 | zip = { version = "0.6", default-features = false, features = ["time"], optional = true } 35 | tempfile = "3" 36 | semver = "1.0" 37 | 38 | # allocator 39 | tcmalloc = { version = "0.3.0", optional = true } 40 | snmalloc-rs = { version = "0.3.4", optional = true } 41 | rpmalloc = { version = "0.2.2", optional = true } 42 | jemallocator = { package = "tikv-jemallocator", version = "0.5.4", optional = true } 43 | mimalloc = { version = "0.1.39", default-features = false, optional = true } 44 | 45 | [target.'cfg(target_family = "unix")'.dependencies] 46 | daemonize = "0.5.0" 47 | nix = { version = "0.27.1", features = ["signal", "user", "ptrace"]} 48 | 49 | [target.'cfg(target_os = "linux")'.dependencies] 50 | sysctl = "0.5.4" 51 | 52 | [dev-dependencies] 53 | futures-util = "0.3.28" 54 | serde_json = "1.0.107" 55 | tokio = { version = "1.35.1", default-features = false, features = ["rt"] } 56 | reqwest = { package = "reqwest-impersonate", version ="0.11.49", default-features = false, features = [ 57 | "boring-tls", "impersonate","json", "cookies", "stream", "multipart", "socks", "blocking" 58 | ] } 59 | base64 = "0.21.4" 60 | viuer = "0.6.2" 61 | hex = "0.4.3" 62 | sha2 = "0.10.8" 63 | env_logger = "0.10.0" 64 | csv = "1.3.0" 65 | scraper = "0.18.1" 66 | regex = "1.10.2" 67 | redis = { version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"]} 68 | redis-macros = { version = "0.2.1"} 69 | openai = { path = "./crates/openai" } 70 | 71 | [features] 72 | default = ["serve", "mitm", "self_update"] 73 | self_update = ["dep:serde_json"] 74 | mitm = ["openai/preauth", "dep:mitm"] 75 | terminal = [ 76 | "openai/api", 77 | "dep:tokio", 78 | "dep:serde_json", 79 | "dep:inquire", 80 | "dep:colored_json", 81 | "dep:indicatif", 82 | "dep:json_to_table", 83 | "dep:tabled" 84 | ] 85 | serve = ["limit"] 86 | limit = ["openai/limit", "openai/serve"] 87 | # Enable jemalloc for binaries 88 | jemalloc = ["jemallocator"] 89 | # Enable bundled tcmalloc 90 | tcmalloc = ["tcmalloc/bundled"] 91 | # Enable snmalloc for binaries 92 | snmalloc = ["snmalloc-rs"] 93 | # Enable bundled rpmalloc 94 | rpmalloc = ["dep:rpmalloc"] 95 | # Enable mimalloc for binaries 96 | mimalloc = ["dep:mimalloc"] 97 | 98 | [[bin]] 99 | name = "ninja" 100 | path = "src/main.rs" 101 | 102 | [[example]] 103 | name = "chatgpt" 104 | required-features = ["openai/api"] 105 | 106 | [profile.release] 107 | lto = true 108 | opt-level = 'z' 109 | codegen-units = 1 110 | strip = true 111 | panic = "abort" 112 | 113 | [package.metadata.deb] 114 | maintainer = "gngpp " 115 | copyright = "2023, gngpp " 116 | license-file = ["LICENSE", "4"] 117 | extended-description = "Reverse engineered ChatGPT proxy (bypass Cloudflare 403 Access Denied)" 118 | depends = "$auto" 119 | section = "utility" 120 | priority = "optional" 121 | assets = [ 122 | ["target/release/ninja", "usr/bin/ninja", "755"], 123 | ["README.md", "usr/share/doc/ninja/README", "644"], 124 | ] 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
English | [简体中文](README_zh.md) 2 | 3 | [![CI](https://github.com/gngpp/ninja/actions/workflows/CI.yml/badge.svg)](https://github.com/gngpp/ninja/actions/workflows/CI.yml) 4 | [![CI](https://github.com/gngpp/ninja/actions/workflows/Release.yml/badge.svg)](https://github.com/gngpp/ninja/actions/workflows/Release.yml) 5 | 6 | 7 | 8 | # ninja 9 | 10 | Reverse engineered `ChatGPT` proxy 11 | 12 | > Project has ended. 13 | 14 | ### Features 15 | 16 | - API key acquisition 17 | - `Email`/`password` account authentication 18 | - Proxy `ChatGPT-API`/`OpenAI-API` 19 | - ChatGPT WebUI 20 | - Support IP proxy pool 21 | - Very small memory footprint 22 | 23 | ### Installation 24 | 25 | If you need more detailed installation and usage information, please check [wiki](https://github.com/gngpp/ninja/wiki) 26 | 27 | ### Contributing 28 | 29 | If you would like to submit your contribution, please open a [Pull Request](https://github.com/gngpp/ninja/pulls). 30 | 31 | ### Getting help 32 | 33 | Your question might already be answered on the [issues](https://github.com/gngpp/ninja/issues) 34 | 35 | ### Instructions 36 | 37 | 1. Open source projects can be modified, but please keep the original author information to avoid losing technical support. 38 | 2. Submit an issue if there are errors, bugs, etc., and I will fix them. 39 | 40 | ### License 41 | 42 | **ninja** © [gngpp](https://github.com/gngpp), Released under the [GPL-3.0](./LICENSE) License. 43 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |
简体中文 | [English](README.md) 2 | 3 | [![CI](https://github.com/gngpp/ninja/actions/workflows/CI.yml/badge.svg)](https://github.com/gngpp/ninja/actions/workflows/CI.yml) 4 | [![CI](https://github.com/gngpp/ninja/actions/workflows/Release.yml/badge.svg)](https://github.com/gngpp/ninja/actions/workflows/Release.yml) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | [![](https://img.shields.io/docker/image-size/gngpp/ninja)](https://registry.hub.docker.com/r/gngpp/ninja) 14 | [![Docker Image](https://img.shields.io/docker/pulls/gngpp/ninja.svg)](https://hub.docker.com/r/gngpp/ninja/) 15 | 16 | # ninja 17 | 18 | 逆向工程的 `ChatGPT` 代理 19 | 20 | > 项目已闭源发布。 21 | 22 | ### 特性 23 | 24 | - API密钥获取 25 | - `电子邮件`/`密码`帐户认证 26 | - 代理 `ChatGPT-API`/`OpenAI-API` 27 | - ChatGPT WebUI 28 | - 支持IP代理池 29 | - 极少的内存占用 30 | 31 | ### 安装 32 | 33 | 如果您需要更详细的安装与使用信息,请查看[wiki](https://github.com/gngpp/ninja/wiki) 34 | 35 | ### 贡献 36 | 37 | 如果想提交您的贡献,请打开[Pull Request](https://github.com/gngpp/ninja/pulls). 38 | 39 | ### 帮助 40 | 41 | 您的问题可能已在 [issues](https://github.com/gngpp/ninja/issues) 上得到解答 42 | 43 | ### 说明 44 | 45 | 1. 开源项目可以魔改,但请保留原作者信息,以免失去技术支持。 46 | 2. 报错、BUG之类的提出Issue,我会修复。 47 | 48 | ### License 49 | 50 | **ninja** © [gngpp](https://github.com/gngpp),根据 [GPL-3.0](./LICENSE) 许可证发布。 -------------------------------------------------------------------------------- /crates/mitm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | log = "0.4.20" 10 | anyhow = "1.0.75" 11 | thiserror = "1.0.48" 12 | reqwest = { package = "reqwest-impersonate", version ="0.11.49", default-features = false, features = [ 13 | "boring-tls", "impersonate", "stream", "socks" 14 | ] } 15 | typed-builder = "0.18.0" 16 | time = "0.3.30" 17 | rand = "0.8.5" 18 | moka = { version = "0.12.1", default-features = false, features = ["sync"] } 19 | tokio = { version = "1.15.0", default-features = false } 20 | rcgen = { version = "0.10", features = ["x509-parser"] } 21 | hyper = { version = "0.14.27", default-features = false } 22 | tokio-rustls = { version = "0.24.1", default-features = false, features = ["tls12"] } 23 | rustls = { version = "0.21.8", features = ["dangerous_configuration"] } 24 | wildmatch = "2.1" 25 | http = "0.2.11" 26 | pin-project = "1" 27 | byteorder = "1.4" 28 | rustls-pemfile = "1.0" -------------------------------------------------------------------------------- /crates/mitm/src/cagen.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use rcgen::Certificate; 3 | 4 | use std::fs; 5 | 6 | use crate::proxy::CertificateAuthority; 7 | 8 | pub fn gen_ca() -> Certificate { 9 | let cert = CertificateAuthority::gen_ca().expect("preauth generate cert"); 10 | let cert_crt = cert.serialize_pem().unwrap(); 11 | 12 | fs::create_dir("ca").unwrap(); 13 | 14 | println!("{}", cert_crt); 15 | if let Err(err) = fs::write("ca/cert.crt", cert_crt) { 16 | error!("cert file write failed: {}", err); 17 | } 18 | 19 | let private_key = cert.serialize_private_key_pem(); 20 | println!("{}", private_key); 21 | if let Err(err) = fs::write("ca/key.pem", private_key) { 22 | error!("private key file write failed: {}", err); 23 | } 24 | 25 | cert 26 | } 27 | -------------------------------------------------------------------------------- /crates/mitm/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cagen; 2 | pub mod proxy; 3 | 4 | use anyhow::Context; 5 | use std::{fs, net::SocketAddr, path::PathBuf}; 6 | use typed_builder::TypedBuilder; 7 | 8 | use crate::proxy::{handler::HttpHandler, CertificateAuthority}; 9 | use log::info; 10 | 11 | #[derive(TypedBuilder)] 12 | pub struct Builder { 13 | bind: SocketAddr, 14 | upstream_proxy: Option, 15 | cert: PathBuf, 16 | key: PathBuf, 17 | graceful_shutdown: tokio::sync::mpsc::Receiver<()>, 18 | cerificate_cache_size: u32, 19 | mitm_filters: Vec, 20 | handler: T, 21 | } 22 | 23 | impl Builder { 24 | pub async fn proxy(self) -> anyhow::Result<()> { 25 | info!("PreAuth CA Private key use: {}", self.key.display()); 26 | let private_key_bytes = 27 | fs::read(self.key).context("ca private key file path not valid!")?; 28 | let private_key = rustls_pemfile::pkcs8_private_keys(&mut private_key_bytes.as_slice()) 29 | .context("Failed to parse private key")?; 30 | let key = rustls::PrivateKey(private_key[0].clone()); 31 | 32 | info!("PreAuth CA Certificate use: {}", self.cert.display()); 33 | let ca_cert_bytes = fs::read(self.cert).context("ca cert file path not valid!")?; 34 | let ca_cert = rustls_pemfile::certs(&mut ca_cert_bytes.as_slice()) 35 | .context("Failed to parse CA certificate")?; 36 | let cert = rustls::Certificate(ca_cert[0].clone()); 37 | 38 | let ca = CertificateAuthority::new( 39 | key, 40 | cert, 41 | String::from_utf8(ca_cert_bytes).context("Failed to parse CA certificate")?, 42 | self.cerificate_cache_size.into(), 43 | ) 44 | .context("Failed to create Certificate Authority")?; 45 | 46 | info!("PreAuth Http MITM Proxy listen on: http://{}", self.bind); 47 | 48 | let proxy = proxy::Proxy::builder() 49 | .ca(ca.clone()) 50 | .listen_addr(self.bind) 51 | .upstream_proxy(self.upstream_proxy) 52 | .mitm_filters(self.mitm_filters) 53 | .handler(self.handler) 54 | .graceful_shutdown(self.graceful_shutdown) 55 | .build(); 56 | 57 | tokio::spawn(proxy.start_proxy()); 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/mitm/src/proxy/error.rs: -------------------------------------------------------------------------------- 1 | use rcgen::RcgenError; 2 | use std::io; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("invalid CA")] 8 | Tls(#[from] RcgenError), 9 | #[error("network error")] 10 | HyperError(#[from] hyper::Error), 11 | #[error("body error")] 12 | BodyErrpr(#[from] http::Error), 13 | #[error("request connect error")] 14 | RequestConnectError(#[from] reqwest::Error), 15 | #[error("IO error")] 16 | IO(#[from] io::Error), 17 | } 18 | -------------------------------------------------------------------------------- /crates/mitm/src/proxy/handler.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Request, Response}; 2 | use std::sync::{Arc, RwLock}; 3 | use wildmatch::WildMatch; 4 | 5 | use super::mitm::RequestOrResponse; 6 | 7 | pub trait HttpHandler: Clone + Send + Sync + 'static { 8 | fn handle_request(&self, req: Request) -> RequestOrResponse { 9 | RequestOrResponse::Request(req) 10 | } 11 | 12 | fn handle_response(&self, res: Response) -> Response { 13 | res 14 | } 15 | } 16 | 17 | #[derive(Clone, Default)] 18 | pub struct MitmFilter { 19 | filters: Arc>>, 20 | } 21 | 22 | impl MitmFilter { 23 | pub fn new(filters: Vec) -> Self { 24 | let filters = filters.iter().map(|f| WildMatch::new(f)).collect(); 25 | Self { 26 | filters: Arc::new(RwLock::new(filters)), 27 | ..Default::default() 28 | } 29 | } 30 | 31 | pub async fn filter_req(&self, req: &Request) -> bool { 32 | let host = req.uri().host().unwrap_or_default(); 33 | let list = self.filters.read().unwrap(); 34 | for m in list.iter() { 35 | if m.matches(host) { 36 | return true; 37 | } 38 | } 39 | false 40 | } 41 | 42 | pub async fn filter(&self, host: &str) -> bool { 43 | let list = self.filters.read().unwrap(); 44 | for m in list.iter() { 45 | if m.matches(host) { 46 | return true; 47 | } 48 | } 49 | false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/mitm/src/proxy/http_client.rs: -------------------------------------------------------------------------------- 1 | use http::{response::Builder, Request, Response}; 2 | use hyper::{body, Body}; 3 | use reqwest::impersonate::Impersonate; 4 | 5 | use super::error::Error; 6 | 7 | #[derive(Clone)] 8 | pub struct HttpClient { 9 | inner: reqwest::Client, 10 | } 11 | 12 | impl HttpClient { 13 | pub fn new(proxy: Option) -> Self { 14 | let mut builder = reqwest::Client::builder(); 15 | if let Some(p) = proxy { 16 | builder = builder.proxy(reqwest::Proxy::all(p).expect("faild build proxy")); 17 | } 18 | let inner = builder 19 | .impersonate(Impersonate::Chrome99) 20 | .http1_title_case_headers() 21 | .danger_accept_invalid_certs(true) 22 | .build() 23 | .expect("faild build reqwest client"); 24 | Self { inner } 25 | } 26 | 27 | pub(super) async fn request(&self, req: Request) -> Result, Error> { 28 | let (method, url) = (req.method().clone(), req.uri().to_string()); 29 | let (parts, body) = req.into_parts(); 30 | let resp = self 31 | .inner 32 | .clone() 33 | .request(method, url) 34 | .headers(parts.headers) 35 | .version(parts.version) 36 | .body(body::to_bytes(body).await?) 37 | .send() 38 | .await?; 39 | 40 | let mut builder = Builder::new() 41 | .status(resp.status()) 42 | .version(resp.version()) 43 | .extension(parts.extensions); 44 | 45 | builder 46 | .headers_mut() 47 | .map(|h| h.extend(resp.headers().clone())); 48 | 49 | Ok(builder.body(body::Body::wrap_stream(resp.bytes_stream()))?) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/mitm/src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | use error::Error; 2 | use handler::{HttpHandler, MitmFilter}; 3 | use mitm::MitmProxy; 4 | use std::{net::SocketAddr, sync::Arc}; 5 | use tokio::net::TcpListener; 6 | use typed_builder::TypedBuilder; 7 | 8 | pub use ca::CertificateAuthority; 9 | pub use hyper; 10 | pub use rcgen; 11 | pub use tokio_rustls; 12 | 13 | use crate::info; 14 | 15 | use self::http_client::HttpClient; 16 | 17 | mod ca; 18 | mod error; 19 | pub mod handler; 20 | mod http_client; 21 | pub mod mitm; 22 | mod sni_reader; 23 | 24 | #[derive(TypedBuilder)] 25 | pub struct Proxy 26 | where 27 | H: HttpHandler, 28 | { 29 | /// The address to listen on. 30 | pub listen_addr: SocketAddr, 31 | /// A future that once resolved will cause the proxy server to shut down. 32 | /// The certificate authority to use. 33 | pub ca: CertificateAuthority, 34 | pub upstream_proxy: Option, 35 | pub mitm_filters: Vec, 36 | pub handler: H, 37 | graceful_shutdown: tokio::sync::mpsc::Receiver<()>, 38 | } 39 | 40 | impl Proxy 41 | where 42 | H: HttpHandler, 43 | { 44 | pub async fn start_proxy(mut self) -> Result<(), Error> { 45 | let client = HttpClient::new(self.upstream_proxy); 46 | let ca = Arc::new(self.ca); 47 | let http_handler = Arc::new(self.handler); 48 | let mitm_filter = Arc::new(MitmFilter::new(self.mitm_filters)); 49 | 50 | let tcp_listener = TcpListener::bind(self.listen_addr).await?; 51 | loop { 52 | let client = client.clone(); 53 | let ca = Arc::clone(&ca); 54 | let http_handler = Arc::clone(&http_handler); 55 | let mitm_filter = Arc::clone(&mitm_filter); 56 | 57 | tokio::select! { 58 | _ = self.graceful_shutdown.recv() => { 59 | info!("PreAuth Http MITM Proxy shutdown"); 60 | return Ok(()); 61 | } 62 | 63 | Ok((tcp_stream, _)) = tcp_listener.accept() => { 64 | tokio::spawn(async move { 65 | let mitm_proxy = MitmProxy { 66 | ca: ca.clone(), 67 | client: client.clone(), 68 | http_handler: Arc::clone(&http_handler), 69 | mitm_filter: Arc::clone(&mitm_filter), 70 | }; 71 | 72 | let mut tls_content_type = [0; 1]; 73 | if tcp_stream.peek(&mut tls_content_type).await.is_ok() { 74 | if tls_content_type[0] <= 0x40 { 75 | // ASCII < 'A', assuming tls 76 | mitm_proxy.serve_tls(tcp_stream).await; 77 | } else { 78 | // assuming http 79 | _ = mitm_proxy.serve_stream(tcp_stream).await; 80 | } 81 | } 82 | }); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/openai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openai" 3 | version = "0.9.28" 4 | edition = "2021" 5 | rust-version = "1.75.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1.0.75" 11 | thiserror = "1.0.48" 12 | reqwest = { package = "reqwest-impersonate", version ="0.11.49", default-features = false, features = [ 13 | "boring-tls", "impersonate","json", "cookies", "stream", "multipart", "socks" 14 | ] } 15 | hyper = { package = "hyper_imp", version = "0.14.29", default-features = false, features = [ 16 | "client", 17 | ] } 18 | trust-dns-resolver = { version = "0.23.2", default-features = false, features = ["system-config", "tokio-runtime"] } 19 | tokio = { version = "1.35.1", features = ["fs", "sync", "signal", "rt-multi-thread"] } 20 | serde_json = "1.0.107" 21 | serde = {version = "1.0.188", features = ["derive"] } 22 | regex = "1.9.5" 23 | url = { version = "2.5.0", features = ["serde"] } 24 | base64 = "0.21.4" 25 | rand = "0.8.5" 26 | typed-builder = "0.18.0" 27 | jsonwebtokens = "1.2.0" 28 | sha2 = "0.10.7" 29 | futures-core = { version = "0.3.28", optional = true} 30 | tera = { version = "1.19.1", default-features = false, optional = true } 31 | hotwatch = "0.5.0" 32 | moka = { version = "0.12.1", default-features = false, features = ["sync"], optional = true } 33 | cidr = { version = "0.2.2", features = ["serde"] } 34 | 35 | # native db 36 | native_db = { package = "native_db-32bit", version = "0.5.3" } 37 | native_model = "0.4.6" 38 | 39 | # stream 40 | tokio-util = { version = "0.7.8", features = ["codec"], optional = true } 41 | tokio-stream = { version = "0.1.14", optional = true } 42 | futures = { version = "0.3.28", optional = true } 43 | eventsource-stream = { version = "0.2.3", optional = true } 44 | pin-project-lite = { version = "0.2.13", optional = true } 45 | nom = { version = "7.1.3", optional = true } 46 | mime = { version = "0.3.17", optional = true } 47 | futures-timer = { version = "3.0.2", optional = true } 48 | 49 | # mitm 50 | mitm = { path = "../mitm", optional = true } 51 | 52 | # arkose 53 | aes = "0.8.3" 54 | md5 = "0.7.0" 55 | cbc = "0.1.2" 56 | rand_distr = "0.4.3" 57 | 58 | # axum 59 | axum = { version = "0.6.20", features = ["http2", "multipart", "headers"], optional = true } 60 | axum-extra ={ version = "0.8.0", features = ["cookie"], optional = true } 61 | axum-server = { version = "0.5.1", features = ["tls-rustls"], optional = true } 62 | tower-http = { version = "0.4.4", default-features = false, features = ["fs", "cors", "trace", "map-request-body", "util"], optional = true } 63 | tower = { version = "0.4.13", default-features = false, features = ["limit", "timeout"], optional = true} 64 | bytes = { version = "1.5.0", optional = true } 65 | time = { version = "0.3.30", optional = true } 66 | static-files = { version = "0.2.3", optional = true } 67 | tracing = { version = "0.1.40", optional = true } 68 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"], optional = true } 69 | async-stream = { version = "0.3.5", optional = true } 70 | axum_csrf = { version = "0.8.0", features = ["layer"], optional = true } 71 | serde_urlencoded = { version = "0.7.1", optional = true } 72 | trait-variant = "0.1.1" 73 | 74 | [target.'cfg(target_family = "unix")'.dependencies] 75 | nix = { version = "0.27.1", default-features = false, features = ["user"] } 76 | 77 | 78 | [target.'cfg(windows)'.dependencies.windows-sys] 79 | version = "0.48.0" 80 | default-features = false 81 | features = ["Win32_System_Com_CallObj", "Win32_Foundation", "Win32_Globalization", "Win32_UI_Shell_Common"] 82 | 83 | [build-dependencies] 84 | static-files = "0.2.3" 85 | 86 | [features] 87 | default = ["serve", "limit", "template", "preauth"] 88 | api = ["stream"] 89 | serve = ["dep:serde_urlencoded", "dep:axum_csrf", "stream", "dep:async-stream", "dep:tracing", "dep:tracing-subscriber", "dep:tower-http", "dep:tower", "dep:bytes", "dep:time", "dep:axum-server", "dep:axum-extra", "dep:axum", "dep:static-files", "dep:futures-core", "dep:tera"] 90 | preauth = ["dep:mitm"] 91 | stream = ["dep:tokio-util", "dep:futures", "dep:tokio-stream", "dep:eventsource-stream", "dep:futures-core", "dep:pin-project-lite", "dep:nom", "dep:mime", "dep:futures-timer"] 92 | remote-token = [] 93 | limit = ["dep:moka"] 94 | template = [] 95 | 96 | [lib] 97 | name = "openai" 98 | path = "src/lib.rs" 99 | 100 | [profile.dev] 101 | opt-level = 'z' 102 | 103 | [profile.release] 104 | lto = true 105 | opt-level = 'z' 106 | codegen-units = 1 107 | panic = "abort" 108 | strip = true 109 | -------------------------------------------------------------------------------- /crates/openai/build.rs: -------------------------------------------------------------------------------- 1 | use static_files::resource_dir; 2 | 3 | fn main() -> std::io::Result<()> { 4 | resource_dir("./frontend/static").build() 5 | } 6 | -------------------------------------------------------------------------------- /crates/openai/frontend/har/error.html: -------------------------------------------------------------------------------- 1 | Error
{{.title}}

{{.error}}

-------------------------------------------------------------------------------- /crates/openai/frontend/har/success.html: -------------------------------------------------------------------------------- 1 | Success
{{.title}}

{{.success}}

-------------------------------------------------------------------------------- /crates/openai/frontend/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("fetch",(event)=>{const url=new URL(event.request.url);if(url.protocol!=='http:'&&url.protocol!=='https:'){event.respondWith(fetch(event.request));return}event.respondWith(caches.match(event.request).then((response)=>{if(response){return response}if(/\.(html|css|js|woff|woff2|png|jpg)$/.test(event.request.url)){return caches.open("ninja-cache").then((cache)=>{return fetch(event.request).then((networkResponse)=>{cache.put(event.request,networkResponse.clone());return networkResponse})})}return fetch(event.request)}),)}); 2 | -------------------------------------------------------------------------------- /crates/openai/frontend/static/arkose/cdn/fc/assets/ec-game-core/bootstrap/1.18.0/standard/sri.json: -------------------------------------------------------------------------------- 1 | {"game_core_bootstrap.js":"sha384-6YfRrK+HT8C+OXv1Su9lyivH6T5qMbaBEPQTebJbBu4S7WbMsnRittQJgNn3KFsq"} -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIBold.woff -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIBold.woff2 -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIBoldItalic.woff -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIBoldItalic.woff2 -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIRegular.woff -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIRegular.woff2 -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIRegularItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIRegularItalic.woff -------------------------------------------------------------------------------- /crates/openai/frontend/static/fonts/colfax/ColfaxAIRegularItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/fonts/colfax/ColfaxAIRegularItalic.woff2 -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/dall-e.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/resources/dall-e.webp -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/resources/favicon-16x16.png -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/resources/favicon-32x32.png -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x676e67/ninja/2aa7bdebe813fd4195d4b2dc9539061da812a812/crates/openai/frontend/static/resources/favicon.png -------------------------------------------------------------------------------- /crates/openai/frontend/static/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT", 3 | "short_name": "ChatGPT", 4 | "description": "ChatGPT", 5 | "icons": [ 6 | { 7 | "src": "/resources/apple-touch-icon.png", 8 | "type": "image/png" 9 | } 10 | ], 11 | "id": "/", 12 | "start_url": "/", 13 | "scope": "/", 14 | "display": "standalone", 15 | "orientation":"portrait", 16 | "background_color": "#343540", 17 | "theme_color": "#343540" 18 | } 19 | -------------------------------------------------------------------------------- /crates/openai/src/arkose/blob.rs: -------------------------------------------------------------------------------- 1 | use super::Type; 2 | use crate::with_context; 3 | use serde::Deserialize; 4 | 5 | /// Get arkose blob payload 6 | pub async fn get_blob(typed: Type, identifier: Option) -> anyhow::Result> { 7 | match (typed, identifier) { 8 | (Type::GPT4, Some(identifier)) => { 9 | #[derive(Deserialize)] 10 | struct Blob { 11 | data: String, 12 | } 13 | let resp = with_context!(arkose_client) 14 | .post("https://chat.openai.com/backend-api/sentinel/arkose/dx") 15 | .bearer_auth(identifier) 16 | .send() 17 | .await? 18 | .error_for_status()? 19 | .json::() 20 | .await?; 21 | Ok(Some(resp.data)) 22 | } 23 | (Type::SignUp, Some(identifier)) => Ok(Some(identifier)), 24 | _ => Ok(None), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/openai/src/arkose/error.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum ArkoseError { 5 | /// Anyhow error 6 | #[error("{0}")] 7 | AnyhowError(#[from] anyhow::Error), 8 | 9 | #[error("Submit funcaptcha answer error ({0:?})")] 10 | SubmitAnswerError(anyhow::Error), 11 | #[error("Invalid arkose platform type ({0})")] 12 | InvalidPlatformType(String), 13 | #[error("Invalid public key ({0})")] 14 | InvalidPublicKey(String), 15 | #[error("No solver available or solver is invalid")] 16 | NoSolverAvailable, 17 | #[error("Solver task error: {0}")] 18 | SolverTaskError(String), 19 | #[error("Error creating arkose session error ({0:?})")] 20 | CreateSessionError(anyhow::Error), 21 | #[error("Invalid funcaptcha error")] 22 | InvalidFunCaptcha, 23 | #[error("Hex decode error")] 24 | HexDecodeError, 25 | #[error("Unsupported hash algorithm")] 26 | UnsupportedHashAlgorithm, 27 | #[error("Unable to find har related request entry")] 28 | HarEntryNotFound, 29 | #[error("Invalid HAR file")] 30 | InvalidHarFile, 31 | #[error("{0} not a file")] 32 | NotAFile(String), 33 | #[error("Failed to get HAR entry error ({0:?})")] 34 | FailedToGetHarEntry(Arc), 35 | 36 | /// Deserialize error 37 | #[error("Deserialize error {0:?}")] 38 | DeserializeError(reqwest::Error), 39 | 40 | /// Base64 decode error 41 | #[error("Base64 decode error {0:?}")] 42 | Base64DecodeError(#[from] base64::DecodeError), 43 | 44 | /// Serialize error 45 | #[error("Serialize error {0:?}")] 46 | SerializeError(#[from] serde_urlencoded::ser::Error), 47 | 48 | #[error("Serialize error ({0:?})")] 49 | SerializeError2(#[from] serde_json::Error), 50 | 51 | /// Funcaptcha error 52 | #[error("Funcaptcha submit error ({0})")] 53 | FuncaptchaSubmitError(String), 54 | #[error("Funcaptcha not solved error ({0})")] 55 | FuncaptchaNotSolvedError(String), 56 | #[error("Unknown game type ({0})")] 57 | UnknownGameType(u32), 58 | #[error("Unknown challenge type key: ({0})")] 59 | UnknownChallengeTypeKey(String), 60 | #[error("Unknow challenge")] 61 | UnknownChallenge, 62 | #[error("Invalid arkose token ({0})")] 63 | InvalidArkoseToken(String), 64 | #[error("Faield to get tguess ({0})")] 65 | FaieldTGuess(reqwest::Error), 66 | 67 | #[error("Arkose version not found")] 68 | ArkoseVersionNotFound, 69 | 70 | /// Header parse error 71 | #[error("Invalid header ({0})")] 72 | InvalidHeader(#[from] reqwest::header::InvalidHeaderValue), 73 | 74 | /// Request error 75 | #[error("Arkose request error ({0})")] 76 | RequestError(#[from] reqwest::Error), 77 | } 78 | -------------------------------------------------------------------------------- /crates/openai/src/arkose/funcaptcha/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Serialize)] 5 | pub(super) struct RequestChallenge<'a> { 6 | pub sid: &'a str, 7 | pub token: &'a str, 8 | pub analytics_tier: i32, 9 | pub render_type: &'a str, 10 | pub lang: &'a str, 11 | #[serde(rename = "isAudioGame")] 12 | pub is_audio_game: bool, 13 | #[serde(rename = "apiBreakerVersion")] 14 | pub api_breaker_version: &'a str, 15 | } 16 | 17 | #[derive(Debug, Deserialize, Default)] 18 | #[serde(default)] 19 | pub(super) struct Challenge { 20 | pub session_token: String, 21 | #[serde(rename = "challengeID")] 22 | pub challenge_id: String, 23 | pub game_data: GameData, 24 | pub string_table: HashMap, 25 | pub dapib_url: Option, 26 | } 27 | 28 | #[derive(Debug, Deserialize, Default)] 29 | #[serde(default)] 30 | pub(super) struct GameData { 31 | #[serde(rename = "gameType")] 32 | pub game_type: i32, 33 | pub game_variant: String, 34 | pub instruction_string: String, 35 | #[serde(rename = "customGUI")] 36 | pub custom_gui: CustomGUI, 37 | } 38 | 39 | #[derive(Debug, Deserialize, Default)] 40 | #[serde(default)] 41 | pub(super) struct CustomGUI { 42 | #[serde(rename = "_challenge_imgs")] 43 | pub challenge_imgs: Vec, 44 | pub api_breaker: ApiBreaker, 45 | pub api_breaker_v2_enabled: isize, 46 | } 47 | 48 | #[derive(Debug, Deserialize, Default)] 49 | pub(super) struct ApiBreaker { 50 | pub key: String, 51 | pub value: Vec, 52 | } 53 | 54 | #[derive(Default)] 55 | #[allow(dead_code)] 56 | pub(super) struct ConciseChallenge { 57 | pub game_type: &'static str, 58 | pub urls: Vec, 59 | pub instructions: String, 60 | pub game_variant: String, 61 | } 62 | 63 | #[derive(Debug, Clone)] 64 | pub struct FunCaptcha { 65 | pub image: String, 66 | pub instructions: String, 67 | pub game_variant: String, 68 | } 69 | 70 | #[derive(Serialize, Deserialize)] 71 | pub(super) struct SubmitChallenge<'a> { 72 | pub session_token: &'a str, 73 | pub sid: &'a str, 74 | pub game_token: &'a str, 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub tguess: Option, 77 | pub guess: String, 78 | pub render_type: &'static str, 79 | pub analytics_tier: i32, 80 | pub bio: &'static str, 81 | } 82 | 83 | #[derive(Serialize)] 84 | pub(super) struct TGuess<'a> { 85 | pub session_token: &'a str, 86 | pub dapib_url: &'a str, 87 | pub guess: Vec, 88 | } 89 | 90 | #[derive(Deserialize)] 91 | pub(super) struct TGuessResp { 92 | pub tguess: Vec>, 93 | } 94 | -------------------------------------------------------------------------------- /crates/openai/src/auth/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(thiserror::Error, Debug)] 2 | pub enum AuthError { 3 | /// Request Error 4 | #[error(transparent)] 5 | FailedRequest(#[from] reqwest::Error), 6 | #[error("Bad request (error {0})")] 7 | BadRequest(String), 8 | #[error("Too many requests ({0})")] 9 | TooManyRequests(String), 10 | #[error("Unauthorized request ({0})")] 11 | Unauthorized(String), 12 | #[error("Forbidden request ({0})")] 13 | Forbidden(String), 14 | #[error("Server error ({0})")] 15 | ServerError(reqwest::Error), 16 | 17 | /// Failed Error 18 | #[error("Failed login, it may be an IP or speed limit issue, please try again")] 19 | FailedLogin, 20 | #[error("Failed to get access token (error {0:?})")] 21 | FailedAccessToken(String), 22 | #[error("Failed get code from callback url")] 23 | FailedCallbackCode, 24 | #[error("Failed callback url")] 25 | FailedCallbackURL, 26 | #[error("Failed to get authorized url")] 27 | FailedAuthorizedUrl, 28 | #[error("Failed to get state")] 29 | FailedState, 30 | #[error("Failed get csrf token")] 31 | FailedCsrfToken, 32 | #[error("Failed to get auth session cookie")] 33 | FailedAuthSessionCookie, 34 | 35 | /// Invalid Error 36 | #[error("Invalid login ({0})")] 37 | InvalidLogin(String), 38 | #[error("Invalid arkose token ({0:?})")] 39 | InvalidArkoseToken(anyhow::Error), 40 | #[error("Invalid request login url ({0:?})")] 41 | InvalidLoginUrl(url::ParseError), 42 | #[error("Invalid email or password")] 43 | InvalidEmailOrPassword, 44 | #[error("Invalid request ({0})")] 45 | InvalidRequest(String), 46 | #[error("Invalid email")] 47 | InvalidEmail, 48 | #[error("Invalid Location")] 49 | InvalidLocation, 50 | #[error("Invalid refresh token")] 51 | InvalidRefreshToken, 52 | #[error("Accidentally jumped back to the login homepage, please try again.")] 53 | InvalidLocationPath, 54 | #[error("MFA failed")] 55 | MFAFailed, 56 | #[error("MFA required")] 57 | MFARequired, 58 | #[error("Json deserialize error ({0:?})")] 59 | DeserializeError(reqwest::Error), 60 | #[error("Implementation is not supported")] 61 | NotSupportedImplementation, 62 | #[error("Failed to get preauth cookie")] 63 | PreauthCookieNotFound, 64 | 65 | /// Other Error 66 | #[error("Regex error ({0:?})")] 67 | InvalidRegex(regex::Error), 68 | } 69 | -------------------------------------------------------------------------------- /crates/openai/src/chatgpt/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "api")] 2 | pub mod api; 3 | pub mod model; 4 | -------------------------------------------------------------------------------- /crates/openai/src/chatgpt/model/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typed_builder::TypedBuilder; 3 | pub mod req; 4 | pub mod resp; 5 | 6 | /// A role of a message sender, can be: 7 | /// - `System`, for starting system message, that sets the tone of model 8 | /// - `Assistant`, for messages sent by ChatGPT 9 | /// - `User`, for messages sent by user 10 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize, Eq, Ord)] 11 | #[serde(rename_all = "lowercase")] 12 | pub enum Role { 13 | /// A system message, automatically sent at the start to set the tone of the model 14 | System, 15 | /// A message sent by ChatGPT 16 | Assistant, 17 | /// A message sent by the user 18 | User, 19 | /// A system message 20 | Critic, 21 | } 22 | 23 | impl ToString for Role { 24 | fn to_string(&self) -> String { 25 | match self { 26 | Role::System => "system".to_string(), 27 | Role::Assistant => "assistant".to_string(), 28 | Role::User => "user".to_string(), 29 | Role::Critic => "critic".to_string(), 30 | } 31 | } 32 | } 33 | 34 | #[derive(Serialize, Deserialize, TypedBuilder, Debug)] 35 | pub struct Author { 36 | pub role: Role, 37 | } 38 | -------------------------------------------------------------------------------- /crates/openai/src/constant.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const EMPTY: &str = ""; 2 | pub(crate) const NULL: &str = "null"; 3 | 4 | /// The default API endpoint to use. 5 | pub(crate) const AUTH_KEY: &str = "auth_key"; 6 | pub(crate) const SUPPORT_APPLE: &str = "support_apple"; 7 | pub(crate) const ERROR: &str = "error"; 8 | pub(crate) const SITE_KEY: &str = "site_key"; 9 | pub(crate) const ARKOSE_ENDPOINT: &str = "arkose_endpoint"; 10 | pub(crate) const CSRF_TOKEN: &str = "csrf_token"; 11 | pub(crate) const USERNAME: &str = "username"; 12 | pub(crate) const PICTURE: &str = "picture"; 13 | 14 | /// Authentication 15 | pub(crate) const API_AUTH_SESSION_COOKIE_KEY: &str = "__Secure-next-auth.session-token"; 16 | 17 | /// Serve 18 | pub(crate) const PUID: &str = "_puid"; 19 | pub(crate) const CF_CLEARANCE: &str = "cf_clearance"; 20 | pub(crate) const MODEL: &str = "model"; 21 | pub(crate) const ARKOSE_TOKEN: &str = "arkose_token"; 22 | pub(crate) const NINJA_VERSION: &str = "ninja-version"; 23 | -------------------------------------------------------------------------------- /crates/openai/src/context/arkose/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod har; 2 | pub mod version; 3 | 4 | use self::version::ArkoseVersion; 5 | use crate::arkose::Type; 6 | use crate::homedir::home_dir; 7 | use moka::sync::Cache; 8 | use native_db::{Database, DatabaseBuilder}; 9 | use std::path::PathBuf; 10 | use std::sync::{Arc, OnceLock}; 11 | use std::time::Duration; 12 | use tokio::time::interval; 13 | use tracing::{info, warn}; 14 | 15 | use super::WORKER_DIR; 16 | 17 | const INTERVAL_SECONDS: u16 = 3600; 18 | static DATABASE_BUILDER: OnceLock = OnceLock::new(); 19 | 20 | pub struct ArkoseVersionContext<'a> { 21 | db: Database<'a>, 22 | cache: Cache>, 23 | } 24 | 25 | impl ArkoseVersionContext<'_> { 26 | /// Create a new ArkoseContext 27 | pub(crate) fn new() -> Self { 28 | let builder = DATABASE_BUILDER.get_or_init(|| { 29 | let mut builder = DatabaseBuilder::new(); 30 | builder 31 | .define::() 32 | .expect("define table failed"); 33 | builder 34 | }); 35 | 36 | let path = home_dir() 37 | .unwrap_or(PathBuf::new()) 38 | .join(WORKER_DIR) 39 | .join("arkose.db"); 40 | 41 | if let Some(p) = path.parent() { 42 | // If parent directory does not exist, create it 43 | if !p.exists() { 44 | std::fs::create_dir_all(p) 45 | .expect(&format!("Failed to create directory: {}", p.display())); 46 | } 47 | } 48 | 49 | let db = builder 50 | .create(path) 51 | .expect("Failed to create arkose database"); 52 | 53 | Self { 54 | db, 55 | cache: Cache::builder() 56 | .time_to_live(Duration::from_secs(INTERVAL_SECONDS.into())) 57 | .max_capacity(5) 58 | .build(), 59 | } 60 | } 61 | 62 | /// Get the latest version of the given type 63 | pub fn version(&self, version_type: Type) -> Option> { 64 | // Begin read transaction 65 | if let Ok(r) = self.db.r_transaction() { 66 | if let Some(Some(version)) = r.get().primary::(version_type.pk()).ok() { 67 | return Some(self.cache.get_with(version_type, || Arc::new(version))); 68 | } 69 | } 70 | 71 | None 72 | } 73 | 74 | /// Run a periodic task to upgrade the arkose version 75 | pub async fn periodic_upgrade(&self) { 76 | info!("Arkose Periodic task is running"); 77 | let mut interval = interval(Duration::from_secs(INTERVAL_SECONDS.into())); 78 | loop { 79 | interval.tick().await; 80 | self.upgrade().await; 81 | } 82 | } 83 | 84 | /// Upgrade the arkose version 85 | async fn upgrade(&self) { 86 | // Auth 87 | self.insert_version(Type::Auth).await; 88 | // GPT-4 89 | self.insert_version(Type::GPT4).await; 90 | // GPT-3.5 91 | self.insert_version(Type::GPT3).await; 92 | // Platform 93 | self.insert_version(Type::Platform).await; 94 | // SignUp 95 | self.insert_version(Type::SignUp).await; 96 | 97 | if let Some(v) = self.version(Type::Auth) { 98 | info!("Arkose version: {}", v.version()); 99 | } 100 | } 101 | 102 | async fn insert_version(&self, version_type: Type) { 103 | match version::latest_arkose_version(version_type).await { 104 | Ok(version) => { 105 | if let Ok(rw) = self.db.rw_transaction() { 106 | if let Some(err) = rw.insert(version).err() { 107 | warn!("Failed to insert arkose version: {}", err) 108 | } 109 | if let Some(err) = rw.commit().err() { 110 | warn!("Failed to commit transaction: {}", err) 111 | } 112 | } 113 | } 114 | Err(err) => { 115 | warn!("Failed to get latest arkose version: {}", err) 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/openai/src/context/arkose/version.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use anyhow::Context; 4 | use anyhow::Result; 5 | use native_db::*; 6 | use native_model::{native_model, Model}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{arkose::Type, with_context}; 10 | 11 | static RE: OnceLock = OnceLock::new(); 12 | static RE_VERSION: OnceLock = OnceLock::new(); 13 | 14 | #[native_db] 15 | #[native_model(id = 1, version = 1)] 16 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 17 | pub struct ArkoseVersion { 18 | #[primary_key] 19 | pk: String, 20 | version: String, 21 | ref_enforcement_js: String, 22 | ref_enforcement_html: String, 23 | } 24 | 25 | impl ArkoseVersion { 26 | pub fn version(&self) -> &str { 27 | &self.version 28 | } 29 | 30 | pub fn ref_enforcement_js(&self) -> &str { 31 | &self.ref_enforcement_js 32 | } 33 | 34 | pub fn ref_enforcement_html(&self) -> &str { 35 | &self.ref_enforcement_html 36 | } 37 | 38 | pub fn pk(&self) -> &str { 39 | &self.pk 40 | } 41 | } 42 | 43 | pub(super) async fn latest_arkose_version(typed: Type) -> Result { 44 | let client = with_context!(api_client); 45 | // Response content 46 | let content = client 47 | .get(format!("{}/v2/{}/api.js", typed.origin_url(), typed.pk())) 48 | .send() 49 | .await? 50 | .text() 51 | .await?; 52 | 53 | // Regex to find the enforcement.html file 54 | let re = RE.get_or_init(|| { 55 | regex::Regex::new(r#"file:"([^"]*/enforcement\.[^"]*\.html)""#).expect("Invalid regex") 56 | }); 57 | 58 | let ref_cap = re.captures(&content).context("No match found")?; 59 | let ref_html = ref_cap.get(1).context("No match found")?; 60 | let ref_js = ref_html.as_str().replace(".html", ".js"); 61 | 62 | // Regex to find the version 63 | let re_version = RE_VERSION.get_or_init(|| { 64 | regex::Regex::new(r#"([^/]*)/enforcement\.[^"]*\.html"#).expect("Invalid regex") 65 | }); 66 | 67 | let version_cap = re_version 68 | .captures(ref_html.as_str()) 69 | .context("No match found")?; 70 | 71 | let ref_enforcement_js = format!("/v2/{}", ref_js); 72 | let ref_enforcement_html = format!("/v2/{}", ref_html.as_str()); 73 | 74 | Ok(ArkoseVersion { 75 | pk: typed.pk().to_owned(), 76 | version: version_cap 77 | .get(1) 78 | .context("No match found")? 79 | .as_str() 80 | .to_string(), 81 | ref_enforcement_js, 82 | ref_enforcement_html, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /crates/openai/src/context/init.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | args::Args, 3 | arkose::{ 4 | har::{HarProvider, HAR}, 5 | ArkoseVersionContext, 6 | }, 7 | preauth::PreauthCookieProvider, 8 | CfTurnstile, Context, CTX, 9 | }; 10 | use crate::{arkose, client::ClientRoundRobinBalancer, error}; 11 | use std::{collections::HashMap, sync::RwLock}; 12 | 13 | /// Use Once to guarantee initialization only once 14 | pub fn init(args: Args) { 15 | if let Some(_) = CTX.set(init_context(args.clone())).err() { 16 | error!("Failed to initialize context"); 17 | }; 18 | 19 | if let Some(_) = HAR.set(RwLock::new(init_har_provider(args))).err() { 20 | error!("Failed to initialize har provider"); 21 | }; 22 | } 23 | 24 | /// Get the program context 25 | pub fn instance() -> &'static Context { 26 | CTX.get_or_init(|| init_context(Args::builder().build())) 27 | } 28 | 29 | /// Init the program context 30 | fn init_context(args: Args) -> Context { 31 | Context { 32 | api_client: ClientRoundRobinBalancer::new_client(&args) 33 | .expect("Failed to initialize the requesting client"), 34 | auth_client: ClientRoundRobinBalancer::new_auth_client(&args) 35 | .expect("Failed to initialize the requesting oauth client"), 36 | arkose_client: ClientRoundRobinBalancer::new_arkose_client(&args) 37 | .expect("Failed to initialize the requesting arkose client"), 38 | preauth_provider: args.pbind.is_some().then(|| PreauthCookieProvider::new()), 39 | arkose_endpoint: args.arkose_endpoint, 40 | arkose_context: ArkoseVersionContext::new(), 41 | arkose_solver: args.arkose_solver, 42 | arkose_gpt3_experiment: args.arkose_gpt3_experiment, 43 | arkose_gpt3_experiment_solver: args.arkose_gpt3_experiment_solver, 44 | arkose_solver_tguess_endpoint: args.arkose_solver_tguess_endpoint, 45 | arkose_solver_image_dir: args.arkose_solver_image_dir, 46 | enable_file_proxy: args.enable_file_proxy, 47 | auth_key: args.auth_key, 48 | visitor_email_whitelist: args.visitor_email_whitelist, 49 | cf_turnstile: args.cf_site_key.and_then(|site_key| { 50 | args.cf_secret_key.map(|secret_key| CfTurnstile { 51 | site_key, 52 | secret_key, 53 | }) 54 | }), 55 | } 56 | } 57 | 58 | fn init_har_provider(args: Args) -> HashMap { 59 | let gpt3_har_provider = 60 | HarProvider::new(arkose::Type::GPT3, args.arkose_har_dir.as_ref(), "gpt3"); 61 | let gpt4_har_provider = 62 | HarProvider::new(arkose::Type::GPT4, args.arkose_har_dir.as_ref(), "gpt4"); 63 | let auth_har_provider = 64 | HarProvider::new(arkose::Type::Auth, args.arkose_har_dir.as_ref(), "auth"); 65 | let platform_har_provider = HarProvider::new( 66 | arkose::Type::Platform, 67 | args.arkose_har_dir.as_ref(), 68 | "platform", 69 | ); 70 | let signup_har_provider = 71 | HarProvider::new(arkose::Type::SignUp, args.arkose_har_dir.as_ref(), "signup"); 72 | 73 | let mut har_map = HashMap::with_capacity(5); 74 | har_map.insert(arkose::Type::GPT3, gpt3_har_provider); 75 | har_map.insert(arkose::Type::GPT4, gpt4_har_provider); 76 | har_map.insert(arkose::Type::Auth, auth_har_provider); 77 | har_map.insert(arkose::Type::Platform, platform_har_provider); 78 | har_map.insert(arkose::Type::SignUp, signup_har_provider); 79 | 80 | har_map 81 | } 82 | -------------------------------------------------------------------------------- /crates/openai/src/dns/fast.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 3 | time::Instant, 4 | }; 5 | 6 | use futures::future::join_all; 7 | use tokio::sync::OnceCell; 8 | use trust_dns_resolver::{ 9 | config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, 10 | TokioAsyncResolver, 11 | }; 12 | 13 | pub(super) const FASTEST_DNS_CONFIG: OnceCell = OnceCell::const_new(); 14 | 15 | /// IP addresses for Tencent Public DNS 16 | pub const TENCENT_IPS: &[IpAddr] = &[ 17 | IpAddr::V4(Ipv4Addr::new(119, 29, 29, 29)), 18 | IpAddr::V4(Ipv4Addr::new(119, 29, 29, 30)), 19 | IpAddr::V6(Ipv6Addr::new(0x2402, 0x4e00, 0, 0, 0, 0, 0, 0x1)), 20 | ]; 21 | 22 | /// IP addresses for Aliyun Public DNS 23 | pub const ALIYUN_IPS: &[IpAddr] = &[ 24 | IpAddr::V4(Ipv4Addr::new(223, 5, 5, 5)), 25 | IpAddr::V4(Ipv4Addr::new(223, 6, 6, 6)), 26 | IpAddr::V6(Ipv6Addr::new(0x2400, 0x3200, 0, 0, 0, 0, 0, 0x1)), 27 | ]; 28 | 29 | pub trait ResolverConfigExt { 30 | fn tencent() -> ResolverConfig; 31 | fn aliyun() -> ResolverConfig; 32 | } 33 | 34 | impl ResolverConfigExt for ResolverConfig { 35 | fn tencent() -> ResolverConfig { 36 | ResolverConfig::from_parts( 37 | None, 38 | vec![], 39 | NameServerConfigGroup::from_ips_clear(TENCENT_IPS, 53, true), 40 | ) 41 | } 42 | 43 | fn aliyun() -> ResolverConfig { 44 | ResolverConfig::from_parts( 45 | None, 46 | vec![], 47 | NameServerConfigGroup::from_ips_clear(ALIYUN_IPS, 53, true), 48 | ) 49 | } 50 | } 51 | 52 | /// Fastest DNS resolver 53 | pub async fn load_fastest_dns(enabled: bool) -> anyhow::Result<()> { 54 | if !enabled { 55 | return Ok(()); 56 | } 57 | 58 | let mut tasks = Vec::new(); 59 | 60 | let mut opts = ResolverOpts::default(); 61 | opts.ip_strategy = trust_dns_resolver::config::LookupIpStrategy::Ipv4AndIpv6; 62 | 63 | let configs = vec![ 64 | ResolverConfig::google(), 65 | ResolverConfig::quad9(), 66 | ResolverConfig::cloudflare(), 67 | ResolverConfig::tencent(), 68 | ResolverConfig::aliyun(), 69 | ]; 70 | 71 | for config in configs { 72 | let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone()); 73 | let task = async move { 74 | let start = Instant::now(); 75 | let ips = resolver.lookup_ip("chat.openai.com").await?; 76 | let elapsed = start.elapsed(); 77 | let ips = ips.iter().collect::>(); 78 | tracing::debug!("Fastest DNS resovler: {ips:?} ({elapsed:?})"); 79 | Ok((elapsed, config)) 80 | }; 81 | tasks.push(task); 82 | } 83 | 84 | // Join all tasks and return the fastest DNS 85 | let r = join_all(tasks) 86 | .await 87 | .into_iter() 88 | .collect::>>()?; 89 | 90 | let (elapsed, conf) = r 91 | .into_iter() 92 | .min_by_key(|(elapsed, _)| *elapsed) 93 | .ok_or_else(|| anyhow::anyhow!("No fastest dns"))?; 94 | 95 | // '\n*' split fastest_dns_group 96 | let mut fastest_dns_group = conf 97 | .name_servers() 98 | .iter() 99 | .map(|ns| ns.socket_addr.to_string()) 100 | .collect::>(); 101 | 102 | // this removes all duplicates 103 | fastest_dns_group.dedup(); 104 | 105 | tracing::info!( 106 | "Fastest DNS group ({elapsed:?}):\n* {}", 107 | fastest_dns_group.join("\n* ") 108 | ); 109 | 110 | // Set fastest dns group 111 | FASTEST_DNS_CONFIG 112 | .set(conf) 113 | .map_err(|_| anyhow::anyhow!("Failed to set fastest dns group"))?; 114 | Ok(()) 115 | } 116 | -------------------------------------------------------------------------------- /crates/openai/src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | //! DNS resolution via the [trust_dns_resolver](https://github.com/bluejekyll/trust-dns) crate 2 | pub mod fast; 3 | 4 | use hyper::client::connect::dns::Name; 5 | use reqwest::dns::{Addrs, Resolve, Resolving}; 6 | use tokio::sync::OnceCell; 7 | use trust_dns_resolver::config::{LookupIpStrategy, NameServerConfigGroup}; 8 | pub use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; 9 | use trust_dns_resolver::{lookup_ip::LookupIpIntoIter, system_conf, TokioAsyncResolver}; 10 | 11 | use std::io; 12 | use std::net::SocketAddr; 13 | use std::sync::Arc; 14 | 15 | /// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait. 16 | #[derive(Debug, Clone)] 17 | pub(crate) struct TrustDnsResolver { 18 | /// Since we might not have been called in the context of a 19 | /// Tokio Runtime in initialization, so we must delay the actual 20 | /// construction of the resolver. 21 | state: Arc>, 22 | /// The DNS strategy to use when resolving addresses. 23 | ip_strategy: LookupIpStrategy, 24 | /// Use fastest DNS resolver 25 | fastest_dns: bool, 26 | } 27 | 28 | impl TrustDnsResolver { 29 | /// Create a new `TrustDnsResolver` with the default configuration, 30 | /// which reads from `/etc/resolve.conf`. 31 | pub(crate) fn new(ip_strategy: LookupIpStrategy, fastest_dns: bool) -> Self { 32 | Self { 33 | state: Arc::new(OnceCell::new()), 34 | ip_strategy, 35 | fastest_dns, 36 | } 37 | } 38 | } 39 | 40 | struct SocketAddrs { 41 | iter: LookupIpIntoIter, 42 | } 43 | 44 | impl Resolve for TrustDnsResolver { 45 | fn resolve(&self, name: Name) -> Resolving { 46 | let resolver = self.clone(); 47 | Box::pin(async move { 48 | let resolver = resolver 49 | .state 50 | .get_or_try_init(|| async { 51 | new_resolver(resolver.ip_strategy, resolver.fastest_dns) 52 | }) 53 | .await?; 54 | let lookup = resolver.lookup_ip(name.as_str()).await?; 55 | let addrs: Addrs = Box::new(SocketAddrs { 56 | iter: lookup.into_iter(), 57 | }); 58 | Ok(addrs) 59 | }) 60 | } 61 | } 62 | 63 | impl Iterator for SocketAddrs { 64 | type Item = SocketAddr; 65 | 66 | fn next(&mut self) -> Option { 67 | self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0)) 68 | } 69 | } 70 | 71 | /// Create a new resolver with the default configuration, 72 | /// which reads from `/etc/resolve.conf`. 73 | fn new_resolver( 74 | ip_strategy: LookupIpStrategy, 75 | fastest_dns: bool, 76 | ) -> io::Result { 77 | // If we can't read the system conf, just use the defaults. 78 | let (mut config, mut opts) = match system_conf::read_system_conf() { 79 | Ok((config, opts)) => (config, opts), 80 | Err(err) => { 81 | tracing::warn!("Error reading DNS system conf: {}", err); 82 | // Use Google DNS, Cloudflare DNS and Quad9 DNS 83 | let mut group = NameServerConfigGroup::new(); 84 | 85 | // Google DNS 86 | group.extend(NameServerConfigGroup::google().into_inner()); 87 | 88 | // Cloudflare DNS 89 | group.extend(NameServerConfigGroup::cloudflare().into_inner()); 90 | 91 | // Quad9 DNS 92 | group.extend(NameServerConfigGroup::quad9().into_inner()); 93 | 94 | let config = ResolverConfig::from_parts(None, vec![], group); 95 | (config, ResolverOpts::default()) 96 | } 97 | }; 98 | 99 | // Use built-in fastest DNS group 100 | if fastest_dns { 101 | config = fast::FASTEST_DNS_CONFIG 102 | .get() 103 | .cloned() 104 | .unwrap_or_else(|| config) 105 | } 106 | 107 | // Check /ect/hosts file before dns requery (only works for unix like OS) 108 | opts.use_hosts_file = true; 109 | // The ip_strategy for the Resolver to use when lookup Ipv4 or Ipv6 addresses 110 | opts.ip_strategy = ip_strategy; 111 | 112 | Ok(TokioAsyncResolver::tokio(config, opts)) 113 | } 114 | -------------------------------------------------------------------------------- /crates/openai/src/eventsource/error.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use eventsource_stream::EventStreamError; 3 | use nom::error::Error as NomError; 4 | use reqwest::header::HeaderValue; 5 | use reqwest::Error as ReqwestError; 6 | use std::string::FromUtf8Error; 7 | use thiserror::Error; 8 | 9 | #[cfg(doc)] 10 | use reqwest::RequestBuilder; 11 | 12 | /// Error raised when a [`RequestBuilder`] cannot be cloned. See [`RequestBuilder::try_clone`] for 13 | /// more information 14 | #[derive(Debug, Clone, Copy)] 15 | pub struct CannotCloneRequestError; 16 | 17 | impl fmt::Display for CannotCloneRequestError { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | f.write_str("expected a cloneable request") 20 | } 21 | } 22 | 23 | impl std::error::Error for CannotCloneRequestError {} 24 | 25 | /// Error raised by the EventSource stream fetching and parsing 26 | #[derive(Debug, Error)] 27 | pub enum Error { 28 | /// Source stream is not valid UTF8 29 | #[error(transparent)] 30 | Utf8(FromUtf8Error), 31 | /// Source stream is not a valid EventStream 32 | #[error(transparent)] 33 | Parser(NomError), 34 | /// The HTTP Request could not be completed 35 | #[error(transparent)] 36 | Transport(ReqwestError), 37 | /// The `Content-Type` returned by the server is invalid 38 | #[error("Invalid header value: {0:?}")] 39 | InvalidContentType(HeaderValue), 40 | /// The status code returned by the server is invalid 41 | #[error("Invalid status code: {}", .0.status())] 42 | InvalidStatusCode(reqwest::Response), 43 | /// The `Last-Event-ID` cannot be formed into a Header to be submitted to the server 44 | #[error("Invalid `Last-Event-ID`: {0}")] 45 | InvalidLastEventId(String), 46 | /// The stream ended 47 | #[error("Stream ended")] 48 | StreamEnded, 49 | } 50 | 51 | impl From> for Error { 52 | fn from(err: EventStreamError) -> Self { 53 | match err { 54 | EventStreamError::Utf8(err) => Self::Utf8(err), 55 | EventStreamError::Parser(err) => Self::Parser(err), 56 | EventStreamError::Transport(err) => Self::Transport(err), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/openai/src/eventsource/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides a simple wrapper for [`reqwest`] to provide an Event Source implementation. 2 | //! You can learn more about Server Sent Events (SSE) take a look at [the MDN 3 | //! docs](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) 4 | //! This crate uses [`eventsource_stream`] to wrap the underlying Bytes stream, and retries failed 5 | //! requests. 6 | //! 7 | //! # Example 8 | //! 9 | //! ```ignore 10 | //! let mut es = EventSource::get("http://localhost:8000/events"); 11 | //! while let Some(event) = es.next().await { 12 | //! match event { 13 | //! Ok(Event::Open) => println!("Connection Open!"), 14 | //! Ok(Event::Message(message)) => println!("Message: {:#?}", message), 15 | //! Err(err) => { 16 | //! println!("Error: {}", err); 17 | //! es.close(); 18 | //! } 19 | //! } 20 | //! } 21 | //! ``` 22 | 23 | mod error; 24 | mod event_source; 25 | mod reqwest_ext; 26 | pub mod retry; 27 | 28 | pub use error::{CannotCloneRequestError, Error}; 29 | pub use event_source::{Event, EventSource, ReadyState}; 30 | pub use reqwest_ext::RequestBuilderExt; 31 | -------------------------------------------------------------------------------- /crates/openai/src/eventsource/reqwest_ext.rs: -------------------------------------------------------------------------------- 1 | use super::error::CannotCloneRequestError; 2 | use super::event_source::EventSource; 3 | use reqwest::RequestBuilder; 4 | 5 | /// Provides an easy interface to build an [`EventSource`] from a [`RequestBuilder`] 6 | pub trait RequestBuilderExt { 7 | /// Create a new [`EventSource`] from a [`RequestBuilder`] 8 | fn eventsource(self) -> Result; 9 | } 10 | 11 | impl RequestBuilderExt for RequestBuilder { 12 | fn eventsource(self) -> Result { 13 | EventSource::new(self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/openai/src/eventsource/retry.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to handle connection delays when receiving errors 2 | 3 | use super::error::Error; 4 | use std::time::Duration; 5 | 6 | #[cfg(doc)] 7 | use crate::event_source::{Event, EventSource}; 8 | 9 | /// Describes how an [`EventSource`] should retry on receiving an [`enum@Error`] 10 | pub trait RetryPolicy { 11 | /// Submit a new retry delay based on the [`enum@Error`], last retry number and duration, if 12 | /// available. A policy may also return `None` if it does not want to retry 13 | fn retry(&self, error: &Error, last_retry: Option<(usize, Duration)>) -> Option; 14 | 15 | /// Set a new reconnection time if received from an [`Event`] 16 | fn set_reconnection_time(&mut self, duration: Duration); 17 | } 18 | 19 | /// A [`RetryPolicy`] which backs off exponentially 20 | #[derive(Debug, Clone)] 21 | pub struct ExponentialBackoff { 22 | /// The start of the backoff 23 | pub start: Duration, 24 | /// The factor of which to backoff by 25 | pub factor: f64, 26 | /// The maximum duration to delay 27 | pub max_duration: Option, 28 | /// The maximum number of retries before giving up 29 | pub max_retries: Option, 30 | } 31 | 32 | impl ExponentialBackoff { 33 | /// Create a new exponential backoff retry policy 34 | pub const fn new( 35 | start: Duration, 36 | factor: f64, 37 | max_duration: Option, 38 | max_retries: Option, 39 | ) -> Self { 40 | Self { 41 | start, 42 | factor, 43 | max_duration, 44 | max_retries, 45 | } 46 | } 47 | } 48 | 49 | impl RetryPolicy for ExponentialBackoff { 50 | fn retry(&self, _error: &Error, last_retry: Option<(usize, Duration)>) -> Option { 51 | if let Some((retry_num, last_duration)) = last_retry { 52 | if self.max_retries.is_none() || retry_num < self.max_retries.unwrap() { 53 | let duration = last_duration.mul_f64(self.factor); 54 | if let Some(max_duration) = self.max_duration { 55 | Some(duration.min(max_duration)) 56 | } else { 57 | Some(duration) 58 | } 59 | } else { 60 | None 61 | } 62 | } else { 63 | Some(self.start) 64 | } 65 | } 66 | fn set_reconnection_time(&mut self, duration: Duration) { 67 | self.start = duration; 68 | if let Some(max_duration) = self.max_duration { 69 | self.max_duration = Some(max_duration.max(duration)) 70 | } 71 | } 72 | } 73 | 74 | /// A [`RetryPolicy`] which always emits the same delay 75 | #[derive(Debug, Clone)] 76 | pub struct Constant { 77 | /// The delay to return 78 | pub delay: Duration, 79 | /// The maximum number of retries to return before giving up 80 | pub max_retries: Option, 81 | } 82 | 83 | impl Constant { 84 | /// Create a new constant retry policy 85 | pub const fn new(delay: Duration, max_retries: Option) -> Self { 86 | Self { delay, max_retries } 87 | } 88 | } 89 | 90 | impl RetryPolicy for Constant { 91 | fn retry(&self, _error: &Error, last_retry: Option<(usize, Duration)>) -> Option { 92 | if let Some((retry_num, _)) = last_retry { 93 | if self.max_retries.is_none() || retry_num < self.max_retries.unwrap() { 94 | Some(self.delay) 95 | } else { 96 | None 97 | } 98 | } else { 99 | Some(self.delay) 100 | } 101 | } 102 | fn set_reconnection_time(&mut self, duration: Duration) { 103 | self.delay = duration; 104 | } 105 | } 106 | 107 | /// A [`RetryPolicy`] which never retries 108 | #[derive(Debug, Clone, Copy, Default)] 109 | pub struct Never; 110 | 111 | impl RetryPolicy for Never { 112 | fn retry(&self, _error: &Error, _last_retry: Option<(usize, Duration)>) -> Option { 113 | None 114 | } 115 | fn set_reconnection_time(&mut self, _duration: Duration) {} 116 | } 117 | 118 | /// The default [`RetryPolicy`] when initializing an [`EventSource`] 119 | pub const DEFAULT_RETRY: ExponentialBackoff = ExponentialBackoff::new( 120 | Duration::from_millis(300), 121 | 2., 122 | Some(Duration::from_secs(5)), 123 | None, 124 | ); 125 | -------------------------------------------------------------------------------- /crates/openai/src/gpt_model.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde::{Serialize, Serializer}; 4 | 5 | #[derive(PartialEq, Eq, Clone, Debug)] 6 | pub enum GPTModel { 7 | Gpt35, 8 | Gpt4, 9 | Gpt4Mobile, 10 | } 11 | 12 | impl Serialize for GPTModel { 13 | fn serialize(&self, serializer: S) -> Result { 14 | let model = match self { 15 | GPTModel::Gpt35 => "text-davinci-002-render-sha", 16 | GPTModel::Gpt4 => "gpt-4", 17 | GPTModel::Gpt4Mobile => "gpt-4-mobile", 18 | }; 19 | serializer.serialize_str(model) 20 | } 21 | } 22 | 23 | impl GPTModel { 24 | pub fn is_gpt3(&self) -> bool { 25 | match self { 26 | GPTModel::Gpt35 => true, 27 | _ => false, 28 | } 29 | } 30 | 31 | pub fn is_gpt4(&self) -> bool { 32 | match self { 33 | GPTModel::Gpt4 | GPTModel::Gpt4Mobile => true, 34 | _ => false, 35 | } 36 | } 37 | } 38 | 39 | impl FromStr for GPTModel { 40 | type Err = anyhow::Error; 41 | 42 | fn from_str(value: &str) -> Result { 43 | match value { 44 | // If the model starts with gpt-3.5 or text-davinci/code-davinci, we assume it's gpt-3.5 45 | s if s.starts_with("gpt-3.5") 46 | || s.starts_with("text-davinci") 47 | || s.starts_with("code-davinci") => 48 | { 49 | Ok(GPTModel::Gpt35) 50 | } 51 | // If the model is gpt-4-mobile, we assume it's gpt-4-mobile 52 | "gpt-4-mobile" => Ok(GPTModel::Gpt4Mobile), 53 | // If the model starts with gpt-4, we assume it's gpt-4 54 | s if s.starts_with("gpt-4") => Ok(GPTModel::Gpt4), 55 | _ => anyhow::bail!("Invalid GPT model: {value}"), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/openai/src/homedir.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | mod home_dir_windows { 3 | use { 4 | core::slice::from_raw_parts, 5 | std::{ 6 | ffi::{c_void, OsString}, 7 | os::windows::prelude::OsStringExt, 8 | path::PathBuf, 9 | ptr::null_mut, 10 | }, 11 | windows_sys::Win32::{ 12 | Globalization::lstrlenW, 13 | System::Com::CoTaskMemFree, 14 | UI::Shell::{FOLDERID_Profile, SHGetKnownFolderPath}, 15 | }, 16 | }; 17 | 18 | /// Return the user's home directory. 19 | /// 20 | /// ``` 21 | /// // "C:\Users\USER" 22 | /// let path = home_dir::home_dir().unwrap(); 23 | /// ``` 24 | pub fn home_dir() -> Option { 25 | let mut path_ptr = null_mut(); 26 | (unsafe { SHGetKnownFolderPath(&FOLDERID_Profile, 0, 0, &mut path_ptr) } == 0).then_some({ 27 | let wide = unsafe { from_raw_parts(path_ptr, lstrlenW(path_ptr) as usize) }; 28 | let ostr = OsString::from_wide(wide); 29 | unsafe { CoTaskMemFree(path_ptr as *const c_void) } 30 | ostr.into() 31 | }) 32 | } 33 | } 34 | 35 | #[cfg(not(target_os = "windows"))] 36 | mod home_dir_ne_windows { 37 | use std::{env::var_os, path::PathBuf}; 38 | 39 | const HOME: &str = "HOME"; 40 | 41 | /// Return the user's home directory. 42 | /// 43 | /// ``` 44 | /// // "/home/USER" 45 | /// let path = home_dir::home_dir().unwrap(); 46 | /// ``` 47 | pub fn home_dir() -> Option { 48 | if let Ok(user) = std::env::var("SUDO_USER") { 49 | if let Ok(Some(real_user)) = nix::unistd::User::from_name(&user) { 50 | return Some(real_user.dir); 51 | } 52 | } 53 | var_os(HOME).map(Into::into) 54 | } 55 | } 56 | 57 | #[cfg(target_os = "windows")] 58 | pub use home_dir_windows::*; 59 | 60 | #[cfg(not(target_os = "windows"))] 61 | pub use home_dir_ne_windows::*; 62 | -------------------------------------------------------------------------------- /crates/openai/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod arkose; 2 | pub mod auth; 3 | pub mod chatgpt; 4 | pub mod client; 5 | mod constant; 6 | pub mod context; 7 | mod dns; 8 | pub mod eventsource; 9 | pub mod gpt_model; 10 | pub mod homedir; 11 | mod log; 12 | pub mod platform; 13 | pub mod proxy; 14 | 15 | #[cfg(feature = "serve")] 16 | pub mod serve; 17 | pub mod token; 18 | pub mod unescape; 19 | pub mod urldecoding; 20 | pub mod uuid; 21 | 22 | use std::time::Duration; 23 | 24 | pub const LIB_VERSION: &str = env!("CARGO_PKG_VERSION"); 25 | pub const URL_CHATGPT_API: &str = "https://chat.openai.com"; 26 | pub const URL_PLATFORM_API: &str = "https://api.openai.com"; 27 | 28 | pub fn now_duration() -> anyhow::Result { 29 | let now = std::time::SystemTime::now(); 30 | let duration = now.duration_since(std::time::UNIX_EPOCH)?; 31 | Ok(duration) 32 | } 33 | 34 | pub fn format_time_to_rfc3399(timestamp: i64) -> anyhow::Result { 35 | let time = time::OffsetDateTime::from_unix_timestamp(timestamp)? 36 | .format(&time::format_description::well_known::Rfc3339)?; 37 | Ok(time) 38 | } 39 | 40 | pub fn generate_random_string(len: usize) -> String { 41 | use rand::distributions::Alphanumeric; 42 | use rand::{thread_rng, Rng}; 43 | const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 44 | let rng = thread_rng(); 45 | rng.sample_iter(&Alphanumeric) 46 | .take(len) 47 | .map(|x| CHARSET[x as usize % CHARSET.len()] as char) 48 | .collect() 49 | } 50 | -------------------------------------------------------------------------------- /crates/openai/src/log.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! info { 3 | // info!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 4 | // info!(target: "my_target", "a {} event", "log") 5 | (target: $target:expr, $($arg:tt)+) => (log::info!($($arg)+)); 6 | 7 | // info!("a {} event", "log") 8 | ($($arg:tt)+) => (tracing::info!($($arg)+)) 9 | } 10 | 11 | #[macro_export] 12 | macro_rules! debug { 13 | // debug!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 14 | // debug!(target: "my_target", "a {} event", "log") 15 | (target: $target:expr, $($arg:tt)+) => (log::debug!("[{}] {}",std::panic::Location::caller(), $($arg)+)); 16 | 17 | // debug!("a {} event", "log") 18 | ($($arg:tt)+) => (tracing::debug!("[{}] {}", std::panic::Location::caller(), format!($($arg)+))) 19 | } 20 | 21 | #[macro_export] 22 | macro_rules! warn { 23 | // warn!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 24 | // warn!(target: "my_target", "a {} event", "log") 25 | (target: $target:expr, $($arg:tt)+) => (log::warn!("[{}] {}",std::panic::Location::caller(), $($arg)+)); 26 | 27 | // warn!("a {} event", "log") 28 | ($($arg:tt)+) => (tracing::warn!("[{}] {}", std::panic::Location::caller(), format!($($arg)+))) 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! trace { 33 | // trace!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 34 | // trace!(target: "my_target", "a {} event", "log") 35 | (target: $target:expr, $($arg:tt)+) => (log::trace!("[{}] {}",std::panic::Location::caller(), $($arg)+)); 36 | 37 | // trace!("a {} event", "log") 38 | ($($arg:tt)+) => (tracing::trace!("[{}] {}", std::panic::Location::caller(), format!($($arg)+))) 39 | } 40 | 41 | #[macro_export] 42 | macro_rules! error { 43 | // error!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 44 | // error!(target: "my_target", "a {} event", "log") 45 | (target: $target:expr, $($arg:tt)+) => (log::error!("[{}] {}",std::panic::Location::caller(), $($arg)+)); 46 | 47 | // error!("a {} event", "log") 48 | ($($arg:tt)+) => (tracing::error!("[{}] {}", std::panic::Location::caller(), format!($($arg)+))) 49 | } 50 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/endpoints/chat.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::api::Client; 2 | use crate::platform::v1::error::APIError; 3 | use crate::platform::v1::resources::chat_completion::{ 4 | ChatCompletionParameters, ChatCompletionResponse, 5 | }; 6 | use serde_json::Value; 7 | 8 | #[cfg(feature = "stream")] 9 | use crate::platform::v1::resources::chat_completion::ChatMessage; 10 | #[cfg(feature = "stream")] 11 | use crate::platform::v1::resources::chat_completion_stream::ChatCompletionStreamResponse; 12 | #[cfg(feature = "stream")] 13 | use crate::platform::v1::resources::shared::StopToken; 14 | #[cfg(feature = "stream")] 15 | use futures::Stream; 16 | #[cfg(feature = "stream")] 17 | use serde::Serialize; 18 | #[cfg(feature = "stream")] 19 | use std::collections::HashMap; 20 | #[cfg(feature = "stream")] 21 | use std::pin::Pin; 22 | 23 | #[cfg(feature = "simple")] 24 | use crate::platform::v1::resources::chat_completion::SimpleChatCompletionParameters; 25 | 26 | pub struct Chat<'a> { 27 | pub client: &'a Client, 28 | } 29 | 30 | impl Client { 31 | pub fn chat(&self) -> Chat { 32 | Chat { client: self } 33 | } 34 | } 35 | 36 | impl Chat<'_> { 37 | pub async fn create( 38 | &self, 39 | parameters: ChatCompletionParameters, 40 | ) -> Result { 41 | let response = self.client.post("/chat/completions", ¶meters).await?; 42 | 43 | let value: Value = serde_json::from_str(&response).unwrap(); 44 | let chat_completion_response: ChatCompletionResponse = serde_json::from_value(value) 45 | .map_err(|error| APIError::ParseError(error.to_string()))?; 46 | 47 | Ok(chat_completion_response) 48 | } 49 | 50 | #[deprecated(since = "0.2.8")] 51 | #[cfg(feature = "simple")] 52 | pub async fn create_simple( 53 | &self, 54 | parameters: SimpleChatCompletionParameters, 55 | ) -> Result { 56 | let response = self.client.post("/chat/completions", ¶meters).await?; 57 | 58 | let value: Value = serde_json::from_str(&response).unwrap(); 59 | let chat_completion_response: ChatCompletionResponse = serde_json::from_value(value) 60 | .map_err(|error| APIError::ParseError(error.to_string()))?; 61 | 62 | Ok(chat_completion_response) 63 | } 64 | 65 | #[cfg(feature = "stream")] 66 | pub async fn create_stream( 67 | &self, 68 | parameters: ChatCompletionParameters, 69 | ) -> Result< 70 | Pin> + Send>>, 71 | APIError, 72 | > { 73 | let stream_parameters = ChatCompletionStreamParameters { 74 | model: parameters.model, 75 | messages: parameters.messages, 76 | temperature: parameters.temperature, 77 | top_p: parameters.top_p, 78 | n: parameters.n, 79 | stream: true, 80 | stop: parameters.stop, 81 | max_tokens: parameters.max_tokens, 82 | presence_penalty: parameters.presence_penalty, 83 | frequency_penalty: parameters.frequency_penalty, 84 | logit_bias: parameters.logit_bias, 85 | }; 86 | 87 | Ok(self 88 | .client 89 | .post_stream("/chat/completions", &stream_parameters) 90 | .await) 91 | } 92 | } 93 | 94 | #[cfg(feature = "stream")] 95 | #[derive(Serialize, Debug)] 96 | struct ChatCompletionStreamParameters { 97 | pub model: String, 98 | pub messages: Vec, 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub temperature: Option, 101 | #[serde(skip_serializing_if = "Option::is_none")] 102 | pub top_p: Option, 103 | #[serde(skip_serializing_if = "Option::is_none")] 104 | pub n: Option, 105 | pub stream: bool, 106 | #[serde(skip_serializing_if = "Option::is_none")] 107 | pub stop: Option, 108 | #[serde(skip_serializing_if = "Option::is_none")] 109 | pub max_tokens: Option, 110 | #[serde(skip_serializing_if = "Option::is_none")] 111 | pub presence_penalty: Option, 112 | #[serde(skip_serializing_if = "Option::is_none")] 113 | pub frequency_penalty: Option, 114 | #[serde(skip_serializing_if = "Option::is_none")] 115 | pub logit_bias: Option>, 116 | } 117 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/endpoints/completions.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::api::Client; 2 | use crate::platform::v1::error::APIError; 3 | use crate::platform::v1::resources::completion::{CompletionParameters, CompletionResponse}; 4 | use serde_json::Value; 5 | 6 | #[cfg(feature = "stream")] 7 | use crate::platform::v1::resources::completion_stream::CompletionStreamResponse; 8 | #[cfg(feature = "stream")] 9 | use crate::platform::v1::resources::shared::StopToken; 10 | #[cfg(feature = "stream")] 11 | use futures::Stream; 12 | #[cfg(feature = "stream")] 13 | use serde::Serialize; 14 | #[cfg(feature = "stream")] 15 | use std::collections::HashMap; 16 | #[cfg(feature = "stream")] 17 | use std::pin::Pin; 18 | 19 | #[cfg(feature = "simple")] 20 | use crate::platform::v1::resources::completion::SimpleCompletionParameters; 21 | 22 | pub struct Completions<'a> { 23 | pub client: &'a Client, 24 | } 25 | 26 | impl Client { 27 | pub fn completions(&self) -> Completions { 28 | Completions { client: self } 29 | } 30 | } 31 | 32 | impl Completions<'_> { 33 | pub async fn create( 34 | &self, 35 | parameters: CompletionParameters, 36 | ) -> Result { 37 | let response = self.client.post("/completions", ¶meters).await?; 38 | 39 | let value: Value = serde_json::from_str(&response).unwrap(); 40 | let completion_response: CompletionResponse = serde_json::from_value(value) 41 | .map_err(|error| APIError::ParseError(error.to_string()))?; 42 | 43 | Ok(completion_response) 44 | } 45 | 46 | #[deprecated(since = "0.2.8")] 47 | #[cfg(feature = "simple")] 48 | pub async fn create_simple( 49 | &self, 50 | parameters: SimpleCompletionParameters, 51 | ) -> Result { 52 | let response = self.client.post("/completions", ¶meters).await?; 53 | 54 | let value: Value = serde_json::from_str(&response).unwrap(); 55 | let completion_response: CompletionResponse = serde_json::from_value(value) 56 | .map_err(|error| APIError::ParseError(error.to_string()))?; 57 | 58 | Ok(completion_response) 59 | } 60 | 61 | #[cfg(feature = "stream")] 62 | pub async fn create_stream( 63 | &self, 64 | parameters: CompletionParameters, 65 | ) -> Result< 66 | Pin> + Send>>, 67 | APIError, 68 | > { 69 | let stream_parameters = CompletionStreamParameters { 70 | model: parameters.model, 71 | prompt: parameters.prompt, 72 | suffix: None, 73 | max_tokens: Some(50), 74 | temperature: parameters.temperature, 75 | top_p: parameters.top_p, 76 | n: parameters.n, 77 | stream: true, 78 | logprobs: parameters.logprobs, 79 | echo: parameters.echo, 80 | stop: parameters.stop, 81 | presence_penalty: parameters.presence_penalty, 82 | frequency_penalty: parameters.frequency_penalty, 83 | best_of: parameters.best_of, 84 | logit_bias: parameters.logit_bias, 85 | }; 86 | 87 | Ok(self 88 | .client 89 | .post_stream("/completions", &stream_parameters) 90 | .await) 91 | } 92 | } 93 | 94 | #[cfg(feature = "stream")] 95 | #[derive(Serialize, Debug)] 96 | struct CompletionStreamParameters { 97 | model: String, 98 | prompt: String, 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub suffix: Option, 101 | #[serde(skip_serializing_if = "Option::is_none")] 102 | pub max_tokens: Option, 103 | #[serde(skip_serializing_if = "Option::is_none")] 104 | pub temperature: Option, 105 | #[serde(skip_serializing_if = "Option::is_none")] 106 | pub top_p: Option, 107 | #[serde(skip_serializing_if = "Option::is_none")] 108 | pub n: Option, 109 | pub stream: bool, 110 | #[serde(skip_serializing_if = "Option::is_none")] 111 | pub logprobs: Option, 112 | #[serde(skip_serializing_if = "Option::is_none")] 113 | pub echo: Option, 114 | #[serde(skip_serializing_if = "Option::is_none")] 115 | pub stop: Option, 116 | #[serde(skip_serializing_if = "Option::is_none")] 117 | pub presence_penalty: Option, 118 | #[serde(skip_serializing_if = "Option::is_none")] 119 | pub frequency_penalty: Option, 120 | #[serde(skip_serializing_if = "Option::is_none")] 121 | pub best_of: Option, 122 | #[serde(skip_serializing_if = "Option::is_none")] 123 | pub logit_bias: Option>, 124 | } 125 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/endpoints/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod completions; 3 | pub mod models; 4 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/endpoints/models.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::api::Client; 2 | use crate::platform::v1::error::APIError; 3 | use crate::platform::v1::resources::model::Model; 4 | use serde_json::Value; 5 | 6 | pub struct Models<'a> { 7 | pub client: &'a Client, 8 | } 9 | 10 | impl Client { 11 | pub fn models(&self) -> Models { 12 | Models { client: self } 13 | } 14 | } 15 | 16 | impl Models<'_> { 17 | pub async fn list(&self) -> Result, APIError> { 18 | let response = self.client.get("/models").await?; 19 | 20 | let value: Value = serde_json::from_str(&response).unwrap(); 21 | let models: Vec = serde_json::from_value(value["data"].clone()) 22 | .map_err(|error| APIError::ParseError(error.to_string()))?; 23 | 24 | Ok(models) 25 | } 26 | 27 | pub async fn get(&self, model_id: &str) -> Result { 28 | let path = format!("/models/{}", model_id); 29 | 30 | let response = self.client.get(&path).await?; 31 | 32 | let value: Value = serde_json::from_str(&response).unwrap(); 33 | let model_response: Model = serde_json::from_value(value) 34 | .map_err(|error| APIError::ParseError(error.to_string()))?; 35 | 36 | Ok(model_response) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/error.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::error::Error; 3 | use std::fmt::{Display, Formatter, Result}; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub enum APIError { 7 | EndpointError(String), 8 | ParseError(String), 9 | FileError(String), 10 | StreamError(String), 11 | } 12 | 13 | impl Error for APIError {} 14 | 15 | impl Display for APIError { 16 | fn fmt(&self, f: &mut Formatter) -> Result { 17 | match self { 18 | APIError::EndpointError(message) => write!(f, "{}", message), 19 | APIError::ParseError(message) => write!(f, "{}", message), 20 | APIError::FileError(message) => write!(f, "{}", message), 21 | APIError::StreamError(message) => write!(f, "{}", message), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod error; 3 | pub mod models; 4 | 5 | pub mod endpoints; 6 | pub mod resources; 7 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt::Display; 3 | 4 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 5 | pub enum OpenAIModel { 6 | #[serde(rename = "gpt-4")] 7 | Gpt4, 8 | #[serde(rename = "gpt-4-0314")] 9 | Gpt4_0314, 10 | #[serde(rename = "gpt-4-32k")] 11 | Gpt4_32K, 12 | #[serde(rename = "gpt-4-32k-0314")] 13 | Gpt4_32K0314, 14 | #[serde(rename = "gpt-3.5-turbo")] 15 | Gpt3_5Turbo, 16 | #[serde(rename = "gpt-3.5-turbo-0301")] 17 | Gpt3_5Turbo0301, 18 | #[serde(rename = "text-davinci-003")] 19 | TextDavinci003, 20 | #[serde(rename = "text-davinci-edit-001")] 21 | TextDavinciEdit001, 22 | #[serde(rename = "text-curie-001")] 23 | TextCurie001, 24 | #[serde(rename = "text-babbage-001")] 25 | TextBabbage001, 26 | #[serde(rename = "text-ada-001")] 27 | TextAda001, 28 | #[serde(rename = "text-embedding-ada-002")] 29 | TextEmbeddingAda002, 30 | #[serde(rename = "whisper-1")] 31 | Whisper1, 32 | #[serde(rename = "text-moderation-stable")] 33 | TextModerationStable, 34 | #[serde(rename = "text-moderation-latest")] 35 | TextModerationLatest, 36 | } 37 | 38 | impl Display for OpenAIModel { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!( 41 | f, 42 | "{}", 43 | match self { 44 | OpenAIModel::Gpt4 => "gpt-4", 45 | OpenAIModel::Gpt4_0314 => "gpt-4-0314", 46 | OpenAIModel::Gpt4_32K => "gpt-4-32k", 47 | OpenAIModel::Gpt4_32K0314 => "gpt-4-32k-0314", 48 | OpenAIModel::Gpt3_5Turbo => "gpt-3.5-turbo-0301", 49 | OpenAIModel::Gpt3_5Turbo0301 => "gpt-3.5-turbo", 50 | OpenAIModel::TextDavinci003 => "text-davinci-003", 51 | OpenAIModel::TextDavinciEdit001 => "text-davinci-edit-001", 52 | OpenAIModel::TextCurie001 => "text-curie-001", 53 | OpenAIModel::TextBabbage001 => "text-babbage-001", 54 | OpenAIModel::TextAda001 => "text-ada-001", 55 | OpenAIModel::TextEmbeddingAda002 => "text-embedding-ada-002", 56 | OpenAIModel::Whisper1 => "whisper-1", 57 | OpenAIModel::TextModerationStable => "text-moderation-stable", 58 | OpenAIModel::TextModerationLatest => "text-moderation-latest", 59 | } 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/chat_completion.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::models::OpenAIModel; 2 | use crate::platform::v1::resources::shared::StopToken; 3 | use crate::platform::v1::resources::shared::{FinishReason, Usage}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | use std::fmt::Display; 7 | 8 | #[deprecated(since = "0.2.8")] 9 | #[cfg(feature = "simple")] 10 | #[derive(Serialize, Debug, Clone)] 11 | pub struct SimpleChatCompletionParameters { 12 | pub model: String, 13 | pub messages: Vec, 14 | pub max_tokens: u32, 15 | } 16 | 17 | #[derive(Serialize, Debug, Clone)] 18 | pub struct ChatCompletionParameters { 19 | pub model: String, 20 | pub messages: Vec, 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub temperature: Option, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub top_p: Option, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub n: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub stop: Option, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub max_tokens: Option, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub presence_penalty: Option, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub frequency_penalty: Option, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub logit_bias: Option>, 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub user: Option, 39 | } 40 | 41 | impl Default for ChatCompletionParameters { 42 | fn default() -> Self { 43 | ChatCompletionParameters { 44 | model: OpenAIModel::Gpt3_5Turbo.to_string(), 45 | messages: vec![ChatMessage { 46 | role: Role::User, 47 | content: "Hello!".to_string(), 48 | name: None, 49 | }], 50 | temperature: None, 51 | top_p: None, 52 | n: None, 53 | stop: None, 54 | max_tokens: None, 55 | presence_penalty: None, 56 | frequency_penalty: None, 57 | logit_bias: None, 58 | user: None, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Serialize, Deserialize, Debug, Clone)] 64 | pub struct ChatMessage { 65 | pub role: Role, 66 | pub content: String, 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub name: Option, 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Debug)] 72 | pub struct ChatCompletionResponse { 73 | pub id: String, 74 | pub object: String, 75 | pub created: u32, 76 | pub model: String, 77 | pub choices: Vec, 78 | pub usage: Usage, 79 | } 80 | 81 | #[derive(Serialize, Deserialize, Debug)] 82 | pub struct ChatCompletionChoice { 83 | pub index: u32, 84 | pub message: ChatMessage, 85 | #[serde(skip_serializing_if = "Option::is_none")] 86 | pub finish_reason: Option, 87 | } 88 | 89 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 90 | #[serde(rename_all = "lowercase")] 91 | pub enum Role { 92 | System, 93 | User, 94 | Assistant, 95 | } 96 | 97 | impl Display for Role { 98 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 99 | write!( 100 | f, 101 | "{}", 102 | match self { 103 | Role::System => "System", 104 | Role::User => "User", 105 | Role::Assistant => "Assistant", 106 | } 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/chat_completion_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::resources::chat_completion::Role; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct ChatCompletionStreamResponse { 6 | pub id: String, 7 | pub object: String, 8 | pub created: u32, 9 | pub model: String, 10 | pub choices: Vec, 11 | } 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct DeltaField { 15 | pub delta: DeltaValue, 16 | pub index: u32, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug, Clone)] 20 | pub struct DeltaValue { 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub role: Option, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub content: Option, 25 | } 26 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/completion.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::v1::{ 2 | models::OpenAIModel, 3 | resources::shared::{FinishReason, StopToken, Usage}, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | 8 | #[deprecated(since = "0.2.8")] 9 | #[cfg(feature = "simple")] 10 | #[derive(Serialize, Debug, Clone)] 11 | pub struct SimpleCompletionParameters { 12 | pub model: String, 13 | pub prompt: String, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub suffix: Option, 16 | pub max_tokens: u32, 17 | } 18 | 19 | #[derive(Serialize, Debug, Clone)] 20 | pub struct CompletionParameters { 21 | pub model: String, 22 | pub prompt: String, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub suffix: Option, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub max_tokens: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub temperature: Option, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub top_p: Option, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub n: Option, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub logprobs: Option, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub echo: Option, 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub stop: Option, 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub presence_penalty: Option, 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub frequency_penalty: Option, 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub best_of: Option, 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub logit_bias: Option>, 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub user: Option, 49 | } 50 | 51 | impl Default for CompletionParameters { 52 | fn default() -> Self { 53 | CompletionParameters { 54 | model: OpenAIModel::TextDavinci003.to_string(), 55 | prompt: "Say this is a test".to_string(), 56 | suffix: None, 57 | max_tokens: None, 58 | temperature: None, 59 | top_p: None, 60 | n: None, 61 | logprobs: None, 62 | echo: None, 63 | stop: None, 64 | presence_penalty: None, 65 | frequency_penalty: None, 66 | best_of: None, 67 | logit_bias: None, 68 | user: None, 69 | } 70 | } 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Debug, Clone)] 74 | pub struct CompletionResponse { 75 | pub id: String, 76 | pub object: String, 77 | pub created: u32, 78 | pub model: String, 79 | pub choices: Vec, 80 | pub usage: Usage, 81 | } 82 | 83 | #[derive(Serialize, Deserialize, Debug, Clone)] 84 | pub struct CompletionChoice { 85 | pub text: String, 86 | pub index: u32, 87 | pub finish_reason: FinishReason, 88 | } 89 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/completion_stream.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct CompletionStreamResponse { 5 | pub id: String, 6 | pub object: String, 7 | pub created: u32, 8 | pub model: String, 9 | pub choices: Vec, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Debug, Clone)] 13 | pub struct CompletionStreamChoice { 14 | pub text: String, 15 | pub index: u32, 16 | } 17 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat_completion; 2 | #[cfg(feature = "stream")] 3 | pub mod chat_completion_stream; 4 | pub mod completion; 5 | #[cfg(feature = "stream")] 6 | pub mod completion_stream; 7 | pub mod model; 8 | pub mod shared; 9 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct Model { 5 | id: String, 6 | object: String, 7 | owned_by: String, 8 | } 9 | -------------------------------------------------------------------------------- /crates/openai/src/platform/v1/resources/shared.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[cfg(feature = "download")] 4 | use rand::{distributions::Alphanumeric, Rng}; 5 | 6 | #[derive(Serialize, Deserialize, Debug, Clone)] 7 | pub struct BaseModel { 8 | name: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | pub struct Usage { 13 | pub prompt_tokens: u32, 14 | pub completion_tokens: u32, 15 | pub total_tokens: u32, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 19 | pub enum FinishReason { 20 | #[serde(rename(deserialize = "stop"))] 21 | StopSequenceReached, 22 | #[serde(rename(deserialize = "length"))] 23 | TokenLimitReached, 24 | #[serde(rename(deserialize = "content_filter"))] 25 | ContentFilterFlagged, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 29 | #[serde(untagged)] 30 | pub enum StopToken { 31 | String(String), 32 | Array(Vec), 33 | } 34 | 35 | #[cfg(feature = "download")] 36 | pub fn generate_file_name(path: &str, length: u32, file_type: &str) -> String { 37 | let random_file_name: String = rand::thread_rng() 38 | .sample_iter(&Alphanumeric) 39 | .take(length as usize) 40 | .map(char::from) 41 | .collect(); 42 | 43 | format!("{}/{}.{}", path, random_file_name, file_type) 44 | } 45 | -------------------------------------------------------------------------------- /crates/openai/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Error}; 2 | use cidr::Ipv6Cidr; 3 | use rand::Rng; 4 | use serde::{Deserialize, Serialize}; 5 | use std::net::IpAddr; 6 | use url::Url; 7 | 8 | /// RandomIpv6 trait 9 | pub trait Ipv6CidrExt { 10 | fn random_ipv6(&self) -> IpAddr; 11 | } 12 | 13 | impl Ipv6CidrExt for Ipv6Cidr { 14 | fn random_ipv6(&self) -> IpAddr { 15 | let ipv6: u128 = self.first_address().into(); 16 | let prefix_len = self.network_length(); 17 | let rand: u128 = rand::thread_rng().gen(); 18 | let net_part = (ipv6 >> (128 - prefix_len)) << (128 - prefix_len); 19 | let host_part = (rand << prefix_len) >> prefix_len; 20 | IpAddr::V6((net_part | host_part).into()) 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug, Serialize, Deserialize)] 25 | #[serde(rename_all = "lowercase")] 26 | pub enum InnerProxy { 27 | /// Upstream proxy, supports http, https, socks5 28 | Proxy(Url), 29 | /// Bind to interface, supports ipv4, ipv6 30 | Interface(IpAddr), 31 | /// Bind to ipv6 subnet, ramdomly generate ipv6 address 32 | IPv6Subnet(Ipv6Cidr), 33 | } 34 | 35 | /// Proxy configuration 36 | #[derive(Clone, Debug, Serialize, Deserialize)] 37 | #[serde(rename_all = "lowercase")] 38 | pub enum Proxy { 39 | All(InnerProxy), 40 | Api(InnerProxy), 41 | Auth(InnerProxy), 42 | Arkose(InnerProxy), 43 | } 44 | 45 | impl Proxy { 46 | pub fn proto(&self) -> &'static str { 47 | match self { 48 | Proxy::All(_) => "All", 49 | Proxy::Api(_) => "Api", 50 | Proxy::Auth(_) => "Auth", 51 | Proxy::Arkose(_) => "Arkose", 52 | } 53 | } 54 | } 55 | 56 | const UNSUPPORTED_PROTOCOL: &str = "Unsupported protocol"; 57 | 58 | fn unsupported_protocol(proto: &str) -> Error { 59 | format_err!("{}: {}", UNSUPPORTED_PROTOCOL, proto) 60 | } 61 | 62 | fn make_proxy(inner_proxy: InnerProxy, proto: &str) -> Result { 63 | match proto { 64 | "all" => Ok(Proxy::All(inner_proxy)), 65 | "api" => Ok(Proxy::Api(inner_proxy)), 66 | "auth" => Ok(Proxy::Auth(inner_proxy)), 67 | "arkose" => Ok(Proxy::Arkose(inner_proxy)), 68 | _ => Err(unsupported_protocol(proto)), 69 | } 70 | } 71 | 72 | impl TryFrom<(&str, IpAddr)> for Proxy { 73 | type Error = anyhow::Error; 74 | 75 | fn try_from((proto, ip_addr): (&str, IpAddr)) -> Result { 76 | let inner_proxy = InnerProxy::Interface(ip_addr); 77 | make_proxy(inner_proxy, proto) 78 | } 79 | } 80 | 81 | impl TryFrom<(&str, Url)> for Proxy { 82 | type Error = anyhow::Error; 83 | 84 | fn try_from((proto, url): (&str, Url)) -> Result { 85 | match url.scheme() { 86 | "http" | "https" | "socks5" | "socks5h" => { 87 | let inner_proxy = InnerProxy::Proxy(url); 88 | make_proxy(inner_proxy, proto) 89 | } 90 | _ => Err(unsupported_protocol(url.scheme())), 91 | } 92 | } 93 | } 94 | 95 | impl TryFrom<(&str, &str)> for Proxy { 96 | type Error = anyhow::Error; 97 | 98 | fn try_from((proto, url): (&str, &str)) -> Result { 99 | let url = Url::parse(url)?; 100 | Self::try_from((proto, url)) 101 | } 102 | } 103 | 104 | impl TryFrom<(&str, cidr::Ipv6Cidr)> for Proxy { 105 | type Error = anyhow::Error; 106 | 107 | fn try_from((proto, cidr): (&str, cidr::Ipv6Cidr)) -> Result { 108 | let inner_proxy = InnerProxy::IPv6Subnet(cidr); 109 | make_proxy(inner_proxy, proto) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/openai/src/serve/middleware/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::serve::error::{ProxyError, ResponseError}; 2 | use crate::serve::whitelist; 3 | use crate::token; 4 | use axum::http::header; 5 | use axum::{http::Request, middleware::Next, response::Response}; 6 | 7 | pub(crate) async fn auth_middleware( 8 | request: Request, 9 | next: Next, 10 | ) -> Result { 11 | // Allow access to the public folder 12 | if ["/backend-api/public", "/backend-api/o11y/v1/traces"] 13 | .iter() 14 | .any(|v| request.uri().path().contains(*v)) 15 | { 16 | return Ok(next.run(request).await); 17 | }; 18 | 19 | // Check if the request has an authorization header 20 | let token = match request.headers().get(header::AUTHORIZATION) { 21 | Some(token) => token, 22 | None => return Err(ResponseError::Unauthorized(ProxyError::AccessTokenRequired)), 23 | }; 24 | 25 | // Check if the token is valid 26 | match token::check_for_u8(token.as_bytes()) { 27 | Ok(Some(profile)) => { 28 | whitelist::check_whitelist(profile.email()).map_err(ResponseError::Forbidden)?; 29 | Ok(next.run(request).await) 30 | } 31 | Ok(None) => { 32 | // for now, we don't allow anonymous access 33 | Ok(next.run(request).await) 34 | } 35 | Err(err) => Err(ResponseError::Forbidden(err)), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/openai/src/serve/middleware/csrf.rs: -------------------------------------------------------------------------------- 1 | use axum::http::{Method, Request, StatusCode}; 2 | use axum::{ 3 | body::{self, BoxBody, Full}, 4 | middleware::Next, 5 | response::Response, 6 | Form, 7 | }; 8 | use axum_csrf::CsrfToken; 9 | use mitm::proxy::hyper; 10 | 11 | use crate::auth::model::AuthAccount; 12 | 13 | /// Can only be done with the feature layer enabled 14 | pub async fn csrf_middleware( 15 | token: CsrfToken, 16 | method: Method, 17 | mut request: Request, 18 | next: Next, 19 | ) -> Result { 20 | if method == Method::POST { 21 | let (parts, body) = request.into_parts(); 22 | let bytes = hyper::body::to_bytes(body) 23 | .await 24 | .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 25 | 26 | let value = serde_urlencoded::from_bytes(&bytes) 27 | .map_err(|_| -> StatusCode { StatusCode::BAD_REQUEST })?; 28 | let payload: Form = Form(value); 29 | match payload.0.csrf_token { 30 | Some(csrf_token) => { 31 | if token.verify(&csrf_token).is_err() { 32 | return Err(StatusCode::UNAUTHORIZED); 33 | } 34 | } 35 | None => { 36 | return Err(StatusCode::UNAUTHORIZED); 37 | } 38 | } 39 | request = Request::from_parts(parts, body::boxed(Full::from(bytes))); 40 | } 41 | 42 | Ok(next.run(request).await) 43 | } 44 | -------------------------------------------------------------------------------- /crates/openai/src/serve/middleware/limit.rs: -------------------------------------------------------------------------------- 1 | use crate::serve::error::{ProxyError, ResponseError}; 2 | use axum::{ 3 | extract::{ConnectInfo, State}, 4 | http::Request, 5 | middleware::Next, 6 | response::Response, 7 | }; 8 | 9 | use super::tokenbucket::{TokenBucket, TokenBucketProvider}; 10 | 11 | pub(crate) async fn limit_middleware( 12 | State(limit): State>, 13 | ConnectInfo(socket_addr): ConnectInfo, 14 | request: Request, 15 | next: Next, 16 | ) -> Result { 17 | let addr = socket_addr.ip(); 18 | match limit.acquire(addr) { 19 | Ok(condition) => match condition { 20 | true => Ok(next.run(request).await), 21 | false => Err(ResponseError::TooManyRequests(ProxyError::TooManyRequests)), 22 | }, 23 | Err(err) => Err(ResponseError::BadGateway(err)), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/openai/src/serve/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod csrf; 3 | #[cfg(feature = "limit")] 4 | pub mod limit; 5 | #[cfg(feature = "limit")] 6 | pub mod tokenbucket; 7 | -------------------------------------------------------------------------------- /crates/openai/src/serve/preauth.rs: -------------------------------------------------------------------------------- 1 | use crate::with_context; 2 | use axum_extra::extract::CookieJar; 3 | use mitm::proxy::hyper::{ 4 | body::Body, 5 | http::{HeaderMap, HeaderValue, Request, Response}, 6 | }; 7 | use mitm::proxy::{handler::HttpHandler, mitm::RequestOrResponse}; 8 | use std::fmt::Write; 9 | 10 | #[derive(Clone)] 11 | pub struct PreAuthHanlder; 12 | 13 | impl HttpHandler for PreAuthHanlder { 14 | fn handle_request(&self, req: Request) -> RequestOrResponse { 15 | log_req(&req); 16 | collect_preauth_cookie(req.headers()); 17 | RequestOrResponse::Request(req) 18 | } 19 | 20 | fn handle_response(&self, res: Response) -> Response { 21 | log_res(&res); 22 | collect_preauth_cookie(res.headers()); 23 | res 24 | } 25 | } 26 | 27 | fn collect_preauth_cookie(headers: &HeaderMap) { 28 | let jar = CookieJar::from_headers(headers); 29 | for c in jar.iter() { 30 | // Preauth cookie max age 31 | if c.name().eq("_preauth_devicecheck") { 32 | let max_age = c.max_age().map(|a| a.as_seconds_f32() as u32); 33 | with_context!(push_preauth_cookie, c.value(), max_age); 34 | } 35 | } 36 | } 37 | 38 | pub fn log_req(req: &Request) { 39 | let headers = req.headers(); 40 | let mut header_formated = String::new(); 41 | for (key, value) in headers { 42 | let v = match value.to_str() { 43 | Ok(v) => v.to_string(), 44 | Err(_) => { 45 | format!("[u8]; {}", value.len()) 46 | } 47 | }; 48 | write!( 49 | &mut header_formated, 50 | "\t{:<20}{}\r\n", 51 | format!("{}:", key.as_str()), 52 | v 53 | ) 54 | .unwrap(); 55 | } 56 | 57 | tracing::debug!( 58 | "{} {} 59 | Headers: 60 | {}", 61 | req.method(), 62 | req.uri().to_string(), 63 | header_formated 64 | ) 65 | } 66 | 67 | pub fn log_res(res: &Response) { 68 | let headers = res.headers(); 69 | let mut header_formated = String::new(); 70 | for (key, value) in headers { 71 | let v = match value.to_str() { 72 | Ok(v) => v.to_string(), 73 | Err(_) => { 74 | format!("[u8]; {}", value.len()) 75 | } 76 | }; 77 | write!( 78 | &mut header_formated, 79 | "\t{:<20}{}\r\n", 80 | format!("{}:", key.as_str()), 81 | v 82 | ) 83 | .unwrap(); 84 | } 85 | 86 | tracing::debug!( 87 | "{} {:?} 88 | Headers: 89 | {}", 90 | res.status(), 91 | res.version(), 92 | header_formated 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /crates/openai/src/serve/proxy/ext.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use axum::body::Bytes; 4 | use axum::response::{IntoResponse, Response}; 5 | use axum::{ 6 | async_trait, 7 | extract::FromRequest, 8 | http::{self, Request}, 9 | }; 10 | use axum_extra::extract::CookieJar; 11 | use http::header::CONTENT_TYPE; 12 | use http::{header, Uri}; 13 | use typed_builder::TypedBuilder; 14 | 15 | use crate::serve::error::ResponseError; 16 | 17 | /// Context extension. 18 | #[derive(TypedBuilder)] 19 | pub struct Context { 20 | // Enable stream 21 | pub stream: bool, 22 | // Mapper model 23 | pub model: String, 24 | } 25 | 26 | /// Response extension. 27 | #[derive(TypedBuilder)] 28 | pub struct ResponseExt { 29 | #[builder(setter(into), default)] 30 | pub context: Option, 31 | pub inner: reqwest::Response, 32 | } 33 | 34 | /// Extractor for request parts. 35 | pub struct RequestExt { 36 | pub uri: Uri, 37 | pub method: http::Method, 38 | pub headers: http::HeaderMap, 39 | pub jar: CookieJar, 40 | pub body: Option, 41 | } 42 | 43 | impl RequestExt { 44 | /// Trim start path. 45 | pub(crate) fn trim_start_path(&mut self, path: &str) -> Result<(), ResponseError> { 46 | let path_and_query = self 47 | .uri 48 | .path_and_query() 49 | .map(|v| v.as_str()) 50 | .unwrap_or(self.uri.path()); 51 | let path = path_and_query.trim_start_matches(path); 52 | self.uri = Uri::from_str(path).map_err(ResponseError::BadRequest)?; 53 | Ok(()) 54 | } 55 | 56 | /// Append header. 57 | pub(crate) fn append_haeder( 58 | &mut self, 59 | name: header::HeaderName, 60 | value: &str, 61 | ) -> Result<(), ResponseError> { 62 | self.headers.insert( 63 | name, 64 | header::HeaderValue::from_str(value).map_err(ResponseError::BadRequest)?, 65 | ); 66 | Ok(()) 67 | } 68 | 69 | /// Get bearer auth. 70 | pub(crate) fn bearer_auth(&self) -> Option<&str> { 71 | let mut value = self.headers.get_all(header::AUTHORIZATION).iter(); 72 | let is_missing = value.size_hint() == (0, Some(0)); 73 | if is_missing { 74 | return None; 75 | } 76 | 77 | value.find_map(|v| { 78 | v.to_str().ok().and_then(|s| { 79 | let parts: Vec<&str> = s.split_whitespace().collect(); 80 | match parts.as_slice() { 81 | ["Bearer", token] => Some(*token), 82 | _ => None, 83 | } 84 | }) 85 | }) 86 | } 87 | } 88 | 89 | #[async_trait] 90 | pub(crate) trait SendRequestExt { 91 | async fn send_request( 92 | &self, 93 | origin: &'static str, 94 | req: RequestExt, 95 | ) -> Result; 96 | } 97 | 98 | #[async_trait] 99 | impl FromRequest for RequestExt 100 | where 101 | Bytes: FromRequest, 102 | B: Send + 'static, 103 | S: Send + Sync, 104 | { 105 | type Rejection = Response; 106 | 107 | async fn from_request(req: Request, state: &S) -> Result { 108 | let (parts, body) = req.into_parts(); 109 | 110 | let body = if parts.headers.get(CONTENT_TYPE).is_some() { 111 | Some( 112 | Bytes::from_request(Request::new(body), state) 113 | .await 114 | .map_err(IntoResponse::into_response)?, 115 | ) 116 | } else { 117 | None 118 | }; 119 | 120 | Ok(RequestExt { 121 | uri: parts.uri, 122 | jar: CookieJar::from_headers(&parts.headers), 123 | method: parts.method, 124 | headers: parts.headers, 125 | body, 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/openai/src/serve/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ext; 2 | pub mod req; 3 | pub mod resp; 4 | mod toapi; 5 | 6 | use super::error::ResponseError; 7 | use crate::constant::CF_CLEARANCE; 8 | use crate::constant::PUID; 9 | use crate::debug; 10 | use axum::http::header; 11 | use axum::http::HeaderMap; 12 | use axum_extra::extract::CookieJar; 13 | 14 | /// Request headers convert 15 | pub(crate) fn header_convert( 16 | h: &HeaderMap, 17 | jar: &CookieJar, 18 | origin: &'static str, 19 | ) -> Result { 20 | let mut headers = HeaderMap::new(); 21 | let mut cookies = Vec::new(); 22 | 23 | // Support for team accounts. 24 | // The chat will be sent to the team account if the header is present, otherwise 25 | // it will be sent to the personal account. 26 | h.get("Chatgpt-Account-Id").map(|h| { 27 | headers.insert("Chatgpt-Account-Id", h.clone()); 28 | cookies.push(format!("_account={}", h.to_str().unwrap_or_default())); 29 | }); 30 | 31 | h.get("Access-Control-Request-Headers") 32 | .map(|h| headers.insert("Access-Control-Request-Headers", h.clone())); 33 | 34 | h.get("Access-Control-Request-Method").map(|h| { 35 | headers.insert("Access-Control-Request-Method", h.clone()); 36 | }); 37 | 38 | h.get("X-Ms-Blob-Type") 39 | .map(|v| headers.insert("X-Ms-Blob-Type", v.clone())); 40 | 41 | h.get("X-Ms-Version") 42 | .map(|v| headers.insert("X-Ms-Version", v.clone())); 43 | 44 | h.get(header::ACCEPT_LANGUAGE) 45 | .map(|h| headers.insert(header::ACCEPT_LANGUAGE, h.clone())) 46 | .flatten() 47 | .or_else(|| { 48 | headers.insert( 49 | header::ACCEPT_LANGUAGE, 50 | header::HeaderValue::from_static("en-US,en;q=0.9"), 51 | ) 52 | }); 53 | 54 | h.get(header::ACCEPT_ENCODING) 55 | .map(|h| headers.insert(header::ACCEPT_ENCODING, h.clone())) 56 | .flatten() 57 | .or_else(|| { 58 | headers.insert( 59 | header::ACCEPT_ENCODING, 60 | header::HeaderValue::from_static("gzip, deflate, br"), 61 | ) 62 | }); 63 | 64 | h.get(header::DNT) 65 | .map(|h| headers.insert(header::DNT, h.clone())) 66 | .flatten() 67 | .or_else(|| headers.insert(header::DNT, header::HeaderValue::from_static("1"))); 68 | 69 | h.get(header::UPGRADE_INSECURE_REQUESTS) 70 | .map(|h| headers.insert(header::UPGRADE_INSECURE_REQUESTS, h.clone())) 71 | .flatten() 72 | .or_else(|| { 73 | headers.insert( 74 | header::UPGRADE_INSECURE_REQUESTS, 75 | header::HeaderValue::from_static("1"), 76 | ) 77 | }); 78 | 79 | h.get(header::AUTHORIZATION) 80 | .map(|h| headers.insert(header::AUTHORIZATION, h.clone())); 81 | 82 | h.get(header::CONTENT_TYPE) 83 | .map(|h| headers.insert(header::CONTENT_TYPE, h.clone())); 84 | 85 | headers.insert(header::ORIGIN, header::HeaderValue::from_static(origin)); 86 | headers.insert(header::REFERER, header::HeaderValue::from_static(origin)); 87 | 88 | jar.iter() 89 | .filter(|c| { 90 | let name = c.name().to_lowercase(); 91 | name.eq(PUID) || name.eq(CF_CLEARANCE) 92 | }) 93 | .for_each(|c| { 94 | let c = format!("{}={}", c.name(), cookie_encoded(c.value())); 95 | debug!("cookie: {}", c); 96 | cookies.push(c); 97 | }); 98 | 99 | // setting cookie 100 | if !cookies.is_empty() { 101 | headers.insert( 102 | header::COOKIE, 103 | header::HeaderValue::from_str(&cookies.join(";")) 104 | .map_err(ResponseError::InternalServerError)?, 105 | ); 106 | } 107 | Ok(headers) 108 | } 109 | 110 | fn cookie_encoded(input: &str) -> String { 111 | let separator = ':'; 112 | if let Some((name, value)) = input.split_once(separator) { 113 | let encoded_value = value 114 | .chars() 115 | .map(|ch| match ch { 116 | '!' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | '/' | ':' 117 | | ';' | '=' | '?' | '@' | '[' | ']' | '~' => { 118 | format!("%{:02X}", ch as u8) 119 | } 120 | _ => ch.to_string(), 121 | }) 122 | .collect::(); 123 | 124 | format!("{name}:{encoded_value}") 125 | } else { 126 | input.to_string() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/openai/src/serve/proxy/resp.rs: -------------------------------------------------------------------------------- 1 | use std::time::UNIX_EPOCH; 2 | 3 | use crate::constant::{CF_CLEARANCE, NINJA_VERSION, PUID}; 4 | use crate::with_context; 5 | use crate::LIB_VERSION; 6 | use axum::body::Body; 7 | use axum::body::StreamBody; 8 | use axum::http::header; 9 | use axum::response::{IntoResponse, Response}; 10 | use axum_extra::extract::cookie; 11 | use axum_extra::extract::cookie::Cookie; 12 | use serde_json::Value; 13 | 14 | use crate::serve::error::ResponseError; 15 | 16 | use super::ext::ResponseExt; 17 | use super::toapi; 18 | 19 | /// Response convert 20 | pub(crate) async fn response_convert( 21 | resp: ResponseExt, 22 | ) -> Result { 23 | // If to api is some, then convert to api response 24 | if resp.context.is_some() { 25 | return Ok(toapi::response_convert(resp).await?.into_response()); 26 | } 27 | 28 | // Build new response 29 | let mut builder = Response::builder() 30 | .status(resp.inner.status()) 31 | .header(NINJA_VERSION, LIB_VERSION); 32 | 33 | // Copy headers except for "set-cookie" 34 | for kv in resp 35 | .inner 36 | .headers() 37 | .into_iter() 38 | .filter(|(k, _)| k.ne(&header::SET_COOKIE) && k.ne(&header::CONTENT_LENGTH)) 39 | { 40 | builder = builder.header(kv.0, kv.1); 41 | } 42 | 43 | // Filter and transform cookies 44 | for cookie in resp.inner.cookies() { 45 | let name = cookie.name().to_lowercase(); 46 | if name.eq(PUID) || name.eq(CF_CLEARANCE) { 47 | if let Some(expires) = cookie.expires() { 48 | let timestamp_secs = expires 49 | .duration_since(UNIX_EPOCH) 50 | .map_err(ResponseError::InternalServerError)? 51 | .as_secs_f64(); 52 | let cookie = Cookie::build(cookie.name(), cookie.value()) 53 | .path("/") 54 | .max_age(time::Duration::seconds_f64(timestamp_secs)) 55 | .same_site(cookie::SameSite::Lax) 56 | .secure(false) 57 | .http_only(false) 58 | .finish(); 59 | builder = builder.header(header::SET_COOKIE, cookie.to_string()); 60 | } 61 | } 62 | } 63 | 64 | // Modify files endpoint response 65 | if with_context!(enable_file_proxy) && resp.inner.url().path().contains("/backend-api/files") { 66 | let url = resp.inner.url().clone(); 67 | // Files endpoint handling 68 | let mut json = resp 69 | .inner 70 | .json::() 71 | .await 72 | .map_err(ResponseError::BadRequest)?; 73 | 74 | let body_key = if url.path().contains("download") || url.path().contains("uploaded") { 75 | "download_url" 76 | } else { 77 | "upload_url" 78 | }; 79 | 80 | if let Some(download_upload_url) = json.get_mut(body_key) { 81 | if let Some(download_url_str) = download_upload_url.as_str() { 82 | const FILES_ENDPOINT: &str = "https://files.oaiusercontent.com"; 83 | if download_url_str.starts_with(FILES_ENDPOINT) { 84 | *download_upload_url = 85 | serde_json::json!(download_url_str.replace(FILES_ENDPOINT, "/files")); 86 | } 87 | } 88 | } 89 | 90 | let json_bytes = serde_json::to_vec(&json)?; 91 | Ok(builder 92 | .body(StreamBody::new(Body::from(json_bytes))) 93 | .map_err(ResponseError::InternalServerError)? 94 | .into_response()) 95 | } else { 96 | // Non-files endpoint handling 97 | Ok(builder 98 | .body(StreamBody::new(resp.inner.bytes_stream())) 99 | .map_err(ResponseError::InternalServerError)? 100 | .into_response()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/openai/src/serve/proxy/toapi/model.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::chatgpt::model::Role; 4 | use serde::Serialize; 5 | use typed_builder::TypedBuilder; 6 | 7 | #[derive(Deserialize)] 8 | pub struct Req { 9 | pub model: String, 10 | pub messages: Vec, 11 | #[serde(default)] 12 | pub stream: bool, 13 | } 14 | 15 | #[derive(Serialize, TypedBuilder, Clone)] 16 | pub struct Resp<'a> { 17 | id: &'a str, 18 | object: &'a str, 19 | created: &'a i64, 20 | model: &'a str, 21 | choices: Vec>, 22 | #[builder(default)] 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | usage: Option, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, TypedBuilder, Clone)] 28 | pub struct Message { 29 | pub role: Role, 30 | pub content: String, 31 | } 32 | 33 | #[derive(Serialize, TypedBuilder, Clone)] 34 | pub struct Usage { 35 | pub prompt_tokens: i64, 36 | pub completion_tokens: i64, 37 | pub total_tokens: i64, 38 | } 39 | 40 | #[derive(Serialize, TypedBuilder, Clone)] 41 | pub struct Choice<'a> { 42 | pub index: i64, 43 | #[builder(default)] 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub message: Option, 46 | #[builder(default)] 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub delta: Option>, 49 | #[builder(default)] 50 | pub finish_reason: Option<&'a str>, 51 | } 52 | 53 | #[derive(Serialize, TypedBuilder, Clone)] 54 | pub struct Delta<'a> { 55 | #[builder(default)] 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub role: Option<&'a Role>, 58 | #[builder(default)] 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub content: Option<&'a str>, 61 | } 62 | -------------------------------------------------------------------------------- /crates/openai/src/serve/puid.rs: -------------------------------------------------------------------------------- 1 | use super::error::{ProxyError, ResponseError}; 2 | use crate::{gpt_model::GPTModel, with_context, URL_CHATGPT_API}; 3 | use moka::sync::Cache; 4 | use std::str::FromStr; 5 | use tokio::sync::OnceCell; 6 | 7 | static PUID_CACHE: OnceCell> = OnceCell::const_new(); 8 | 9 | pub(super) fn reduce_key(token: &str) -> Result { 10 | let token_profile = crate::token::check(token) 11 | .map_err(ResponseError::Unauthorized)? 12 | .ok_or(ResponseError::BadRequest(ProxyError::InvalidAccessToken))?; 13 | Ok(token_profile.email().to_owned()) 14 | } 15 | 16 | async fn cache() -> &'static Cache { 17 | PUID_CACHE 18 | .get_or_init(|| async { 19 | Cache::builder() 20 | .time_to_live(std::time::Duration::from_secs(3600 * 24)) 21 | .build() 22 | }) 23 | .await 24 | } 25 | 26 | pub(super) async fn get_or_init( 27 | token: &str, 28 | model: &str, 29 | cache_id: String, 30 | ) -> Result, ResponseError> { 31 | let token = token.trim_start_matches("Bearer "); 32 | let puid_cache = cache().await; 33 | 34 | if let Some(p) = puid_cache.get(&cache_id) { 35 | return Ok(Some(p.clone())); 36 | } 37 | 38 | if GPTModel::from_str(model)?.is_gpt4() { 39 | let resp = with_context!(api_client) 40 | .get(format!("{URL_CHATGPT_API}/backend-api/models")) 41 | .bearer_auth(token) 42 | .send() 43 | .await 44 | .map_err(ResponseError::InternalServerError)? 45 | .error_for_status() 46 | .map_err(ResponseError::BadRequest)?; 47 | 48 | if let Some(c) = resp.cookies().into_iter().find(|c| c.name().eq("_puid")) { 49 | let puid = c.value().to_owned(); 50 | puid_cache.insert(cache_id, puid.clone()); 51 | return Ok(Some(puid)); 52 | }; 53 | } 54 | 55 | Ok(None) 56 | } 57 | -------------------------------------------------------------------------------- /crates/openai/src/serve/router/chat/cookier.rs: -------------------------------------------------------------------------------- 1 | use crate::constant::EMPTY; 2 | 3 | use super::HOME_INDEX; 4 | use axum_extra::extract::cookie; 5 | 6 | pub fn build_cookie<'a>( 7 | key: &'a str, 8 | value: String, 9 | timestamp: i64, 10 | ) -> anyhow::Result> { 11 | let cookie = cookie::Cookie::build(key, value.to_owned()) 12 | .path(HOME_INDEX) 13 | .same_site(cookie::SameSite::Lax) 14 | .expires(time::OffsetDateTime::from_unix_timestamp(timestamp)?) 15 | .secure(false) 16 | .http_only(false) 17 | .finish(); 18 | Ok(cookie) 19 | } 20 | 21 | pub fn clear_cookie<'a>(key: &'a str) -> cookie::Cookie<'a> { 22 | let cookie = cookie::Cookie::build(key, EMPTY) 23 | .path(HOME_INDEX) 24 | .same_site(cookie::SameSite::Lax) 25 | .max_age(time::Duration::seconds(0)) 26 | .secure(false) 27 | .http_only(false) 28 | .finish(); 29 | cookie 30 | } 31 | -------------------------------------------------------------------------------- /crates/openai/src/serve/router/chat/session/session.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::token::TokenProfile; 4 | use crate::{serve::error::ResponseError, token::model::Token}; 5 | use base64::Engine; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// ChatGPT session 9 | #[derive(Serialize, Deserialize)] 10 | pub struct Session { 11 | pub access_token: String, 12 | pub refresh_token: Option, 13 | #[serde(skip_serializing)] 14 | pub session_token: Option, 15 | pub user_id: String, 16 | pub email: String, 17 | pub expires: i64, 18 | } 19 | 20 | impl Session { 21 | /// Convert session to base64 string 22 | pub fn to_string(&self) -> Result { 23 | let json = serde_json::to_string(&self).map_err(ResponseError::Unauthorized)?; 24 | Ok(base64::engine::general_purpose::STANDARD.encode(json.as_bytes())) 25 | } 26 | } 27 | 28 | /// Parse session from base64 string 29 | impl FromStr for Session { 30 | type Err = ResponseError; 31 | 32 | fn from_str(s: &str) -> Result { 33 | let data = base64::engine::general_purpose::STANDARD 34 | .decode(s) 35 | .map_err(ResponseError::Unauthorized)?; 36 | serde_json::from_slice(&data).map_err(ResponseError::Unauthorized) 37 | } 38 | } 39 | 40 | /// Convert token to session 41 | impl From for Session { 42 | fn from(value: Token) -> Self { 43 | Session { 44 | user_id: value.user_id().to_owned(), 45 | email: value.email().to_owned(), 46 | expires: value.expires(), 47 | access_token: value.access_token().to_owned(), 48 | refresh_token: value.refresh_token().map(|v| v.to_owned()), 49 | session_token: value.session_token().map(|v| v.to_owned()), 50 | } 51 | } 52 | } 53 | 54 | /// Parse from access token and token profile 55 | impl From<(&str, TokenProfile)> for Session { 56 | fn from(value: (&str, TokenProfile)) -> Self { 57 | Session { 58 | user_id: value.1.user_id().to_owned(), 59 | email: value.1.email().to_owned(), 60 | expires: value.1.expires(), 61 | access_token: value.0.to_owned(), 62 | refresh_token: None, 63 | session_token: None, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/openai/src/serve/router/files.rs: -------------------------------------------------------------------------------- 1 | use axum::http::header; 2 | use axum::{response::IntoResponse, routing::any, Router}; 3 | 4 | use crate::{ 5 | context::args::Args, 6 | serve::{ 7 | error::ResponseError, proxy::ext::RequestExt, proxy::ext::SendRequestExt, 8 | proxy::resp::response_convert, 9 | }, 10 | with_context, 11 | }; 12 | 13 | /// file endpoint proxy 14 | pub(super) fn config(router: Router, args: &Args) -> Router { 15 | if args.enable_file_proxy { 16 | router.route("/files/*path", any(proxy)) 17 | } else { 18 | router 19 | } 20 | } 21 | 22 | async fn proxy(mut req: RequestExt) -> Result { 23 | req.trim_start_path("/files")?; 24 | req.append_haeder(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")?; 25 | let resp = with_context!(api_client) 26 | .send_request("https://files.oaiusercontent.com", req) 27 | .await?; 28 | response_convert(resp).await 29 | } 30 | -------------------------------------------------------------------------------- /crates/openai/src/serve/router/har/token.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use jsonwebtokens::{encode, Algorithm, AlgorithmID, Verifier}; 4 | use serde_json::json; 5 | use tokio::sync::OnceCell; 6 | 7 | use crate::{ 8 | arkose::{self}, 9 | generate_random_string, 10 | homedir::home_dir, 11 | now_duration, with_context, 12 | }; 13 | 14 | static TOKEN_SECRET: OnceCell = OnceCell::const_new(); 15 | pub(super) const EXP: u64 = 3600 * 24; 16 | 17 | async fn get_or_init_secret() -> &'static String { 18 | TOKEN_SECRET 19 | .get_or_init(|| async { 20 | let path = home_dir() 21 | .unwrap_or_else(|| PathBuf::from(".")) 22 | .join(".token_secret"); 23 | let key = if let Some(upload_key) = with_context!(auth_key) { 24 | upload_key.to_owned() 25 | } else { 26 | generate_random_string(31) 27 | }; 28 | let x = arkose::murmur::murmurhash3_x64_128(key.as_bytes(), 31); 29 | let s = format!("{:x}{:x}", x.0, x.1,); 30 | tokio::fs::write(&path, &s) 31 | .await 32 | .expect("write token secret to file"); 33 | s 34 | }) 35 | .await 36 | } 37 | 38 | pub(super) async fn generate_token() -> anyhow::Result { 39 | let s = get_or_init_secret().await; 40 | let alg = Algorithm::new_hmac(AlgorithmID::HS256, s.to_owned())?; 41 | let header = json!({ "alg": alg.name() }); 42 | let claims = json!({ 43 | "exp": now_duration()?.as_secs() + EXP, 44 | }); 45 | Ok(encode(&header, &claims, &alg)?) 46 | } 47 | 48 | pub(super) async fn verifier(token_str: &str) -> anyhow::Result<()> { 49 | let s = get_or_init_secret().await; 50 | let alg = Algorithm::new_hmac(AlgorithmID::HS256, s.to_owned())?; 51 | let verifier = Verifier::create().build()?; 52 | let _ = verifier.verify(&token_str, &alg)?; 53 | Ok(()) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | 59 | use super::*; 60 | 61 | #[tokio::test] 62 | async fn test_generate_token() { 63 | let token = generate_token().await.unwrap(); 64 | println!("{}", token); 65 | } 66 | 67 | #[tokio::test] 68 | async fn test_verifier() { 69 | let token = generate_token().await.unwrap(); 70 | verifier(&token).await.unwrap(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/openai/src/serve/router/mod.rs: -------------------------------------------------------------------------------- 1 | mod chat; 2 | mod files; 3 | mod har; 4 | 5 | use crate::context::args::Args; 6 | use crate::serve::error::ResponseError; 7 | use axum::http::header; 8 | use axum::http::StatusCode; 9 | use axum::{body::Body, extract::Path, http::Response, Router}; 10 | use std::collections::HashMap; 11 | use tokio::sync::OnceCell; 12 | 13 | pub(super) fn config(router: Router, args: &Args) -> Router { 14 | let router = files::config(router, args); 15 | let router = har::config(router, args); 16 | let router = chat::config(router, args); 17 | router 18 | } 19 | 20 | include!(concat!(env!("OUT_DIR"), "/generated.rs")); 21 | 22 | /// Build-in static files 23 | static STATIC_FILES: OnceCell> = 24 | OnceCell::const_new(); 25 | 26 | /// Get static resource 27 | async fn get_static_resource(path: Path) -> Result, ResponseError> { 28 | let path = path.0; 29 | let mut static_files = STATIC_FILES 30 | .get_or_init(|| async { generate() }) 31 | .await 32 | .iter(); 33 | match static_files.find(|(k, _)| k.contains(&path)) { 34 | Some((_, v)) => { 35 | let mime_type = if v.mime_type.eq(mime::APPLICATION_OCTET_STREAM.as_ref()) { 36 | mime::TEXT_HTML.as_ref() 37 | } else { 38 | v.mime_type 39 | }; 40 | create_response_with_data(StatusCode::OK, mime_type, v.data) 41 | } 42 | None => Ok(Response::builder() 43 | .status(StatusCode::NOT_FOUND) 44 | .body(Body::empty()) 45 | .map_err(ResponseError::InternalServerError)?), 46 | } 47 | } 48 | 49 | fn create_response_with_data( 50 | status: StatusCode, 51 | content_type: &str, 52 | data: impl Into, 53 | ) -> Result, ResponseError> { 54 | Response::builder() 55 | .status(status) 56 | .header(header::CONTENT_TYPE, content_type) 57 | .header("Access-Control-Allow-Credentials", "true") 58 | .header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,HEAD,CONNECT,OPTIONS,TRACE") 59 | .header("Access-Control-Allow-Origin", "*") 60 | .header("Access-Control-Allow-Headers", "Origin,Content-Type,Accept,User-Agent,Cookie,Authorization,X-Auth-Token,X-Requested-With") 61 | .header("Access-Control-Max-Age", "3628800") 62 | .header(header::CONTENT_SECURITY_POLICY, "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: *.arkoselabs.com *.funcaptcha.com *.arkoselabs.cn *.arkose.com.cn *.chat.openai.com;") 63 | .body(data.into()) 64 | .map_err(ResponseError::InternalServerError) 65 | } 66 | -------------------------------------------------------------------------------- /crates/openai/src/serve/signal.rs: -------------------------------------------------------------------------------- 1 | use crate::info; 2 | use axum_server::Handle; 3 | use std::time::Duration; 4 | #[cfg(target_family = "unix")] 5 | use tokio::signal::unix::{signal, SignalKind}; 6 | use tokio::time::sleep; 7 | 8 | pub(super) async fn graceful_shutdown(handle: Handle) { 9 | #[cfg(target_family = "windows")] 10 | { 11 | tokio::signal::ctrl_c() 12 | .await 13 | .expect("Ctrl+C signal hanlde error"); 14 | sending_graceful_shutdown_signal(handle, "SIGINT").await; 15 | } 16 | 17 | #[cfg(target_family = "unix")] 18 | { 19 | let mut sigterm = signal(SignalKind::terminate()).expect("SIGTERM signal hanlde error"); 20 | let mut sigquit = signal(SignalKind::quit()).expect("SIGQUIT signal hanlde error"); 21 | let mut sigchld = signal(SignalKind::child()).expect("SIGCHLD signal hanlde error"); 22 | let mut sighup = signal(SignalKind::hangup()).expect("SIGHUP signal hanlde error"); 23 | tokio::select! { 24 | _ = sigterm.recv() => { 25 | sending_graceful_shutdown_signal(handle, "SIGTERM").await; 26 | }, 27 | _ = sigquit.recv() => { 28 | sending_graceful_shutdown_signal(handle, "SIGQUIT").await; 29 | }, 30 | _ = sigchld.recv() => { 31 | sending_graceful_shutdown_signal(handle, "SIGCHLD").await; 32 | }, 33 | _ = sighup.recv() => { 34 | sending_graceful_shutdown_signal(handle, "SIGHUP").await; 35 | }, 36 | _ = tokio::signal::ctrl_c() => { 37 | sending_graceful_shutdown_signal(handle, "SIGINT").await; 38 | } 39 | }; 40 | } 41 | } 42 | 43 | async fn sending_graceful_shutdown_signal(handle: Handle, signal: &'static str) { 44 | info!("{signal} received: starting graceful shutdown"); 45 | 46 | // Signal the server to shutdown using Handle. 47 | handle.graceful_shutdown(Some(Duration::from_secs(3))); 48 | 49 | // Print alive connection count every second. 50 | loop { 51 | sleep(Duration::from_secs(1)).await; 52 | info!("Alive connections: {}", handle.connection_count()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/openai/src/serve/turnstile.rs: -------------------------------------------------------------------------------- 1 | use crate::{serve::error::ProxyError, with_context}; 2 | use std::net::IpAddr; 3 | 4 | pub(super) async fn cf_turnstile_check( 5 | addr: IpAddr, 6 | cf_response: Option<&str>, 7 | ) -> Result<(), ProxyError> { 8 | #[derive(serde::Serialize)] 9 | struct CfCaptchaForm<'a> { 10 | secret: &'a str, 11 | response: &'a str, 12 | remoteip: &'a IpAddr, 13 | idempotency_key: String, 14 | } 15 | 16 | let ctx = with_context!(); 17 | 18 | if let Some(turnsile) = ctx.cf_turnstile() { 19 | let response = cf_response 20 | .filter(|r| !r.is_empty()) 21 | .ok_or_else(|| ProxyError::CfMissingCaptcha)?; 22 | 23 | let form = CfCaptchaForm { 24 | secret: &turnsile.secret_key, 25 | response, 26 | remoteip: &addr, 27 | idempotency_key: crate::uuid::uuid(), 28 | }; 29 | 30 | let _ = ctx 31 | .api_client() 32 | .post("https://challenges.cloudflare.com/turnstile/v0/siteverify") 33 | .form(&form) 34 | .send() 35 | .await 36 | .map_err(ProxyError::RequestError)? 37 | .error_for_status() 38 | .map_err(ProxyError::CfError)?; 39 | } 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /crates/openai/src/serve/whitelist.rs: -------------------------------------------------------------------------------- 1 | use super::error::ProxyError; 2 | use crate::with_context; 3 | 4 | pub(super) fn check_whitelist(identify: &str) -> Result<(), ProxyError> { 5 | if let Some(w) = with_context!(visitor_email_whitelist) { 6 | if !w.is_empty() { 7 | w.iter() 8 | .find(|&w| w.eq(identify)) 9 | .ok_or(ProxyError::AccessNotInWhitelist)?; 10 | } 11 | } 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /crates/openai/src/unescape.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use std::char; 4 | 5 | macro_rules! try_option { 6 | ($o:expr) => { 7 | match $o { 8 | Some(s) => s, 9 | None => return None, 10 | } 11 | }; 12 | } 13 | 14 | // Takes in a string with backslash escapes written out with literal backslash characters and 15 | // converts it to a string with the proper escaped characters. 16 | pub fn unescape(s: &str) -> Option { 17 | let mut queue: VecDeque<_> = String::from(s).chars().collect(); 18 | let mut s = String::new(); 19 | 20 | while let Some(c) = queue.pop_front() { 21 | if c != '\\' { 22 | s.push(c); 23 | continue; 24 | } 25 | 26 | match queue.pop_front() { 27 | Some('b') => s.push('\u{0008}'), 28 | Some('f') => s.push('\u{000C}'), 29 | Some('n') => s.push('\n'), 30 | Some('r') => s.push('\r'), 31 | Some('t') => s.push('\t'), 32 | Some('\'') => s.push('\''), 33 | Some('\"') => s.push('\"'), 34 | Some('\\') => s.push('\\'), 35 | Some('u') => s.push(try_option!(unescape_unicode(&mut queue))), 36 | Some('x') => s.push(try_option!(unescape_byte(&mut queue))), 37 | Some(c) if c.is_digit(8) => s.push(try_option!(unescape_octal(c, &mut queue))), 38 | _ => return None, 39 | }; 40 | } 41 | 42 | Some(s) 43 | } 44 | 45 | fn unescape_unicode(queue: &mut VecDeque) -> Option { 46 | let mut s = String::new(); 47 | 48 | for _ in 0..4 { 49 | s.push(try_option!(queue.pop_front())); 50 | } 51 | 52 | let u = try_option!(u32::from_str_radix(&s, 16).ok()); 53 | char::from_u32(u) 54 | } 55 | 56 | fn unescape_byte(queue: &mut VecDeque) -> Option { 57 | let mut s = String::new(); 58 | 59 | for _ in 0..2 { 60 | s.push(try_option!(queue.pop_front())); 61 | } 62 | 63 | let u = try_option!(u32::from_str_radix(&s, 16).ok()); 64 | char::from_u32(u) 65 | } 66 | 67 | fn unescape_octal(c: char, queue: &mut VecDeque) -> Option { 68 | match unescape_octal_leading(c, queue) { 69 | Some(ch) => { 70 | let _ = queue.pop_front(); 71 | let _ = queue.pop_front(); 72 | Some(ch) 73 | } 74 | None => unescape_octal_no_leading(c, queue), 75 | } 76 | } 77 | 78 | fn unescape_octal_leading(c: char, queue: &VecDeque) -> Option { 79 | if c != '0' && c != '1' && c != '2' && c != '3' { 80 | return None; 81 | } 82 | 83 | let mut s = String::new(); 84 | s.push(c); 85 | s.push(*try_option!(queue.get(0))); 86 | s.push(*try_option!(queue.get(1))); 87 | 88 | let u = try_option!(u32::from_str_radix(&s, 8).ok()); 89 | char::from_u32(u) 90 | } 91 | 92 | fn unescape_octal_no_leading(c: char, queue: &mut VecDeque) -> Option { 93 | let mut s = String::new(); 94 | s.push(c); 95 | s.push(try_option!(queue.pop_front())); 96 | 97 | let u = try_option!(u32::from_str_radix(&s, 8).ok()); 98 | char::from_u32(u) 99 | } 100 | -------------------------------------------------------------------------------- /crates/openai/src/urldecoding.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::string::FromUtf8Error; 3 | 4 | #[inline] 5 | pub(crate) fn from_hex_digit(digit: u8) -> Option { 6 | match digit { 7 | b'0'..=b'9' => Some(digit - b'0'), 8 | b'A'..=b'F' => Some(digit - b'A' + 10), 9 | b'a'..=b'f' => Some(digit - b'a' + 10), 10 | _ => None, 11 | } 12 | } 13 | 14 | /// Decode percent-encoded string assuming UTF-8 encoding. 15 | /// 16 | /// If you need a `String`, call `.into_owned()` (not `.to_owned()`). 17 | /// 18 | /// Unencoded `+` is preserved literally, and _not_ changed to a space. 19 | pub fn decode(data: &str) -> Result, FromUtf8Error> { 20 | match decode_binary(data.as_bytes()) { 21 | Cow::Borrowed(_) => Ok(Cow::Borrowed(data)), 22 | Cow::Owned(s) => Ok(Cow::Owned(String::from_utf8(s)?)), 23 | } 24 | } 25 | 26 | /// Decode percent-encoded string as binary data, in any encoding. 27 | /// 28 | /// Unencoded `+` is preserved literally, and _not_ changed to a space. 29 | pub fn decode_binary(data: &[u8]) -> Cow<[u8]> { 30 | let offset = data.iter().take_while(|&&c| c != b'%').count(); 31 | if offset >= data.len() { 32 | return Cow::Borrowed(data); 33 | } 34 | 35 | let mut decoded: Vec = Vec::with_capacity(data.len()); 36 | let mut out = NeverRealloc(&mut decoded); 37 | 38 | let (ascii, mut data) = data.split_at(offset); 39 | out.extend_from_slice(ascii); 40 | 41 | loop { 42 | let mut parts = data.splitn(2, |&c| c == b'%'); 43 | // first the decoded non-% part 44 | let non_escaped_part = parts.next().unwrap(); 45 | let rest = parts.next(); 46 | if rest.is_none() && out.0.is_empty() { 47 | // if empty there were no '%' in the string 48 | return data.into(); 49 | } 50 | out.extend_from_slice(non_escaped_part); 51 | 52 | // then decode one %xx 53 | match rest { 54 | Some(rest) => match rest.get(0..2) { 55 | Some(&[first, second]) => match from_hex_digit(first) { 56 | Some(first_val) => match from_hex_digit(second) { 57 | Some(second_val) => { 58 | out.push((first_val << 4) | second_val); 59 | data = &rest[2..]; 60 | } 61 | None => { 62 | out.extend_from_slice(&[b'%', first]); 63 | data = &rest[1..]; 64 | } 65 | }, 66 | None => { 67 | out.push(b'%'); 68 | data = rest; 69 | } 70 | }, 71 | _ => { 72 | // too short 73 | out.push(b'%'); 74 | out.extend_from_slice(rest); 75 | break; 76 | } 77 | }, 78 | None => break, 79 | } 80 | } 81 | Cow::Owned(decoded) 82 | } 83 | 84 | struct NeverRealloc<'a, T>(pub &'a mut Vec); 85 | 86 | impl NeverRealloc<'_, T> { 87 | #[inline] 88 | pub fn push(&mut self, val: T) { 89 | // these branches only exist to remove redundant reallocation code 90 | // (the capacity is always sufficient) 91 | if self.0.len() != self.0.capacity() { 92 | self.0.push(val); 93 | } 94 | } 95 | #[inline] 96 | pub fn extend_from_slice(&mut self, val: &[T]) 97 | where 98 | T: Clone, 99 | { 100 | if self.0.capacity() - self.0.len() >= val.len() { 101 | self.0.extend_from_slice(val); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/openai/src/uuid.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | pub fn uuid() -> String { 4 | let mut rng = rand::thread_rng(); 5 | let bytes: [u8; 16] = rng.gen(); 6 | format!( 7 | "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", 8 | bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], 9 | bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /crates/self_update/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/ 5 | -------------------------------------------------------------------------------- /crates/self_update/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "self_update" 3 | version = "0.39.0" 4 | description = "Self updates for standalone executables" 5 | edition = "2018" 6 | rust = "1.64" 7 | 8 | [dependencies] 9 | serde_json = "1.0.108" 10 | tempfile = "3.8.1" 11 | flate2 = { version = "1.0.28", optional = true } 12 | tar = { version = "0.4.40", optional = true } 13 | semver = "1.0.20" 14 | zip = { version = "0.6.6", default-features = false, features = ["time"], optional = true } 15 | either = { version = "1.9.0", optional = true } 16 | reqwest = { package = "reqwest-impersonate", version = "0.11.49", default-features = false, features = ["impersonate", "blocking", "json"] } 17 | indicatif = "0.17.7" 18 | quick-xml = "0.31.0" 19 | regex = "1.10.2" 20 | log = "0.4.20" 21 | urlencoding = "2.1.3" 22 | self-replace = "1.3.7" 23 | zipsign-api = { version = "0.1.1", default-features = false, optional = true } 24 | 25 | [features] 26 | default = [] 27 | archive-zip = ["zip", "zipsign-api?/verify-zip"] 28 | compression-zip-bzip2 = ["archive-zip", "zip/bzip2"] 29 | compression-zip-deflate = ["archive-zip", "zip/deflate"] 30 | archive-tar = ["tar", "zipsign-api?/verify-tar"] 31 | compression-flate2 = ["archive-tar", "flate2", "either"] 32 | signatures = ["dep:zipsign-api"] 33 | 34 | -------------------------------------------------------------------------------- /crates/self_update/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!( 3 | "cargo:rustc-env=TARGET={}", 4 | std::env::var("TARGET").unwrap() 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /crates/self_update/src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Collection of modules supporting various release distribution backends 3 | */ 4 | 5 | pub mod github; 6 | 7 | /// Search for the first "rel" link-header uri in a full link header string. 8 | /// Seems like reqwest/hyper threw away their link-header parser implementation... 9 | /// 10 | /// ex: 11 | /// `Link: ; rel="next"` 12 | /// `Link: ; rel="next"` 13 | /// 14 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link 15 | /// header values may contain multiple values separated by commas 16 | /// `Link: ; rel="next", ; rel="next"` 17 | pub(crate) fn find_rel_next_link(link_str: &str) -> Option<&str> { 18 | for link in link_str.split(',') { 19 | let mut uri = None; 20 | let mut is_rel_next = false; 21 | for part in link.split(';') { 22 | let part = part.trim(); 23 | if part.starts_with('<') && part.ends_with('>') { 24 | uri = Some(part.trim_start_matches('<').trim_end_matches('>')); 25 | } else if part.starts_with("rel=") { 26 | let part = part 27 | .trim_start_matches("rel=") 28 | .trim_end_matches('"') 29 | .trim_start_matches('"'); 30 | if part == "next" { 31 | is_rel_next = true; 32 | } 33 | } 34 | 35 | if is_rel_next && uri.is_some() { 36 | return uri; 37 | } 38 | } 39 | } 40 | None 41 | } 42 | -------------------------------------------------------------------------------- /crates/self_update/src/errors.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Error type, conversions, and macros 3 | 4 | */ 5 | #[cfg(feature = "archive-zip")] 6 | use zip::result::ZipError; 7 | 8 | pub type Result = std::result::Result; 9 | 10 | #[derive(Debug)] 11 | pub enum Error { 12 | Update(String), 13 | Network(String), 14 | Release(String), 15 | Config(String), 16 | Io(std::io::Error), 17 | #[cfg(feature = "archive-zip")] 18 | Zip(ZipError), 19 | Json(serde_json::Error), 20 | Reqwest(reqwest::Error), 21 | SemVer(semver::Error), 22 | ArchiveNotEnabled(String), 23 | #[cfg(feature = "signatures")] 24 | NoSignatures(crate::ArchiveKind), 25 | #[cfg(feature = "signatures")] 26 | Signature(zipsign_api::ZipsignError), 27 | #[cfg(feature = "signatures")] 28 | NonUTF8, 29 | } 30 | 31 | impl std::fmt::Display for Error { 32 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 33 | use Error::*; 34 | match *self { 35 | Update(ref s) => write!(f, "UpdateError: {}", s), 36 | Network(ref s) => write!(f, "NetworkError: {}", s), 37 | Release(ref s) => write!(f, "ReleaseError: {}", s), 38 | Config(ref s) => write!(f, "ConfigError: {}", s), 39 | Io(ref e) => write!(f, "IoError: {}", e), 40 | Json(ref e) => write!(f, "JsonError: {}", e), 41 | Reqwest(ref e) => write!(f, "ReqwestError: {}", e), 42 | SemVer(ref e) => write!(f, "SemVerError: {}", e), 43 | #[cfg(feature = "archive-zip")] 44 | Zip(ref e) => write!(f, "ZipError: {}", e), 45 | ArchiveNotEnabled(ref s) => write!(f, "ArchiveNotEnabled: Archive extension '{}' not supported, please enable 'archive-{}' feature!", s, s), 46 | #[cfg(feature = "signatures")] 47 | NoSignatures(kind) => { 48 | write!(f, "No signature verification implemented for {:?} files", kind) 49 | } 50 | #[cfg(feature = "signatures")] 51 | Signature(ref e) => write!(f, "SignatureError: {}", e), 52 | #[cfg(feature = "signatures")] 53 | NonUTF8 => write!(f, "Cannot verify signature of a file with a non-UTF-8 name"), 54 | } 55 | } 56 | } 57 | 58 | impl std::error::Error for Error { 59 | fn description(&self) -> &str { 60 | "Self Update Error" 61 | } 62 | 63 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 64 | Some(match *self { 65 | Error::Io(ref e) => e, 66 | Error::Json(ref e) => e, 67 | Error::Reqwest(ref e) => e, 68 | Error::SemVer(ref e) => e, 69 | #[cfg(feature = "signatures")] 70 | Error::Signature(ref e) => e, 71 | _ => return None, 72 | }) 73 | } 74 | } 75 | 76 | impl From for Error { 77 | fn from(e: std::io::Error) -> Error { 78 | Error::Io(e) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(e: serde_json::Error) -> Error { 84 | Error::Json(e) 85 | } 86 | } 87 | 88 | impl From for Error { 89 | fn from(e: reqwest::Error) -> Error { 90 | Error::Reqwest(e) 91 | } 92 | } 93 | 94 | impl From for Error { 95 | fn from(e: semver::Error) -> Error { 96 | Error::SemVer(e) 97 | } 98 | } 99 | 100 | #[cfg(feature = "archive-zip")] 101 | impl From for Error { 102 | fn from(e: ZipError) -> Error { 103 | Error::Zip(e) 104 | } 105 | } 106 | 107 | #[cfg(feature = "signatures")] 108 | impl From for Error { 109 | fn from(e: zipsign_api::ZipsignError) -> Error { 110 | Error::Signature(e) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/self_update/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Allows you to pull the version from your Cargo.toml at compile time as 2 | /// `MAJOR.MINOR.PATCH_PKGVERSION_PRE` 3 | #[macro_export] 4 | macro_rules! cargo_crate_version { 5 | // -- Pulled from clap.rs src/macros.rs 6 | () => { 7 | env!("CARGO_PKG_VERSION") 8 | }; 9 | } 10 | 11 | /// Set ssl cert env. vars to make sure openssl can find required files 12 | macro_rules! set_ssl_vars { 13 | () => { 14 | #[cfg(target_os = "linux")] 15 | { 16 | if ::std::env::var_os("SSL_CERT_FILE").is_none() { 17 | ::std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); 18 | } 19 | if ::std::env::var_os("SSL_CERT_DIR").is_none() { 20 | ::std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | /// Helper to `print!` and immediately `flush` `stdout` 27 | macro_rules! print_flush { 28 | ($literal:expr) => { 29 | print!($literal); 30 | ::std::io::Write::flush(&mut ::std::io::stdout())?; 31 | }; 32 | ($literal:expr, $($arg:expr),*) => { 33 | print!($literal, $($arg),*); 34 | ::std::io::Write::flush(&mut ::std::io::stdout())?; 35 | } 36 | } 37 | 38 | /// Helper for formatting `errors::Error`s 39 | macro_rules! format_err { 40 | ($e_type:expr, $literal:expr) => { 41 | $e_type(format!($literal)) 42 | }; 43 | ($e_type:expr, $literal:expr, $($arg:expr),*) => { 44 | $e_type(format!($literal, $($arg),*)) 45 | }; 46 | } 47 | 48 | /// Helper for formatting `errors::Error`s and returning early 49 | macro_rules! bail { 50 | ($e_type:expr, $literal:expr) => { 51 | return Err(format_err!($e_type, $literal)) 52 | }; 53 | ($e_type:expr, $literal:expr, $($arg:expr),*) => { 54 | return Err(format_err!($e_type, $literal, $($arg),*)) 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /crates/self_update/src/version.rs: -------------------------------------------------------------------------------- 1 | /*! Semver version checks 2 | 3 | The following functions compare two semver compatible version strings. 4 | */ 5 | use crate::errors::*; 6 | use semver::Version; 7 | 8 | /// Check if a version is greater than the current 9 | pub fn bump_is_greater(current: &str, other: &str) -> Result { 10 | Ok(Version::parse(other)? > Version::parse(current)?) 11 | } 12 | 13 | /// Check if a new version is compatible with the current 14 | pub fn bump_is_compatible(current: &str, other: &str) -> Result { 15 | let current = Version::parse(current)?; 16 | let other = Version::parse(other)?; 17 | Ok(if other.major == 0 && current.major == 0 { 18 | current.minor == other.minor && other.patch > current.patch 19 | } else if other.major > 0 { 20 | current.major == other.major 21 | && ((other.minor > current.minor) 22 | || (current.minor == other.minor && other.patch > current.patch)) 23 | } else { 24 | false 25 | }) 26 | } 27 | 28 | /// Check if a new version is a major bump 29 | pub fn bump_is_major(current: &str, other: &str) -> Result { 30 | let current = Version::parse(current)?; 31 | let other = Version::parse(other)?; 32 | Ok(other.major > current.major) 33 | } 34 | 35 | /// Check if a new version is a minor bump 36 | pub fn bump_is_minor(current: &str, other: &str) -> Result { 37 | let current = Version::parse(current)?; 38 | let other = Version::parse(other)?; 39 | Ok(current.major == other.major && other.minor > current.minor) 40 | } 41 | 42 | /// Check if a new version is a patch bump 43 | pub fn bump_is_patch(current: &str, other: &str) -> Result { 44 | let current = Version::parse(current)?; 45 | let other = Version::parse(other)?; 46 | Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch) 47 | } 48 | 49 | #[cfg(test)] 50 | mod test { 51 | use super::*; 52 | 53 | #[test] 54 | fn test_bump_greater() { 55 | assert!(bump_is_greater("1.2.0", "1.2.3").unwrap()); 56 | assert!(bump_is_greater("0.2.0", "1.2.3").unwrap()); 57 | assert!(bump_is_greater("0.2.0", "0.2.3").unwrap()); 58 | } 59 | 60 | #[test] 61 | fn test_bump_is_compatible() { 62 | assert!(!bump_is_compatible("1.2.0", "2.3.1").unwrap()); 63 | assert!(!bump_is_compatible("0.2.0", "2.3.1").unwrap()); 64 | assert!(!bump_is_compatible("1.2.3", "3.3.0").unwrap()); 65 | assert!(!bump_is_compatible("1.2.3", "0.2.0").unwrap()); 66 | assert!(!bump_is_compatible("0.2.0", "0.3.0").unwrap()); 67 | assert!(!bump_is_compatible("0.3.0", "0.2.0").unwrap()); 68 | assert!(!bump_is_compatible("1.2.3", "1.1.0").unwrap()); 69 | assert!(bump_is_compatible("1.2.0", "1.2.3").unwrap()); 70 | assert!(bump_is_compatible("0.2.0", "0.2.3").unwrap()); 71 | assert!(bump_is_compatible("1.2.0", "1.3.3").unwrap()); 72 | } 73 | 74 | #[test] 75 | fn test_bump_is_major() { 76 | assert!(bump_is_major("1.2.0", "2.3.1").unwrap()); 77 | assert!(bump_is_major("0.2.0", "2.3.1").unwrap()); 78 | assert!(bump_is_major("1.2.3", "3.3.0").unwrap()); 79 | assert!(!bump_is_major("1.2.3", "1.2.0").unwrap()); 80 | assert!(!bump_is_major("1.2.3", "0.2.0").unwrap()); 81 | } 82 | 83 | #[test] 84 | fn test_bump_is_minor() { 85 | assert!(!bump_is_minor("1.2.0", "2.3.1").unwrap()); 86 | assert!(!bump_is_minor("0.2.0", "2.3.1").unwrap()); 87 | assert!(!bump_is_minor("1.2.3", "3.3.0").unwrap()); 88 | assert!(bump_is_minor("1.2.3", "1.3.0").unwrap()); 89 | assert!(bump_is_minor("0.2.3", "0.4.0").unwrap()); 90 | } 91 | 92 | #[test] 93 | fn test_bump_is_patch() { 94 | assert!(!bump_is_patch("1.2.0", "2.3.1").unwrap()); 95 | assert!(!bump_is_patch("0.2.0", "2.3.1").unwrap()); 96 | assert!(!bump_is_patch("1.2.3", "3.3.0").unwrap()); 97 | assert!(!bump_is_patch("1.2.3", "1.2.3").unwrap()); 98 | assert!(bump_is_patch("1.2.0", "1.2.3").unwrap()); 99 | assert!(bump_is_patch("0.2.3", "0.2.4").unwrap()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /doc/authorization.md: -------------------------------------------------------------------------------- 1 | - Login: `POST /auth/token` 2 | 3 | ```python 4 | import requests 5 | 6 | url = "http://localhost:7999/auth/token" 7 | 8 | # option values: web, apple, platform, default: web 9 | payload = 'username=admin%40gmail.com&password=admin&option=web' 10 | headers = { 11 | 'Content-Type': 'application/x-www-form-urlencoded' 12 | } 13 | 14 | response = requests.request("POST", url, headers=headers, data=payload) 15 | 16 | print(response.text) 17 | ``` 18 | 19 | - Refresh `RefreshToken`: `POST /auth/refresh_token` 20 | 21 | ``` python 22 | import requests 23 | 24 | url = "http://localhost:7999/auth/refresh_token" 25 | 26 | payload = {} 27 | headers = { 28 | 'Authorization': 'Bearer your_refresh_token' 29 | } 30 | 31 | response = requests.request("POST", url, headers=headers, data=payload) 32 | 33 | print(response.text) 34 | 35 | ``` 36 | 37 | - Revoke `RefreshToken`: `POST /auth/revoke_token` 38 | 39 | ```python 40 | import requests 41 | 42 | url = "http://localhost:7999/auth/revoke_token" 43 | 44 | payload = {} 45 | headers = { 46 | 'Authorization': 'Bearer your_refresh_token' 47 | } 48 | 49 | response = requests.request("POST", url, headers=headers, data=payload) 50 | 51 | print(response.text) 52 | 53 | ``` 54 | 55 | - Refresh `Session`: `POST /auth/refresh_session` 56 | 57 | ```python 58 | import requests 59 | 60 | url = "http://localhost:7999/auth/refresh_session" 61 | 62 | payload = {} 63 | headers = { 64 | 'Authorization': 'Bearer your_refresh_session' 65 | } 66 | 67 | response = requests.request("POST", url, headers=headers, data=payload) 68 | 69 | print(response.text) 70 | 71 | ``` 72 | 73 | - Obtain `Sess token`: `POST /auth/sess_token` 74 | 75 | ```python 76 | import requests 77 | 78 | url = "http://localhost:7999/auth/sess_token" 79 | 80 | payload = {} 81 | headers = { 82 | 'Authorization': 'Bearer your_platform_access_token' 83 | } 84 | 85 | response = requests.request("POST", url, headers=headers, data=payload) 86 | 87 | print(response.text) 88 | 89 | ``` 90 | 91 | - Obtain `Billing`: `GET /auth/billing` 92 | 93 | ```python 94 | import requests 95 | 96 | url = "http://localhost:7999/auth/billing" 97 | 98 | payload = {} 99 | headers = { 100 | 'Authorization': 'Bearer your_sess_token' 101 | } 102 | 103 | response = requests.request("POST", url, headers=headers, data=payload) 104 | 105 | print(response.text) 106 | 107 | ``` 108 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16.6 as builder 2 | 3 | ARG VERSION 4 | ARG TARGETPLATFORM 5 | 6 | RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ 7 | echo "aarch64" > arch; \ 8 | echo "musl" > env; \ 9 | elif [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ 10 | echo "x86_64" > arch; \ 11 | echo "musl" > env; \ 12 | elif [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \ 13 | echo "armv7" > arch; \ 14 | echo "musleabi" > env; \ 15 | elif [ "${TARGETPLATFORM}" = "linux/arm/v6" ]; then \ 16 | echo "arm" > arch; \ 17 | echo "musleabi" > env; \ 18 | fi 19 | RUN apk update && apk add wget 20 | RUN wget https://github.com/gngpp/ninja/releases/download/v${VERSION}/ninja-${VERSION}-$(cat arch)-unknown-linux-$(cat env).tar.gz 21 | RUN tar -xvf ninja-${VERSION}-$(cat arch)-unknown-linux-$(cat env).tar.gz 22 | 23 | FROM alpine:3.16.6 24 | 25 | LABEL org.opencontainers.image.authors "gngpp " 26 | LABEL org.opencontainers.image.source https://github.com/gngpp/ninja 27 | LABEL name ninja 28 | LABEL url https://github.com/gngpp/ninja 29 | 30 | ENV LANG=C.UTF-8 DEBIAN_FRONTEND=noninteractive LANG=zh_CN.UTF-8 LANGUAGE=zh_CN.UTF-8 LC_ALL=C 31 | 32 | COPY --from=builder /ninja /bin/ninja 33 | 34 | ENTRYPOINT ["/bin/ninja"] -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${tag=latest} 4 | 5 | cd docker 6 | docker buildx build --platform linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6 \ 7 | --tag ghcr.io/gngpp/ninja:$tag \ 8 | --tag ghcr.io/gngpp/ninja:latest \ 9 | --tag gngpp/ninja:$tag \ 10 | --tag gngpp/ninja:latest \ 11 | --build-arg VERSION=$tag --push . 12 | cd - 13 | -------------------------------------------------------------------------------- /docker/warp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tcpfw" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "TCP forward" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread", "io-util", "net"] } 11 | 12 | [profile.release] 13 | lto = true 14 | opt-level = 'z' 15 | codegen-units = 1 16 | panic = "abort" 17 | strip = true -------------------------------------------------------------------------------- /docker/warp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | LABEL org.opencontainers.image.authors "gngpp " 4 | LABEL org.opencontainers.image.source https://github.com/gngpp/ninja 5 | LABEL name ninja 6 | LABEL url https://github.com/gngpp/ninja 7 | 8 | RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC 9 | RUN apt update && apt install -y curl gpg systemd tzdata socat 10 | RUN curl https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg 11 | RUN echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ focal main' | tee /etc/apt/sources.list.d/cloudflare-client.list 12 | RUN apt update -y && apt install -y cloudflare-warp 13 | 14 | COPY boot.sh boot.sh 15 | CMD ["/bin/bash", "/boot.sh"] -------------------------------------------------------------------------------- /docker/warp/boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -m 4 | 5 | if [ -z "$LOG_LEVEL" ]; then 6 | export LOG_LEVEL=OFF 7 | fi 8 | 9 | warp-svc | grep "$LOG_LEVEL" & 10 | 11 | sleep 1 12 | 13 | warp-cli --accept-tos set-mode proxy 14 | 15 | if [ -z "$TEAMS_ENROLL_TOKEN" ]; then 16 | warp-cli --accept-tos register 17 | warp-cli --accept-tos connect 18 | else 19 | warp-cli --accept-tos teams-enroll-token $TEAMS_ENROLL_TOKEN 20 | fi 21 | 22 | warp-cli --accept-tos enable-always-on 23 | 24 | sleep 3 25 | 26 | # This I guess is because they don't want warp-cli to be used for a sharing proxy 27 | socat TCP-LISTEN:10000,fork TCP:127.0.0.1:40000 & 28 | 29 | OUT=$(curl -s --retry 10 --retry-delay 3 --proxy socks5://127.0.0.1:10000 ifconfig.me) 30 | echo "Cloudflare-Warp IP: $OUT" 31 | 32 | if [ -z "$TEAMS_ENROLL_TOKEN" ]; then 33 | while true; do 34 | if [[ $(warp-cli --accept-tos warp-stats | awk 'NR==3') == *GB ]]; then 35 | warp-cli --accept-tos delete 36 | warp-cli --accept-tos register 37 | warp-cli --accept-tos set-mode proxy 38 | warp-cli --accept-tos connect 39 | warp-cli --accept-tos enable-always-on 40 | fi 41 | 42 | sleep 300 43 | done 44 | fi 45 | 46 | fg %1 -------------------------------------------------------------------------------- /docker/warp/src/main.rs: -------------------------------------------------------------------------------- 1 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 2 | use tokio::net::{TcpListener, TcpStream}; 3 | 4 | fn print_help(program_name: &str) { 5 | eprintln!( 6 | "Usage: {} [--listen LISTEN_ADDR] --target TARGET_ADDR", 7 | program_name 8 | ); 9 | eprintln!("Options:"); 10 | eprintln!(" -l, --listen Address and port to listen on. Default: 127.0.0.1:9999"); 11 | eprintln!(" -t, --target Target address and port to forward to. (required)"); 12 | eprintln!(" -h, --help Print this help message."); 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<(), Box> { 17 | let args: Vec = std::env::args().collect(); 18 | 19 | let mut listen_addr = "127.0.0.1:9999".to_string(); 20 | let mut target_addr = String::new(); 21 | 22 | let mut idx = 1; 23 | while idx < args.len() { 24 | match args[idx].as_str() { 25 | "--listen" | "-l" => { 26 | if idx + 1 < args.len() { 27 | listen_addr = args[idx + 1].clone(); 28 | idx += 2; 29 | } else { 30 | eprintln!( 31 | "Error: '--listen' requires an address. Use '--help' for more information." 32 | ); 33 | print_help(&args[0]); 34 | return Ok(()); 35 | } 36 | } 37 | "--target" | "-t" => { 38 | if idx + 1 < args.len() { 39 | target_addr.push_str(&args[idx + 1]); 40 | idx += 2; 41 | } else { 42 | eprintln!( 43 | "Error: '--target' requires an address. Use '--help' for more information." 44 | ); 45 | print_help(&args[0]); 46 | return Ok(()); 47 | } 48 | } 49 | "--help" | "-h" => { 50 | print_help(&args[0]); 51 | return Ok(()); 52 | } 53 | _ => { 54 | eprintln!( 55 | "Error: Unknown argument '{}'. Use '--help' for more information.", 56 | args[idx] 57 | ); 58 | print_help(&args[0]); 59 | return Ok(()); 60 | } 61 | } 62 | } 63 | 64 | if target_addr.is_empty() { 65 | eprintln!("Error: '--target' must be set. Use '--help' for more information."); 66 | return Ok(()); 67 | } 68 | 69 | println!("Listening on: {}", listen_addr); 70 | println!("Forwarding to: {}", target_addr); 71 | 72 | let listener = TcpListener::bind(listen_addr).await?; 73 | 74 | loop { 75 | match listener.accept().await { 76 | Ok((src, _)) => { 77 | tokio::spawn(handle_client(src, target_addr.clone())); 78 | } 79 | Err(e) => { 80 | println!("Accept error: {:?}", e); 81 | } 82 | } 83 | } 84 | } 85 | 86 | async fn handle_client(mut src: TcpStream, target_addr: String) { 87 | match TcpStream::connect(target_addr).await { 88 | Ok(mut dst) => { 89 | let (mut src_reader, mut src_writer) = src.split(); 90 | let (mut dst_reader, mut dst_writer) = dst.split(); 91 | 92 | let src_to_dst = async { 93 | let mut buf = vec![0u8; 4096]; 94 | loop { 95 | match src_reader.read(&mut buf).await { 96 | Ok(0) => return, 97 | Ok(n) => { 98 | if dst_writer.write_all(&buf[..n]).await.is_err() { 99 | return; 100 | } 101 | } 102 | Err(_) => return, 103 | } 104 | } 105 | }; 106 | 107 | let dst_to_src = async { 108 | let mut buf = vec![0u8; 4096]; 109 | loop { 110 | match dst_reader.read(&mut buf).await { 111 | Ok(0) => return, 112 | Ok(n) => { 113 | if src_writer.write_all(&buf[..n]).await.is_err() { 114 | return; 115 | } 116 | } 117 | Err(_) => return, 118 | } 119 | } 120 | }; 121 | 122 | let _ = tokio::join!(src_to_dst, dst_to_src); 123 | } 124 | Err(_) => { 125 | println!("Failed to connect to the target."); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /examples/auth.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, time}; 2 | 3 | use openai::{ 4 | arkose::funcaptcha::solver::{ArkoseSolver, Solver}, 5 | auth::{ 6 | model::{AuthAccount, AuthStrategy}, 7 | provide::AuthProvider, 8 | AuthClientBuilder, 9 | }, 10 | }; 11 | use reqwest::impersonate::Impersonate; 12 | 13 | #[tokio::main] 14 | async fn main() -> anyhow::Result<()> { 15 | env_logger::init(); 16 | let client_key = std::env::var("KEY").expect("Need solver client key"); 17 | let solver = Solver::from_str(&std::env::var("SOLVER").expect("Need solver")) 18 | .expect("Not support solver"); 19 | 20 | let ctx = openai::context::args::Args::builder() 21 | .arkose_solver(ArkoseSolver::new(solver, client_key, None, 1)) 22 | .build(); 23 | openai::context::init(ctx); 24 | 25 | let email = std::env::var("EMAIL")?; 26 | let password = std::env::var("PASSWORD")?; 27 | let auth = AuthClientBuilder::builder() 28 | .impersonate(Impersonate::Chrome100) 29 | .timeout(time::Duration::from_secs(30)) 30 | .connect_timeout(time::Duration::from_secs(10)) 31 | .build(); 32 | let token = auth 33 | .do_access_token( 34 | &AuthAccount::builder() 35 | .username(email) 36 | .password(password) 37 | .option(AuthStrategy::Web) 38 | .build(), 39 | ) 40 | .await?; 41 | let auth_token = openai::token::model::Token::try_from(token)?; 42 | println!("AuthenticationToken: {:#?}", auth_token); 43 | println!("AccessToken: {}", auth_token.access_token()); 44 | 45 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 46 | if let Some(refresh_token) = auth_token.refresh_token() { 47 | println!("RefreshToken: {}", refresh_token); 48 | let refresh_token = auth.do_refresh_token(refresh_token).await?; 49 | if let Some(refresh_token) = refresh_token.refresh_token { 50 | println!("RefreshToken: {}", refresh_token); 51 | auth.do_revoke_token(&refresh_token).await?; 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/crypto.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let x = openai::arkose::murmur::murmurhash3_x64_128(b"test", 31); 3 | // ff55565a476832ed3409c64597508ca4 4 | println!("{:x}{:x}", x.0, x.1); 5 | 6 | let encode = openai::arkose::crypto::encrypt("Hello, World", "my_secret_key").unwrap(); 7 | println!("encode: {encode}"); 8 | let decode = openai::arkose::crypto::decrypt(encode.into_bytes(), "my_secret_key").unwrap(); 9 | println!("decode: {decode}"); 10 | } 11 | -------------------------------------------------------------------------------- /examples/funcaptcha.rs: -------------------------------------------------------------------------------- 1 | use openai::arkose; 2 | use openai::arkose::funcaptcha::solver::ArkoseSolver; 3 | use openai::arkose::funcaptcha::solver::Solver; 4 | use openai::arkose::ArkoseContext; 5 | use openai::arkose::ArkoseToken; 6 | use openai::context::args::Args; 7 | use openai::{ 8 | context::{self}, 9 | with_context, 10 | }; 11 | use std::str::FromStr; 12 | use tokio::time::Instant; 13 | 14 | #[tokio::main] 15 | async fn main() -> anyhow::Result<()> { 16 | env_logger::init(); 17 | let client_key = std::env::var("KEY").expect("Need solver client key"); 18 | let solver = Solver::from_str(&std::env::var("SOLVER").expect("Need solver")) 19 | .expect("Not support solver"); 20 | let solver_type = std::env::var("SOLVER_TYPE").expect("Need solver type"); 21 | 22 | context::init( 23 | Args::builder() 24 | .arkose_solver_tguess_endpoint(Some("https://example.com/tguess".to_owned())) 25 | .arkose_solver(ArkoseSolver::new(solver, client_key, None, 1)) 26 | .build(), 27 | ); 28 | 29 | let typed = match solver_type.as_str() { 30 | "auth" => arkose::Type::Auth, 31 | "platform" => arkose::Type::Platform, 32 | "signup" => arkose::Type::SignUp, 33 | "gpt3" => arkose::Type::GPT3, 34 | "gpt4" => arkose::Type::GPT4, 35 | _ => anyhow::bail!("Not support solver type: {solver_type}"), 36 | }; 37 | 38 | // start time 39 | let now = Instant::now(); 40 | 41 | let arkose_token = ArkoseToken::new_from_context( 42 | ArkoseContext::builder() 43 | .client(with_context!(arkose_client)) 44 | .typed(typed) 45 | .build(), 46 | ) 47 | .await?; 48 | 49 | println!("Arkose token: {}", arkose_token.json()); 50 | 51 | println!("Function execution time: {:?}", now.elapsed()); 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /examples/har.rs: -------------------------------------------------------------------------------- 1 | use openai::{ 2 | arkose::{ArkoseContext, ArkoseToken, Type}, 3 | context::{self, args::Args}, 4 | with_context, 5 | }; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | env_logger::init(); 10 | context::init(Args::builder().build()); 11 | for _ in 0..100 { 12 | match ArkoseToken::new_from_har( 13 | &mut ArkoseContext::builder() 14 | .client(with_context!(arkose_client)) 15 | .typed(Type::GPT4) 16 | .build(), 17 | ) 18 | .await 19 | { 20 | Ok(token) => { 21 | println!("{}", token.json()); 22 | } 23 | Err(err) => { 24 | println!("{}", err); 25 | } 26 | }; 27 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/pow.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | use std::time::SystemTime; 3 | 4 | const PREFIX_LENGTH: usize = 5; // 调整这个值来增加/减少挑战的难度 5 | 6 | pub struct ProofOfWork { 7 | pub data: String, 8 | pub difficulty: usize, 9 | } 10 | 11 | impl ProofOfWork { 12 | pub fn new(data: String, difficulty: usize) -> Self { 13 | ProofOfWork { data, difficulty } 14 | } 15 | 16 | pub fn calculate(&self) -> (u64, String) { 17 | let mut nonce = 0u64; 18 | loop { 19 | let input = format!("{}{}", self.data, nonce); 20 | let hash = hex::encode(Sha256::digest(input.as_bytes())); 21 | if &hash[..self.difficulty] == &"0".repeat(self.difficulty) { 22 | return (nonce, hash); 23 | } 24 | nonce += 1; 25 | } 26 | } 27 | 28 | pub fn valid(&self) -> bool { 29 | let input = format!("{}{}", self.data, self.calculate().0); 30 | let hash = hex::encode(Sha256::digest(input.as_bytes())); 31 | &hash[..self.difficulty] == &"0".repeat(self.difficulty) 32 | } 33 | } 34 | 35 | fn main() { 36 | let now = SystemTime::now(); 37 | 38 | let pow = ProofOfWork::new("some data".into(), PREFIX_LENGTH); 39 | let (nonce, hash) = pow.calculate(); 40 | 41 | let elapsed = now.elapsed().unwrap(); 42 | println!("Found nonce: {}, hash: {}", nonce, hash); 43 | println!("Time elapsed: {:.2?}", elapsed); 44 | } 45 | -------------------------------------------------------------------------------- /examples/print_image.rs: -------------------------------------------------------------------------------- 1 | // src/main.rs 2 | use viuer::{print_from_file, Config}; 3 | 4 | fn main() { 5 | let conf = Config { 6 | // set offset 7 | x: 0, 8 | y: 0, 9 | use_iterm: true, 10 | ..Default::default() 11 | }; 12 | 13 | // starting from row 4 and column 20, 14 | // display `img.jpg` with dimensions 80x25 (in terminal cells) 15 | // note that the actual resolution in the terminal will be 80x50 16 | print_from_file( 17 | "/Users/gngpp/GolandProjects/funcaptcha/image_1693180034986.png", 18 | &conf, 19 | ) 20 | .expect("Image printing failed."); 21 | } 22 | -------------------------------------------------------------------------------- /examples/upgrade.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | let data = std::fs::read("/Users/gngpp/Desktop/sRVMr7pl79_Jt3fw8-hEr.json").unwrap(); 5 | let target: Vec = serde_json::from_slice(&data).unwrap(); 6 | let host = "https://cdn.oaistatic.com"; 7 | let client = reqwest::blocking::ClientBuilder::new() 8 | .impersonate(reqwest::impersonate::Impersonate::Chrome104) 9 | .timeout(std::time::Duration::from_secs(60)) 10 | .connect_timeout(std::time::Duration::from_secs(30)) 11 | .cookie_store(true) 12 | .build() 13 | .unwrap(); 14 | 15 | for path in target { 16 | let file_path = PathBuf::from("upgrade").join(&path); 17 | if file_path.exists() { 18 | continue; 19 | } 20 | if let Some(p) = file_path.parent() { 21 | if !p.exists() { 22 | std::fs::create_dir_all(p).unwrap(); 23 | } 24 | } 25 | 26 | let req_url = format!("{}/{}", host, path); 27 | let resp = client.get(&req_url).send().unwrap(); 28 | 29 | if resp.status().is_success() { 30 | std::fs::write(file_path, resp.bytes().unwrap()).unwrap(); 31 | println!("downloaded: {}", req_url); 32 | } else { 33 | panic!("request failed: {}", req_url); 34 | } 35 | 36 | std::thread::sleep(std::time::Duration::from_secs(3)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/alloc.rs: -------------------------------------------------------------------------------- 1 | //! Memory allocator 2 | 3 | #[cfg(feature = "jemalloc")] 4 | #[global_allocator] 5 | static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; 6 | 7 | #[cfg(feature = "tcmalloc")] 8 | #[global_allocator] 9 | static ALLOC: tcmalloc::TCMalloc = tcmalloc::TCMalloc; 10 | 11 | #[cfg(feature = "mimalloc")] 12 | #[global_allocator] 13 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; 14 | 15 | #[cfg(feature = "snmalloc")] 16 | #[global_allocator] 17 | static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc; 18 | 19 | #[cfg(feature = "rpmalloc")] 20 | #[global_allocator] 21 | static ALLOC: rpmalloc::RpMalloc = rpmalloc::RpMalloc; 22 | -------------------------------------------------------------------------------- /src/inter/authorize/mod.rs: -------------------------------------------------------------------------------- 1 | use inquire::{min_length, required, Select, Text}; 2 | 3 | use crate::inter::{context::Context, render_config, standard}; 4 | use inquire::{Password, PasswordDisplayMode}; 5 | use openai::auth::{ 6 | model::{AccessToken, AuthAccount, AuthStrategy}, 7 | provide::AuthProvider, 8 | }; 9 | 10 | use super::new_spinner; 11 | 12 | pub mod auth; 13 | pub mod oauth; 14 | 15 | pub async fn prompt() -> anyhow::Result<()> { 16 | loop { 17 | let wizard = tokio::task::spawn_blocking(move || { 18 | Select::new( 19 | "Authorization Wizard ›", 20 | standard::Authorize::AUTHORIZE_VARS.to_vec(), 21 | ) 22 | .with_render_config(render_config()) 23 | .with_formatter(&|i| format!("${i}")) 24 | .with_help_message("↑↓ to move, enter to select, type to filter, Esc to quit") 25 | .with_vim_mode(true) 26 | .prompt_skippable() 27 | }) 28 | .await??; 29 | 30 | if let Some(wizard) = wizard { 31 | match wizard { 32 | standard::Authorize::Auth => auth::sign_in_prompt().await?, 33 | standard::Authorize::OAuth => oauth::oauth_prompt().await?, 34 | } 35 | } else { 36 | // Esc to quit 37 | return Ok(()); 38 | } 39 | } 40 | } 41 | 42 | pub async fn login_prompt(auth_strategy: Option) -> anyhow::Result { 43 | let auth_strategy = if let Some(auth_strategy) = auth_strategy { 44 | auth_strategy 45 | } else { 46 | tokio::task::spawn_blocking(move || { 47 | Select::new( 48 | "Please choose the authentication strategy ›", 49 | vec![ 50 | AuthStrategy::Web, 51 | AuthStrategy::Apple, 52 | AuthStrategy::Platform, 53 | ], 54 | ) 55 | .prompt() 56 | }) 57 | .await?? 58 | }; 59 | 60 | let (username, password, mfa_res) = tokio::task::spawn_blocking(move || { 61 | let username = Text::new("Email ›") 62 | .with_render_config(render_config()) 63 | .with_validator(required!("email is required")) 64 | .with_validator(min_length!(5)) 65 | .with_help_message("OpenAI account email, Format: example@gmail.com") 66 | .prompt(); 67 | 68 | let password = Password::new("Password ›") 69 | .with_render_config(render_config()) 70 | .with_display_mode(PasswordDisplayMode::Masked) 71 | .with_validator(required!("password is required")) 72 | .with_validator(min_length!(5)) 73 | .with_help_message("OpenAI account password") 74 | .without_confirmation() 75 | .prompt(); 76 | 77 | let mfa_res = Text::new("MFA Code [Option] ›") 78 | .with_render_config(render_config()) 79 | .with_help_message("OpenAI account MFA Code, If it is empty, please enter directly.") 80 | .prompt_skippable(); 81 | 82 | (username, password, mfa_res) 83 | }) 84 | .await?; 85 | 86 | let username = username?; 87 | let password = password?; 88 | 89 | let mfa_code = mfa_res 90 | .map_err(|_| { 91 | println!("An error happened when asking for your mfa code, try again later."); 92 | }) 93 | .unwrap_or(None); 94 | 95 | let auth_account = AuthAccount::builder() 96 | .username(username) 97 | .password(password) 98 | .mfa(mfa_code) 99 | .option(auth_strategy) 100 | .build(); 101 | 102 | let pb = new_spinner("Authenticating..."); 103 | let access_token = Context::get_auth_client() 104 | .await 105 | .do_access_token(&auth_account) 106 | .await?; 107 | pb.finish_and_clear(); 108 | Ok(access_token) 109 | } 110 | -------------------------------------------------------------------------------- /src/inter/authorize/oauth.rs: -------------------------------------------------------------------------------- 1 | use colored_json::prelude::*; 2 | use inquire::{min_length, required, Select, Text}; 3 | use openai::auth::provide::AuthProvider; 4 | 5 | use crate::inter::{context::Context, new_spinner, render_config, standard::OAuth}; 6 | 7 | use super::login_prompt; 8 | 9 | pub async fn oauth_prompt() -> anyhow::Result<()> { 10 | loop { 11 | let wizard = tokio::task::spawn_blocking(move || { 12 | Select::new("OAuth Wizard ›", OAuth::OAUTH_VARS.to_vec()) 13 | .with_render_config(render_config()) 14 | .with_formatter(&|i| format!("${i}")) 15 | .with_help_message("↑↓ to move, enter to select, type to filter, Esc to quit") 16 | .with_vim_mode(true) 17 | .prompt_skippable() 18 | }) 19 | .await??; 20 | 21 | if let Some(wizard) = wizard { 22 | match wizard { 23 | OAuth::AccessToken => do_access_token().await?, 24 | OAuth::RefreshToken => do_refresh_token().await?, 25 | OAuth::RevokeToken => do_revoke_token().await?, 26 | } 27 | } else { 28 | // Esc to quit 29 | return Ok(()); 30 | } 31 | } 32 | } 33 | 34 | async fn do_access_token() -> anyhow::Result<()> { 35 | match login_prompt(None).await { 36 | Ok(token) => { 37 | println!( 38 | "{}", 39 | serde_json::to_string_pretty(&token)?.to_colored_json_auto()? 40 | ) 41 | } 42 | Err(err) => println!("Error: {err}"), 43 | } 44 | Ok(()) 45 | } 46 | 47 | async fn do_refresh_token() -> anyhow::Result<()> { 48 | let refresh_token = tokio::task::spawn_blocking(move || { 49 | Text::new("Please enter refresh token:") 50 | .with_render_config(render_config()) 51 | .with_validator(required!("refresh token is required")) 52 | .with_validator(min_length!(5)) 53 | .with_help_message("OpenAI account refresh token, Esc to quit") 54 | .prompt_skippable() 55 | }) 56 | .await??; 57 | 58 | if let Some(refresh_token) = refresh_token { 59 | let pb = new_spinner("Waiting..."); 60 | match Context::get_auth_client() 61 | .await 62 | .do_refresh_token(&refresh_token) 63 | .await 64 | { 65 | Ok(token) => { 66 | println!( 67 | "{}", 68 | serde_json::to_string_pretty(&token)?.to_colored_json_auto()? 69 | ) 70 | } 71 | Err(error) => { 72 | println!("Error: {error}") 73 | } 74 | } 75 | pb.finish_with_message("Done"); 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | async fn do_revoke_token() -> anyhow::Result<()> { 82 | let refresh_token = tokio::task::spawn_blocking(move || { 83 | Text::new("Please enter refresh token:") 84 | .with_render_config(render_config()) 85 | .with_validator(required!("refresh token is required")) 86 | .with_validator(min_length!(5)) 87 | .with_help_message("OpenAI account refresh token, Esc to quit") 88 | .prompt_skippable() 89 | }) 90 | .await??; 91 | 92 | if let Some(refresh_token) = refresh_token { 93 | let pb = new_spinner("Waiting..."); 94 | let _ = Context::get_auth_client() 95 | .await 96 | .do_revoke_token(&refresh_token) 97 | .await; 98 | pb.finish_with_message("Done") 99 | } 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /src/inter/conversation/api.rs: -------------------------------------------------------------------------------- 1 | pub(crate) async fn prompt() -> anyhow::Result<()> { 2 | Ok(()) 3 | } 4 | -------------------------------------------------------------------------------- /src/inter/conversation/chatgpt.rs: -------------------------------------------------------------------------------- 1 | pub(crate) async fn prompt() -> anyhow::Result<()> { 2 | Ok(()) 3 | } 4 | -------------------------------------------------------------------------------- /src/inter/conversation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod chatgpt; 3 | 4 | use tokio::io::AsyncWriteExt; 5 | 6 | #[allow(dead_code)] 7 | pub async fn print_stream( 8 | out: &mut tokio::io::Stdout, 9 | previous_message: String, 10 | message: String, 11 | ) -> anyhow::Result { 12 | if message.starts_with(&*previous_message) { 13 | let new_chars: String = message.chars().skip(previous_message.len()).collect(); 14 | out.write_all(new_chars.as_bytes()).await?; 15 | } else { 16 | out.write_all(message.as_bytes()).await?; 17 | } 18 | out.flush().await?; 19 | Ok(message) 20 | } 21 | -------------------------------------------------------------------------------- /src/inter/standard.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(Debug, Copy, Clone)] 4 | pub enum Usage { 5 | ChatGPT, 6 | TurboAPI, 7 | Dashboard, 8 | Authorize, 9 | Config, 10 | } 11 | 12 | impl Usage { 13 | // could be generated by macro 14 | pub const USAGE_VARS: &'static [Usage] = &[ 15 | Self::TurboAPI, 16 | Self::ChatGPT, 17 | Self::Dashboard, 18 | Self::Authorize, 19 | Self::Config, 20 | ]; 21 | } 22 | 23 | impl Display for Usage { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 25 | match self { 26 | Usage::TurboAPI => write!(f, "{self:?} - Turbo API interactive conversation"), 27 | Usage::ChatGPT => write!(f, "{self:?} - ChatGPT API interactive conversation"), 28 | Usage::Dashboard => write!(f, "{self:?} - Dashboard settings"), 29 | Usage::Authorize => write!(f, "{self:?} - Authorization settings"), 30 | Usage::Config => write!(f, "{self:?} - Configuration settings"), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug, Copy, Clone)] 36 | pub enum Authorize { 37 | Auth, 38 | OAuth, 39 | } 40 | 41 | impl Authorize { 42 | // could be generated by macro 43 | pub const AUTHORIZE_VARS: &'static [Authorize] = &[Self::Auth, Self::OAuth]; 44 | } 45 | 46 | impl Display for Authorize { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 48 | match self { 49 | Authorize::Auth => write!(f, "{self:?} - Login Authorization"), 50 | Authorize::OAuth => write!(f, "{self:?} - OAuth Authorization"), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Copy, Clone)] 56 | #[allow(clippy::enum_variant_names)] 57 | pub enum OAuth { 58 | AccessToken, 59 | RefreshToken, 60 | RevokeToken, 61 | } 62 | 63 | impl OAuth { 64 | // could be generated by macro 65 | pub const OAUTH_VARS: &'static [OAuth] = 66 | &[Self::AccessToken, Self::RefreshToken, Self::RevokeToken]; 67 | } 68 | 69 | impl Display for OAuth { 70 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 71 | match self { 72 | OAuth::AccessToken => write!(f, "{self:?} - Get AccessToken"), 73 | OAuth::RefreshToken => write!(f, "{self:?} - Refresh to get AccessToken"), 74 | OAuth::RevokeToken => write!(f, "{self:?} - Revoke AccessToken"), 75 | } 76 | } 77 | } 78 | 79 | #[derive(Debug, Copy, Clone)] 80 | pub enum Auth { 81 | State, 82 | Login, 83 | Logout, 84 | User, 85 | } 86 | 87 | impl Auth { 88 | // could be generated by macro 89 | pub const SIGN_IN_VARS: &'static [Auth] = &[Self::User, Self::State, Self::Login, Self::Logout]; 90 | } 91 | 92 | impl Display for Auth { 93 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 94 | match self { 95 | Auth::User => write!(f, "{self:?} - Current account user"), 96 | Auth::State => write!(f, "{self:?} - Account login state"), 97 | Auth::Login => write!(f, "{self:?} - Account SignIn"), 98 | Auth::Logout => write!(f, "{self:?} - Account SignOut"), 99 | } 100 | } 101 | } 102 | 103 | #[derive(Debug, Copy, Clone)] 104 | pub enum Dashboard { 105 | List, 106 | Create, 107 | Delete, 108 | Billing, 109 | } 110 | 111 | impl Dashboard { 112 | // could be generated by macro 113 | pub const DASHBOARD_VARS: &'static [Dashboard] = 114 | &[Self::Billing, Self::List, Self::Create, Self::Delete]; 115 | } 116 | 117 | impl Display for Dashboard { 118 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 119 | match self { 120 | Dashboard::Billing => write!(f, "{self:?} - ApiKey Billing"), 121 | Dashboard::List => write!(f, "{self:?} - List ApiKey"), 122 | Dashboard::Create => write!(f, "{self:?} - Create ApiKey"), 123 | Dashboard::Delete => write!(f, "{self:?} - Delete ApiKey"), 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/inter/valid.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::str::FromStr; 3 | 4 | use inquire::validator::Validation; 5 | use openai::arkose::funcaptcha::Solver; 6 | 7 | use crate::parse; 8 | 9 | pub fn valid_url( 10 | s: &str, 11 | ) -> Result> { 12 | if !s.is_empty() { 13 | match parse::parse_url(s) { 14 | Ok(_) => Ok(Validation::Valid), 15 | Err(err) => Ok(Validation::Invalid( 16 | inquire::validator::ErrorMessage::Custom(err.to_string()), 17 | )), 18 | } 19 | } else { 20 | Ok(Validation::Valid) 21 | } 22 | } 23 | 24 | pub fn valid_file_path( 25 | s: &str, 26 | ) -> Result> { 27 | if !s.is_empty() { 28 | match PathBuf::from(s).is_file() { 29 | true => Ok(Validation::Valid), 30 | false => Ok(Validation::Invalid( 31 | inquire::validator::ErrorMessage::Custom(format!("file: {s} not exists")), 32 | )), 33 | } 34 | } else { 35 | Ok(Validation::Valid) 36 | } 37 | } 38 | 39 | pub fn valid_solver( 40 | s: &str, 41 | ) -> Result> { 42 | if !s.is_empty() { 43 | match Solver::from_str(s) { 44 | Ok(_) => Ok(Validation::Valid), 45 | Err(err) => Ok(Validation::Invalid( 46 | inquire::validator::ErrorMessage::Custom(err.to_string()), 47 | )), 48 | } 49 | } else { 50 | Ok(Validation::Valid) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use openai::proxy; 3 | use std::net::IpAddr; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | 7 | // parse socket address 8 | pub fn parse_socket_addr(s: &str) -> anyhow::Result { 9 | let addr = s 10 | .parse::() 11 | .map_err(|_| anyhow::anyhow!(format!("`{}` isn't a socket address", s)))?; 12 | Ok(addr) 13 | } 14 | 15 | // url parse 16 | pub fn parse_url(s: &str) -> anyhow::Result { 17 | let url = url::Url::parse(s) 18 | .context("The Proxy Url format must be `protocol://user:pass@ip:port`")?; 19 | let protocol = url.scheme().to_string(); 20 | match protocol.as_str() { 21 | "http" | "https" | "socks5" | "redis" | "rediss" => Ok(s.to_string()), 22 | _ => anyhow::bail!("Unsupported protocol: {}", protocol), 23 | } 24 | } 25 | 26 | // proxy proto, format: proto|type, support proto: all/api/auth/arkose, support type: ip/url/cidr 27 | pub fn parse_proxies_url(s: &str) -> anyhow::Result> { 28 | let split = s.split(','); 29 | let mut proxies: Vec<_> = vec![]; 30 | 31 | for ele in split { 32 | let parts: Vec<_> = ele.split('|').collect(); 33 | let (proto, typer) = if parts.len() != 2 { 34 | ("all", ele.trim()) 35 | } else { 36 | (parts[0].trim(), parts[1].trim()) 37 | }; 38 | match ( 39 | typer.parse::(), 40 | url::Url::parse(typer), 41 | typer.parse::(), 42 | ) { 43 | (Ok(ip_addr), _, _) => proxies.push(proxy::Proxy::try_from((proto, ip_addr))?), 44 | (_, Ok(url), _) => proxies.push(proxy::Proxy::try_from((proto, url))?), 45 | (_, _, Ok(cidr)) => proxies.push(proxy::Proxy::try_from((proto, cidr))?), 46 | _ => anyhow::bail!("Invalid proxy format: {}", typer), 47 | } 48 | } 49 | 50 | Ok(proxies) 51 | } 52 | 53 | /// parse file path 54 | pub fn parse_file_path(s: &str) -> anyhow::Result { 55 | let path = 56 | PathBuf::from_str(s).map_err(|_| anyhow::anyhow!(format!("`{}` isn't a path", s)))?; 57 | 58 | if !path.exists() { 59 | anyhow::bail!(format!("Path {} not exists", path.display())) 60 | } 61 | 62 | if !path.is_file() { 63 | anyhow::bail!(format!("{} not a file", path.display())) 64 | } 65 | 66 | Ok(path) 67 | } 68 | 69 | // parse directory path 70 | pub fn parse_dir_path(s: &str) -> anyhow::Result { 71 | let path = 72 | PathBuf::from_str(s).map_err(|_| anyhow::anyhow!(format!("`{}` isn't a path", s)))?; 73 | 74 | if !path.exists() { 75 | anyhow::bail!(format!("Path {} not exists", path.display())) 76 | } 77 | 78 | if !path.is_dir() { 79 | anyhow::bail!(format!("{} not a directory", path.display())) 80 | } 81 | 82 | Ok(path) 83 | } 84 | 85 | /// parse email whitelist 86 | /// format: email1,email2,email3 87 | pub fn parse_email_whitelist(s: &str) -> anyhow::Result> { 88 | let split = s.split(','); 89 | let mut emails: Vec<_> = vec![]; 90 | 91 | for ele in split { 92 | let email = ele.trim(); 93 | if email.is_empty() { 94 | continue; 95 | } 96 | 97 | if email.contains('@') { 98 | emails.push(email.to_string()); 99 | } else { 100 | anyhow::bail!("Invalid email format: {}", email) 101 | } 102 | } 103 | 104 | Ok(emails) 105 | } 106 | 107 | // parse impersonate user-agent 108 | pub fn parse_impersonate_uas(s: &str) -> anyhow::Result> { 109 | let split = s.split(','); 110 | let mut uas: Vec<_> = vec![]; 111 | 112 | for ele in split { 113 | let ua = ele.trim(); 114 | if ua.is_empty() { 115 | continue; 116 | } 117 | 118 | uas.push(ua.to_string()); 119 | } 120 | 121 | Ok(uas) 122 | } 123 | -------------------------------------------------------------------------------- /src/store/account.rs: -------------------------------------------------------------------------------- 1 | use super::{Store, StoreId, StoreResult}; 2 | use openai::homedir::home_dir; 3 | use openai::{auth::model::AuthStrategy, token::model::Token}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{collections::HashMap, ops::Not, path::PathBuf}; 6 | 7 | pub struct AccountStore(PathBuf); 8 | 9 | impl AccountStore { 10 | pub fn new() -> Self { 11 | let path = match home_dir() { 12 | Some(home_dir) => home_dir.join(".ninja_accounts"), 13 | None => PathBuf::from(".ninja_accounts"), 14 | }; 15 | if let Some(parent) = path.parent() { 16 | if path.exists().not() { 17 | std::fs::create_dir_all(parent) 18 | .expect("Unable to create default file Account storage file") 19 | } 20 | } 21 | if path.exists().not() { 22 | std::fs::File::create(&path) 23 | .unwrap_or_else(|_| panic!("Unable to create file: {}", path.display())); 24 | } 25 | AccountStore(path) 26 | } 27 | } 28 | 29 | impl Default for AccountStore { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl Store for AccountStore { 36 | fn store(&self, target: Account) -> StoreResult> { 37 | let bytes = std::fs::read(&self.0)?; 38 | let mut data: HashMap = if bytes.is_empty() { 39 | HashMap::new() 40 | } else { 41 | serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!(e))? 42 | }; 43 | let v = data.insert(target.email.to_string(), target); 44 | let json = serde_json::to_string_pretty(&data)?; 45 | std::fs::write(&self.0, json.as_bytes())?; 46 | Ok(v) 47 | } 48 | 49 | fn read(&self, target: Account) -> StoreResult> { 50 | let bytes = std::fs::read(&self.0)?; 51 | if bytes.is_empty() { 52 | return Ok(None); 53 | } 54 | let data: HashMap = 55 | serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!(e))?; 56 | Ok(data.get(&target.id()).cloned()) 57 | } 58 | 59 | fn remove(&self, target: Account) -> StoreResult> { 60 | let bytes = std::fs::read(&self.0)?; 61 | if bytes.is_empty() { 62 | return Ok(None); 63 | } 64 | let mut data: HashMap = 65 | serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!(e))?; 66 | let v = data.remove(&target.id()); 67 | let json = serde_json::to_string_pretty(&data)?; 68 | std::fs::write(&self.0, json)?; 69 | Ok(v) 70 | } 71 | 72 | fn list(&self) -> StoreResult> { 73 | let bytes = std::fs::read(&self.0)?; 74 | if bytes.is_empty() { 75 | return Ok(vec![]); 76 | } 77 | let data: HashMap = 78 | serde_json::from_slice(&bytes).map_err(|e| anyhow::anyhow!(e))?; 79 | Ok(data.into_values().collect::>()) 80 | } 81 | 82 | type Obj = Account; 83 | } 84 | 85 | #[derive(Serialize, Deserialize, Clone)] 86 | pub struct Account { 87 | email: String, 88 | state: HashMap, 89 | } 90 | 91 | impl Account { 92 | pub fn new(email: &str) -> Self { 93 | Self { 94 | email: email.to_owned(), 95 | state: HashMap::default(), 96 | } 97 | } 98 | 99 | pub fn email(&self) -> &str { 100 | &self.email 101 | } 102 | 103 | pub fn state_mut(&mut self) -> &mut HashMap { 104 | &mut self.state 105 | } 106 | 107 | pub fn push_state(&mut self, auth_strategy: AuthStrategy, token: Token) { 108 | self.state.insert(auth_strategy, token); 109 | } 110 | 111 | pub fn remove_state(&mut self, auth_strategy: &AuthStrategy) { 112 | self.state.remove(auth_strategy); 113 | } 114 | } 115 | 116 | impl StoreId for Account { 117 | fn id(&self) -> String { 118 | self.email.to_owned() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/store/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod conf; 3 | 4 | use serde::de::DeserializeOwned; 5 | 6 | pub type StoreResult = anyhow::Result; 7 | 8 | pub trait StoreId { 9 | fn id(&self) -> String; 10 | } 11 | 12 | pub trait Store: Send + Sync + 'static { 13 | type Obj; 14 | // Add store target return an old target 15 | fn store(&self, target: T) -> StoreResult>; 16 | 17 | // Read target return a copy of the target 18 | fn read(&self, target: T) -> StoreResult>; 19 | 20 | // Delete target return an current target 21 | fn remove(&self, target: T) -> StoreResult>; 22 | 23 | /// List target return an current target list 24 | fn list(&self) -> StoreResult>; 25 | } 26 | -------------------------------------------------------------------------------- /src/update.rs: -------------------------------------------------------------------------------- 1 | use self_update::cargo_crate_version; 2 | 3 | pub(super) fn update() -> anyhow::Result<()> { 4 | use self_update::update::UpdateStatus; 5 | let status = self_update::backends::github::Update::configure() 6 | .repo_owner("gngpp") 7 | .repo_name("ninja") 8 | .bin_name("ninja") 9 | .target(self_update::get_target()) 10 | .show_output(true) 11 | .show_download_progress(true) 12 | .no_confirm(true) 13 | .current_version(cargo_crate_version!()) 14 | .build()? 15 | .update_extended()?; 16 | if let UpdateStatus::Updated(ref release) = status { 17 | if let Some(body) = &release.body { 18 | if !body.trim().is_empty() { 19 | println!("ninja upgraded to {}:\n", release.version); 20 | println!("{}", body); 21 | } else { 22 | println!("ninja upgraded to {}", release.version); 23 | } 24 | } 25 | } else { 26 | println!("ninja is up-to-date"); 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod unix; 2 | -------------------------------------------------------------------------------- /src/utils/unix.rs: -------------------------------------------------------------------------------- 1 | use crate::args::ServeArgs; 2 | 3 | #[cfg(target_family = "unix")] 4 | pub(crate) const PID_PATH: &str = "/var/run/ninja.pid"; 5 | #[cfg(target_family = "unix")] 6 | pub(crate) const DEFAULT_STDOUT_PATH: &str = "/var/run/ninja.out"; 7 | #[cfg(target_family = "unix")] 8 | pub(crate) const DEFAULT_STDERR_PATH: &str = "/var/run/ninja.err"; 9 | #[cfg(target_family = "unix")] 10 | pub(crate) const DEFAULT_WORK_DIR: &str = "/"; 11 | 12 | #[cfg(target_family = "unix")] 13 | pub fn check_root() { 14 | use nix::unistd::Uid; 15 | 16 | if !Uid::effective().is_root() { 17 | println!("You must run this executable with root permissions"); 18 | std::process::exit(-1) 19 | } 20 | } 21 | 22 | #[cfg(target_os = "linux")] 23 | /// Try to add a route to the given subnet to the loopback interface. 24 | pub(crate) fn sysctl_route_add_ipv6_subnet(subnet: &cidr::Ipv6Cidr) { 25 | if !nix::unistd::Uid::effective().is_root() { 26 | return; 27 | } 28 | 29 | let res = std::process::Command::new("ip") 30 | .args(&["route", "add", "local", &format!("{subnet}"), "dev", "lo"]) 31 | .output(); 32 | if let Err(err) = res { 33 | println!("Failed to add route to the loopback interface: {}", err); 34 | } 35 | } 36 | 37 | #[cfg(target_os = "linux")] 38 | /// Try to remove a route to the given subnet to the loopback interface. 39 | pub(crate) fn sysctl_ipv6_no_local_bind() { 40 | if !nix::unistd::Uid::effective().is_root() { 41 | return; 42 | } 43 | 44 | use sysctl::Sysctl; 45 | const CTLNAME: &str = "net.ipv6.ip_nonlocal_bind"; 46 | 47 | let ctl = ::new(CTLNAME) 48 | .expect(&format!("could not get sysctl '{}'", CTLNAME)); 49 | let _ = ctl.name().expect("could not get sysctl name"); 50 | 51 | let old_value = ctl.value_string().expect("could not get sysctl value"); 52 | 53 | let target_value = match old_value.as_ref() { 54 | "0" => "1", 55 | "1" | _ => &old_value, 56 | }; 57 | 58 | ctl.set_value_string(target_value).unwrap_or_else(|e| { 59 | panic!( 60 | "could not set sysctl '{}' to '{}': {}", 61 | CTLNAME, target_value, e 62 | ) 63 | }); 64 | } 65 | 66 | #[cfg(target_family = "unix")] 67 | pub(crate) fn get_pid() -> Option { 68 | if let Ok(data) = std::fs::read(PID_PATH) { 69 | let binding = String::from_utf8(data).expect("pid file is not utf8"); 70 | return Some(binding.trim().to_string()); 71 | } 72 | None 73 | } 74 | 75 | pub(crate) fn fix_relative_path(args: &mut ServeArgs) { 76 | if let Some(c) = args.config.as_mut() { 77 | // fix relative path 78 | if c.is_relative() { 79 | args.config = Some( 80 | std::env::current_dir() 81 | .expect("cannot get current exe") 82 | .join(c), 83 | ) 84 | } 85 | } 86 | } 87 | --------------------------------------------------------------------------------