├── .dockerignore ├── cmd └── nth-dump │ ├── opts_unix.go │ ├── opts_windows.go │ └── main.go ├── go.mod ├── .gitignore ├── Dockerfile ├── .github ├── stale.yml └── workflows │ └── docker-ci.yml ├── LICENSE ├── go.sum ├── nthclient ├── pubkey.go ├── crypto.go ├── types.go ├── settings.go └── client.go ├── README.md └── Makefile /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /cmd/nth-dump/opts_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | ) 9 | 10 | var ( 11 | noqr = flag.Bool("noqr", false, "do not print QR code with URL") 12 | nowait = flag.Bool("nowait", true, "do not wait for key press after output") 13 | ) 14 | -------------------------------------------------------------------------------- /cmd/nth-dump/opts_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | ) 9 | 10 | var ( 11 | noqr = flag.Bool("noqr", true, "do not print QR code with URL") 12 | nowait = flag.Bool("nowait", false, "do not wait for key press after output") 13 | ) 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Snawoot/nth-dump 2 | 3 | go 1.18 4 | 5 | require github.com/google/uuid v1.3.0 6 | 7 | require ( 8 | github.com/hashicorp/errwrap v1.0.0 // indirect 9 | github.com/hashicorp/go-multierror v1.1.1 // indirect 10 | github.com/mdp/qrterminal/v3 v3.0.0 // indirect 11 | rsc.io/qr v0.2.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | bin/ 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS build 2 | 3 | ARG GIT_DESC=undefined 4 | 5 | WORKDIR /go/src/github.com/Snawoot/nth-dump 6 | COPY . . 7 | RUN CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static" -X main.version='"$GIT_DESC" ./cmd/nth-dump 8 | ADD https://curl.haxx.se/ca/cacert.pem /certs.crt 9 | RUN chmod 0644 /certs.crt 10 | 11 | FROM scratch AS arrange 12 | COPY --from=build /go/src/github.com/Snawoot/nth-dump/nth-dump / 13 | COPY --from=build /certs.crt /etc/ssl/certs/ca-certificates.crt 14 | 15 | FROM scratch 16 | COPY --from=arrange / / 17 | USER 9999:9999 18 | ENTRYPOINT ["/nth-dump"] 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Snawoot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 4 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 5 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 6 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 7 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 8 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 9 | github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= 10 | github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= 11 | github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= 12 | github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= 13 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= 15 | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= 16 | -------------------------------------------------------------------------------- /nthclient/pubkey.go: -------------------------------------------------------------------------------- 1 | package nthclient 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | ) 8 | 9 | const DefaultPublicKeyPEM = ` 10 | -----BEGIN PUBLIC KEY----- 11 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9/W/2SBYDG9rlQ3XJt39 12 | p2ebGsZo81o1Oq6cPwP0BHIvfjeWf3l0fQaNS1zAgTenyBWNxV4Sk526mGFnnpeP 13 | 2Fjx6YMsIdULSFoz63is1Inii82DGLE5CWvzM1RvZkV8rQ5UcWRPh3je2g6Vzyd0 14 | AKA0xxTqvQQbnsK1sEK9biMI2242yvzUEOI36M9dVr5WOzZurIC+RgE4OjAsfGNc 15 | 5rNu2ILO+T0Zq5YOiOaqh1CmvlVwlazvjUcdsEPitsMi01w4DLdAi8qJFO1dNNaE 16 | jDFMVXT5Sxk/lmpoeRzG+aYBnd3LlIDlaaSG1ja0gxf8GHoqckLAiiV8OyDJA5Jn 17 | ySGh0rjkuUkncmhAyrK6bEFnQYhaqxXEEUTikKhYFi0A/17JOkRXyOW/uNhS3lQo 18 | Z42GkYlAaKSqFR4TA6nNmpup3eTyGpUKwjZqy37PT8SKytD9I1yM3No5KvtSV/lh 19 | 05yf0+JJZL0a4ChDLWa0OEuuaY/ocKO4VuVB+3KpbgfF8uAOvGBMk60QUGoG6vDK 20 | jm2TIzxYCWojihmThx319mFytovJd/JP/c8vXVvDO4fJOYMbPhjYMju8/HmH2atE 21 | W1dgnzDHpO9ngALzJ8XM94V0DGPvqqKg/UqOCCYZy9Zc4YofE34/7tIicI/ho4Kw 22 | zMZ1ek4b30+kpMJ/b0xQ0UkCAwEAAQ== 23 | -----END PUBLIC KEY-----` 24 | 25 | func DefaultPublicKey() *rsa.PublicKey { 26 | block, _ := pem.Decode([]byte(DefaultPublicKeyPEM)) 27 | if block == nil { 28 | panic("failed to parse PEM block containing the public key") 29 | } 30 | 31 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 32 | if err != nil { 33 | panic("failed to parse DER encoded public key: " + err.Error()) 34 | } 35 | return pub.(*rsa.PublicKey) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/docker-ci.yml: -------------------------------------------------------------------------------- 1 | name: docker-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - 20 | name: Find Git Tag 21 | id: tagger 22 | uses: jimschubert/query-tag-action@v2 23 | with: 24 | include: 'v*' 25 | exclude: '*-rc*' 26 | commit-ish: 'HEAD' 27 | skip-unshallow: 'true' 28 | abbrev: 7 29 | - 30 | name: Determine image tag type 31 | uses: haya14busa/action-cond@v1 32 | id: imgtag 33 | with: 34 | cond: ${{ github.event_name == 'release' }} 35 | if_true: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.event.release.tag_name }},${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest 36 | if_false: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest 37 | - 38 | name: Set up QEMU 39 | uses: docker/setup-qemu-action@v1 40 | - 41 | name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v1 43 | - 44 | name: Login to DockerHub 45 | uses: docker/login-action@v1 46 | with: 47 | username: ${{ secrets.DOCKERHUB_USERNAME }} 48 | password: ${{ secrets.DOCKERHUB_TOKEN }} 49 | - 50 | name: Build and push 51 | id: docker_build 52 | uses: docker/build-push-action@v2 53 | with: 54 | context: . 55 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7 56 | push: true 57 | tags: ${{ steps.imgtag.outputs.value }} 58 | build-args: 'GIT_DESC=${{steps.tagger.outputs.tag}}' 59 | -------------------------------------------------------------------------------- /nthclient/crypto.go: -------------------------------------------------------------------------------- 1 | package nthclient 2 | 3 | import ( 4 | "crypto" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/md5" 8 | "crypto/rsa" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "encoding/hex" 12 | "fmt" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | func CalculateAPIHostname(seed, tld string) string { 18 | t := time.Now().Truncate(0).UTC().Format("2006-01-02") 19 | digest := md5.Sum([]byte(seed + t)) 20 | return fmt.Sprintf("www.%s.%s", 21 | hex.EncodeToString(digest[0:6]), 22 | tld) 23 | } 24 | 25 | func VerifyResponse(response string, pubkey *rsa.PublicKey) ([]byte, error) { 26 | parts := strings.SplitN(response, "*-*", 2) 27 | if len(parts) != 2 { 28 | return nil, fmt.Errorf("data was not found in the response. parts found: %d, response: %q", len(parts), response) 29 | } 30 | 31 | signature, err := base64.StdEncoding.DecodeString(parts[0]) 32 | if err != nil { 33 | return nil, fmt.Errorf("signature decoding failed: %w", err) 34 | } 35 | hashed := sha256.Sum256([]byte(parts[1])) 36 | 37 | err = rsa.VerifyPKCS1v15(pubkey, crypto.SHA256, hashed[:], signature) 38 | if err != nil { 39 | return nil, fmt.Errorf("signature verification failed: %w", err) 40 | } 41 | return []byte(parts[1]), nil 42 | } 43 | 44 | func Decrypt(ciphertext, key string) ([]byte, error) { 45 | bytes, err := hex.DecodeString(ciphertext) 46 | if err != nil { 47 | return nil, fmt.Errorf("ciphertexttext hex decoding failed: %w", err) 48 | } 49 | 50 | if len(bytes) < 16 { 51 | return nil, fmt.Errorf("too short ciphertext bytes len: %d", len(bytes)) 52 | } 53 | 54 | iv := bytes[0:16] 55 | data := bytes[16:] 56 | 57 | block, err := aes.NewCipher([]byte(key)) 58 | if err != nil { 59 | return nil, fmt.Errorf("can't create block ciphertext: %w", err) 60 | } 61 | 62 | stream := cipher.NewCTR(block, iv) 63 | plaintext := make([]byte, len(data)) 64 | stream.XORKeyStream(plaintext, data) 65 | 66 | return plaintext, nil 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nth-dump 2 | 3 | --- 4 | 5 | **WARNING!** Please read about risks of using nthLink! 6 | 7 | * [English](https://snawoot.github.io/stripping_nthlink_vpn_encryption/) 8 | * [Russian](https://habr.com/ru/post/684676/) 9 | 10 | --- 11 | 12 | [nthLink](https://www.nthlink.com/) API client. Retrieves shadowsocks servers and credentials. Can generate SIP-002 compatible URLs and QR codes corresponding to such URLs. 13 | 14 | ![Screenshot](https://user-images.githubusercontent.com/3524671/184556478-aaffc263-13ff-4e6f-9b3f-2dfda87cf88b.png) 15 | 16 | ## Features 17 | 18 | * Plug and Play approach: bring your favorite shadowsocks client! 19 | * Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD) 20 | * Zero configuration 21 | * Simple and straightforward 22 | 23 | ## Installation 24 | 25 | #### Binaries 26 | 27 | Pre-built binaries are available [here](https://github.com/Snawoot/nth-dump/releases/latest). 28 | 29 | #### Build from source 30 | 31 | Alternatively, you may install nth-dump from source. Run the following within the source directory: 32 | 33 | ``` 34 | make install 35 | ``` 36 | 37 | #### Docker 38 | 39 | ```sh 40 | docker run -it --rm yarmak/nth-dump 41 | ``` 42 | 43 | ## Usage 44 | 45 | Just run binary and it will output credentials. 46 | 47 | ## Synopsis 48 | 49 | ``` 50 | $ nth-dump -h 51 | Usage of /home/user/go/bin/nth-dump: 52 | -format string 53 | output format: text, raw, json (default "text") 54 | -load-profile string 55 | load JSON with settings profile from file 56 | -noqr 57 | do not print QR code with URL 58 | -nowait 59 | do not wait for key press after output (default true) 60 | -profile string 61 | secrets and constants profile (android/win/mac/ios) (default "android") 62 | -save-profile string 63 | save JSON profile for chosen configuration and exit 64 | -timeout duration 65 | operation timeout (default 30s) 66 | -url-format string 67 | output URL format: sip002, sip002u, sip002qs (default "sip002") 68 | -version 69 | show program version and exit 70 | ``` 71 | -------------------------------------------------------------------------------- /nthclient/types.go: -------------------------------------------------------------------------------- 1 | package nthclient 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | const ( 11 | FormatSIP002 = iota 12 | FormatSIP002Unshielded 13 | FormatSIP002QSAuth 14 | ) 15 | 16 | type ServerDefinition struct { 17 | Host string `json:"host"` 18 | Port uint16 `json:"port,string"` 19 | Method string `json:"method"` 20 | Name string `json:"name"` 21 | Password string `json:"password"` 22 | } 23 | 24 | func (sd *ServerDefinition) String() string { 25 | return sd.Format(FormatSIP002) 26 | } 27 | 28 | func (sd *ServerDefinition) Format(format int) string { 29 | auth := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", sd.Method, sd.Password))) 30 | switch format { 31 | case FormatSIP002Unshielded: 32 | return fmt.Sprintf("ss://%s:%s@%s:%d#%s", sd.Method, sd.Password, sd.Host, sd.Port, url.PathEscape(sd.Name)) 33 | case FormatSIP002QSAuth: 34 | return fmt.Sprintf("ss://%s:%d?auth=%s#%s", sd.Host, sd.Port, auth, url.PathEscape(sd.Name)) 35 | default: 36 | return fmt.Sprintf("ss://%s@%s:%d#%s", auth, sd.Host, sd.Port, url.PathEscape(sd.Name)) 37 | } 38 | } 39 | 40 | type DomainSeedTLDDefinition struct { 41 | Seed string `json:"seed"` 42 | TLD string `json:"tld"` 43 | } 44 | 45 | type ServerConfigResponse struct { 46 | Servers []*ServerDefinition `json:"servers"` 47 | DomainSeed string `json:"domainSeed,omitempty"` 48 | DomainSeedTLD []*DomainSeedTLDDefinition `json:"domainSeedTLD,omitempty"` 49 | FilterdFeaturedNewsHost string `json:"filterdFeaturedNewsHost,omitempty"` 50 | OFUInterval int64 `json:"ofuInterval,string"` 51 | OFUMax int64 `json:"ofuMax,string"` 52 | } 53 | 54 | func UnmarshalServerConfig(input []byte) (*ServerConfigResponse, error) { 55 | var serverConfig ServerConfigResponse 56 | 57 | err := json.Unmarshal(input, &serverConfig) 58 | if err != nil { 59 | return nil, fmt.Errorf("JSON unmarshalling failed: %w", err) 60 | } 61 | 62 | return &serverConfig, nil 63 | } 64 | -------------------------------------------------------------------------------- /nthclient/settings.go: -------------------------------------------------------------------------------- 1 | package nthclient 2 | 3 | import ( 4 | "crypto/rsa" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | const ConfigRoutePath = "/getserver-190831.php" 11 | 12 | // Settings define important constants required for Client operation 13 | type Settings struct { 14 | DomainSeed string `json:"domainSeed"` 15 | PlatformKey string `json:"platformKey"` 16 | JSONSeed string `json:"jsonSeed"` 17 | TLD string `json:"tld"` 18 | Language string `json:"lang"` 19 | ID string `json:"id"` 20 | AppVersion string `json:"appVersion"` 21 | UserAgent string `json:"userAgent"` 22 | PublicKey *rsa.PublicKey `json:"publicKey"` 23 | BackupDomains []string `json:"backupDomains"` 24 | Timeout time.Duration `json:"timeout"` 25 | } 26 | 27 | var DefaultWinSettings = &Settings{ 28 | DomainSeed: "ewriWabKW6aMTa2W7vFNxKqgUutgpWwH", 29 | //DomainSeed: "7thb8GDjE39iaXXjgutYbgEI8g0aqxnf", 30 | PlatformKey: "jk8Gh9wweC4gF8et", 31 | JSONSeed: "Gu82kdDgus0248gzkqpsl948ab7a8dse", 32 | TLD: "info", 33 | Language: "en-US", 34 | ID: uuid.Must(uuid.NewRandom()).String(), 35 | AppVersion: "5.0.0", 36 | UserAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) nthLink/5.0.0 Chrome/78.0.3905.1 Electron/7.0.0 Safari/537.36", 37 | PublicKey: DefaultPublicKey(), 38 | BackupDomains: []string{ 39 | "https://s3.us-west-1.amazonaws.com/nthassets/getserver.w", 40 | "https://s3-ap-northeast-1.amazonaws.com/nthassets-tokyo/getserver.w", 41 | "https://s3.eu-west-2.amazonaws.com/nthassets-london/getserver.w", 42 | }, 43 | Timeout: 5 * time.Second, 44 | } 45 | 46 | var DefaultIOSSettings = &Settings{ 47 | DomainSeed: "ewriWabKW6aMTa2W7vFNxKqgUutgpWwH", 48 | PlatformKey: "gvaiDcY7Z5ufX4b6", 49 | JSONSeed: "Gu82kdDgus0248gzkqpsl948ab7a8dse", 50 | TLD: "info", 51 | Language: "en-US", 52 | ID: uuid.Must(uuid.NewRandom()).String(), 53 | AppVersion: "5.1.0", 54 | UserAgent: "", 55 | PublicKey: DefaultPublicKey(), 56 | BackupDomains: []string{ 57 | "https://s3.us-west-1.amazonaws.com/nthassets/getserver.i", 58 | "https://s3-ap-northeast-1.amazonaws.com/nthassets-tokyo/getserver.i", 59 | "https://s3.eu-west-2.amazonaws.com/nthassets-london/getserver.i", 60 | }, 61 | Timeout: 5 * time.Second, 62 | } 63 | 64 | var DefaultAndroidSettings = &Settings{ 65 | DomainSeed: "ewriWabKW6aMTa2W7vFNxKqgUutgpWwH", 66 | PlatformKey: "Cxgh48fDSJhiWpk9", 67 | JSONSeed: "Gu82kdDgus0248gzkqpsl948ab7a8dse", 68 | TLD: "info", 69 | Language: "en-US", 70 | ID: uuid.Must(uuid.NewRandom()).String(), 71 | AppVersion: "5.1.0", 72 | UserAgent: "", 73 | PublicKey: DefaultPublicKey(), 74 | BackupDomains: []string{ 75 | "https://s3.us-west-1.amazonaws.com/nthassets/getserver.a", 76 | "https://s3-ap-northeast-1.amazonaws.com/nthassets-tokyo/getserver.a", 77 | "https://s3.eu-west-2.amazonaws.com/nthassets-london/getserver.a", 78 | }, 79 | Timeout: 5 * time.Second, 80 | } 81 | 82 | var DefaultMacSettings = &Settings{ 83 | DomainSeed: "ewriWabKW6aMTa2W7vFNxKqgUutgpWwH", 84 | PlatformKey: "HnxjpP2gd6sZGdkh", 85 | JSONSeed: "Gu82kdDgus0248gzkqpsl948ab7a8dse", 86 | TLD: "info", 87 | Language: "en-US", 88 | ID: uuid.Must(uuid.NewRandom()).String(), 89 | AppVersion: "5.1.0", 90 | UserAgent: "", 91 | PublicKey: DefaultPublicKey(), 92 | BackupDomains: []string{ 93 | "https://s3.us-west-1.amazonaws.com/nthassets/getserver.m", 94 | "https://s3-ap-northeast-1.amazonaws.com/nthassets-tokyo/getserver.m", 95 | "https://s3.eu-west-2.amazonaws.com/nthassets-london/getserver.m", 96 | }, 97 | Timeout: 5 * time.Second, 98 | } 99 | 100 | // DefaultSettings is Settings with working defaults 101 | var DefaultSettings = DefaultAndroidSettings 102 | -------------------------------------------------------------------------------- /nthclient/client.go: -------------------------------------------------------------------------------- 1 | package nthclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/hashicorp/go-multierror" 12 | ) 13 | 14 | // Client for nthLink API backend, capable of retrieving server configuration from it. 15 | type Client struct { 16 | settings *Settings 17 | httpClient *http.Client 18 | } 19 | 20 | // Creates new client 21 | func New() *Client { 22 | return &Client{ 23 | settings: DefaultSettings, 24 | httpClient: &http.Client{}, 25 | } 26 | } 27 | 28 | // Copies Client 29 | func (c *Client) Clone() *Client { 30 | newClient := *c 31 | return &newClient 32 | } 33 | 34 | // Creates new Client with specified http.Client 35 | func (c *Client) WithHTTPClient(httpClient *http.Client) *Client { 36 | newClient := c.Clone() 37 | newClient.httpClient = httpClient 38 | return newClient 39 | } 40 | 41 | // Creates new Client with specified Settings 42 | func (c *Client) WithSettings(settings *Settings) *Client { 43 | newClient := c.Clone() 44 | newClient.settings = settings 45 | return newClient 46 | } 47 | 48 | func (c *Client) prepareRequest(ctx context.Context, urlString string) (*http.Request, error) { 49 | if urlString == "" { 50 | urlObject := url.URL{ 51 | Scheme: "https", 52 | Host: CalculateAPIHostname(c.settings.DomainSeed, c.settings.TLD), 53 | Path: ConfigRoutePath, 54 | RawQuery: url.Values{ 55 | "key": []string{c.settings.PlatformKey}, 56 | "id": []string{c.settings.ID}, 57 | "lang": []string{c.settings.Language}, 58 | "appVersion": []string{c.settings.AppVersion}, 59 | }.Encode(), 60 | } 61 | urlString = urlObject.String() 62 | } 63 | log.Printf("trying URL: %q", urlString) 64 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlString, nil) 65 | if err != nil { 66 | return nil, fmt.Errorf("unable to construct request: %w", err) 67 | } 68 | req.Header.Set("Accept", "application/json") 69 | req.Header.Set("User-Agent", c.settings.UserAgent) 70 | req.Header.Set("Accept-Language", c.settings.Language) 71 | return req, nil 72 | } 73 | 74 | func (c *Client) getEncryptedBody(ctx context.Context) ([]byte, error) { 75 | var resErr error 76 | defer func() { 77 | if resErr != nil { 78 | log.Printf("getEncryptedBody(): errors occured: %v", resErr) 79 | } 80 | }() 81 | targets := append([]string{"", "", ""}, c.settings.BackupDomains...) 82 | for _, urlString := range targets { 83 | ctx1, cl := context.WithTimeout(ctx, c.settings.Timeout) 84 | defer cl() 85 | req, err := c.prepareRequest(ctx1, urlString) 86 | if err != nil { 87 | resErr = multierror.Append(resErr, fmt.Errorf("unable to prepare request: %w", err)) 88 | continue 89 | } 90 | 91 | resp, err := c.httpClient.Do(req) 92 | if err != nil { 93 | resErr = multierror.Append(resErr, fmt.Errorf("API request failed: %w", err)) 94 | continue 95 | } 96 | defer cleanupBody(resp.Body) 97 | 98 | rd := &io.LimitedReader{ 99 | R: resp.Body, 100 | N: readLimit, 101 | } 102 | 103 | if resp.StatusCode != http.StatusOK { 104 | respBytes, _ := io.ReadAll(rd) 105 | resErr = multierror.Append(resErr, 106 | fmt.Errorf("bad status code from API: code = %d, body = %q", resp.StatusCode, string(respBytes)), 107 | ) 108 | continue 109 | } 110 | 111 | respBytes, err := io.ReadAll(rd) 112 | if err != nil { 113 | resErr = multierror.Append(resErr, fmt.Errorf("API response read failed: %w", err)) 114 | continue 115 | } 116 | 117 | payload, err := VerifyResponse(string(respBytes), c.settings.PublicKey) 118 | if err != nil { 119 | resErr = multierror.Append(resErr, fmt.Errorf("API response verification failed: %w", err)) 120 | continue 121 | } 122 | 123 | decrypted, err := Decrypt(string(payload), c.settings.JSONSeed) 124 | if err != nil { 125 | resErr = multierror.Append(resErr, fmt.Errorf("payload decryption failed: %w", err)) 126 | continue 127 | } 128 | 129 | return decrypted, nil 130 | } 131 | return nil, resErr 132 | } 133 | 134 | func (c *Client) GetServerConfig(ctx context.Context) ([]byte, error) { 135 | return c.getEncryptedBody(ctx) 136 | } 137 | 138 | const readLimit int64 = 128 * 1024 139 | 140 | // Does cleanup of HTTP response in order to make it reusable by keep-alive 141 | // logic of HTTP client 142 | func cleanupBody(body io.ReadCloser) { 143 | io.Copy(io.Discard, &io.LimitedReader{ 144 | R: body, 145 | N: readLimit, 146 | }) 147 | body.Close() 148 | } 149 | -------------------------------------------------------------------------------- /cmd/nth-dump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/mdp/qrterminal/v3" 13 | 14 | "github.com/Snawoot/nth-dump/nthclient" 15 | ) 16 | 17 | var version = "undefined" 18 | 19 | var ( 20 | // global options 21 | showVersion = flag.Bool("version", false, "show program version and exit") 22 | timeout = flag.Duration("timeout", 30*time.Second, "operation timeout") 23 | format = flag.String("format", "text", "output format: text, raw, json") 24 | urlFormat = flag.String("url-format", "sip002", "output URL format: sip002, sip002u, sip002qs") 25 | profile = flag.String("profile", "android", "secrets and constants profile (android/win/mac/ios)") 26 | loadProfile = flag.String("load-profile", "", "load JSON with settings profile from file") 27 | saveProfile = flag.String("save-profile", "", "save JSON profile for chosen configuration and exit") 28 | ) 29 | 30 | func run() int { 31 | flag.Parse() 32 | if *showVersion { 33 | fmt.Println(version) 34 | return 0 35 | } 36 | 37 | ctx, cl := context.WithTimeout(context.Background(), *timeout) 38 | defer cl() 39 | 40 | settings := nthclient.DefaultSettings 41 | switch *profile { 42 | case "mac": 43 | settings = nthclient.DefaultMacSettings 44 | case "win": 45 | settings = nthclient.DefaultWinSettings 46 | case "ios": 47 | settings = nthclient.DefaultIOSSettings 48 | case "android": 49 | settings = nthclient.DefaultAndroidSettings 50 | } 51 | 52 | if *loadProfile != "" { 53 | loadedSettings, err := loadSettings(*loadProfile) 54 | if err != nil { 55 | log.Fatalf("unable to load settings file: %v", err) 56 | } 57 | settings = loadedSettings 58 | } 59 | 60 | if *saveProfile != "" { 61 | err := saveSettings(*saveProfile, settings) 62 | if err != nil { 63 | log.Fatalf("unable to save settings file: %v", err) 64 | } 65 | return 0 66 | } 67 | 68 | nc := nthclient.New().WithSettings(settings) 69 | b, err := nc.GetServerConfig(ctx) 70 | if err != nil { 71 | log.Fatalf("can't get server config: %v", err) 72 | } 73 | 74 | switch *format { 75 | case "raw": 76 | fmt.Println(string(b)) 77 | case "json": 78 | serverConfig, err := nthclient.UnmarshalServerConfig(b) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | enc := json.NewEncoder(os.Stdout) 84 | enc.SetIndent("", " ") 85 | 86 | if err := enc.Encode(serverConfig.Servers); err != nil { 87 | log.Fatalf("can't marshal server list to json: %v", err) 88 | } 89 | default: 90 | serverConfig, err := nthclient.UnmarshalServerConfig(b) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | for _, server := range serverConfig.Servers { 96 | var url string 97 | switch *urlFormat { 98 | case "sip002u": 99 | url = server.Format(nthclient.FormatSIP002Unshielded) 100 | case "sip002qs": 101 | url = server.Format(nthclient.FormatSIP002QSAuth) 102 | default: 103 | url = server.Format(nthclient.FormatSIP002) 104 | } 105 | fmt.Println("\n----------\n") 106 | if !*noqr { 107 | qrterminal.Generate(url, qrterminal.L, os.Stdout) 108 | } 109 | fmt.Printf("Name:\t\t%s\n", server.Name) 110 | fmt.Printf("Host:\t\t%s\n", server.Host) 111 | fmt.Printf("Port:\t\t%d\n", server.Port) 112 | fmt.Printf("Method:\t\t%s\n", server.Method) 113 | fmt.Printf("Password:\t%s\n", server.Password) 114 | fmt.Printf("URL:\t\t%s\n", url) 115 | } 116 | fmt.Println("\n----------\n") 117 | if !*nowait { 118 | fmt.Fprintln(os.Stderr, "Press ENTER to exit...") 119 | fmt.Scanln() 120 | } 121 | } 122 | 123 | return 0 124 | } 125 | 126 | func loadSettings(filename string) (*nthclient.Settings, error) { 127 | file, err := os.Open(filename) 128 | if err != nil { 129 | return nil, err 130 | } 131 | defer file.Close() 132 | 133 | var state nthclient.Settings 134 | dec := json.NewDecoder(file) 135 | err = dec.Decode(&state) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | return &state, nil 141 | } 142 | 143 | func saveSettings(filename string, state *nthclient.Settings) error { 144 | file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600) 145 | if err != nil { 146 | return err 147 | } 148 | defer file.Close() 149 | 150 | enc := json.NewEncoder(file) 151 | enc.SetIndent("", " ") 152 | err = enc.Encode(state) 153 | return err 154 | } 155 | 156 | func main() { 157 | log.Default().SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile) 158 | log.Default().SetPrefix("NTH-DUMP: ") 159 | os.Exit(run()) 160 | } 161 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROGNAME = nth-dump 2 | OUTSUFFIX = bin/$(PROGNAME) 3 | VERSION := $(shell git describe) 4 | BUILDOPTS = -a -tags netgo 5 | LDFLAGS = -ldflags '-s -w -extldflags "-static" -X main.version=$(VERSION)' 6 | LDFLAGS_NATIVE = -ldflags '-s -w -X main.version=$(VERSION)' 7 | MAIN_PACKAGE = ./cmd/$(PROGNAME) 8 | 9 | NDK_CC_ARM = $(abspath ../../ndk-toolchain-arm/bin/arm-linux-androideabi-gcc) 10 | NDK_CC_ARM64 = $(abspath ../../ndk-toolchain-arm64/bin/aarch64-linux-android21-clang) 11 | 12 | GO := go 13 | 14 | src = $(wildcard *.go */*.go */*/*.go go.mod go.sum) 15 | 16 | native: bin-native 17 | all: bin-linux-amd64 bin-linux-386 bin-linux-arm bin-linux-arm64 \ 18 | bin-linux-mips bin-linux-mipsle bin-linux-mips64 bin-linux-mips64le \ 19 | bin-freebsd-amd64 bin-freebsd-386 bin-freebsd-arm bin-freebsd-arm64 \ 20 | bin-netbsd-amd64 bin-netbsd-386 bin-netbsd-arm bin-netbsd-arm64 \ 21 | bin-openbsd-amd64 bin-openbsd-386 bin-openbsd-arm bin-openbsd-arm64 \ 22 | bin-darwin-amd64 bin-darwin-arm64 \ 23 | bin-windows-amd64 bin-windows-386 bin-windows-arm 24 | 25 | allplus: all \ 26 | bin-android-arm bin-android-arm64 27 | 28 | bin-native: $(OUTSUFFIX) 29 | bin-linux-amd64: $(OUTSUFFIX).linux-amd64 30 | bin-linux-386: $(OUTSUFFIX).linux-386 31 | bin-linux-arm: $(OUTSUFFIX).linux-arm 32 | bin-linux-arm64: $(OUTSUFFIX).linux-arm64 33 | bin-linux-mips: $(OUTSUFFIX).linux-mips 34 | bin-linux-mipsle: $(OUTSUFFIX).linux-mipsle 35 | bin-linux-mips64: $(OUTSUFFIX).linux-mips64 36 | bin-linux-mips64le: $(OUTSUFFIX).linux-mips64le 37 | bin-freebsd-amd64: $(OUTSUFFIX).freebsd-amd64 38 | bin-freebsd-386: $(OUTSUFFIX).freebsd-386 39 | bin-freebsd-arm: $(OUTSUFFIX).freebsd-arm 40 | bin-freebsd-arm64: $(OUTSUFFIX).freebsd-arm64 41 | bin-netbsd-amd64: $(OUTSUFFIX).netbsd-amd64 42 | bin-netbsd-386: $(OUTSUFFIX).netbsd-386 43 | bin-netbsd-arm: $(OUTSUFFIX).netbsd-arm 44 | bin-netbsd-arm64: $(OUTSUFFIX).netbsd-arm64 45 | bin-openbsd-amd64: $(OUTSUFFIX).openbsd-amd64 46 | bin-openbsd-386: $(OUTSUFFIX).openbsd-386 47 | bin-openbsd-arm: $(OUTSUFFIX).openbsd-arm 48 | bin-openbsd-arm64: $(OUTSUFFIX).openbsd-arm64 49 | bin-darwin-amd64: $(OUTSUFFIX).darwin-amd64 50 | bin-darwin-arm64: $(OUTSUFFIX).darwin-arm64 51 | bin-windows-amd64: $(OUTSUFFIX).windows-amd64.exe 52 | bin-windows-386: $(OUTSUFFIX).windows-386.exe 53 | bin-windows-arm: $(OUTSUFFIX).windows-arm.exe 54 | bin-android-arm: $(OUTSUFFIX).android-arm 55 | bin-android-arm64: $(OUTSUFFIX).android-arm64 56 | 57 | $(OUTSUFFIX): $(src) 58 | $(GO) build $(LDFLAGS_NATIVE) -o $@ $(MAIN_PACKAGE) 59 | 60 | $(OUTSUFFIX).linux-amd64: $(src) 61 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 62 | 63 | $(OUTSUFFIX).linux-386: $(src) 64 | CGO_ENABLED=0 GOOS=linux GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 65 | 66 | $(OUTSUFFIX).linux-arm: $(src) 67 | CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 68 | 69 | $(OUTSUFFIX).linux-arm64: $(src) 70 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 71 | 72 | $(OUTSUFFIX).linux-mips: $(src) 73 | CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 74 | 75 | $(OUTSUFFIX).linux-mips64: $(src) 76 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64 GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 77 | 78 | $(OUTSUFFIX).linux-mipsle: $(src) 79 | CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 80 | 81 | $(OUTSUFFIX).linux-mips64le: $(src) 82 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64le GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 83 | 84 | $(OUTSUFFIX).freebsd-amd64: $(src) 85 | CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 86 | 87 | $(OUTSUFFIX).freebsd-386: $(src) 88 | CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 89 | 90 | $(OUTSUFFIX).freebsd-arm: $(src) 91 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 92 | 93 | $(OUTSUFFIX).freebsd-arm64: $(src) 94 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 95 | 96 | $(OUTSUFFIX).netbsd-amd64: $(src) 97 | CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 98 | 99 | $(OUTSUFFIX).netbsd-386: $(src) 100 | CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 101 | 102 | $(OUTSUFFIX).netbsd-arm: $(src) 103 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 104 | 105 | $(OUTSUFFIX).netbsd-arm64: $(src) 106 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 107 | 108 | $(OUTSUFFIX).openbsd-amd64: $(src) 109 | CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 110 | 111 | $(OUTSUFFIX).openbsd-386: $(src) 112 | CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 113 | 114 | $(OUTSUFFIX).openbsd-arm: $(src) 115 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 116 | 117 | $(OUTSUFFIX).openbsd-arm64: $(src) 118 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 119 | 120 | $(OUTSUFFIX).darwin-amd64: $(src) 121 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 122 | 123 | $(OUTSUFFIX).darwin-arm64: $(src) 124 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 125 | 126 | $(OUTSUFFIX).windows-amd64.exe: $(src) 127 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 128 | 129 | $(OUTSUFFIX).windows-386.exe: $(src) 130 | CGO_ENABLED=0 GOOS=windows GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 131 | 132 | $(OUTSUFFIX).windows-arm.exe: $(src) 133 | CGO_ENABLED=0 GOOS=windows GOARCH=arm GOARM=7 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@ $(MAIN_PACKAGE) 134 | 135 | $(OUTSUFFIX).android-arm: $(src) 136 | CC=$(NDK_CC_ARM) CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 $(GO) build $(LDFLAGS_NATIVE) -o $@ $(MAIN_PACKAGE) 137 | 138 | $(OUTSUFFIX).android-arm64: $(src) 139 | CC=$(NDK_CC_ARM64) CGO_ENABLED=1 GOOS=android GOARCH=arm64 $(GO) build $(LDFLAGS_NATIVE) -o $@ $(MAIN_PACKAGE) 140 | 141 | clean: 142 | rm -f bin/* 143 | 144 | fmt: 145 | $(GO) fmt ./... 146 | 147 | run: 148 | $(GO) run $(LDFLAGS) $(MAIN_PACKAGE) 149 | 150 | install: 151 | $(GO) install $(LDFLAGS_NATIVE) $(MAIN_PACKAGE) 152 | 153 | .PHONY: clean all native fmt install \ 154 | bin-native \ 155 | bin-linux-amd64 \ 156 | bin-linux-386 \ 157 | bin-linux-arm \ 158 | bin-linux-arm64 \ 159 | bin-linux-mips \ 160 | bin-linux-mipsle \ 161 | bin-linux-mips64 \ 162 | bin-linux-mips64le \ 163 | bin-freebsd-amd64 \ 164 | bin-freebsd-386 \ 165 | bin-freebsd-arm \ 166 | bin-freebsd-arm64 \ 167 | bin-netbsd-amd64 \ 168 | bin-netbsd-386 \ 169 | bin-netbsd-arm \ 170 | bin-netbsd-arm64 \ 171 | bin-openbsd-amd64 \ 172 | bin-openbsd-386 \ 173 | bin-openbsd-arm \ 174 | bin-openbsd-arm64 \ 175 | bin-darwin-amd64 \ 176 | bin-darwin-arm64 \ 177 | bin-windows-amd64 \ 178 | bin-windows-386 \ 179 | bin-windows-arm \ 180 | bin-android-arm \ 181 | bin-android-arm64 182 | --------------------------------------------------------------------------------