├── .github └── workflows │ └── build-push.yml ├── .gitignore ├── .idx └── dev.nix ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config.toml ├── go.mod ├── go.sum ├── handler.go ├── limits.go ├── main.go ├── main_test.go ├── matcher.go ├── proxy.go ├── release.sh ├── version.go └── websocketproxy.go /.github/workflows/build-push.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.23 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | 27 | bump: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | if: github.event_name == 'push' 31 | permissions: 32 | contents: write 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Bump version 36 | run: | 37 | git config --global user.email "github+actions@gmail.com" 38 | git config --global user.name "Actions" 39 | git fetch --tags 40 | wget -O - https://raw.githubusercontent.com/treeder/bump/master/gitbump.sh | bash 41 | 42 | # Push image to GitHub Packages. 43 | push: 44 | needs: bump 45 | runs-on: ubuntu-latest 46 | if: github.event_name == 'push' 47 | 48 | permissions: 49 | contents: read 50 | packages: write 51 | 52 | steps: 53 | - uses: actions/checkout@v4 54 | 55 | - name: Build image 56 | run: docker build . -t rpc-proxy 57 | 58 | - name: Log into registry 59 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin 60 | 61 | - name: Push image 62 | run: | 63 | IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/rpc-proxy 64 | 65 | # Change all uppercase to lowercase 66 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') 67 | 68 | # Strip git ref prefix from version 69 | # VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 70 | git fetch --tags # checkout action does not get these 71 | VERSION=$(git tag --sort=-v:refname --list "v[0-9]*" | head -n 1) 72 | echo "Image version tag: $VERSION" 73 | 74 | # Strip "v" prefix from tag name 75 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 76 | 77 | # Use Docker `latest` tag convention 78 | [ "$VERSION" == "master" ] && VERSION=latest 79 | 80 | echo IMAGE_ID=$IMAGE_ID 81 | echo VERSION=$VERSION 82 | 83 | docker tag rpc-proxy $IMAGE_ID:$VERSION 84 | docker push $IMAGE_ID:$VERSION 85 | 86 | docker tag rpc-proxy $IMAGE_ID:latest 87 | docker push $IMAGE_ID:latest 88 | 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rpc-proxy 2 | vendor/ 3 | /tmp 4 | .idea 5 | /web3 6 | -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | # To learn more about how to use Nix to configure your environment 2 | # see: https://developers.google.com/idx/guides/customize-idx-env 3 | { pkgs, ... }: { 4 | # Which nixpkgs channel to use. 5 | channel = "stable-24.05"; # or "unstable" 6 | 7 | # Use https://search.nixos.org/packages to find packages 8 | packages = [ 9 | pkgs.go 10 | # pkgs.python311 11 | # pkgs.python311Packages.pip 12 | # pkgs.nodejs_20 13 | # pkgs.nodePackages.nodemon 14 | pkgs.gnumake 15 | ]; 16 | 17 | # Sets environment variables in the workspace 18 | env = {}; 19 | services.docker.enable = true; 20 | idx = { 21 | # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" 22 | extensions = [ 23 | # "vscodevim.vim" 24 | "golang.go" 25 | ]; 26 | 27 | # Enable previews 28 | previews = { 29 | enable = true; 30 | previews = { 31 | # web = { 32 | # # Example: run "npm run dev" with PORT set to IDX's defined port for previews, 33 | # # and show it in IDX's web preview panel 34 | # command = ["npm" "run" "dev"]; 35 | # manager = "web"; 36 | # env = { 37 | # # Environment variables to set for your server 38 | # PORT = "$PORT"; 39 | # }; 40 | # }; 41 | }; 42 | }; 43 | 44 | # Workspace lifecycle hooks 45 | workspace = { 46 | # Runs when a workspace is first created 47 | onCreate = { 48 | # Example: install JS dependencies from NPM 49 | # npm-install = "npm install"; 50 | }; 51 | # Runs when the workspace is (re)started 52 | onStart = { 53 | # Example: start a background task to watch and re-build backend code 54 | # watch-backend = "npm run watch-backend"; 55 | }; 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDX.aI.enableInlineCompletion": true, 3 | "IDX.aI.enableCodebaseIndexing": true 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build GoChain in a stock Go builder container 2 | FROM golang:1.23-alpine as builder 3 | 4 | RUN apk --no-cache add build-base git mercurial gcc linux-headers 5 | ENV D=/rpc-proxy 6 | WORKDIR $D 7 | # cache dependencies 8 | ADD go.mod $D 9 | ADD go.sum $D 10 | RUN go mod download 11 | # build 12 | ADD . $D 13 | RUN cd $D && go build && cp rpc-proxy /tmp 14 | 15 | # Pull all binaries into a second stage deploy alpine container 16 | FROM alpine:latest 17 | COPY --from=builder /tmp/rpc-proxy /usr/local/bin/ 18 | ENTRYPOINT ["rpc-proxy"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build docker run install test 2 | 3 | build: 4 | go build 5 | 6 | run: build 7 | ./rpc-proxy -url http://35.228.129.142/ -config config.toml 8 | 9 | docker: 10 | docker build -t gochain/rpc-proxy . 11 | 12 | # Proxy to the testnet node http://35.228.129.142/ 13 | run-docker: docker 14 | docker run --rm -it -p 8545:8545 -v ${PWD}/config.toml:/proxy.toml gochain/rpc-proxy -url http://35.228.129.142/ -port 8545 -rpm 1000 -config /proxy.toml -verbose 15 | 16 | install: 17 | go install 18 | 19 | test: 20 | go test ./... 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpc-proxy 2 | 3 | A proxy for `web3` JSONRPC featuring: 4 | 5 | - rate limiting 6 | - method filtering 7 | - stats 8 | 9 | ## Getting Started 10 | 11 | ### Prerequisites 12 | 13 | At least Go 1.20. Installation documentation here: https://golang.org/doc/install 14 | 15 | ### How to Use 16 | 17 | By default, `rpc-proxy` will run on port `8545` and redirect requests to `http://localhost:8040`. These values 18 | can be changed with the `port` and `url` flags, along with other options: 19 | 20 | ```sh 21 | > rpc-proxy help 22 | NAME: 23 | rpc-proxy - A proxy for web3 JSONRPC 24 | 25 | USAGE: 26 | rpc-proxy [global options] command [command options] [arguments...] 27 | 28 | VERSION: 29 | 0.0.28 30 | 31 | COMMANDS: 32 | help, h Shows a list of commands or help for one command 33 | 34 | GLOBAL OPTIONS: 35 | --config value, -c value path to toml config file 36 | --port value, -p value port to serve (default: "8545") 37 | --url value, -u value redirect url (default: "http://127.0.0.1:8040") 38 | --allow value, -a value comma separated list of allowed paths 39 | --rpm value limit for number of requests per minute from single IP (default: 1000) 40 | --nolimit value, -n value list of ips allowed unlimited requests(separated by commas) 41 | --verbose verbose logging enabled 42 | --help, -h show help 43 | --version, -v print the version 44 | ``` 45 | 46 | ## Docker 47 | 48 | Run our Docker image: 49 | 50 | ```sh 51 | docker run ghcr.io/gochain/rpc-proxy/rpc-proxy:latest 52 | ``` 53 | 54 | Build Docker image: 55 | 56 | ```sh 57 | make docker 58 | ``` 59 | 60 | Run it: 61 | 62 | ```sh 63 | make run 64 | ``` 65 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # This is an example configuration file. 2 | Allow = [ 3 | "clique_getSigners", 4 | "clique_getSignersAtHash", 5 | "clique_getSnapshot", 6 | "clique_getSnapshotAtHash", 7 | "clique_getVoters", 8 | "clique_getVotersAtHash", 9 | "eth_blockNumber", 10 | "eth_call", 11 | "eth_chainId", 12 | "eth_estimateGas", 13 | "eth_gasPrice", 14 | "eth_genesisAlloc", 15 | "eth_getBalance", 16 | "eth_getBlockByHash", 17 | "eth_getBlockByNumber", 18 | "eth_getBlockTransactionCountByHash", 19 | "eth_getBlockTransactionCountByNumber", 20 | "eth_getCode", 21 | "eth_getFilterChanges", 22 | "eth_getLogs", 23 | "eth_getStorageAt", 24 | "eth_getTransactionByBlockHashAndIndex", 25 | "eth_getTransactionByBlockNumberAndIndex", 26 | "eth_getTransactionByHash", 27 | "eth_getTransactionCount", 28 | "eth_getTransactionReceipt", 29 | "eth_newBlockFilter", 30 | "eth_newPendingTransactionFilter", 31 | "eth_sendRawTransaction", 32 | "eth_subscribe", 33 | "eth_totalSupply", 34 | "eth_uninstallFilter", 35 | "eth_unsubscribe", 36 | "net_listening", 37 | "net_version", 38 | "rpc_modules", 39 | "web3_clientVersion", 40 | ] 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gochain-io/rpc-proxy 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | cloud.google.com/go v0.117.0 // indirect 9 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 10 | github.com/go-chi/chi/v5 v5.2.2 11 | github.com/gochain/gochain/v3 v3.4.9 12 | github.com/golang/snappy v0.0.4 // indirect 13 | github.com/gorilla/websocket v1.5.3 14 | github.com/pelletier/go-toml v1.9.5 15 | github.com/rs/cors v1.11.1 16 | github.com/treeder/gcputils v0.1.10 17 | github.com/treeder/gotils/v2 v2.1.17 18 | github.com/urfave/cli/v2 v2.27.5 19 | golang.org/x/crypto v0.36.0 // indirect 20 | golang.org/x/net v0.38.0 // indirect 21 | golang.org/x/sys v0.31.0 // indirect 22 | golang.org/x/time v0.8.0 23 | google.golang.org/genproto v0.0.0-20241219192143-6b3ec007d9bb // indirect 24 | ) 25 | 26 | require ( 27 | cloud.google.com/go/auth v0.13.0 // indirect 28 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect 29 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 30 | cloud.google.com/go/iam v1.3.0 // indirect 31 | cloud.google.com/go/kms v1.20.3 // indirect 32 | cloud.google.com/go/logging v1.12.0 // indirect 33 | cloud.google.com/go/longrunning v0.6.3 // indirect 34 | github.com/allegro/bigcache v1.2.1 // indirect 35 | github.com/btcsuite/btcd v0.21.0-beta // indirect 36 | github.com/cespare/xxhash v1.1.0 // indirect 37 | github.com/deckarep/golang-set v1.8.0 // indirect 38 | github.com/edsrzf/mmap-go v1.2.0 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/go-logr/logr v1.4.2 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/go-stack/stack v1.8.1 // indirect 43 | github.com/google/s2a-go v0.1.8 // indirect 44 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 45 | github.com/googleapis/gax-go/v2 v2.14.0 // indirect 46 | github.com/hashicorp/golang-lru v1.0.2 // indirect 47 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 48 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 49 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 50 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 51 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 52 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 53 | go.opentelemetry.io/otel v1.33.0 // indirect 54 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 55 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 56 | golang.org/x/oauth2 v0.27.0 // indirect 57 | golang.org/x/sync v0.12.0 // indirect 58 | golang.org/x/text v0.23.0 // indirect 59 | google.golang.org/api v0.213.0 // indirect 60 | google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb // indirect 61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect 62 | google.golang.org/grpc v1.69.2 // indirect 63 | google.golang.org/protobuf v1.36.0 // indirect 64 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.117.0 h1:Z5TNFfQxj7WG2FgOGX1ekC5RiXrYgms6QscOm32M/4s= 2 | cloud.google.com/go v0.117.0/go.mod h1:ZbwhVTb1DBGt2Iwb3tNO6SEK4q+cplHZmLWH+DelYYc= 3 | cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= 4 | cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= 7 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 8 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 9 | cloud.google.com/go/iam v1.3.0 h1:4Wo2qTaGKFtajbLpF6I4mywg900u3TLlHDb6mriLDPU= 10 | cloud.google.com/go/iam v1.3.0/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= 11 | cloud.google.com/go/kms v1.20.3 h1:a61yIN5LN8ozWxOC6tjUx5V5SEzfkS+b69kYMQfzGzE= 12 | cloud.google.com/go/kms v1.20.3/go.mod h1:YvX+xhp2E2Sc3vol5IcRlBhH14Ecl3kegUY/DtH7EWQ= 13 | cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= 14 | cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= 15 | cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= 16 | cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= 17 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 18 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 19 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 20 | github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= 21 | github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 22 | github.com/aristanetworks/goarista v0.0.0-20180424004133-70dca2f27708 h1:QHczF0ONAhgjtlNxlRedLZ0Hszmjs6Cmqw/oTJ4+K3s= 23 | github.com/aristanetworks/goarista v0.0.0-20180424004133-70dca2f27708/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= 24 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 25 | github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M= 26 | github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= 27 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 28 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 29 | github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 30 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 31 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 32 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 33 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 34 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 35 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 36 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 37 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 38 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 39 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 40 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 41 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 43 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= 45 | github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= 46 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 47 | github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= 48 | github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= 49 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 50 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 51 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 52 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 53 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 54 | github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= 55 | github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 56 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 57 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 58 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 59 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 60 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 61 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 62 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 63 | github.com/gochain/gochain/v3 v3.4.9 h1:hhe0/DYL0Bndr+9zVmt1nrrEt/fgvVG2fye/o7aeZZ8= 64 | github.com/gochain/gochain/v3 v3.4.9/go.mod h1:N44cXICmMZfydSkEEifeNgDD6W5wYI+8euk2Wab8M8c= 65 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 66 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 67 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 69 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 70 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 71 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 72 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 73 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 74 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 75 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 76 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 77 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 78 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 79 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 81 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 82 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 83 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= 84 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= 85 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 86 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 88 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 89 | github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= 90 | github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= 91 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 92 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 93 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 94 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 95 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 96 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 97 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 98 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 99 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 100 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 101 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 102 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 103 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 104 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 105 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 106 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 107 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 108 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 109 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 110 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 111 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 112 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 113 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 114 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 115 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 116 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 117 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 118 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 119 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 120 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 121 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 122 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 123 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 124 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 125 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 126 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= 127 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 128 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 129 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 130 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 131 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 132 | github.com/treeder/gcputils v0.1.10 h1:hXs3vzW2HIsZtazU8O6clfiMhjaU4Q2aYoJkLxPpk3Y= 133 | github.com/treeder/gcputils v0.1.10/go.mod h1:whTRwkcKj7rgOAg4BoTBW1Q/VsMY4IZ4Dhc9nxdF08M= 134 | github.com/treeder/gotils/v2 v2.1.17 h1:qQWEi5mpLXNzaj9QSQvHDUYZrCfW8AvvoCYA/TVH6TM= 135 | github.com/treeder/gotils/v2 v2.1.17/go.mod h1:0IT4tWQm4amQRTr3Gp4mH18Z3tKiK8lwy+ZRJUdi/9Y= 136 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 137 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 138 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 139 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 140 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 141 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 142 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 143 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 144 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= 145 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= 146 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 147 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 148 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= 149 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 150 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= 151 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= 152 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 153 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 154 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 155 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 156 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= 157 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 158 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 159 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 160 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 161 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 162 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 163 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 164 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 165 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 166 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 167 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 168 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 169 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 170 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 171 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 172 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 173 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 174 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 175 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 176 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 177 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 178 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 179 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 187 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 188 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 189 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 190 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 191 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 192 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 193 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 194 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 195 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 196 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 198 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | google.golang.org/api v0.213.0 h1:KmF6KaDyFqB417T68tMPbVmmwtIXs2VB60OJKIHB0xQ= 200 | google.golang.org/api v0.213.0/go.mod h1:V0T5ZhNUUNpYAlL306gFZPFt5F5D/IeyLoktduYYnvQ= 201 | google.golang.org/genproto v0.0.0-20241219192143-6b3ec007d9bb h1:JGs+s1Q6osip3cDY197L1HmkuPn8wPp9Hfy9jl+Uz+U= 202 | google.golang.org/genproto v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:o8GgNarfULyZPNaIY8RDfXM7AZcmcKC/tbMWp/ZOFDw= 203 | google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= 204 | google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= 205 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= 206 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= 207 | google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= 208 | google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 209 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 210 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 211 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 212 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 213 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 214 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 215 | google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= 216 | google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 217 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 219 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 220 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 221 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 222 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 223 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 224 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 225 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 226 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 227 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 228 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 229 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 230 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 231 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "math/big" 10 | "net" 11 | "net/http" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/go-chi/chi/v5/middleware" 17 | "github.com/gochain/gochain/v3/goclient" 18 | "github.com/gochain/gochain/v3/rpc" 19 | "github.com/treeder/gotils/v2" 20 | ) 21 | 22 | type myTransport struct { 23 | blockRangeLimit uint64 // 0 means none 24 | 25 | matcher 26 | limiters 27 | 28 | latestBlock 29 | } 30 | 31 | type ModifiedRequest struct { 32 | Path string 33 | RemoteAddr string // Original IP, not CloudFlare or load balancer. 34 | ID json.RawMessage 35 | Params []json.RawMessage 36 | } 37 | 38 | func isBatch(msg []byte) bool { 39 | for _, c := range msg { 40 | if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d { 41 | continue 42 | } 43 | return c == '[' 44 | } 45 | return false 46 | } 47 | 48 | // getIP returns the original IP address from the request, checking special headers before falling back to RemoteAddr. 49 | func getIP(r *http.Request) string { 50 | if ip := r.Header.Get("CF-Connecting-IP"); ip != "" { 51 | return ip 52 | } 53 | if ip := r.Header.Get("X-Forwarded-For"); ip != "" { 54 | // Trim off any others: A.B.C.D[,X.X.X.X,Y.Y.Y.Y,] 55 | return strings.SplitN(ip, ",", 1)[0] 56 | } 57 | if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { 58 | return ip 59 | } 60 | return r.RemoteAddr 61 | } 62 | 63 | func parseRequests(r *http.Request) (string, []string, []ModifiedRequest, error) { 64 | var res []ModifiedRequest 65 | var methods []string 66 | ip := getIP(r) 67 | if r.Body != nil { 68 | body, err := ioutil.ReadAll(r.Body) 69 | r.Body.Close() 70 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // must be done, even when err 71 | if err != nil { 72 | return "", nil, nil, fmt.Errorf("failed to read body: %v", err) 73 | } 74 | methods, res, err = parseMessage(body, ip) 75 | if err != nil { 76 | return "", nil, nil, err 77 | } 78 | } 79 | if len(res) == 0 { 80 | methods = append(methods, r.URL.Path) 81 | res = append(res, ModifiedRequest{ 82 | Path: r.URL.Path, 83 | RemoteAddr: ip, 84 | }) 85 | } 86 | return ip, methods, res, nil 87 | } 88 | 89 | func parseMessage(body []byte, ip string) (methods []string, res []ModifiedRequest, err error) { 90 | type rpcRequest struct { 91 | ID json.RawMessage `json:"id"` 92 | Method string `json:"method"` 93 | Params []json.RawMessage `json:"params"` 94 | } 95 | if isBatch(body) { 96 | var arr []rpcRequest 97 | err := json.Unmarshal(body, &arr) 98 | if err != nil { 99 | return nil, nil, fmt.Errorf("failed to parse JSON batch request: %v", err) 100 | } 101 | for _, t := range arr { 102 | methods = append(methods, t.Method) 103 | res = append(res, ModifiedRequest{ 104 | ID: t.ID, 105 | Path: t.Method, 106 | RemoteAddr: ip, 107 | Params: t.Params, 108 | }) 109 | } 110 | } else { 111 | var t rpcRequest 112 | err := json.Unmarshal(body, &t) 113 | if err != nil { 114 | return nil, nil, fmt.Errorf("failed to parse JSON request: %v", err) 115 | } 116 | methods = append(methods, t.Method) 117 | res = append(res, ModifiedRequest{ 118 | ID: t.ID, 119 | Path: t.Method, 120 | RemoteAddr: ip, 121 | Params: t.Params, 122 | }) 123 | } 124 | return methods, res, nil 125 | } 126 | 127 | const ( 128 | jsonRPCTimeout = -32000 129 | jsonRPCUnavailable = -32601 130 | jsonRPCInvalidParams = -32602 131 | jsonRPCInternal = -32603 132 | ) 133 | 134 | type ErrResponse struct { 135 | Version string `json:"jsonrpc"` 136 | ID json.RawMessage `json:"id"` 137 | Error struct { 138 | Code int `json:"code"` 139 | Message string `json:"message"` 140 | } `json:"error"` 141 | } 142 | 143 | func jsonRPCError(id json.RawMessage, jsonCode int, msg string) interface{} { 144 | 145 | resp := ErrResponse{ 146 | Version: "2.0", 147 | ID: id, 148 | } 149 | resp.Error.Code = jsonCode 150 | resp.Error.Message = msg 151 | return resp 152 | } 153 | 154 | func jsonRPCUnauthorized(id json.RawMessage, method string) interface{} { 155 | return jsonRPCError(id, jsonRPCUnavailable, "You are not authorized to make this request: "+method) 156 | } 157 | 158 | func jsonRPCLimit(id json.RawMessage) interface{} { 159 | return jsonRPCError(id, jsonRPCTimeout, "You hit the request limit") 160 | } 161 | 162 | func jsonRPCBlockRangeLimit(id json.RawMessage, blocks, limit uint64) interface{} { 163 | return jsonRPCError(id, jsonRPCInvalidParams, fmt.Sprintf("Requested range of blocks (%d) is larger than limit (%d).", blocks, limit)) 164 | } 165 | 166 | // jsonRPCResponse returns a JSON response containing v, or a plaintext generic 167 | // response for this httpCode and an error when JSON marshalling fails. 168 | func jsonRPCResponse(httpCode int, v interface{}) (*http.Response, error) { 169 | body, err := json.Marshal(v) 170 | if err != nil { 171 | return &http.Response{ 172 | Body: ioutil.NopCloser(strings.NewReader(http.StatusText(httpCode))), 173 | StatusCode: httpCode, 174 | }, fmt.Errorf("failed to serialize JSON: %v", err) 175 | } 176 | return &http.Response{ 177 | Body: ioutil.NopCloser(bytes.NewReader(body)), 178 | StatusCode: httpCode, 179 | }, nil 180 | } 181 | 182 | func (t *myTransport) RoundTrip(req *http.Request) (*http.Response, error) { 183 | ctx := req.Context() 184 | if reqID := middleware.GetReqID(req.Context()); reqID != "" { 185 | ctx = gotils.With(ctx, "requestID", reqID) 186 | } 187 | 188 | ip, methods, parsedRequests, err := parseRequests(req) 189 | if err != nil { 190 | gotils.L(ctx).Error().Printf("Failed to parse requests: %v", err) 191 | resp, err := jsonRPCResponse(http.StatusBadRequest, jsonRPCError(json.RawMessage("1"), jsonRPCInvalidParams, err.Error())) 192 | if err != nil { 193 | gotils.L(ctx).Error().Printf("Failed to construct invalid params response: %v", err) 194 | } 195 | return resp, nil 196 | } 197 | 198 | ctx = gotils.With(ctx, "remoteIp", ip) 199 | ctx = gotils.With(ctx, "methods", methods) 200 | errorCode, resp := t.block(ctx, parsedRequests) 201 | if resp != nil { 202 | resp, err := jsonRPCResponse(errorCode, resp) 203 | if err != nil { 204 | gotils.L(ctx).Error().Printf("Failed to construct a response: %v", err) 205 | } 206 | return resp, nil 207 | } 208 | 209 | // gotils.L(ctx).Debug().Print("Forwarding request") 210 | req.Host = req.RemoteAddr //workaround for CloudFlare 211 | return http.DefaultTransport.RoundTrip(req) 212 | } 213 | 214 | // block returns a response only if the request should be blocked, otherwise it returns nil if allowed. 215 | func (t *myTransport) block(ctx context.Context, parsedRequests []ModifiedRequest) (int, interface{}) { 216 | var union *blockRange 217 | for _, parsedRequest := range parsedRequests { 218 | ctx = gotils.With(ctx, "ip", parsedRequest.RemoteAddr) 219 | if allowed, _ := t.AllowVisitor(parsedRequest); !allowed { 220 | gotils.L(ctx).Info().Print("Request blocked: Rate limited") 221 | return http.StatusTooManyRequests, jsonRPCLimit(parsedRequest.ID) 222 | } //else if added { 223 | // gotils.L(ctx).Debug().Printf("Added new visitor, ip: %v", parsedRequest.RemoteAddr) 224 | // } 225 | 226 | if !t.MatchAnyRule(parsedRequest.Path) { 227 | // gotils.L(ctx).Debug().Print("Request blocked: Method not allowed") 228 | return http.StatusMethodNotAllowed, jsonRPCUnauthorized(parsedRequest.ID, parsedRequest.Path) 229 | } 230 | if t.blockRangeLimit > 0 && parsedRequest.Path == "eth_getLogs" { 231 | r, invalid, err := t.parseRange(ctx, parsedRequest) 232 | if err != nil { 233 | return http.StatusInternalServerError, jsonRPCError(parsedRequest.ID, jsonRPCInternal, err.Error()) 234 | } else if invalid != nil { 235 | gotils.L(ctx).Info().Printf("Request blocked: Invalid params: %v", invalid) 236 | return http.StatusBadRequest, jsonRPCError(parsedRequest.ID, jsonRPCInvalidParams, invalid.Error()) 237 | } 238 | if r != nil { 239 | if l := r.len(); l > t.blockRangeLimit { 240 | gotils.L(ctx).Info().Println("Request blocked: Exceeds block range limit, range:", l, "limit:", t.blockRangeLimit) 241 | return http.StatusBadRequest, jsonRPCBlockRangeLimit(parsedRequest.ID, l, t.blockRangeLimit) 242 | } 243 | if union == nil { 244 | union = r 245 | } else { 246 | union.extend(r) 247 | if l := union.len(); l > t.blockRangeLimit { 248 | gotils.L(ctx).Info().Println("Request blocked: Exceeds block range limit, range:", l, "limit:", t.blockRangeLimit) 249 | return http.StatusBadRequest, jsonRPCBlockRangeLimit(parsedRequest.ID, l, t.blockRangeLimit) 250 | } 251 | } 252 | } 253 | } 254 | } 255 | return 0, nil 256 | } 257 | 258 | type blockRange struct{ start, end uint64 } 259 | 260 | func (b blockRange) len() uint64 { 261 | return b.end - b.start + 1 262 | } 263 | 264 | func (b *blockRange) extend(b2 *blockRange) { 265 | if b2.start < b.start { 266 | b.start = b2.start 267 | } 268 | if b2.end > b.end { 269 | b.end = b2.end 270 | } 271 | } 272 | 273 | // parseRange returns a block range if one exists, or an error if the request is invalid. 274 | func (t *myTransport) parseRange(ctx context.Context, request ModifiedRequest) (r *blockRange, invalid, internal error) { 275 | if len(request.Params) == 0 { 276 | return nil, nil, nil 277 | } 278 | type filterQuery struct { 279 | BlockHash *string `json:"blockHash"` 280 | FromBlock *rpc.BlockNumber `json:"fromBlock"` 281 | ToBlock *rpc.BlockNumber `json:"toBlock"` 282 | } 283 | var fq filterQuery 284 | err := json.Unmarshal(request.Params[0], &fq) 285 | if err != nil { 286 | return nil, err, nil 287 | } 288 | if fq.BlockHash != nil { 289 | return nil, nil, nil 290 | } 291 | var start, end uint64 292 | if fq.FromBlock != nil { 293 | switch *fq.FromBlock { 294 | case rpc.LatestBlockNumber, rpc.PendingBlockNumber: 295 | l, err := t.latestBlock.get(ctx) 296 | if err != nil { 297 | return nil, nil, err 298 | } 299 | start = l 300 | default: 301 | start = uint64(*fq.FromBlock) 302 | } 303 | } 304 | if fq.ToBlock == nil { 305 | l, err := t.latestBlock.get(ctx) 306 | if err != nil { 307 | return nil, nil, err 308 | } 309 | end = l 310 | } else { 311 | switch *fq.ToBlock { 312 | case rpc.LatestBlockNumber, rpc.PendingBlockNumber: 313 | l, err := t.latestBlock.get(ctx) 314 | if err != nil { 315 | return nil, nil, err 316 | } 317 | end = l 318 | default: 319 | end = uint64(*fq.ToBlock) 320 | } 321 | } 322 | 323 | return &blockRange{start: start, end: end}, nil, nil 324 | } 325 | 326 | type latestBlock struct { 327 | url string 328 | client *goclient.Client 329 | 330 | mu sync.RWMutex // Protects everything below. 331 | 332 | next chan struct{} // Set when an update is running, and closed when the next result is available. 333 | 334 | num uint64 335 | err error 336 | at *time.Time // When num and err were set. 337 | } 338 | 339 | func (l *latestBlock) get(ctx context.Context) (uint64, error) { 340 | l.mu.RLock() 341 | next, num, err, at := l.next, l.num, l.err, l.at 342 | l.mu.RUnlock() 343 | if at != nil && time.Since(*at) < 5*time.Second { 344 | return num, err 345 | } 346 | if next == nil { 347 | // No update in progress, so try to trigger one. 348 | next, num, err = l.update() 349 | } 350 | if next != nil { 351 | // Wait on update to complete. 352 | select { 353 | case <-ctx.Done(): 354 | return 0, ctx.Err() 355 | case <-next: 356 | } 357 | l.mu.RLock() 358 | num = l.num 359 | err = l.err 360 | l.mu.RUnlock() 361 | } 362 | 363 | return num, err 364 | 365 | } 366 | 367 | // update updates (num, err, at). Only one instance may run at a time, and it 368 | // spot is reserved by setting next, which is closed when the operation completes. 369 | // Returns a chan to wait on if another instance is already running. Otherwise 370 | // returns num and err if the operation is complete. 371 | func (l *latestBlock) update() (chan struct{}, uint64, error) { 372 | l.mu.Lock() 373 | if next := l.next; next != nil { 374 | // Someone beat us to it, return their next chan. 375 | l.mu.Unlock() 376 | return next, 0, nil 377 | } 378 | next := make(chan struct{}) 379 | l.next = next 380 | l.mu.Unlock() 381 | 382 | var latest uint64 383 | var err error 384 | if l.client == nil { 385 | l.client, err = goclient.Dial(l.url) 386 | } 387 | if err == nil { 388 | var lBig *big.Int 389 | lBig, err = l.client.LatestBlockNumber(context.Background()) 390 | if err == nil { 391 | latest = lBig.Uint64() 392 | } 393 | } 394 | now := time.Now() 395 | 396 | l.mu.Lock() 397 | l.num = latest 398 | l.err = err 399 | l.at = &now 400 | l.next = nil 401 | l.mu.Unlock() 402 | 403 | close(next) 404 | 405 | return nil, latest, err 406 | } 407 | -------------------------------------------------------------------------------- /limits.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "golang.org/x/time/rate" 8 | ) 9 | 10 | type limiters struct { 11 | noLimitIPs map[string]struct{} // concurrent read safe after init. 12 | visitors map[string]*rate.Limiter 13 | sync.RWMutex 14 | } 15 | 16 | func (ls *limiters) tryAddVisitor(ip string) (*rate.Limiter, bool) { 17 | ls.Lock() 18 | defer ls.Unlock() 19 | limiter, exists := ls.visitors[ip] 20 | if exists { 21 | return limiter, false 22 | } 23 | limit := rate.Every(time.Minute / time.Duration(requestsPerMinuteLimit)) 24 | limiter = rate.NewLimiter(limit, requestsPerMinuteLimit/10) 25 | ls.visitors[ip] = limiter 26 | return limiter, true 27 | } 28 | 29 | func (ls *limiters) getVisitor(ip string) (*rate.Limiter, bool) { 30 | ls.RLock() 31 | limiter, exists := ls.visitors[ip] 32 | ls.RUnlock() 33 | if !exists { 34 | return ls.tryAddVisitor(ip) 35 | } 36 | return limiter, false 37 | } 38 | 39 | func (ls *limiters) AllowVisitor(r ModifiedRequest) (allowed, added bool) { 40 | if _, ok := ls.noLimitIPs[r.RemoteAddr]; ok { 41 | return true, false 42 | } 43 | limiter, added := ls.getVisitor(r.RemoteAddr) 44 | return limiter.Allow(), added 45 | } 46 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/chi/v5/middleware" 14 | toml "github.com/pelletier/go-toml" 15 | "github.com/rs/cors" 16 | "github.com/treeder/gcputils" 17 | "github.com/treeder/gotils/v2" 18 | "github.com/urfave/cli/v2" 19 | ) 20 | 21 | var requestsPerMinuteLimit int 22 | 23 | type ConfigData struct { 24 | Port string `toml:",omitempty"` 25 | URL string `toml:",omitempty"` 26 | WSURL string `toml:",omitempty"` 27 | Allow []string `toml:",omitempty"` 28 | RPM int `toml:",omitempty"` 29 | NoLimit []string `toml:",omitempty"` 30 | BlockRangeLimit uint64 `toml:",omitempty"` 31 | } 32 | 33 | func main() { 34 | ctx := context.Background() 35 | 36 | gotils.SetLoggable(gcputils.NewLogger()) 37 | 38 | var configPath string 39 | var port string 40 | var redirecturl string 41 | var redirectWSUrl string 42 | var allowedPaths string 43 | var noLimitIPs string 44 | var blockRangeLimit uint64 45 | 46 | app := cli.NewApp() 47 | app.Name = "rpc-proxy" 48 | app.Usage = "A proxy for web3 JSONRPC" 49 | app.Version = Version 50 | 51 | app.Flags = []cli.Flag{ 52 | &cli.StringFlag{ 53 | Name: "config, c", 54 | Usage: "path to toml config file", 55 | Destination: &configPath, 56 | }, 57 | &cli.StringFlag{ 58 | Name: "port, p", 59 | Value: "8545", 60 | Usage: "port to serve", 61 | Destination: &port, 62 | }, 63 | &cli.StringFlag{ 64 | Name: "url, u", 65 | Value: "http://127.0.0.1:8040", 66 | Usage: "redirect url", 67 | Destination: &redirecturl, 68 | }, 69 | &cli.StringFlag{ 70 | Name: "wsurl, w", 71 | Value: "ws://127.0.0.1:8041", 72 | Usage: "redirect websocket url", 73 | Destination: &redirectWSUrl, 74 | }, 75 | &cli.StringFlag{ 76 | Name: "allow, a", 77 | Usage: "comma separated list of allowed paths", 78 | Destination: &allowedPaths, 79 | }, 80 | &cli.IntFlag{ 81 | Name: "rpm", 82 | Value: 1000, 83 | Usage: "limit for number of requests per minute from single IP", 84 | Destination: &requestsPerMinuteLimit, 85 | }, 86 | &cli.StringFlag{ 87 | Name: "nolimit, n", 88 | Usage: "list of ips allowed unlimited requests(separated by commas)", 89 | Destination: &noLimitIPs, 90 | }, 91 | &cli.Uint64Flag{ 92 | Name: "blocklimit, b", 93 | Usage: "block range query limit", 94 | Destination: &blockRangeLimit, 95 | }, 96 | } 97 | 98 | app.Action = func(c *cli.Context) error { 99 | var cfg ConfigData 100 | if configPath != "" { 101 | t, err := toml.LoadFile(configPath) 102 | if err != nil { 103 | return err 104 | } 105 | if err := t.Unmarshal(&cfg); err != nil { 106 | return err 107 | } 108 | } 109 | 110 | if port != "" { 111 | if cfg.Port != "" { 112 | return errors.New("port set in two places") 113 | } 114 | cfg.Port = port 115 | } 116 | if redirecturl != "" { 117 | if cfg.URL != "" { 118 | return errors.New("url set in two places") 119 | } 120 | cfg.URL = redirecturl 121 | } 122 | if redirectWSUrl != "" { 123 | if cfg.WSURL != "" { 124 | return errors.New("ws url set in two places") 125 | } 126 | cfg.WSURL = redirectWSUrl 127 | } 128 | if requestsPerMinuteLimit != 0 { 129 | if cfg.RPM != 0 { 130 | return errors.New("rpm set in two places") 131 | } 132 | cfg.RPM = requestsPerMinuteLimit 133 | } 134 | if allowedPaths != "" { 135 | if len(cfg.Allow) > 0 { 136 | return errors.New("allow set in two places") 137 | } 138 | cfg.Allow = strings.Split(allowedPaths, ",") 139 | } 140 | if noLimitIPs != "" { 141 | if len(cfg.NoLimit) > 0 { 142 | return errors.New("nolimit set in two places") 143 | } 144 | cfg.NoLimit = strings.Split(noLimitIPs, ",") 145 | } 146 | if blockRangeLimit > 0 { 147 | if cfg.BlockRangeLimit > 0 { 148 | return errors.New("block range limit set in two places") 149 | } 150 | cfg.BlockRangeLimit = blockRangeLimit 151 | } 152 | 153 | return cfg.run(ctx) 154 | } 155 | 156 | if err := app.Run(os.Args); err != nil { 157 | gotils.L(ctx).Error().Printf("Fatal error: %v", err) 158 | return 159 | } 160 | gotils.L(ctx).Info().Print("Shutting down") 161 | } 162 | 163 | func (cfg *ConfigData) run(ctx context.Context) error { 164 | sort.Strings(cfg.Allow) 165 | sort.Strings(cfg.NoLimit) 166 | 167 | gotils.L(ctx).Info().Println("Server starting, port:", cfg.Port, "redirectURL:", cfg.URL, "redirectWSURL:", cfg.WSURL, 168 | "rpmLimit:", cfg.RPM, "exempt:", cfg.NoLimit, "allowed:", cfg.Allow) 169 | 170 | // Create proxy server. 171 | server, err := cfg.NewServer() 172 | if err != nil { 173 | return fmt.Errorf("failed to start server: %s", err) 174 | } 175 | 176 | r := chi.NewRouter() 177 | r.Use(middleware.RequestID) 178 | r.Use(middleware.Recoverer) 179 | // Use default options 180 | r.Use(cors.New(cors.Options{ 181 | AllowedOrigins: []string{"*"}, 182 | AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"}, 183 | AllowedHeaders: []string{"*"}, 184 | AllowCredentials: false, 185 | MaxAge: 3600, 186 | }).Handler) 187 | 188 | r.Get("/", server.HomePage) 189 | r.Head("/", func(w http.ResponseWriter, _ *http.Request) { 190 | w.WriteHeader(http.StatusOK) 191 | }) 192 | r.Get("/x/{method}", server.Example) 193 | r.Get("/x/{method}/{arg}", server.Example) 194 | r.Get("/x/{method}/{arg}/{arg2}", server.Example) 195 | r.Get("/x/{method}/{arg}/{arg2}/{arg3}", server.Example) 196 | r.Head("/x/net_version", func(w http.ResponseWriter, r *http.Request) { 197 | _, err := server.example("net_version") 198 | if err != nil { 199 | gotils.L(ctx).Error().Printf("Failed to ping RPC: %v", err) 200 | w.WriteHeader(http.StatusInternalServerError) 201 | return 202 | } 203 | w.WriteHeader(http.StatusOK) 204 | }) 205 | r.HandleFunc("/*", server.RPCProxy) 206 | r.HandleFunc("/ws", server.WSProxy) 207 | return http.ListenAndServe(":"+cfg.Port, r) 208 | } 209 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/pelletier/go-toml" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestConfigDataTOML_empty(t *testing.T) { 11 | b, err := toml.Marshal(ConfigData{}) 12 | if err != nil { 13 | t.Errorf("failed to marshal") 14 | } 15 | if len(b) > 0 { 16 | t.Errorf("expected no data but got: %s", string(b)) 17 | } 18 | } 19 | 20 | func TestConfigDataTOML_all(t *testing.T) { 21 | cfg := ConfigData{ 22 | URL: "http://127.0.0.1:8040", 23 | Port: "8545", 24 | RPM: 1000, 25 | NoLimit: []string{"test", "test2"}, 26 | Allow: []string{"eth_method", "eth_method2"}, 27 | } 28 | data := `Allow = [ 29 | "eth_method", 30 | "eth_method2", 31 | ] 32 | NoLimit = [ 33 | "test", 34 | "test2", 35 | ] 36 | Port = "8545" 37 | RPM = 1000 38 | URL = "http://127.0.0.1:8040" 39 | ` 40 | var buf bytes.Buffer 41 | err := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true).Encode(cfg) 42 | if err != nil { 43 | t.Errorf("failed to marshal: %v", err) 44 | } 45 | if have := buf.String(); have != data { 46 | t.Errorf("failed\n\twant: %s\n\thave: %s", data, have) 47 | } 48 | 49 | var cfg2 ConfigData 50 | if err := toml.Unmarshal([]byte(data), &cfg2); err != nil { 51 | t.Errorf("failed to unmarshal: %v", err) 52 | } 53 | if !reflect.DeepEqual(cfg2, cfg) { 54 | t.Errorf("failed\n\twant: %#v\n\thave: %#v", cfg, cfg2) 55 | } 56 | } 57 | 58 | func TestConfigDataTOML_omitempty(t *testing.T) { 59 | cfg := ConfigData{ 60 | URL: "ws://127.0.0.1:8041", 61 | Port: "8546", 62 | } 63 | data := `Port = "8546" 64 | URL = "ws://127.0.0.1:8041" 65 | ` 66 | var buf bytes.Buffer 67 | err := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true).Encode(cfg) 68 | if err != nil { 69 | t.Errorf("failed to marshal: %v", err) 70 | } 71 | if have := buf.String(); have != data { 72 | t.Errorf("failed\n\twant: %s\n\thave: %s", data, have) 73 | } 74 | 75 | var cfg2 ConfigData 76 | if err := toml.Unmarshal([]byte(data), &cfg2); err != nil { 77 | t.Errorf("failed to unmarshal: %v", err) 78 | } 79 | if !reflect.DeepEqual(cfg2, cfg) { 80 | t.Errorf("failed\n\twant: %#v\n\thave: %#v", cfg, cfg2) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type matcher []*regexp.Regexp 8 | 9 | func (m matcher) MatchAnyRule(method string) bool { 10 | if method == "" { 11 | return false 12 | } 13 | for _, matcher := range m { 14 | if matcher.MatchString(method) { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func newMatcher(rules []string) (matcher, error) { 22 | var m matcher 23 | for _, p := range rules { 24 | compiled, err := regexp.Compile(p) 25 | if err != nil { 26 | return nil, err 27 | } 28 | m = append(m, compiled) 29 | } 30 | return m, nil 31 | } 32 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "io/ioutil" 10 | "math/big" 11 | "net/http" 12 | "net/http/httputil" 13 | "net/url" 14 | "sort" 15 | "strconv" 16 | 17 | "github.com/go-chi/chi/v5" 18 | "github.com/gochain/gochain/v3/common" 19 | "github.com/treeder/gotils/v2" 20 | "golang.org/x/time/rate" 21 | ) 22 | 23 | type Server struct { 24 | target *url.URL 25 | proxy *httputil.ReverseProxy 26 | wsProxy *WebsocketProxy 27 | myTransport 28 | homepage []byte 29 | } 30 | 31 | func (cfg *ConfigData) NewServer() (*Server, error) { 32 | url, err := url.Parse(cfg.URL) 33 | if err != nil { 34 | return nil, err 35 | } 36 | wsurl, err := url.Parse(cfg.WSURL) 37 | if err != nil { 38 | return nil, err 39 | } 40 | s := &Server{target: url, proxy: httputil.NewSingleHostReverseProxy(url), wsProxy: NewProxy(wsurl)} 41 | s.myTransport.blockRangeLimit = cfg.BlockRangeLimit 42 | s.myTransport.url = cfg.URL 43 | s.matcher, err = newMatcher(cfg.Allow) 44 | if err != nil { 45 | return nil, err 46 | } 47 | s.visitors = make(map[string]*rate.Limiter) 48 | s.noLimitIPs = make(map[string]struct{}) 49 | for _, ip := range cfg.NoLimit { 50 | s.noLimitIPs[ip] = struct{}{} 51 | } 52 | s.proxy.Transport = &s.myTransport 53 | s.wsProxy.Transport = &s.myTransport 54 | 55 | // Generate static home page. 56 | id := json.RawMessage([]byte(`"ID"`)) 57 | responseRateLimit, err := json.MarshalIndent(jsonRPCLimit(id), "", " ") 58 | if err != nil { 59 | return nil, err 60 | } 61 | responseUnauthorized, err := json.MarshalIndent(jsonRPCUnauthorized(id, "method_name"), "", " ") 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | data := &homePageData{ 67 | Limit: requestsPerMinuteLimit, 68 | Methods: cfg.Allow, 69 | ResponseRateLimit: string(responseRateLimit), 70 | ResponseUnauthorized: string(responseUnauthorized), 71 | } 72 | sort.Strings(data.Methods) 73 | 74 | var buf bytes.Buffer 75 | if err := homePageTmpl.Execute(&buf, &data); err != nil { 76 | return nil, err 77 | } 78 | s.homepage = buf.Bytes() 79 | 80 | return s, nil 81 | } 82 | 83 | func (p *Server) HomePage(w http.ResponseWriter, r *http.Request) { 84 | ctx := r.Context() 85 | if _, err := io.Copy(w, bytes.NewReader(p.homepage)); err != nil { 86 | gotils.L(ctx).Error().Printf("Failed to serve homepage: %v", err) 87 | return 88 | } 89 | } 90 | 91 | func (p *Server) RPCProxy(w http.ResponseWriter, r *http.Request) { 92 | w.Header().Set("X-rpc-proxy", "rpc-proxy") 93 | p.proxy.ServeHTTP(w, r) 94 | } 95 | 96 | func (p *Server) WSProxy(w http.ResponseWriter, r *http.Request) { 97 | w.Header().Set("X-rpc-proxy", "rpc-proxy") 98 | p.wsProxy.ServeHTTP(w, r) 99 | } 100 | 101 | func (p *Server) Example(w http.ResponseWriter, r *http.Request) { 102 | method := chi.URLParam(r, "method") 103 | args := []string{ 104 | chi.URLParam(r, "arg"), 105 | chi.URLParam(r, "arg2"), 106 | chi.URLParam(r, "arg3"), 107 | } 108 | do := func(params ...func(string) (interface{}, error)) { 109 | var fmtd []interface{} 110 | for i, p := range params { 111 | if i > len(args) { 112 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 113 | return 114 | } 115 | if p == nil { 116 | fmtd = append(fmtd, args[i]) 117 | continue 118 | } 119 | arg, err := p(args[i]) 120 | if err != nil { 121 | http.Error(w, err.Error(), http.StatusBadRequest) 122 | return 123 | } 124 | fmtd = append(fmtd, arg) 125 | } 126 | data, err := p.example(method, fmtd...) 127 | if err != nil { 128 | http.Error(w, err.Error(), http.StatusInternalServerError) 129 | return 130 | } 131 | 132 | w.Write(data) 133 | } 134 | switch method { 135 | case "clique_getSigners": 136 | do(hexNumOrLatest) 137 | case "clique_getSignersAtHash": 138 | do(hexHash) 139 | case "clique_getSnapshot": 140 | do(hexNumOrLatest) 141 | case "clique_getSnapshotAtHash": 142 | do(hexHash) 143 | case "clique_getVoters": 144 | do(hexNumOrLatest) 145 | case "clique_getVotersAtHash": 146 | do(hexHash) 147 | case "eth_blockNumber": 148 | do() 149 | case "eth_chainId": 150 | do() 151 | case "eth_gasPrice": 152 | do() 153 | case "eth_genesisAlloc": 154 | do() 155 | case "eth_getBalance": 156 | do(hexAddr, hexNumOrLatest) 157 | case "eth_getBlockByHash": 158 | do(hexHash, boolOrFalse) 159 | case "eth_getBlockByNumber": 160 | do(hexNumOrLatest, boolOrFalse) 161 | case "eth_getBlockTransactionCountByHash": 162 | do(hexHash) 163 | case "eth_getBlockTransactionCountByNumber": 164 | do(hexNumOrLatest) 165 | case "eth_getCode": 166 | do(hexAddr, hexNumOrLatest) 167 | case "eth_getFilterChanges": 168 | do(nil) 169 | case "eth_getLogs": 170 | do(func(arg string) (interface{}, error) { 171 | if hasHexPrefix(arg) { 172 | arg = arg[2:] 173 | } 174 | if !isHex(arg) { 175 | return nil, fmt.Errorf("non-hex argument: %s", arg) 176 | } 177 | return map[string]interface{}{"blockhash": "0x" + arg}, nil 178 | }) 179 | case "eth_getStorageAt": 180 | do(hexAddr, hexNumOrZero, hexNumOrLatest) 181 | case "eth_getTransactionByBlockHashAndIndex": 182 | do(nil, hexNumOrZero) 183 | case "eth_getTransactionByBlockNumberAndIndex": 184 | do(hexNumOrLatest, hexNumOrZero) 185 | case "eth_getTransactionCount": 186 | do(hexAddr, hexNumOrLatest) 187 | case "eth_getTransactionByHash", "eth_getTransactionReceipt": 188 | do(hexHash) 189 | case "eth_totalSupply": 190 | do(hexNumOrLatest) 191 | case "net_listening": 192 | do() 193 | case "net_version": 194 | do() 195 | case "rpc_modules": 196 | do() 197 | case "web3_clientVersion": 198 | do() 199 | 200 | default: 201 | http.NotFound(w, r) 202 | } 203 | } 204 | 205 | func hexAddr(arg string) (interface{}, error) { 206 | if !common.IsHexAddress(arg) { 207 | return nil, fmt.Errorf("not a hex address: %s", arg) 208 | } 209 | return arg, nil 210 | } 211 | 212 | func isHexHash(s string) bool { 213 | if hasHexPrefix(s) { 214 | s = s[2:] 215 | } 216 | return len(s) == 2*common.HashLength && isHex(s) 217 | } 218 | 219 | func hexHash(arg string) (interface{}, error) { 220 | if !isHexHash(arg) { 221 | return nil, fmt.Errorf("not a hex hash: %s", arg) 222 | } 223 | return arg, nil 224 | } 225 | 226 | func boolOrFalse(arg string) (interface{}, error) { 227 | if arg == "" { 228 | return false, nil 229 | } 230 | v, err := strconv.ParseBool(arg) 231 | if err != nil { 232 | return nil, fmt.Errorf("not a bool: %v", err) 233 | } 234 | return v, nil 235 | } 236 | 237 | func hexNumOrLatest(arg string) (interface{}, error) { 238 | return hexNumOr(arg, "latest", "pending", "earliest") 239 | } 240 | 241 | func hexNumOrZero(arg string) (interface{}, error) { 242 | return hexNumOr(arg, "0x0") 243 | } 244 | 245 | // hexNumOr reforms decimal integers as '0x' prefixed hex and returns 246 | // or for empty, otherwise an error is returned. 247 | func hexNumOr(arg string, or string, allow ...string) (interface{}, error) { 248 | if arg == "" { 249 | return or, nil 250 | } 251 | for _, a := range allow { 252 | if arg == a { 253 | return arg, nil 254 | } 255 | } 256 | i, ok := new(big.Int).SetString(arg, 0) 257 | if !ok { 258 | return nil, fmt.Errorf("not an integer: %s", arg) 259 | } 260 | return fmt.Sprintf("0x%X", i), nil 261 | } 262 | 263 | // hasHexPrefix validates str begins with '0x' or '0X'. 264 | func hasHexPrefix(str string) bool { 265 | return len(str) >= 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X') 266 | } 267 | 268 | // isHexCharacter returns bool of c being a valid hexadecimal. 269 | func isHexCharacter(c byte) bool { 270 | return ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') 271 | } 272 | 273 | // isHex validates whether each byte is valid hexadecimal string. 274 | func isHex(str string) bool { 275 | if len(str)%2 != 0 { 276 | return false 277 | } 278 | for _, c := range []byte(str) { 279 | if !isHexCharacter(c) { 280 | return false 281 | } 282 | } 283 | return true 284 | } 285 | 286 | func (p *Server) example(method string, params ...interface{}) ([]byte, error) { 287 | body, err := json.Marshal(struct { 288 | JSONRPC string `json:"jsonrpc"` 289 | ID string `json:"id"` 290 | Method string `json:"method"` 291 | Params []interface{} `json:"params"` 292 | }{ 293 | JSONRPC: "2.0", 294 | ID: "1", 295 | Method: method, 296 | Params: params, 297 | }) 298 | if err != nil { 299 | return nil, err 300 | } 301 | 302 | req, err := http.NewRequest(http.MethodPost, p.target.String(), bytes.NewReader(body)) 303 | if err != nil { 304 | return nil, err 305 | } 306 | const contentType = "application/json" 307 | req.Header.Set("Content-Type", contentType) 308 | req.Header.Set("Accept", contentType) 309 | resp, err := http.DefaultClient.Do(req) 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | respBody, err := ioutil.ReadAll(resp.Body) 315 | if err != nil { 316 | return nil, err 317 | } 318 | var formattedResp string 319 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 320 | formattedResp = fmt.Sprintf("Code: %d (%s)\nBody: %s\n", resp.StatusCode, resp.Status, string(respBody)) 321 | } else { 322 | formattedResp = indent(respBody) 323 | } 324 | 325 | var buf bytes.Buffer 326 | if err := exampleTmpl.Execute(&buf, &exampleData{Method: method, Request: indent(body), Response: formattedResp}); err != nil { 327 | return nil, err 328 | } 329 | return buf.Bytes(), nil 330 | } 331 | 332 | func indent(b []byte) string { 333 | var buf bytes.Buffer 334 | _ = json.Indent(&buf, b, "", " ") 335 | return buf.String() 336 | } 337 | 338 | type homePageData struct { 339 | Limit int 340 | Methods []string 341 | ResponseRateLimit string 342 | ResponseUnauthorized string 343 | } 344 | 345 | var homePageTmpl = template.Must(template.New("").Parse(` 346 | 347 | 348 | GoChain RPC Proxy 349 | 360 | 361 | 362 | 363 |

GoChain RPC Proxy

364 | 365 |

This is an RPC endpoint for GoChain. It provides access to a limited subset of services. Rate limits apply.

366 | 367 |

Rate Limit

368 | 369 |

The rate limit is {{.Limit}} requests per minute. If you exceed this limit, you will receive a 429 response:

370 | 371 |
{{.ResponseRateLimit}}
372 | 373 |

Allowed Methods

374 | 375 |

Only the following listed methods are allowed. Click for an example. If you attempt to call any other methods, you will receive a 401 response:

376 | 377 |
{{.ResponseUnauthorized}}
378 | 379 | 382 | 383 | 384 | `)) 385 | 386 | type exampleData struct { 387 | Method, Request, Response string 388 | } 389 | 390 | var exampleTmpl = template.Must(template.New("").Parse(` 391 | 392 | 393 | GoChain RPC Proxy 394 | 405 | 406 | 407 | 408 |

GoChain RPC Proxy

409 | 410 |

This is an example call for {{.Method}}.

411 | 412 |

Request

413 | 414 |
{{.Request}}
415 | 416 |

Response

417 | 418 |
{{.Response}}
419 | 420 | 421 | `)) 422 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exuo pipefail 3 | 4 | user="gochain" 5 | image="rpc-proxy" 6 | gcr_project="gochain-core" 7 | 8 | # ensure working dir is clean 9 | git status 10 | if [[ -z $(git status -s) ]] 11 | then 12 | echo "tree is clean" 13 | else 14 | echo "tree is dirty, please commit changes before running this" 15 | exit 1 16 | fi 17 | 18 | version_file="version.go" 19 | docker create -v /data --name file alpine /bin/true 20 | docker cp $version_file file:/data/$version_file 21 | # Bump version, patch by default - also checks if previous commit message contains `[bump X]`, and if so, bumps the appropriate semver number - https://github.com/treeder/dockers/tree/master/bump 22 | docker run --rm -it --volumes-from file -w / treeder/bump --filename /data/$version_file "$(git log -1 --pretty=%B)" 23 | docker cp file:/data/$version_file $version_file 24 | version=$(grep -m1 -Eo "[0-9]+\.[0-9]+\.[0-9]+" $version_file) 25 | echo "Version: $version" 26 | 27 | make docker 28 | 29 | git add -u 30 | git commit -m "$image: $version release [skip ci]" 31 | git tag -f -a "$version" -m "version $version" 32 | git push 33 | git push origin $version 34 | 35 | # Push docker hub images 36 | docker tag $user/$image:latest $user/$image:$version 37 | docker push $user/$image:$version 38 | docker push $user/$image:latest 39 | 40 | # Push GCR docker images 41 | ./tmp/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json 42 | docker tag $user/$image:latest gcr.io/$gcr_project/$image:latest 43 | docker tag $user/$image:latest gcr.io/$gcr_project/$image:$version 44 | docker push gcr.io/$gcr_project/$image:latest 45 | docker push gcr.io/$gcr_project/$image:$version -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Version = "0.0.60" 4 | -------------------------------------------------------------------------------- /websocketproxy.go: -------------------------------------------------------------------------------- 1 | // Package websocketproxy is a reverse proxy for WebSocket connections. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | 14 | "github.com/gorilla/websocket" 15 | "github.com/treeder/gotils/v2" 16 | ) 17 | 18 | var ( 19 | // DefaultUpgrader specifies the parameters for upgrading an HTTP 20 | // connection to a WebSocket connection. 21 | DefaultUpgrader = &websocket.Upgrader{ 22 | ReadBufferSize: 1024, 23 | WriteBufferSize: 1024, 24 | // Resolve cross-domain problems 25 | CheckOrigin: func(r *http.Request) bool { 26 | return true 27 | }, 28 | } 29 | 30 | // DefaultDialer is a dialer with all fields set to the default zero values. 31 | DefaultDialer = websocket.DefaultDialer 32 | ) 33 | 34 | // WebsocketProxy is an HTTP Handler that takes an incoming WebSocket 35 | // connection and proxies it to another server. 36 | type WebsocketProxy struct { 37 | // Director, if non-nil, is a function that may copy additional request 38 | // headers from the incoming WebSocket connection into the output headers 39 | // which will be forwarded to another server. 40 | Director func(incoming *http.Request, out http.Header) 41 | 42 | // Backend returns the backend URL which the proxy uses to reverse proxy 43 | // the incoming WebSocket connection. Request is the initial incoming and 44 | // unmodified request. 45 | Backend func(*http.Request) *url.URL 46 | 47 | // Upgrader specifies the parameters for upgrading a incoming HTTP 48 | // connection to a WebSocket connection. If nil, DefaultUpgrader is used. 49 | Upgrader *websocket.Upgrader 50 | 51 | // Dialer contains options for connecting to the backend WebSocket server. 52 | // If nil, DefaultDialer is used. 53 | Dialer *websocket.Dialer 54 | 55 | Transport *myTransport 56 | } 57 | 58 | // NewProxy returns a new Websocket reverse proxy that rewrites the 59 | // URL's to the scheme, host and base path provider in target. 60 | func NewProxy(target *url.URL) *WebsocketProxy { 61 | backend := func(r *http.Request) *url.URL { 62 | // Shallow copy 63 | u := *target 64 | u.Fragment = r.URL.Fragment 65 | u.Path = r.URL.Path 66 | u.RawQuery = r.URL.RawQuery 67 | return &u 68 | } 69 | return &WebsocketProxy{Backend: backend} 70 | } 71 | 72 | // ServeHTTP implements the http.Handler that proxies WebSocket connections. 73 | func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 74 | ctx := req.Context() 75 | if w.Backend == nil { 76 | gotils.L(ctx).Error().Print("webproxy backend function is not defined") 77 | http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) 78 | return 79 | } 80 | 81 | backendURL := w.Backend(req) 82 | if backendURL == nil { 83 | gotils.L(ctx).Error().Print("websocketproxy: backend URL is nil") 84 | http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) 85 | return 86 | } 87 | 88 | dialer := w.Dialer 89 | if w.Dialer == nil { 90 | dialer = DefaultDialer 91 | } 92 | 93 | // Pass headers from the incoming request to the dialer to forward them to 94 | // the final destinations. 95 | requestHeader := http.Header{} 96 | if origin := req.Header.Get("Origin"); origin != "" { 97 | requestHeader.Add("Origin", origin) 98 | } 99 | for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { 100 | requestHeader.Add("Sec-WebSocket-Protocol", prot) 101 | } 102 | for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { 103 | requestHeader.Add("Cookie", cookie) 104 | } 105 | if req.Host != "" { 106 | requestHeader.Set("Host", req.Host) 107 | } 108 | 109 | // Pass X-Forwarded-For headers too, code below is a part of 110 | // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For 111 | // for more information 112 | // TODO: use RFC7239 http://tools.ietf.org/html/rfc7239 113 | if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { 114 | // If we aren't the first proxy retain prior 115 | // X-Forwarded-For information as a comma+space 116 | // separated list and fold multiple headers into one. 117 | if prior, ok := req.Header["X-Forwarded-For"]; ok { 118 | clientIP = strings.Join(prior, ", ") + ", " + clientIP 119 | } 120 | requestHeader.Set("X-Forwarded-For", clientIP) 121 | } 122 | 123 | // Set the originating protocol of the incoming HTTP request. The SSL might 124 | // be terminated on our site and because we doing proxy adding this would 125 | // be helpful for applications on the backend. 126 | requestHeader.Set("X-Forwarded-Proto", "http") 127 | if req.TLS != nil { 128 | requestHeader.Set("X-Forwarded-Proto", "https") 129 | } 130 | 131 | // Enable the director to copy any additional headers it desires for 132 | // forwarding to the remote server. 133 | if w.Director != nil { 134 | w.Director(req, requestHeader) 135 | } 136 | 137 | // Connect to the backend URL, also pass the headers we get from the request 138 | // together with the Forwarded headers we prepared above. 139 | // TODO: support multiplexing on the same backend connection instead of 140 | // opening a new TCP connection time for each request. This should be 141 | // optional: 142 | // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 143 | connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) 144 | if err != nil { 145 | gotils.L(ctx).Error().Printf("websocketproxy:%s", err) 146 | if resp != nil { 147 | // If the WebSocket handshake fails, ErrBadHandshake is returned 148 | // along with a non-nil *http.Response so that callers can handle 149 | // redirects, authentication, etcetera. 150 | if err := copyResponse(rw, resp); err != nil { 151 | gotils.L(ctx).Error().Printf("websocketproxy: couldn't write response after failed remote backend handshake %s", err) 152 | } 153 | } else { 154 | http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 155 | } 156 | return 157 | } 158 | defer connBackend.Close() 159 | 160 | upgrader := w.Upgrader 161 | if w.Upgrader == nil { 162 | upgrader = DefaultUpgrader 163 | } 164 | 165 | // Only pass those headers to the upgrader. 166 | upgradeHeader := http.Header{} 167 | if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { 168 | upgradeHeader.Set("Sec-Websocket-Protocol", hdr) 169 | } 170 | if hdr := resp.Header.Get("Set-Cookie"); hdr != "" { 171 | upgradeHeader.Set("Set-Cookie", hdr) 172 | } 173 | 174 | // Now upgrade the existing incoming request to a WebSocket connection. 175 | // Also pass the header that we gathered from the Dial handshake. 176 | connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) 177 | if err != nil { 178 | gotils.L(ctx).Error().Printf("websocketproxy: couldn't upgrade %s", err) 179 | return 180 | } 181 | defer connPub.Close() 182 | 183 | errClient := make(chan error, 1) 184 | errBackend := make(chan error, 1) 185 | replicateWebsocketConn := func(ctx context.Context, ip string, limit bool, dst, src *websocket.Conn, errc chan error) { 186 | for { 187 | msgType, msg, err := src.ReadMessage() 188 | if err != nil { 189 | gotils.L(ctx).Error().Printf("websocketproxy: ReadMessage %s", err) 190 | m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) 191 | if e, ok := err.(*websocket.CloseError); ok { 192 | if e.Code != websocket.CloseNoStatusReceived && e.Code != websocket.CloseAbnormalClosure { 193 | m = websocket.FormatCloseMessage(e.Code, e.Text) 194 | } 195 | } 196 | errc <- err 197 | dst.WriteMessage(websocket.CloseMessage, m) 198 | break 199 | } 200 | if limit && len(msg) > 0 { 201 | methods, res, err := parseMessage(msg, ip) 202 | if err != nil { 203 | errc <- err 204 | err = src.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))) 205 | if err != nil { 206 | errc <- err 207 | } 208 | break 209 | } 210 | ctx = gotils.With(ctx, "remoteIp", ip) 211 | ctx = gotils.With(ctx, "methods", methods) 212 | if len(methods) > 0 { 213 | _, resp := w.Transport.block(ctx, res) 214 | if resp != nil { 215 | errc <- errors.New(resp.(ErrResponse).Error.Message) 216 | err = src.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, resp.(ErrResponse).Error.Message)) 217 | if err != nil { 218 | errc <- err 219 | } 220 | break 221 | } 222 | } 223 | } 224 | if len(msg) == 0 { //workaround for empty message and a wrong type 225 | if limit { 226 | msgType = websocket.PingMessage 227 | } else { 228 | msgType = websocket.PongMessage 229 | } 230 | } 231 | err = dst.WriteMessage(msgType, msg) 232 | if err != nil { 233 | errc <- err 234 | break 235 | } 236 | } 237 | } 238 | ip := getIP(req) 239 | go replicateWebsocketConn(ctx, ip, true, connBackend, connPub, errBackend) 240 | go replicateWebsocketConn(ctx, ip, false, connPub, connBackend, errClient) 241 | 242 | var message string 243 | select { 244 | case err = <-errClient: 245 | message = "websocketproxy: Error when copying from backend to client: %v" 246 | case err = <-errBackend: 247 | message = "websocketproxy: Error when copying from client to backend: %v" 248 | 249 | } 250 | if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { 251 | gotils.L(ctx).Error().Printf("%s %s", message, err) 252 | } 253 | } 254 | 255 | func copyHeader(dst, src http.Header) { 256 | for k, vv := range src { 257 | for _, v := range vv { 258 | dst.Add(k, v) 259 | } 260 | } 261 | } 262 | 263 | func copyResponse(rw http.ResponseWriter, resp *http.Response) error { 264 | copyHeader(rw.Header(), resp.Header) 265 | rw.WriteHeader(resp.StatusCode) 266 | defer resp.Body.Close() 267 | 268 | _, err := io.Copy(rw, resp.Body) 269 | return err 270 | } 271 | --------------------------------------------------------------------------------