├── .editorconfig ├── .github └── workflows │ ├── README.md │ ├── cron-daily-fuzz.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo-minimal.lock ├── Cargo-recent.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── contrib ├── crates.sh ├── test_vars.sh ├── update-lock-files.sh └── whitelist_deps.sh ├── fuzz ├── .gitignore ├── Cargo.toml ├── cycle.sh ├── fuzz-util.sh ├── fuzz.sh ├── fuzz_targets │ ├── minreq_http.rs │ └── simple_http.rs └── generate-files.sh ├── githooks └── pre-commit ├── integration_test ├── Cargo.toml ├── contrib │ └── test_vars.sh ├── run.sh └── src │ └── main.rs ├── justfile ├── nightly-version ├── rustfmt.toml └── src ├── client.rs ├── error.rs ├── http ├── minreq_http.rs ├── mod.rs └── simple_http.rs ├── lib.rs ├── simple_tcp.rs └── simple_uds.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org for more options, and setup instructions for yours editor 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # rust-miniscript workflow notes 2 | 3 | We are attempting to run max 20 parallel jobs using GitHub actions (usage limit for free tier). 4 | 5 | ref: https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration 6 | 7 | The minimal/recent lock files are handled by CI (`rust.yml`). 8 | 9 | ## Jobs 10 | 11 | Run from `rust.yml` unless stated otherwise. Total 11 jobs. 12 | 13 | 1. `Stable - minimal` 14 | 2. `Stable - recent` 15 | 3. `Nightly - minimal` 16 | 4. `Nightly - recent` 17 | 5. `MSRV - minimal` 18 | 6. `MSRV - recent` 19 | 7. `Lint` 20 | 8. `Docs` 21 | 9. `Docsrs` 22 | 10. `Format` 23 | 11. `Arch32bit` 24 | 12. `Cross` 25 | -------------------------------------------------------------------------------- /.github/workflows/cron-daily-fuzz.yml: -------------------------------------------------------------------------------- 1 | # Automatically generated by fuzz/generate-files.sh 2 | name: Fuzz 3 | 4 | on: 5 | schedule: 6 | # 6am every day UTC, this correlates to: 7 | # - 11pm PDT 8 | # - 7am CET 9 | # - 5pm AEDT 10 | - cron: '00 06 * * *' 11 | 12 | jobs: 13 | fuzz: 14 | if: ${{ !github.event.act }} 15 | runs-on: ubuntu-20.04 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | fuzz_target: [ 20 | minreq_http, 21 | simple_http, 22 | ] 23 | steps: 24 | - name: Install test dependencies 25 | run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev 26 | - uses: actions/checkout@v4 27 | - uses: actions/cache@v4 28 | id: cache-fuzz 29 | with: 30 | path: | 31 | ~/.cargo/bin 32 | fuzz/target 33 | target 34 | key: cache-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 35 | - uses: dtolnay/rust-toolchain@stable 36 | with: 37 | toolchain: '1.65.0' 38 | - name: fuzz 39 | run: | 40 | export RUSTFLAGS='--cfg=jsonrpc_fuzz' 41 | echo "Using RUSTFLAGS $RUSTFLAGS" 42 | cd fuzz && ./fuzz.sh "${{ matrix.fuzz_target }}" 43 | - run: echo "${{ matrix.fuzz_target }}" >executed_${{ matrix.fuzz_target }} 44 | - uses: actions/upload-artifact@v2 45 | with: 46 | name: executed_${{ matrix.fuzz_target }} 47 | path: executed_${{ matrix.fuzz_target }} 48 | 49 | verify-execution: 50 | if: ${{ !github.event.act }} 51 | needs: fuzz 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions/download-artifact@v2 56 | - name: Display structure of downloaded files 57 | run: ls -R 58 | - run: find executed_* -type f -exec cat {} + | sort > executed 59 | - run: source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed 60 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | --- # rust-miniscript CI: If you edit this file please update README.md 2 | on: # yamllint disable-line rule:truthy 3 | push: 4 | branches: 5 | - master 6 | - 'test-ci/**' 7 | pull_request: 8 | 9 | name: Continuous integration 10 | 11 | jobs: 12 | Prepare: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | nightly_version: ${{ steps.read_toolchain.outputs.nightly_version }} 16 | steps: 17 | - name: Checkout Crate 18 | uses: actions/checkout@v4 19 | - name: Read nightly version 20 | id: read_toolchain 21 | run: echo "nightly_version=$(cat nightly-version)" >> $GITHUB_OUTPUT 22 | 23 | Stable: # 2 jobs, one per lock file. 24 | name: Test - stable toolchain 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | dep: [minimal, recent] 30 | steps: 31 | - name: "Checkout repo" 32 | uses: actions/checkout@v4 33 | - name: "Checkout maintainer tools" 34 | uses: actions/checkout@v4 35 | with: 36 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 37 | rev: b2ac115 38 | path: maintainer-tools 39 | - name: "Select toolchain" 40 | uses: dtolnay/rust-toolchain@stable 41 | - name: "Set dependencies" 42 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 43 | - name: "Run test script" 44 | run: ./maintainer-tools/ci/run_task.sh stable 45 | 46 | Nightly: # 2 jobs, one per lock file. 47 | name: Test - nightly toolchain 48 | needs: Prepare 49 | runs-on: ubuntu-latest 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | dep: [minimal, recent] 54 | steps: 55 | - name: "Checkout repo" 56 | uses: actions/checkout@v4 57 | - name: "Checkout maintainer tools" 58 | uses: actions/checkout@v4 59 | with: 60 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 61 | rev: b2ac115 62 | path: maintainer-tools 63 | - name: "Select toolchain" 64 | uses: dtolnay/rust-toolchain@v1 65 | with: 66 | toolchain: ${{ needs.Prepare.outputs.nightly_version }} 67 | - name: "Set dependencies" 68 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 69 | - name: "Run test script" 70 | run: ./maintainer-tools/ci/run_task.sh nightly 71 | 72 | MSRV: # 2 jobs, one per lock file. 73 | name: Test - 1.63.0 toolchain 74 | runs-on: ubuntu-latest 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | dep: [minimal, recent] 79 | steps: 80 | - name: "Checkout repo" 81 | uses: actions/checkout@v4 82 | - name: "Checkout maintainer tools" 83 | uses: actions/checkout@v4 84 | with: 85 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 86 | rev: b2ac115 87 | path: maintainer-tools 88 | - name: "Select toolchain" 89 | uses: dtolnay/rust-toolchain@stable 90 | with: 91 | toolchain: "1.63.0" 92 | - name: "Set dependencies" 93 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 94 | - name: "Run test script" 95 | run: ./maintainer-tools/ci/run_task.sh msrv 96 | 97 | Lint: 98 | name: Lint - nightly toolchain 99 | needs: Prepare 100 | runs-on: ubuntu-latest 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | dep: [recent] 105 | steps: 106 | - name: "Checkout repo" 107 | uses: actions/checkout@v4 108 | - name: "Checkout maintainer tools" 109 | uses: actions/checkout@v4 110 | with: 111 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 112 | rev: b2ac115 113 | path: maintainer-tools 114 | - name: "Select toolchain" 115 | uses: dtolnay/rust-toolchain@v1 116 | with: 117 | toolchain: ${{ needs.Prepare.outputs.nightly_version }} 118 | - name: "Install clippy" 119 | run: rustup component add clippy 120 | - name: "Set dependencies" 121 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 122 | - name: "Run test script" 123 | run: ./maintainer-tools/ci/run_task.sh lint 124 | 125 | Docs: 126 | name: Docs - stable toolchain 127 | runs-on: ubuntu-latest 128 | strategy: 129 | fail-fast: false 130 | matrix: 131 | dep: [recent] 132 | steps: 133 | - name: "Checkout repo" 134 | uses: actions/checkout@v4 135 | - name: "Checkout maintainer tools" 136 | uses: actions/checkout@v4 137 | with: 138 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 139 | rev: b2ac115 140 | path: maintainer-tools 141 | - name: "Select toolchain" 142 | uses: dtolnay/rust-toolchain@stable 143 | - name: "Set dependencies" 144 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 145 | - name: "Run test script" 146 | run: ./maintainer-tools/ci/run_task.sh docs 147 | 148 | Docsrs: 149 | name: Docs - nightly toolchain 150 | needs: Prepare 151 | runs-on: ubuntu-latest 152 | strategy: 153 | fail-fast: false 154 | matrix: 155 | dep: [recent] 156 | steps: 157 | - name: "Checkout repo" 158 | uses: actions/checkout@v4 159 | - name: "Checkout maintainer tools" 160 | uses: actions/checkout@v4 161 | with: 162 | repository: rust-bitcoin/rust-bitcoin-maintainer-tools 163 | rev: b2ac115 164 | path: maintainer-tools 165 | - name: "Select toolchain" 166 | uses: dtolnay/rust-toolchain@v1 167 | with: 168 | toolchain: ${{ needs.Prepare.outputs.nightly_version }} 169 | - name: "Set dependencies" 170 | run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock 171 | - name: "Run test script" 172 | run: ./maintainer-tools/ci/run_task.sh docsrs 173 | 174 | Format: # 1 jobs, run cargo fmt directly. 175 | name: Format - nightly toolchain 176 | runs-on: ubuntu-latest 177 | strategy: 178 | fail-fast: false 179 | steps: 180 | - name: "Checkout repo" 181 | uses: actions/checkout@v4 182 | - name: "Select toolchain" 183 | uses: dtolnay/rust-toolchain@nightly 184 | - name: "Install rustfmt" 185 | run: rustup component add rustfmt 186 | - name: "Check formatting" 187 | run: cargo +nightly fmt --all -- --check 188 | 189 | Arch32bit: 190 | name: Test 32-bit version 191 | runs-on: ubuntu-latest 192 | steps: 193 | - name: "Checkout repo" 194 | uses: actions/checkout@v4 195 | - name: "Select toolchain" 196 | uses: dtolnay/rust-toolchain@stable 197 | - name: "Add architecture i386" 198 | run: sudo dpkg --add-architecture i386 199 | - name: "Install i686 gcc" 200 | run: sudo apt-get update -y && sudo apt-get install -y gcc-multilib 201 | - name: "Install target" 202 | run: rustup target add i686-unknown-linux-gnu 203 | - name: "Run test on i686" 204 | run: cargo test --target i686-unknown-linux-gnu 205 | 206 | Cross: 207 | name: Cross test - stable toolchain 208 | if: ${{ !github.event.act }} 209 | runs-on: ubuntu-latest 210 | steps: 211 | - name: "Checkout repo" 212 | uses: actions/checkout@v4 213 | - name: "Select toolchain" 214 | uses: dtolnay/rust-toolchain@stable 215 | - name: "Install target" 216 | run: rustup target add s390x-unknown-linux-gnu 217 | - name: "Install cross" 218 | run: cargo install cross --locked 219 | - name: "Run cross test" 220 | run: cross test --target s390x-unknown-linux-gnu 221 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | fuzz/hfuzz_workspace/ 5 | fuzz/hfuzz_target/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.18.0 - 2024-04-12 2 | 3 | * simple_http: throw a specific error when transfer encoding is chunked 4 | [#114](https://github.com/apoelstra/rust-jsonrpc/pull/114) 5 | 6 | # 0.17.0 - 2023-12-22 7 | 8 | * `params` field in `Request` changed to a generic `RawValue` instead of an array. 9 | [#108](https://github.com/apoelstra/rust-jsonrpc/pull/108) 10 | 11 | # 0.16.0 - 2023-06-29 12 | 13 | * Re-export the `minreq` crate when the feature is set 14 | [#102](https://github.com/apoelstra/rust-jsonrpc/pull/102) 15 | * Don't treat HTTP errors with no JSON as JSON parsing errors 16 | [#103](https://github.com/apoelstra/rust-jsonrpc/pull/103) 17 | 18 | # 0.15.0 - 2023-05-28 19 | 20 | * Add new transport that uses `minreq` 21 | [#94](https://github.com/apoelstra/rust-jsonrpc/pull/94) 22 | * Bump MSRV to rust 1.48.0 23 | [#91](https://github.com/apoelstra/rust-jsonrpc/pull/91) 24 | 25 | # 0.14.1 - 2023-04-03 26 | 27 | * simple_http: fix "re-open socket on write failure" behavior 28 | [#84](https://github.com/apoelstra/rust-jsonrpc/pull/84) 29 | [#86](https://github.com/apoelstra/rust-jsonrpc/pull/86) 30 | * simple_http: add "host" header (required by HTTP 1.1) 31 | [#85](https://github.com/apoelstra/rust-jsonrpc/pull/85) 32 | * simple_http: add ability to replace URL/path; minor ergonomic improvements 33 | [#89](https://github.com/apoelstra/rust-jsonrpc/pull/89) 34 | 35 | # 0.14.0 - 2022-11-28 36 | 37 | This release significantly improves our `simple_http` client, though at the 38 | apparent cost of a performance regression when making repeated RPC calls to 39 | a local bitcoind. We are unsure what to make of this, since our code now uses 40 | fewer sockets, less memory and does less redundant processing. 41 | 42 | The highlights are: 43 | 44 | * Support JSON replies that span multiple lines 45 | [#70](https://github.com/apoelstra/rust-jsonrpc/pull/69) 46 | * Add feature-gated support for using a SOCKS proxy 47 | [#70](https://github.com/apoelstra/rust-jsonrpc/pull/70) 48 | * Fix resource exhaustive bug on MacOS by reusing sockets 49 | [#72](https://github.com/apoelstra/rust-jsonrpc/pull/72) 50 | [#76](https://github.com/apoelstra/rust-jsonrpc/pull/76) 51 | 52 | As well as improvements to our code quality and test infrastructure. 53 | 54 | # 0.13.0 - 2022-07-21 "Edition 2018 Release" 55 | 56 | This release increases the MSRV to 1.41.1, bringing with it a bunch of new language features. 57 | 58 | Some highlights: 59 | 60 | - The MSRV bump [#58](https://github.com/apoelstra/rust-jsonrpc/pull/58) 61 | - Add IPv6 support [#63](https://github.com/apoelstra/rust-jsonrpc/pull/63) 62 | - Remove `serder_derive` dependency [#61](https://github.com/apoelstra/rust-jsonrpc/pull/61) 63 | 64 | # 0.12.1 - 2022-01-20 65 | 66 | ## Features 67 | 68 | * A new set of transports were added for JSONRPC over raw TCP sockets (one using `SocketAddr`, and 69 | one UNIX-only using Unix Domain Sockets) 70 | 71 | ## Bug fixes 72 | 73 | * The `Content-Type` HTTP header is now correctly set to `application/json` 74 | * The `Connection: Close` HTTP header is now sent for requests 75 | 76 | # 0.12.0 - 2020-12-16 77 | 78 | * Remove `http` and `hyper` dependencies 79 | * Implement our own simple HTTP transport for Bitcoin Core 80 | * But allow use of generic transports 81 | 82 | # 0.11.0 - 2019-04-05 83 | 84 | * [Clean up the API](https://github.com/apoelstra/rust-jsonrpc/pull/19) 85 | * [Set the content-type header to json]((https://github.com/apoelstra/rust-jsonrpc/pull/21) 86 | * [Allow no `result` field in responses](https://github.com/apoelstra/rust-jsonrpc/pull/16) 87 | * [Add batch request support](https://github.com/apoelstra/rust-jsonrpc/pull/24) 88 | 89 | -------------------------------------------------------------------------------- /Cargo-minimal.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "backtrace" 22 | version = "0.3.68" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" 25 | dependencies = [ 26 | "addr2line", 27 | "cc", 28 | "cfg-if 1.0.0", 29 | "libc", 30 | "miniz_oxide", 31 | "object", 32 | "rustc-demangle", 33 | ] 34 | 35 | [[package]] 36 | name = "base64" 37 | version = "0.13.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 40 | 41 | [[package]] 42 | name = "byteorder" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8" 46 | 47 | [[package]] 48 | name = "cc" 49 | version = "1.1.8" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" 52 | 53 | [[package]] 54 | name = "cfg-if" 55 | version = "0.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "gimli" 67 | version = "0.27.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 70 | 71 | [[package]] 72 | name = "honggfuzz" 73 | version = "0.5.56" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "7c76b6234c13c9ea73946d1379d33186151148e0da231506b964b44f3d023505" 76 | dependencies = [ 77 | "lazy_static", 78 | "memmap2", 79 | "rustc_version", 80 | ] 81 | 82 | [[package]] 83 | name = "integration_test" 84 | version = "0.1.0" 85 | dependencies = [ 86 | "backtrace", 87 | "jsonrpc", 88 | "lazy_static", 89 | "log", 90 | "serde_json", 91 | ] 92 | 93 | [[package]] 94 | name = "itoa" 95 | version = "0.4.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 98 | 99 | [[package]] 100 | name = "jsonrpc" 101 | version = "0.18.0" 102 | dependencies = [ 103 | "base64", 104 | "minreq", 105 | "serde", 106 | "serde_json", 107 | "socks", 108 | ] 109 | 110 | [[package]] 111 | name = "jsonrpc-fuzz" 112 | version = "0.0.1" 113 | dependencies = [ 114 | "honggfuzz", 115 | "jsonrpc", 116 | ] 117 | 118 | [[package]] 119 | name = "lazy_static" 120 | version = "1.4.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 123 | 124 | [[package]] 125 | name = "libc" 126 | version = "0.2.155" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 129 | 130 | [[package]] 131 | name = "log" 132 | version = "0.4.5" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" 135 | dependencies = [ 136 | "cfg-if 0.1.2", 137 | ] 138 | 139 | [[package]] 140 | name = "memchr" 141 | version = "2.7.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 144 | 145 | [[package]] 146 | name = "memmap2" 147 | version = "0.9.4" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" 150 | dependencies = [ 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "miniz_oxide" 156 | version = "0.7.4" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 159 | dependencies = [ 160 | "adler", 161 | ] 162 | 163 | [[package]] 164 | name = "minreq" 165 | version = "2.7.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "41979ac2a5aa373c6e294b4a67fbe5e428e91a4cd0524376681f2bc6d872399b" 168 | dependencies = [ 169 | "log", 170 | "serde", 171 | "serde_json", 172 | ] 173 | 174 | [[package]] 175 | name = "object" 176 | version = "0.31.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" 179 | dependencies = [ 180 | "memchr", 181 | ] 182 | 183 | [[package]] 184 | name = "proc-macro2" 185 | version = "1.0.86" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 188 | dependencies = [ 189 | "unicode-ident", 190 | ] 191 | 192 | [[package]] 193 | name = "quote" 194 | version = "1.0.36" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 197 | dependencies = [ 198 | "proc-macro2", 199 | ] 200 | 201 | [[package]] 202 | name = "rustc-demangle" 203 | version = "0.1.24" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 206 | 207 | [[package]] 208 | name = "rustc_version" 209 | version = "0.4.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 212 | dependencies = [ 213 | "semver", 214 | ] 215 | 216 | [[package]] 217 | name = "ryu" 218 | version = "1.0.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" 221 | 222 | [[package]] 223 | name = "semver" 224 | version = "1.0.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "76b5842e81eb9bbea19276a9dbbda22ac042532f390a67ab08b895617978abf3" 227 | 228 | [[package]] 229 | name = "serde" 230 | version = "1.0.156" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" 233 | dependencies = [ 234 | "serde_derive", 235 | ] 236 | 237 | [[package]] 238 | name = "serde_derive" 239 | version = "1.0.156" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" 242 | dependencies = [ 243 | "proc-macro2", 244 | "quote", 245 | "syn", 246 | ] 247 | 248 | [[package]] 249 | name = "serde_json" 250 | version = "1.0.68" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" 253 | dependencies = [ 254 | "itoa", 255 | "ryu", 256 | "serde", 257 | ] 258 | 259 | [[package]] 260 | name = "socks" 261 | version = "0.3.4" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" 264 | dependencies = [ 265 | "byteorder", 266 | "libc", 267 | "winapi", 268 | ] 269 | 270 | [[package]] 271 | name = "syn" 272 | version = "1.0.109" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 275 | dependencies = [ 276 | "proc-macro2", 277 | "quote", 278 | "unicode-ident", 279 | ] 280 | 281 | [[package]] 282 | name = "unicode-ident" 283 | version = "1.0.12" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 286 | 287 | [[package]] 288 | name = "winapi" 289 | version = "0.3.9" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 292 | dependencies = [ 293 | "winapi-i686-pc-windows-gnu", 294 | "winapi-x86_64-pc-windows-gnu", 295 | ] 296 | 297 | [[package]] 298 | name = "winapi-i686-pc-windows-gnu" 299 | version = "0.4.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 302 | 303 | [[package]] 304 | name = "winapi-x86_64-pc-windows-gnu" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 308 | -------------------------------------------------------------------------------- /Cargo-recent.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "backtrace" 22 | version = "0.3.68" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" 25 | dependencies = [ 26 | "addr2line", 27 | "cc", 28 | "cfg-if 1.0.0", 29 | "libc", 30 | "miniz_oxide", 31 | "object", 32 | "rustc-demangle", 33 | ] 34 | 35 | [[package]] 36 | name = "base64" 37 | version = "0.13.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 40 | 41 | [[package]] 42 | name = "byteorder" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8" 46 | 47 | [[package]] 48 | name = "cc" 49 | version = "1.1.8" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" 52 | 53 | [[package]] 54 | name = "cfg-if" 55 | version = "0.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "gimli" 67 | version = "0.27.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 70 | 71 | [[package]] 72 | name = "honggfuzz" 73 | version = "0.5.56" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "7c76b6234c13c9ea73946d1379d33186151148e0da231506b964b44f3d023505" 76 | dependencies = [ 77 | "lazy_static", 78 | "memmap2", 79 | "rustc_version", 80 | ] 81 | 82 | [[package]] 83 | name = "integration_test" 84 | version = "0.1.0" 85 | dependencies = [ 86 | "backtrace", 87 | "jsonrpc", 88 | "lazy_static", 89 | "log", 90 | "serde_json", 91 | ] 92 | 93 | [[package]] 94 | name = "itoa" 95 | version = "0.4.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 98 | 99 | [[package]] 100 | name = "jsonrpc" 101 | version = "0.18.0" 102 | dependencies = [ 103 | "base64", 104 | "minreq", 105 | "serde", 106 | "serde_json", 107 | "socks", 108 | ] 109 | 110 | [[package]] 111 | name = "jsonrpc-fuzz" 112 | version = "0.0.1" 113 | dependencies = [ 114 | "honggfuzz", 115 | "jsonrpc", 116 | ] 117 | 118 | [[package]] 119 | name = "lazy_static" 120 | version = "1.4.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 123 | 124 | [[package]] 125 | name = "libc" 126 | version = "0.2.155" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 129 | 130 | [[package]] 131 | name = "log" 132 | version = "0.4.5" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" 135 | dependencies = [ 136 | "cfg-if 0.1.2", 137 | ] 138 | 139 | [[package]] 140 | name = "memchr" 141 | version = "2.7.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 144 | 145 | [[package]] 146 | name = "memmap2" 147 | version = "0.9.4" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" 150 | dependencies = [ 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "miniz_oxide" 156 | version = "0.7.4" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 159 | dependencies = [ 160 | "adler", 161 | ] 162 | 163 | [[package]] 164 | name = "minreq" 165 | version = "2.7.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "41979ac2a5aa373c6e294b4a67fbe5e428e91a4cd0524376681f2bc6d872399b" 168 | dependencies = [ 169 | "log", 170 | "serde", 171 | "serde_json", 172 | ] 173 | 174 | [[package]] 175 | name = "object" 176 | version = "0.31.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" 179 | dependencies = [ 180 | "memchr", 181 | ] 182 | 183 | [[package]] 184 | name = "proc-macro2" 185 | version = "1.0.86" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 188 | dependencies = [ 189 | "unicode-ident", 190 | ] 191 | 192 | [[package]] 193 | name = "quote" 194 | version = "1.0.36" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 197 | dependencies = [ 198 | "proc-macro2", 199 | ] 200 | 201 | [[package]] 202 | name = "rustc-demangle" 203 | version = "0.1.24" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 206 | 207 | [[package]] 208 | name = "rustc_version" 209 | version = "0.4.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 212 | dependencies = [ 213 | "semver", 214 | ] 215 | 216 | [[package]] 217 | name = "ryu" 218 | version = "1.0.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" 221 | 222 | [[package]] 223 | name = "semver" 224 | version = "1.0.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "76b5842e81eb9bbea19276a9dbbda22ac042532f390a67ab08b895617978abf3" 227 | 228 | [[package]] 229 | name = "serde" 230 | version = "1.0.156" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" 233 | dependencies = [ 234 | "serde_derive", 235 | ] 236 | 237 | [[package]] 238 | name = "serde_derive" 239 | version = "1.0.156" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" 242 | dependencies = [ 243 | "proc-macro2", 244 | "quote", 245 | "syn", 246 | ] 247 | 248 | [[package]] 249 | name = "serde_json" 250 | version = "1.0.68" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" 253 | dependencies = [ 254 | "itoa", 255 | "ryu", 256 | "serde", 257 | ] 258 | 259 | [[package]] 260 | name = "socks" 261 | version = "0.3.4" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" 264 | dependencies = [ 265 | "byteorder", 266 | "libc", 267 | "winapi", 268 | ] 269 | 270 | [[package]] 271 | name = "syn" 272 | version = "1.0.109" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 275 | dependencies = [ 276 | "proc-macro2", 277 | "quote", 278 | "unicode-ident", 279 | ] 280 | 281 | [[package]] 282 | name = "unicode-ident" 283 | version = "1.0.12" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 286 | 287 | [[package]] 288 | name = "winapi" 289 | version = "0.3.9" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 292 | dependencies = [ 293 | "winapi-i686-pc-windows-gnu", 294 | "winapi-x86_64-pc-windows-gnu", 295 | ] 296 | 297 | [[package]] 298 | name = "winapi-i686-pc-windows-gnu" 299 | version = "0.4.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 302 | 303 | [[package]] 304 | name = "winapi-x86_64-pc-windows-gnu" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 308 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jsonrpc" 3 | version = "0.18.0" 4 | authors = ["Andrew Poelstra "] 5 | license = "CC0-1.0" 6 | homepage = "https://github.com/apoelstra/rust-jsonrpc/" 7 | repository = "https://github.com/apoelstra/rust-jsonrpc/" 8 | documentation = "https://docs.rs/jsonrpc/" 9 | description = "Rust support for the JSON-RPC 2.0 protocol" 10 | keywords = [ "protocol", "json", "http", "jsonrpc" ] 11 | readme = "README.md" 12 | edition = "2021" 13 | rust-version = "1.56.1" 14 | exclude = ["tests", "contrib"] 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [features] 21 | default = [ "simple_http", "simple_tcp" ] 22 | # A bare-minimum HTTP transport. 23 | simple_http = [ "base64" ] 24 | # A transport that uses `minreq` as the HTTP client. 25 | minreq_http = [ "base64", "minreq" ] 26 | # Basic transport over a raw TcpListener 27 | simple_tcp = [] 28 | # Basic transport over a raw UnixStream 29 | simple_uds = [] 30 | # Enable Socks5 Proxy in transport 31 | proxy = ["socks"] 32 | 33 | [dependencies] 34 | serde = { version = "1", features = ["derive"] } 35 | serde_json = { version = "1", features = [ "raw_value" ] } 36 | 37 | base64 = { version = "0.13.0", optional = true } 38 | minreq = { version = "2.7.0", features = ["json-using-serde"], optional = true } 39 | socks = { version = "0.3.4", optional = true} 40 | 41 | [workspace] 42 | members = ["fuzz", "integration_test"] 43 | 44 | [lints.rust] 45 | unexpected_cfgs = { level = "deny", check-cfg = ['cfg(jsonrpc_fuzz)'] } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This repo is now archived. The new home of rust-jsonrpc is in https://github.com/rust-bitcoin/rust-bitcoind-json-rpc/ 4 | 5 | # Rust Version compatibility 6 | 7 | This library is compatible with Rust **1.63.0** or higher. 8 | 9 | # Rust JSONRPC Client 10 | 11 | Rudimentary support for sending JSONRPC 2.0 requests and receiving responses. 12 | 13 | As an example, hit a local bitcoind JSON-RPC endpoint and call the `uptime` command. 14 | 15 | ```rust 16 | use jsonrpc::Client; 17 | use jsonrpc::simple_http::{self, SimpleHttpTransport}; 18 | 19 | fn client(url: &str, user: &str, pass: &str) -> Result { 20 | let t = SimpleHttpTransport::builder() 21 | .url(url)? 22 | .auth(user, Some(pass)) 23 | .build(); 24 | 25 | Ok(Client::with_transport(t)) 26 | } 27 | 28 | // Demonstrate an example JSON-RCP call against bitcoind. 29 | fn main() { 30 | let client = client("localhost:18443", "user", "pass").expect("failed to create client"); 31 | let request = client.build_request("uptime", None); 32 | let response = client.send_request(request).expect("send_request failed"); 33 | 34 | // For other commands this would be a struct matching the returned json. 35 | let result: u64 = response.result().expect("response is an error, use check_error"); 36 | println!("bitcoind uptime: {}", result); 37 | } 38 | ``` 39 | 40 | ## Githooks 41 | 42 | To assist devs in catching errors _before_ running CI we provide some githooks. If you do not 43 | already have locally configured githooks you can use the ones in this repository by running, in the 44 | root directory of the repository: 45 | ``` 46 | git config --local core.hooksPath githooks/ 47 | ``` 48 | 49 | Alternatively add symlinks in your `.git/hooks` directory to any of the githooks we provide. 50 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.63.0" 2 | -------------------------------------------------------------------------------- /contrib/crates.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Crates in this workspace to test. 4 | CRATES=("." "integration_test" "fuzz") 5 | -------------------------------------------------------------------------------- /contrib/test_vars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # `rust-jsonrpc` does not have a std feature. 4 | FEATURES_WITH_STD="" 5 | 6 | # So this is the var to use for all tests. 7 | FEATURES_WITHOUT_STD="simple_http minreq_http simple_tcp simple_uds proxy" 8 | 9 | # Run these examples. 10 | EXAMPLES="" 11 | -------------------------------------------------------------------------------- /contrib/update-lock-files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Update the minimal/recent lock file 4 | 5 | set -euo pipefail 6 | 7 | for file in Cargo-minimal.lock Cargo-recent.lock; do 8 | cp --force "$file" Cargo.lock 9 | cargo check 10 | cp --force Cargo.lock "$file" 11 | done 12 | -------------------------------------------------------------------------------- /contrib/whitelist_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Remove once we upgrade to `bitcoin v0.32.0`. 4 | DUPLICATE_DEPS=("bech32") 5 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jsonrpc-fuzz" 3 | edition = "2018" 4 | version = "0.0.1" 5 | authors = ["Generated by fuzz/generate-files.sh"] 6 | publish = false 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | honggfuzz = { version = "0.5.56", default-features = false } 13 | jsonrpc = { path = "..", features = ["minreq_http"] } 14 | 15 | [[bin]] 16 | name = "minreq_http" 17 | path = "fuzz_targets/minreq_http.rs" 18 | 19 | [[bin]] 20 | name = "simple_http" 21 | path = "fuzz_targets/simple_http.rs" 22 | 23 | [lints.rust] 24 | unexpected_cfgs = { level = "deny", check-cfg = ['cfg(jsonrpc_fuzz)'] } 25 | -------------------------------------------------------------------------------- /fuzz/cycle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Continuosly cycle over fuzz targets running each for 1 hour. 4 | # It uses chrt SCHED_IDLE so that other process takes priority. 5 | # 6 | # For hfuzz options see https://github.com/google/honggfuzz/blob/master/docs/USAGE.md 7 | 8 | set -e 9 | REPO_DIR=$(git rev-parse --show-toplevel) 10 | # shellcheck source=./fuzz-util.sh 11 | source "$REPO_DIR/fuzz/fuzz-util.sh" 12 | 13 | while : 14 | do 15 | for targetFile in $(listTargetFiles); do 16 | targetName=$(targetFileToName "$targetFile") 17 | echo "Fuzzing target $targetName ($targetFile)" 18 | 19 | # fuzz for one hour 20 | HFUZZ_RUN_ARGS='--run_time 3600' chrt -i 0 cargo hfuzz run "$targetName" 21 | # minimize the corpus 22 | HFUZZ_RUN_ARGS="-i hfuzz_workspace/$targetName/input/ -P -M" chrt -i 0 cargo hfuzz run "$targetName" 23 | done 24 | done 25 | -------------------------------------------------------------------------------- /fuzz/fuzz-util.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO_DIR=$(git rev-parse --show-toplevel) 4 | 5 | # Sort order is effected by locale. See `man sort`. 6 | # > Set LC_ALL=C to get the traditional sort order that uses native byte values. 7 | export LC_ALL=C 8 | 9 | listTargetFiles() { 10 | pushd "$REPO_DIR/fuzz" > /dev/null || exit 1 11 | find fuzz_targets/ -type f -name "*.rs" | sort 12 | popd > /dev/null || exit 1 13 | } 14 | 15 | targetFileToName() { 16 | echo "$1" \ 17 | | sed 's/^fuzz_targets\///' \ 18 | | sed 's/\.rs$//' \ 19 | | sed 's/\//_/g' 20 | } 21 | 22 | targetFileToHFuzzInputArg() { 23 | baseName=$(basename "$1") 24 | dirName="${baseName%.*}" 25 | if [ -d "hfuzz_input/$dirName" ]; then 26 | echo "HFUZZ_INPUT_ARGS=\"-f hfuzz_input/$FILE/input\"" 27 | fi 28 | } 29 | 30 | listTargetNames() { 31 | for target in $(listTargetFiles); do 32 | targetFileToName "$target" 33 | done 34 | } 35 | 36 | # Utility function to avoid CI failures on Windows 37 | checkWindowsFiles() { 38 | incorrectFilenames=$(find . -type f -name "*,*" -o -name "*:*" -o -name "*<*" -o -name "*>*" -o -name "*|*" -o -name "*\?*" -o -name "*\**" -o -name "*\"*" | wc -l) 39 | if [ "$incorrectFilenames" -gt 0 ]; then 40 | echo "Bailing early because there is a Windows-incompatible filename in the tree." 41 | exit 2 42 | fi 43 | } 44 | 45 | # Checks whether a fuzz case output some report, and dumps it in hex 46 | checkReport() { 47 | reportFile="hfuzz_workspace/$1/HONGGFUZZ.REPORT.TXT" 48 | if [ -f "$reportFile" ]; then 49 | cat "$reportFile" 50 | for CASE in "hfuzz_workspace/$1/SIG"*; do 51 | xxd -p -c10000 < "$CASE" 52 | done 53 | exit 1 54 | fi 55 | } 56 | -------------------------------------------------------------------------------- /fuzz/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | REPO_DIR=$(git rev-parse --show-toplevel) 5 | 6 | # shellcheck source=./fuzz-util.sh 7 | source "$REPO_DIR/fuzz/fuzz-util.sh" 8 | 9 | # Check that input files are correct Windows file names 10 | checkWindowsFiles 11 | 12 | if [ "$1" == "" ]; then 13 | targetFiles="$(listTargetFiles)" 14 | else 15 | targetFiles=fuzz_targets/"$1".rs 16 | fi 17 | 18 | cargo --version 19 | rustc --version 20 | 21 | # Testing 22 | cargo install --force honggfuzz --no-default-features 23 | for targetFile in $targetFiles; do 24 | targetName=$(targetFileToName "$targetFile") 25 | echo "Fuzzing target $targetName ($targetFile)" 26 | if [ -d "hfuzz_input/$targetName" ]; then 27 | HFUZZ_INPUT_ARGS="-f hfuzz_input/$targetName/input\"" 28 | else 29 | HFUZZ_INPUT_ARGS="" 30 | fi 31 | RUSTFLAGS="--cfg=jsonrpc_fuzz" HFUZZ_RUN_ARGS="--run_time 30 --exit_upon_crash -v $HFUZZ_INPUT_ARGS" cargo hfuzz run "$targetName" 32 | 33 | checkReport "$targetName" 34 | done 35 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/minreq_http.rs: -------------------------------------------------------------------------------- 1 | extern crate jsonrpc; 2 | 3 | // Note, tests are empty if "jsonrpc_fuzz" is not set but still show up in output of `cargo test --workspace`. 4 | 5 | #[allow(unused_variables)] // `data` is not used when "jsonrpc_fuzz" is not set. 6 | fn do_test(data: &[u8]) { 7 | #[cfg(jsonrpc_fuzz)] 8 | { 9 | use std::io; 10 | 11 | use jsonrpc::minreq_http::{MinreqHttpTransport, FUZZ_TCP_SOCK}; 12 | use jsonrpc::Client; 13 | 14 | *FUZZ_TCP_SOCK.lock().unwrap() = Some(io::Cursor::new(data.to_vec())); 15 | 16 | let t = MinreqHttpTransport::builder() 17 | .url("localhost:123") 18 | .expect("parse url") 19 | .basic_auth("".to_string(), None) 20 | .build(); 21 | 22 | let client = Client::with_transport(t); 23 | let request = client.build_request("uptime", None); 24 | let _ = client.send_request(request); 25 | } 26 | } 27 | 28 | fn main() { 29 | loop { 30 | honggfuzz::fuzz!(|data| { 31 | do_test(data); 32 | }); 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | fn extend_vec_from_hex(hex: &str) -> Vec { 39 | let mut out = vec![]; 40 | let mut b = 0; 41 | for (idx, c) in hex.as_bytes().iter().enumerate() { 42 | b <<= 4; 43 | match *c { 44 | b'A'..=b'F' => b |= c - b'A' + 10, 45 | b'a'..=b'f' => b |= c - b'a' + 10, 46 | b'0'..=b'9' => b |= c - b'0', 47 | _ => panic!("Bad hex"), 48 | } 49 | if (idx & 1) == 1 { 50 | out.push(b); 51 | b = 0; 52 | } 53 | } 54 | out 55 | } 56 | 57 | #[test] 58 | fn duplicate_crash() { super::do_test(&extend_vec_from_hex("00")); } 59 | } 60 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/simple_http.rs: -------------------------------------------------------------------------------- 1 | extern crate jsonrpc; 2 | 3 | // Note, tests are if empty "jsonrpc_fuzz" is not set but still show up in output of `cargo test --workspace`. 4 | 5 | #[allow(unused_variables)] // `data` is not used when "jsonrpc_fuzz" is not set. 6 | fn do_test(data: &[u8]) { 7 | #[cfg(jsonrpc_fuzz)] 8 | { 9 | use std::io; 10 | 11 | use jsonrpc::simple_http::{SimpleHttpTransport, FUZZ_TCP_SOCK}; 12 | use jsonrpc::Client; 13 | 14 | *FUZZ_TCP_SOCK.lock().unwrap() = Some(io::Cursor::new(data.to_vec())); 15 | 16 | let t = SimpleHttpTransport::builder() 17 | .url("localhost:123") 18 | .expect("parse url") 19 | .auth("", None) 20 | .build(); 21 | 22 | let client = Client::with_transport(t); 23 | let request = client.build_request("uptime", None); 24 | let _ = client.send_request(request); 25 | } 26 | } 27 | 28 | fn main() { 29 | loop { 30 | honggfuzz::fuzz!(|data| { 31 | do_test(data); 32 | }); 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | fn extend_vec_from_hex(hex: &str) -> Vec { 39 | let mut out = vec![]; 40 | let mut b = 0; 41 | for (idx, c) in hex.as_bytes().iter().enumerate() { 42 | b <<= 4; 43 | match *c { 44 | b'A'..=b'F' => b |= c - b'A' + 10, 45 | b'a'..=b'f' => b |= c - b'a' + 10, 46 | b'0'..=b'9' => b |= c - b'0', 47 | _ => panic!("Bad hex"), 48 | } 49 | if (idx & 1) == 1 { 50 | out.push(b); 51 | b = 0; 52 | } 53 | } 54 | out 55 | } 56 | 57 | #[test] 58 | fn duplicate_crash() { super::do_test(&extend_vec_from_hex("00")); } 59 | } 60 | -------------------------------------------------------------------------------- /fuzz/generate-files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | REPO_DIR=$(git rev-parse --show-toplevel) 6 | 7 | # shellcheck source=./fuzz-util.sh 8 | source "$REPO_DIR/fuzz/fuzz-util.sh" 9 | 10 | # 1. Generate fuzz/Cargo.toml 11 | cat > "$REPO_DIR/fuzz/Cargo.toml" <> "$REPO_DIR/fuzz/Cargo.toml" < "$REPO_DIR/.github/workflows/cron-daily-fuzz.yml" <executed_\${{ matrix.fuzz_target }} 81 | - uses: actions/upload-artifact@v2 82 | with: 83 | name: executed_\${{ matrix.fuzz_target }} 84 | path: executed_\${{ matrix.fuzz_target }} 85 | 86 | verify-execution: 87 | if: \${{ !github.event.act }} 88 | needs: fuzz 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v2 92 | - uses: actions/download-artifact@v2 93 | - name: Display structure of downloaded files 94 | run: ls -R 95 | - run: find executed_* -type f -exec cat {} + | sort > executed 96 | - run: source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed 97 | EOF 98 | -------------------------------------------------------------------------------- /githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Verify what is about to be committed. Called by "git commit" with no 4 | # arguments. The hook should exit with non-zero status after issuing an 5 | # appropriate message if it wants to stop the commit. 6 | 7 | if git rev-parse --verify HEAD >/dev/null 2>&1 8 | then 9 | against=HEAD 10 | else 11 | # Initial commit: diff against an empty tree object 12 | against=$(git hash-object -t tree /dev/null) 13 | fi 14 | 15 | # If you want to allow non-ASCII filenames set this variable to true. 16 | allownonascii=$(git config --bool hooks.allownonascii) 17 | 18 | # Redirect output to stderr. 19 | exec 1>&2 20 | 21 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 22 | # them from being added to the repository. We exploit the fact that the 23 | # printable range starts at the space character and ends with tilde. 24 | if [ "$allownonascii" != "true" ] && 25 | # Note that the use of brackets around a tr range is ok here, (it's 26 | # even required, for portability to Solaris 10's /usr/bin/tr), since 27 | # the square bracket bytes happen to fall in the designated range. 28 | test $(git diff --cached --name-only --diff-filter=A -z $against | 29 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 30 | then 31 | cat <<\EOF 32 | Error: Attempt to add a non-ASCII file name. 33 | 34 | This can cause problems if you want to work with people on other platforms. 35 | 36 | To be portable it is advisable to rename the file. 37 | 38 | If you know what you are doing you can disable this check using: 39 | 40 | git config hooks.allownonascii true 41 | EOF 42 | exit 1 43 | fi 44 | 45 | # If there are whitespace errors, print the offending file names and fail. 46 | git diff-index --check --cached $against || exit 1 47 | 48 | # Check that code lints cleanly. 49 | cargo clippy --all-features --all-targets -- -D warnings || exit 1 50 | 51 | # Check that there are no formatting issues. 52 | cargo +nightly fmt -- --check || exit 1 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /integration_test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integration_test" 3 | version = "0.1.0" 4 | authors = ["Steven Roose , Tobin C. Harding "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | jsonrpc = { path = "..", features = ["minreq_http"] } 9 | lazy_static = "1.4.0" 10 | log = "0.4" 11 | backtrace = "0.3.50" 12 | serde_json = { version = "1.0", features = ["raw_value"] } 13 | -------------------------------------------------------------------------------- /integration_test/contrib/test_vars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # We just need this file to exist. 4 | -------------------------------------------------------------------------------- /integration_test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BITCOIND_PATH="${BITCOIND_PATH:-bitcoind}" 4 | TESTDIR=/tmp/rust_bitcoincore_rpc_test 5 | 6 | rm -rf ${TESTDIR} 7 | mkdir -p ${TESTDIR}/1 ${TESTDIR}/2 8 | 9 | # To kill any remaining open bitcoind. 10 | killall -9 bitcoind 11 | 12 | ${BITCOIND_PATH} -regtest \ 13 | -datadir=${TESTDIR}/1 \ 14 | -port=12348 \ 15 | -server=0 \ 16 | -printtoconsole=0 & 17 | PID1=$! 18 | 19 | # Make sure it's listening on its p2p port. 20 | sleep 3 21 | 22 | BLOCKFILTERARG="" 23 | if ${BITCOIND_PATH} -version | grep -q "v\(2\|0\.19\|0.2\)"; then 24 | BLOCKFILTERARG="-blockfilterindex=1" 25 | fi 26 | 27 | FALLBACKFEEARG="" 28 | if ${BITCOIND_PATH} -version | grep -q "v\(2\|0\.2\)"; then 29 | FALLBACKFEEARG="-fallbackfee=0.00001000" 30 | fi 31 | 32 | ${BITCOIND_PATH} -regtest $BLOCKFILTERARG $FALLBACKFEEARG \ 33 | -datadir=${TESTDIR}/2 \ 34 | -connect=127.0.0.1:12348 \ 35 | -rpcport=12349 \ 36 | -server=1 \ 37 | -txindex=1 \ 38 | -printtoconsole=0 & 39 | PID2=$! 40 | 41 | # Let it connect to the other node. 42 | sleep 5 43 | 44 | RPC_URL=http://localhost:12349 \ 45 | RPC_COOKIE=${TESTDIR}/2/regtest/.cookie \ 46 | cargo run 47 | 48 | RESULT=$? 49 | 50 | kill -9 $PID1 $PID2 51 | 52 | exit $RESULT 53 | -------------------------------------------------------------------------------- /integration_test/src/main.rs: -------------------------------------------------------------------------------- 1 | //! # rust-bitcoincore-rpc integration test 2 | //! 3 | //! The test methods are named to mention the methods tested. 4 | //! Individual test methods don't use any methods not tested before or 5 | //! mentioned in the test method name. 6 | //! 7 | //! The goal of this test is not to test the correctness of the server, but 8 | //! to test the serialization of arguments and deserialization of responses. 9 | //! 10 | 11 | #![deny(unused)] 12 | #![allow(deprecated)] 13 | 14 | #[macro_use] 15 | extern crate lazy_static; 16 | 17 | use std::cell::RefCell; 18 | use std::sync::Mutex; 19 | use std::time::Duration; 20 | use std::{fs, panic}; 21 | 22 | use backtrace::Backtrace; 23 | use jsonrpc::http::minreq_http; 24 | use jsonrpc::{Client, Request}; 25 | use serde_json::json; 26 | use serde_json::value::to_raw_value; 27 | 28 | struct StdLogger; 29 | 30 | impl log::Log for StdLogger { 31 | fn enabled(&self, metadata: &log::Metadata) -> bool { 32 | metadata.target().contains("jsonrpc") || metadata.target().contains("bitcoincore_rpc") 33 | } 34 | 35 | fn log(&self, record: &log::Record) { 36 | if self.enabled(record.metadata()) { 37 | println!("[{}][{}]: {}", record.level(), record.metadata().target(), record.args()); 38 | } 39 | } 40 | 41 | fn flush(&self) {} 42 | } 43 | 44 | static LOGGER: StdLogger = StdLogger; 45 | 46 | fn get_rpc_url() -> String { std::env::var("RPC_URL").expect("RPC_URL must be set") } 47 | 48 | fn get_auth() -> (String, Option) { 49 | if let Ok(cookie) = std::env::var("RPC_COOKIE") { 50 | let contents = fs::read_to_string(&cookie) 51 | .unwrap_or_else(|_| panic!("failed to read cookie file: {}", cookie)); 52 | let mut split = contents.split(':'); 53 | let user = split.next().expect("failed to get username from cookie file"); 54 | let pass = split.next().map_or("".to_string(), |s| s.to_string()); 55 | (user.to_string(), Some(pass)) 56 | } else if let Ok(user) = std::env::var("RPC_USER") { 57 | (user, std::env::var("RPC_PASS").ok()) 58 | } else { 59 | panic!("Either RPC_COOKIE or RPC_USER + RPC_PASS must be set.") 60 | } 61 | } 62 | 63 | fn make_client() -> Client { 64 | let (user, pass) = get_auth(); 65 | let tp = minreq_http::Builder::new() 66 | .timeout(Duration::from_secs(1)) 67 | .url(&get_rpc_url()) 68 | .unwrap() 69 | .basic_auth(user, pass) 70 | .build(); 71 | Client::with_transport(tp) 72 | } 73 | 74 | lazy_static! { 75 | static ref CLIENT: Client = make_client(); 76 | 77 | /// Here we will collect all the results of the individual tests, preserving ordering. 78 | /// Ideally this would be preset with capacity, but static prevents this. 79 | static ref RESULTS: Mutex> = Mutex::new(Vec::new()); 80 | } 81 | 82 | thread_local! { 83 | static LAST_PANIC: RefCell> = const { RefCell::new(None) }; 84 | } 85 | 86 | macro_rules! run_test { 87 | ($method:ident) => { 88 | println!("Running {}...", stringify!($method)); 89 | let result = panic::catch_unwind(|| { 90 | $method(&*CLIENT); 91 | }); 92 | if result.is_err() { 93 | let (msg, bt) = LAST_PANIC.with(|b| b.borrow_mut().take()).unwrap(); 94 | println!("{}", msg); 95 | println!("{:?}", bt); 96 | println!("--"); 97 | } 98 | 99 | RESULTS.lock().unwrap().push((stringify!($method), result.is_ok())); 100 | }; 101 | } 102 | 103 | fn main() { 104 | log::set_logger(&LOGGER).map(|()| log::set_max_level(log::LevelFilter::max())).unwrap(); 105 | 106 | // let default_hook = std::panic::take_hook() 107 | std::panic::set_hook(Box::new(|panic_info| { 108 | let bt = Backtrace::new(); 109 | LAST_PANIC.with(move |b| b.borrow_mut().replace((panic_info.to_string(), bt))); 110 | })); 111 | 112 | run_test!(test_get_network_info); 113 | 114 | run_test!(test_get_block_hash_list); 115 | run_test!(test_get_block_hash_named); 116 | 117 | // Print results 118 | println!(); 119 | println!(); 120 | println!("Summary:"); 121 | let mut error_count = 0; 122 | for (name, success) in RESULTS.lock().unwrap().iter() { 123 | if !success { 124 | println!(" - {}: FAILED", name); 125 | error_count += 1; 126 | } else { 127 | println!(" - {}: PASSED", name); 128 | } 129 | } 130 | 131 | println!(); 132 | 133 | if error_count == 0 { 134 | println!("All tests succesful!"); 135 | } else { 136 | println!("{} tests failed", error_count); 137 | std::process::exit(1); 138 | } 139 | } 140 | 141 | fn test_get_network_info(cl: &Client) { 142 | let request = Request { 143 | method: "getnetworkinfo", 144 | params: None, 145 | id: serde_json::json!(1), 146 | jsonrpc: Some("2.0"), 147 | }; 148 | 149 | let _ = cl.send_request(request).unwrap(); 150 | } 151 | 152 | fn test_get_block_hash_list(cl: &Client) { 153 | let param = json!([0]); 154 | let raw_value = Some(to_raw_value(¶m).unwrap()); 155 | 156 | let request = Request { 157 | method: "getblockhash", 158 | params: raw_value.as_deref(), 159 | id: serde_json::json!(2), 160 | jsonrpc: Some("2.0"), 161 | }; 162 | 163 | let resp = cl.send_request(request).unwrap(); 164 | assert_eq!( 165 | resp.result.unwrap().to_string(), 166 | "\"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206\"" 167 | ); 168 | } 169 | 170 | fn test_get_block_hash_named(cl: &Client) { 171 | let param = json!({ "height": 0 }); 172 | let raw_value = Some(to_raw_value(¶m).unwrap()); 173 | 174 | let request = Request { 175 | method: "getblockhash", 176 | params: raw_value.as_deref(), 177 | id: serde_json::json!(2), 178 | jsonrpc: Some("2.0"), 179 | }; 180 | 181 | let resp = cl.send_request(request).unwrap(); 182 | assert_eq!( 183 | resp.result.unwrap().to_string(), 184 | "\"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206\"" 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | @just --list 3 | 4 | # Cargo build everything. 5 | build: 6 | cargo build --workspace --all-targets --all-features 7 | 8 | # Cargo check everything. 9 | check: 10 | cargo check --workspace --all-targets --all-features 11 | 12 | # Lint everything. 13 | lint: 14 | cargo +$(cat ./nightly-version) clippy --workspace --all-targets --all-features -- --deny warnings 15 | 16 | # Run cargo fmt 17 | fmt: 18 | cargo +$(cat ./nightly-version) fmt --all 19 | 20 | # Check the formatting 21 | format: 22 | cargo +$(cat ./nightly-version) fmt --all --check 23 | 24 | # Quick and dirty CI useful for pre-push checks. 25 | sane: lint 26 | cargo test --quiet --workspace --all-targets --no-default-features > /dev/null || exit 1 27 | cargo test --quiet --workspace --all-targets > /dev/null || exit 1 28 | cargo test --quiet --workspace --all-targets --all-features > /dev/null || exit 1 29 | 30 | # doctests don't get run from workspace root with `cargo test`. 31 | cargo test --quiet --workspace --doc || exit 1 32 | 33 | # Make an attempt to catch feature gate problems in doctests 34 | cargo test --manifest-path bitcoin/Cargo.toml --doc --no-default-features > /dev/null || exit 1 35 | 36 | # Update the recent and minimal lock files. 37 | update-lock-files: 38 | contrib/update-lock-files.sh 39 | -------------------------------------------------------------------------------- /nightly-version: -------------------------------------------------------------------------------- 1 | nightly-2024-08-04 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | ignore = [] 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | indent_style = "Block" 6 | 7 | max_width = 100 # This is number of characters. 8 | # `use_small_heuristics` is ignored if the granular width config values are explicitly set. 9 | use_small_heuristics = "Max" # "Max" == All granular width settings same as `max_width`. 10 | # # Granular width configuration settings. These are percentages of `max_width`. 11 | # fn_call_width = 60 12 | # attr_fn_like_width = 70 13 | # struct_lit_width = 18 14 | # struct_variant_width = 35 15 | # array_width = 60 16 | # chain_width = 60 17 | # single_line_if_else_max_width = 50 18 | 19 | wrap_comments = false 20 | format_code_in_doc_comments = false 21 | comment_width = 100 # Default 80 22 | normalize_comments = false 23 | normalize_doc_attributes = false 24 | format_strings = false 25 | format_macro_matchers = false 26 | format_macro_bodies = true 27 | hex_literal_case = "Preserve" 28 | empty_item_single_line = true 29 | struct_lit_single_line = true 30 | fn_single_line = true # Default false 31 | where_single_line = false 32 | imports_indent = "Block" 33 | imports_layout = "Mixed" 34 | imports_granularity = "Module" # Default "Preserve" 35 | group_imports = "StdExternalCrate" # Default "Preserve" 36 | reorder_imports = true 37 | reorder_modules = true 38 | reorder_impl_items = false 39 | type_punctuation_density = "Wide" 40 | space_before_colon = false 41 | space_after_colon = true 42 | spaces_around_ranges = false 43 | binop_separator = "Front" 44 | remove_nested_parens = true 45 | combine_control_expr = true 46 | overflow_delimited_expr = false 47 | struct_field_align_threshold = 0 48 | enum_discrim_align_threshold = 0 49 | match_arm_blocks = false # Default true 50 | match_arm_leading_pipes = "Never" 51 | force_multiline_blocks = false 52 | fn_params_layout = "Tall" 53 | brace_style = "SameLineWhere" 54 | control_brace_style = "AlwaysSameLine" 55 | trailing_semicolon = true 56 | trailing_comma = "Vertical" 57 | match_block_trailing_comma = false 58 | blank_lines_upper_bound = 1 59 | blank_lines_lower_bound = 0 60 | edition = "2018" 61 | version = "One" 62 | inline_attribute_width = 0 63 | format_generated_files = true 64 | merge_derives = true 65 | use_try_shorthand = false 66 | use_field_init_shorthand = false 67 | force_explicit_abi = true 68 | condense_wildcard_suffixes = false 69 | color = "Auto" 70 | unstable_features = false 71 | disable_all_formatting = false 72 | skip_children = false 73 | show_parse_errors = true 74 | error_on_line_overflow = false 75 | error_on_unformatted = false 76 | emit_mode = "Files" 77 | make_backup = false 78 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! # Client support 4 | //! 5 | //! Support for connecting to JSONRPC servers over HTTP, sending requests, 6 | //! and parsing responses 7 | 8 | use std::borrow::Cow; 9 | use std::collections::HashMap; 10 | use std::fmt; 11 | use std::hash::{Hash, Hasher}; 12 | use std::sync::atomic; 13 | 14 | use serde_json::value::RawValue; 15 | use serde_json::Value; 16 | 17 | use crate::error::Error; 18 | use crate::{Request, Response}; 19 | 20 | /// An interface for a transport over which to use the JSONRPC protocol. 21 | pub trait Transport: Send + Sync + 'static { 22 | /// Sends an RPC request over the transport. 23 | fn send_request(&self, _: Request) -> Result; 24 | /// Sends a batch of RPC requests over the transport. 25 | fn send_batch(&self, _: &[Request]) -> Result, Error>; 26 | /// Formats the target of this transport. I.e. the URL/socket/... 27 | fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result; 28 | } 29 | 30 | /// A JSON-RPC client. 31 | /// 32 | /// Creates a new Client using one of the transport-specific constructors e.g., 33 | /// [`Client::simple_http`] for a bare-minimum HTTP transport. 34 | pub struct Client { 35 | pub(crate) transport: Box, 36 | nonce: atomic::AtomicUsize, 37 | } 38 | 39 | impl Client { 40 | /// Creates a new client with the given transport. 41 | pub fn with_transport(transport: T) -> Client { 42 | Client { transport: Box::new(transport), nonce: atomic::AtomicUsize::new(1) } 43 | } 44 | 45 | /// Builds a request. 46 | /// 47 | /// To construct the arguments, one can use one of the shorthand methods 48 | /// [`crate::arg`] or [`crate::try_arg`]. 49 | pub fn build_request<'a>(&self, method: &'a str, params: Option<&'a RawValue>) -> Request<'a> { 50 | let nonce = self.nonce.fetch_add(1, atomic::Ordering::Relaxed); 51 | Request { method, params, id: serde_json::Value::from(nonce), jsonrpc: Some("2.0") } 52 | } 53 | 54 | /// Sends a request to a client. 55 | pub fn send_request(&self, request: Request) -> Result { 56 | self.transport.send_request(request) 57 | } 58 | 59 | /// Sends a batch of requests to the client. 60 | /// 61 | /// Note that the requests need to have valid IDs, so it is advised to create the requests 62 | /// with [`Client::build_request`]. 63 | /// 64 | /// # Returns 65 | /// 66 | /// The return vector holds the response for the request at the corresponding index. If no 67 | /// response was provided, it's [`None`]. 68 | pub fn send_batch(&self, requests: &[Request]) -> Result>, Error> { 69 | if requests.is_empty() { 70 | return Err(Error::EmptyBatch); 71 | } 72 | 73 | // If the request body is invalid JSON, the response is a single response object. 74 | // We ignore this case since we are confident we are producing valid JSON. 75 | let responses = self.transport.send_batch(requests)?; 76 | if responses.len() > requests.len() { 77 | return Err(Error::WrongBatchResponseSize); 78 | } 79 | 80 | //TODO(stevenroose) check if the server preserved order to avoid doing the mapping 81 | 82 | // First index responses by ID and catch duplicate IDs. 83 | let mut by_id = HashMap::with_capacity(requests.len()); 84 | for resp in responses.into_iter() { 85 | let id = HashableValue(Cow::Owned(resp.id.clone())); 86 | if let Some(dup) = by_id.insert(id, resp) { 87 | return Err(Error::BatchDuplicateResponseId(dup.id)); 88 | } 89 | } 90 | // Match responses to the requests. 91 | let results = 92 | requests.iter().map(|r| by_id.remove(&HashableValue(Cow::Borrowed(&r.id)))).collect(); 93 | 94 | // Since we're also just producing the first duplicate ID, we can also just produce the 95 | // first incorrect ID in case there are multiple. 96 | if let Some(id) = by_id.keys().next() { 97 | return Err(Error::WrongBatchResponseId((*id.0).clone())); 98 | } 99 | 100 | Ok(results) 101 | } 102 | 103 | /// Makes a request and deserializes the response. 104 | /// 105 | /// To construct the arguments, one can use one of the shorthand methods 106 | /// [`crate::arg`] or [`crate::try_arg`]. 107 | pub fn call serde::de::Deserialize<'a>>( 108 | &self, 109 | method: &str, 110 | args: Option<&RawValue>, 111 | ) -> Result { 112 | let request = self.build_request(method, args); 113 | let id = request.id.clone(); 114 | 115 | let response = self.send_request(request)?; 116 | if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) { 117 | return Err(Error::VersionMismatch); 118 | } 119 | if response.id != id { 120 | return Err(Error::NonceMismatch); 121 | } 122 | 123 | response.result() 124 | } 125 | } 126 | 127 | impl fmt::Debug for crate::Client { 128 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 129 | write!(f, "jsonrpc::Client(")?; 130 | self.transport.fmt_target(f)?; 131 | write!(f, ")") 132 | } 133 | } 134 | 135 | impl From for Client { 136 | fn from(t: T) -> Client { Client::with_transport(t) } 137 | } 138 | 139 | /// Newtype around `Value` which allows hashing for use as hashmap keys, 140 | /// this is needed for batch requests. 141 | /// 142 | /// The reason `Value` does not support `Hash` or `Eq` by itself 143 | /// is that it supports `f64` values; but for batch requests we 144 | /// will only be hashing the "id" field of the request/response 145 | /// pair, which should never need decimal precision and therefore 146 | /// never use `f64`. 147 | #[derive(Clone, PartialEq, Debug)] 148 | struct HashableValue<'a>(pub Cow<'a, Value>); 149 | 150 | impl<'a> Eq for HashableValue<'a> {} 151 | 152 | impl<'a> Hash for HashableValue<'a> { 153 | fn hash(&self, state: &mut H) { 154 | match *self.0.as_ref() { 155 | Value::Null => "null".hash(state), 156 | Value::Bool(false) => "false".hash(state), 157 | Value::Bool(true) => "true".hash(state), 158 | Value::Number(ref n) => { 159 | "number".hash(state); 160 | if let Some(n) = n.as_i64() { 161 | n.hash(state); 162 | } else if let Some(n) = n.as_u64() { 163 | n.hash(state); 164 | } else { 165 | n.to_string().hash(state); 166 | } 167 | } 168 | Value::String(ref s) => { 169 | "string".hash(state); 170 | s.hash(state); 171 | } 172 | Value::Array(ref v) => { 173 | "array".hash(state); 174 | v.len().hash(state); 175 | for obj in v { 176 | HashableValue(Cow::Borrowed(obj)).hash(state); 177 | } 178 | } 179 | Value::Object(ref m) => { 180 | "object".hash(state); 181 | m.len().hash(state); 182 | for (key, val) in m { 183 | key.hash(state); 184 | HashableValue(Cow::Borrowed(val)).hash(state); 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use std::borrow::Cow; 194 | use std::collections::HashSet; 195 | use std::str::FromStr; 196 | use std::sync; 197 | 198 | use super::*; 199 | 200 | struct DummyTransport; 201 | impl Transport for DummyTransport { 202 | fn send_request(&self, _: Request) -> Result { Err(Error::NonceMismatch) } 203 | fn send_batch(&self, _: &[Request]) -> Result, Error> { Ok(vec![]) } 204 | fn fmt_target(&self, _: &mut fmt::Formatter) -> fmt::Result { Ok(()) } 205 | } 206 | 207 | #[test] 208 | fn sanity() { 209 | let client = Client::with_transport(DummyTransport); 210 | assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 1); 211 | let req1 = client.build_request("test", None); 212 | assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 2); 213 | let req2 = client.build_request("test", None); 214 | assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 3); 215 | assert!(req1.id != req2.id); 216 | } 217 | 218 | #[test] 219 | fn hash_value() { 220 | let val = HashableValue(Cow::Owned(Value::from_str("null").unwrap())); 221 | let t = HashableValue(Cow::Owned(Value::from_str("true").unwrap())); 222 | let f = HashableValue(Cow::Owned(Value::from_str("false").unwrap())); 223 | let ns = 224 | HashableValue(Cow::Owned(Value::from_str("[0, -0, 123.4567, -100000000]").unwrap())); 225 | let m = 226 | HashableValue(Cow::Owned(Value::from_str("{ \"field\": 0, \"field\": -0 }").unwrap())); 227 | 228 | let mut coll = HashSet::new(); 229 | 230 | assert!(!coll.contains(&val)); 231 | coll.insert(val.clone()); 232 | assert!(coll.contains(&val)); 233 | 234 | assert!(!coll.contains(&t)); 235 | assert!(!coll.contains(&f)); 236 | coll.insert(t.clone()); 237 | assert!(coll.contains(&t)); 238 | assert!(!coll.contains(&f)); 239 | coll.insert(f.clone()); 240 | assert!(coll.contains(&t)); 241 | assert!(coll.contains(&f)); 242 | 243 | assert!(!coll.contains(&ns)); 244 | coll.insert(ns.clone()); 245 | assert!(coll.contains(&ns)); 246 | 247 | assert!(!coll.contains(&m)); 248 | coll.insert(m.clone()); 249 | assert!(coll.contains(&m)); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! # Error handling 4 | //! 5 | //! Some useful methods for creating Error objects. 6 | 7 | use std::{error, fmt}; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::Response; 12 | 13 | /// A library error 14 | #[derive(Debug)] 15 | #[non_exhaustive] 16 | pub enum Error { 17 | /// A transport error 18 | Transport(Box), 19 | /// Json error 20 | Json(serde_json::Error), 21 | /// Error response 22 | Rpc(RpcError), 23 | /// Response to a request did not have the expected nonce 24 | NonceMismatch, 25 | /// Response to a request had a jsonrpc field other than "2.0" 26 | VersionMismatch, 27 | /// Batches can't be empty 28 | EmptyBatch, 29 | /// Too many responses returned in batch 30 | WrongBatchResponseSize, 31 | /// Batch response contained a duplicate ID 32 | BatchDuplicateResponseId(serde_json::Value), 33 | /// Batch response contained an ID that didn't correspond to any request ID 34 | WrongBatchResponseId(serde_json::Value), 35 | } 36 | 37 | impl From for Error { 38 | fn from(e: serde_json::Error) -> Error { Error::Json(e) } 39 | } 40 | 41 | impl From for Error { 42 | fn from(e: RpcError) -> Error { Error::Rpc(e) } 43 | } 44 | 45 | impl fmt::Display for Error { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | use Error::*; 48 | 49 | match *self { 50 | Transport(ref e) => write!(f, "transport error: {}", e), 51 | Json(ref e) => write!(f, "JSON decode error: {}", e), 52 | Rpc(ref r) => write!(f, "RPC error response: {:?}", r), 53 | BatchDuplicateResponseId(ref v) => write!(f, "duplicate RPC batch response ID: {}", v), 54 | WrongBatchResponseId(ref v) => write!(f, "wrong RPC batch response ID: {}", v), 55 | NonceMismatch => write!(f, "nonce of response did not match nonce of request"), 56 | VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""), 57 | EmptyBatch => write!(f, "batches can't be empty"), 58 | WrongBatchResponseSize => write!(f, "too many responses returned in batch"), 59 | } 60 | } 61 | } 62 | 63 | impl error::Error for Error { 64 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 65 | use self::Error::*; 66 | 67 | match *self { 68 | Rpc(_) 69 | | NonceMismatch 70 | | VersionMismatch 71 | | EmptyBatch 72 | | WrongBatchResponseSize 73 | | BatchDuplicateResponseId(_) 74 | | WrongBatchResponseId(_) => None, 75 | Transport(ref e) => Some(&**e), 76 | Json(ref e) => Some(e), 77 | } 78 | } 79 | } 80 | 81 | /// Standard error responses, as described at at 82 | /// 83 | /// 84 | /// # Documentation Copyright 85 | /// Copyright (C) 2007-2010 by the JSON-RPC Working Group 86 | /// 87 | /// This document and translations of it may be used to implement JSON-RPC, it 88 | /// may be copied and furnished to others, and derivative works that comment 89 | /// on or otherwise explain it or assist in its implementation may be prepared, 90 | /// copied, published and distributed, in whole or in part, without restriction 91 | /// of any kind, provided that the above copyright notice and this paragraph 92 | /// are included on all such copies and derivative works. However, this document 93 | /// itself may not be modified in any way. 94 | /// 95 | /// The limited permissions granted above are perpetual and will not be revoked. 96 | /// 97 | /// This document and the information contained herein is provided "AS IS" and 98 | /// ALL WARRANTIES, EXPRESS OR IMPLIED are DISCLAIMED, INCLUDING BUT NOT LIMITED 99 | /// TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY 100 | /// RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A 101 | /// PARTICULAR PURPOSE. 102 | /// 103 | #[derive(Debug)] 104 | pub enum StandardError { 105 | /// Invalid JSON was received by the server. 106 | /// An error occurred on the server while parsing the JSON text. 107 | ParseError, 108 | /// The JSON sent is not a valid Request object. 109 | InvalidRequest, 110 | /// The method does not exist / is not available. 111 | MethodNotFound, 112 | /// Invalid method parameter(s). 113 | InvalidParams, 114 | /// Internal JSON-RPC error. 115 | InternalError, 116 | } 117 | 118 | /// A JSONRPC error object 119 | #[derive(Clone, Debug, Deserialize, Serialize)] 120 | pub struct RpcError { 121 | /// The integer identifier of the error 122 | pub code: i32, 123 | /// A string describing the error 124 | pub message: String, 125 | /// Additional data specific to the error 126 | pub data: Option>, 127 | } 128 | 129 | /// Create a standard error responses 130 | pub fn standard_error( 131 | code: StandardError, 132 | data: Option>, 133 | ) -> RpcError { 134 | match code { 135 | StandardError::ParseError => 136 | RpcError { code: -32700, message: "Parse error".to_string(), data }, 137 | StandardError::InvalidRequest => 138 | RpcError { code: -32600, message: "Invalid Request".to_string(), data }, 139 | StandardError::MethodNotFound => 140 | RpcError { code: -32601, message: "Method not found".to_string(), data }, 141 | StandardError::InvalidParams => 142 | RpcError { code: -32602, message: "Invalid params".to_string(), data }, 143 | StandardError::InternalError => 144 | RpcError { code: -32603, message: "Internal error".to_string(), data }, 145 | } 146 | } 147 | 148 | /// Converts a Rust `Result` to a JSONRPC response object 149 | pub fn result_to_response( 150 | result: Result, 151 | id: serde_json::Value, 152 | ) -> Response { 153 | match result { 154 | Ok(data) => Response { 155 | result: Some( 156 | serde_json::value::RawValue::from_string(serde_json::to_string(&data).unwrap()) 157 | .unwrap(), 158 | ), 159 | error: None, 160 | id, 161 | jsonrpc: Some(String::from("2.0")), 162 | }, 163 | Err(err) => 164 | Response { result: None, error: Some(err), id, jsonrpc: Some(String::from("2.0")) }, 165 | } 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | use serde_json; 171 | 172 | use super::StandardError::{ 173 | InternalError, InvalidParams, InvalidRequest, MethodNotFound, ParseError, 174 | }; 175 | use super::{result_to_response, standard_error}; 176 | 177 | #[test] 178 | fn test_parse_error() { 179 | let resp = result_to_response(Err(standard_error(ParseError, None)), From::from(1)); 180 | assert!(resp.result.is_none()); 181 | assert!(resp.error.is_some()); 182 | assert_eq!(resp.id, serde_json::Value::from(1)); 183 | assert_eq!(resp.error.unwrap().code, -32700); 184 | } 185 | 186 | #[test] 187 | fn test_invalid_request() { 188 | let resp = result_to_response(Err(standard_error(InvalidRequest, None)), From::from(1)); 189 | assert!(resp.result.is_none()); 190 | assert!(resp.error.is_some()); 191 | assert_eq!(resp.id, serde_json::Value::from(1)); 192 | assert_eq!(resp.error.unwrap().code, -32600); 193 | } 194 | 195 | #[test] 196 | fn test_method_not_found() { 197 | let resp = result_to_response(Err(standard_error(MethodNotFound, None)), From::from(1)); 198 | assert!(resp.result.is_none()); 199 | assert!(resp.error.is_some()); 200 | assert_eq!(resp.id, serde_json::Value::from(1)); 201 | assert_eq!(resp.error.unwrap().code, -32601); 202 | } 203 | 204 | #[test] 205 | fn test_invalid_params() { 206 | let resp = result_to_response(Err(standard_error(InvalidParams, None)), From::from("123")); 207 | assert!(resp.result.is_none()); 208 | assert!(resp.error.is_some()); 209 | assert_eq!(resp.id, serde_json::Value::from("123")); 210 | assert_eq!(resp.error.unwrap().code, -32602); 211 | } 212 | 213 | #[test] 214 | fn test_internal_error() { 215 | let resp = result_to_response(Err(standard_error(InternalError, None)), From::from(-1)); 216 | assert!(resp.result.is_none()); 217 | assert!(resp.error.is_some()); 218 | assert_eq!(resp.id, serde_json::Value::from(-1)); 219 | assert_eq!(resp.error.unwrap().code, -32603); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/http/minreq_http.rs: -------------------------------------------------------------------------------- 1 | //! This module implements the [`crate::client::Transport`] trait using [`minreq`] 2 | //! as the underlying HTTP transport. 3 | //! 4 | //! [minreq]: 5 | 6 | #[cfg(jsonrpc_fuzz)] 7 | use std::io::{self, Read, Write}; 8 | #[cfg(jsonrpc_fuzz)] 9 | use std::sync::Mutex; 10 | use std::time::Duration; 11 | use std::{error, fmt}; 12 | 13 | use crate::client::Transport; 14 | use crate::{Request, Response}; 15 | 16 | const DEFAULT_URL: &str = "http://localhost"; 17 | const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. 18 | #[cfg(not(jsonrpc_fuzz))] 19 | const DEFAULT_TIMEOUT_SECONDS: u64 = 15; 20 | #[cfg(jsonrpc_fuzz)] 21 | const DEFAULT_TIMEOUT_SECONDS: u64 = 1; 22 | 23 | /// An HTTP transport that uses [`minreq`] and is useful for running a bitcoind RPC client. 24 | #[derive(Clone, Debug)] 25 | pub struct MinreqHttpTransport { 26 | /// URL of the RPC server. 27 | url: String, 28 | /// timeout only supports second granularity. 29 | timeout: Duration, 30 | /// The value of the `Authorization` HTTP header, i.e., a base64 encoding of 'user:password'. 31 | basic_auth: Option, 32 | } 33 | 34 | impl Default for MinreqHttpTransport { 35 | fn default() -> Self { 36 | MinreqHttpTransport { 37 | url: format!("{}:{}", DEFAULT_URL, DEFAULT_PORT), 38 | timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECONDS), 39 | basic_auth: None, 40 | } 41 | } 42 | } 43 | 44 | impl MinreqHttpTransport { 45 | /// Constructs a new [`MinreqHttpTransport`] with default parameters. 46 | pub fn new() -> Self { MinreqHttpTransport::default() } 47 | 48 | /// Returns a builder for [`MinreqHttpTransport`]. 49 | pub fn builder() -> Builder { Builder::new() } 50 | 51 | fn request(&self, req: impl serde::Serialize) -> Result 52 | where 53 | R: for<'a> serde::de::Deserialize<'a>, 54 | { 55 | let req = match &self.basic_auth { 56 | Some(auth) => minreq::Request::new(minreq::Method::Post, &self.url) 57 | .with_timeout(self.timeout.as_secs()) 58 | .with_header("Authorization", auth) 59 | .with_json(&req)?, 60 | None => minreq::Request::new(minreq::Method::Post, &self.url) 61 | .with_timeout(self.timeout.as_secs()) 62 | .with_json(&req)?, 63 | }; 64 | 65 | // Send the request and parse the response. If the response is an error that does not 66 | // contain valid JSON in its body (for instance if the bitcoind HTTP server work queue 67 | // depth is exceeded), return the raw HTTP error so users can match against it. 68 | let resp = req.send()?; 69 | match resp.json() { 70 | Ok(json) => Ok(json), 71 | Err(minreq_err) => 72 | if resp.status_code != 200 { 73 | Err(Error::Http(HttpError { 74 | status_code: resp.status_code, 75 | body: resp.as_str().unwrap_or("").to_string(), 76 | })) 77 | } else { 78 | Err(Error::Minreq(minreq_err)) 79 | }, 80 | } 81 | } 82 | } 83 | 84 | impl Transport for MinreqHttpTransport { 85 | fn send_request(&self, req: Request) -> Result { 86 | Ok(self.request(req)?) 87 | } 88 | 89 | fn send_batch(&self, reqs: &[Request]) -> Result, crate::Error> { 90 | Ok(self.request(reqs)?) 91 | } 92 | 93 | fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.url) } 94 | } 95 | 96 | /// Builder for simple bitcoind [`MinreqHttpTransport`]. 97 | #[derive(Clone, Debug)] 98 | pub struct Builder { 99 | tp: MinreqHttpTransport, 100 | } 101 | 102 | impl Builder { 103 | /// Constructs a new [`Builder`] with default configuration and the URL to use. 104 | pub fn new() -> Builder { Builder { tp: MinreqHttpTransport::new() } } 105 | 106 | /// Sets the timeout after which requests will abort if they aren't finished. 107 | pub fn timeout(mut self, timeout: Duration) -> Self { 108 | self.tp.timeout = timeout; 109 | self 110 | } 111 | 112 | /// Sets the URL of the server to the transport. 113 | #[allow(clippy::assigning_clones)] // clone_into is only available in Rust 1.63 114 | pub fn url(mut self, url: &str) -> Result { 115 | self.tp.url = url.to_owned(); 116 | Ok(self) 117 | } 118 | 119 | /// Adds authentication information to the transport. 120 | pub fn basic_auth(mut self, user: String, pass: Option) -> Self { 121 | let mut s = user; 122 | s.push(':'); 123 | if let Some(ref pass) = pass { 124 | s.push_str(pass.as_ref()); 125 | } 126 | self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(s.as_bytes()))); 127 | self 128 | } 129 | 130 | /// Adds authentication information to the transport using a cookie string ('user:pass'). 131 | /// 132 | /// Does no checking on the format of the cookie string, just base64 encodes whatever is passed in. 133 | /// 134 | /// # Examples 135 | /// 136 | /// ```no_run 137 | /// # use jsonrpc::minreq_http::MinreqHttpTransport; 138 | /// # use std::fs::{self, File}; 139 | /// # use std::path::Path; 140 | /// # let cookie_file = Path::new("~/.bitcoind/.cookie"); 141 | /// let mut file = File::open(cookie_file).expect("couldn't open cookie file"); 142 | /// let mut cookie = String::new(); 143 | /// fs::read_to_string(&mut cookie).expect("couldn't read cookie file"); 144 | /// let client = MinreqHttpTransport::builder().cookie_auth(cookie); 145 | /// ``` 146 | pub fn cookie_auth>(mut self, cookie: S) -> Self { 147 | self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(cookie.as_ref().as_bytes()))); 148 | self 149 | } 150 | 151 | /// Builds the final [`MinreqHttpTransport`]. 152 | pub fn build(self) -> MinreqHttpTransport { self.tp } 153 | } 154 | 155 | impl Default for Builder { 156 | fn default() -> Self { Builder::new() } 157 | } 158 | 159 | /// An HTTP error. 160 | #[derive(Debug)] 161 | pub struct HttpError { 162 | /// Status code of the error response. 163 | pub status_code: i32, 164 | /// Raw body of the error response. 165 | pub body: String, 166 | } 167 | 168 | impl fmt::Display for HttpError { 169 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 170 | write!(f, "status: {}, body: {}", self.status_code, self.body) 171 | } 172 | } 173 | 174 | impl error::Error for HttpError {} 175 | 176 | /// Error that can happen when sending requests. In case of error, a JSON error is returned if the 177 | /// body of the response could be parsed as such. Otherwise, an HTTP error is returned containing 178 | /// the status code and the raw body. 179 | #[non_exhaustive] 180 | #[derive(Debug)] 181 | pub enum Error { 182 | /// JSON parsing error. 183 | Json(serde_json::Error), 184 | /// Minreq error. 185 | Minreq(minreq::Error), 186 | /// HTTP error that does not contain valid JSON as body. 187 | Http(HttpError), 188 | } 189 | 190 | impl fmt::Display for Error { 191 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 192 | match *self { 193 | Error::Json(ref e) => write!(f, "parsing JSON failed: {}", e), 194 | Error::Minreq(ref e) => write!(f, "minreq: {}", e), 195 | Error::Http(ref e) => write!(f, "http ({})", e), 196 | } 197 | } 198 | } 199 | 200 | impl error::Error for Error { 201 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 202 | use self::Error::*; 203 | 204 | match *self { 205 | Json(ref e) => Some(e), 206 | Minreq(ref e) => Some(e), 207 | Http(ref e) => Some(e), 208 | } 209 | } 210 | } 211 | 212 | impl From for Error { 213 | fn from(e: serde_json::Error) -> Self { Error::Json(e) } 214 | } 215 | 216 | impl From for Error { 217 | fn from(e: minreq::Error) -> Self { Error::Minreq(e) } 218 | } 219 | 220 | impl From for crate::Error { 221 | fn from(e: Error) -> crate::Error { 222 | match e { 223 | Error::Json(e) => crate::Error::Json(e), 224 | e => crate::Error::Transport(Box::new(e)), 225 | } 226 | } 227 | } 228 | 229 | /// Global mutex used by the fuzzing harness to inject data into the read end of the TCP stream. 230 | #[cfg(jsonrpc_fuzz)] 231 | pub static FUZZ_TCP_SOCK: Mutex>>> = Mutex::new(None); 232 | 233 | #[cfg(jsonrpc_fuzz)] 234 | #[derive(Clone, Debug)] 235 | struct TcpStream; 236 | 237 | #[cfg(jsonrpc_fuzz)] 238 | mod impls { 239 | use super::*; 240 | 241 | impl Read for TcpStream { 242 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 243 | match *FUZZ_TCP_SOCK.lock().unwrap() { 244 | Some(ref mut cursor) => io::Read::read(cursor, buf), 245 | None => Ok(0), 246 | } 247 | } 248 | } 249 | impl Write for TcpStream { 250 | fn write(&mut self, buf: &[u8]) -> io::Result { io::sink().write(buf) } 251 | fn flush(&mut self) -> io::Result<()> { Ok(()) } 252 | } 253 | } 254 | 255 | #[cfg(test)] 256 | mod tests { 257 | use super::*; 258 | use crate::Client; 259 | 260 | #[test] 261 | fn construct() { 262 | let tp = Builder::new() 263 | .timeout(Duration::from_millis(100)) 264 | .url("http://localhost:22") 265 | .unwrap() 266 | .basic_auth("user".to_string(), None) 267 | .build(); 268 | let _ = Client::with_transport(tp); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP transport modules. 2 | 3 | #[cfg(feature = "simple_http")] 4 | pub mod simple_http; 5 | 6 | #[cfg(feature = "minreq_http")] 7 | pub mod minreq_http; 8 | 9 | /// The default TCP port to use for connections. 10 | /// Set to 8332, the default RPC port for bitcoind. 11 | pub const DEFAULT_PORT: u16 = 8332; 12 | 13 | /// The Default SOCKS5 Port to use for proxy connection. 14 | /// Set to 9050, the default RPC port for tor. 15 | // Currently only used by `simple_http` module, here for consistency. 16 | #[cfg(feature = "proxy")] 17 | pub const DEFAULT_PROXY_PORT: u16 = 9050; 18 | -------------------------------------------------------------------------------- /src/http/simple_http.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! This module implements a minimal and non standard conforming HTTP 1.0 4 | //! round-tripper that works with the bitcoind RPC server. This can be used 5 | //! if minimal dependencies are a goal and synchronous communication is ok. 6 | 7 | use std::io::{BufRead, BufReader, Read, Write}; 8 | #[cfg(not(jsonrpc_fuzz))] 9 | use std::net::TcpStream; 10 | use std::net::{SocketAddr, ToSocketAddrs}; 11 | use std::sync::{Arc, Mutex, MutexGuard}; 12 | use std::time::Duration; 13 | use std::{error, fmt, io, net, num}; 14 | 15 | #[cfg(feature = "proxy")] 16 | use socks::Socks5Stream; 17 | 18 | use crate::client::Transport; 19 | use crate::http::DEFAULT_PORT; 20 | #[cfg(feature = "proxy")] 21 | use crate::http::DEFAULT_PROXY_PORT; 22 | use crate::{Request, Response}; 23 | 24 | /// Absolute maximum content length allowed before cutting off the response. 25 | const FINAL_RESP_ALLOC: u64 = 1024 * 1024 * 1024; 26 | 27 | #[cfg(not(jsonrpc_fuzz))] 28 | const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15); 29 | 30 | #[cfg(jsonrpc_fuzz)] 31 | const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1); 32 | 33 | /// Simple HTTP transport that implements the necessary subset of HTTP for 34 | /// running a bitcoind RPC client. 35 | #[derive(Clone, Debug)] 36 | pub struct SimpleHttpTransport { 37 | addr: net::SocketAddr, 38 | path: String, 39 | timeout: Duration, 40 | /// The value of the `Authorization` HTTP header. 41 | basic_auth: Option, 42 | #[cfg(feature = "proxy")] 43 | proxy_addr: net::SocketAddr, 44 | #[cfg(feature = "proxy")] 45 | proxy_auth: Option<(String, String)>, 46 | sock: Arc>>>, 47 | } 48 | 49 | impl Default for SimpleHttpTransport { 50 | fn default() -> Self { 51 | SimpleHttpTransport { 52 | addr: net::SocketAddr::new( 53 | net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 54 | DEFAULT_PORT, 55 | ), 56 | path: "/".to_owned(), 57 | timeout: DEFAULT_TIMEOUT, 58 | basic_auth: None, 59 | #[cfg(feature = "proxy")] 60 | proxy_addr: net::SocketAddr::new( 61 | net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 62 | DEFAULT_PROXY_PORT, 63 | ), 64 | #[cfg(feature = "proxy")] 65 | proxy_auth: None, 66 | sock: Arc::new(Mutex::new(None)), 67 | } 68 | } 69 | } 70 | 71 | impl SimpleHttpTransport { 72 | /// Constructs a new [`SimpleHttpTransport`] with default parameters. 73 | pub fn new() -> Self { SimpleHttpTransport::default() } 74 | 75 | /// Returns a builder for [`SimpleHttpTransport`]. 76 | pub fn builder() -> Builder { Builder::new() } 77 | 78 | /// Replaces the URL of the transport. 79 | pub fn set_url(&mut self, url: &str) -> Result<(), Error> { 80 | let url = check_url(url)?; 81 | self.addr = url.0; 82 | self.path = url.1; 83 | Ok(()) 84 | } 85 | 86 | /// Replaces only the path part of the URL. 87 | pub fn set_url_path(&mut self, path: String) { self.path = path; } 88 | 89 | fn request(&self, req: impl serde::Serialize) -> Result 90 | where 91 | R: for<'a> serde::de::Deserialize<'a>, 92 | { 93 | match self.try_request(req) { 94 | Ok(response) => Ok(response), 95 | Err(err) => { 96 | // No part of this codebase should panic, so unwrapping a mutex lock is fine 97 | *self.sock.lock().expect("poisoned mutex") = None; 98 | Err(err) 99 | } 100 | } 101 | } 102 | 103 | #[cfg(feature = "proxy")] 104 | fn fresh_socket(&self) -> Result { 105 | let stream = if let Some((username, password)) = &self.proxy_auth { 106 | Socks5Stream::connect_with_password( 107 | self.proxy_addr, 108 | self.addr, 109 | username.as_str(), 110 | password.as_str(), 111 | )? 112 | } else { 113 | Socks5Stream::connect(self.proxy_addr, self.addr)? 114 | }; 115 | Ok(stream.into_inner()) 116 | } 117 | 118 | #[cfg(not(feature = "proxy"))] 119 | fn fresh_socket(&self) -> Result { 120 | let stream = TcpStream::connect_timeout(&self.addr, self.timeout)?; 121 | stream.set_read_timeout(Some(self.timeout))?; 122 | stream.set_write_timeout(Some(self.timeout))?; 123 | Ok(stream) 124 | } 125 | 126 | fn try_request(&self, req: impl serde::Serialize) -> Result 127 | where 128 | R: for<'a> serde::de::Deserialize<'a>, 129 | { 130 | // No part of this codebase should panic, so unwrapping a mutex lock is fine 131 | let mut sock_lock: MutexGuard> = self.sock.lock().expect("poisoned mutex"); 132 | if sock_lock.is_none() { 133 | *sock_lock = Some(BufReader::new(self.fresh_socket()?)); 134 | }; 135 | // In the immediately preceding block, we made sure that `sock` is non-`None`, 136 | // so unwrapping here is fine. 137 | let sock: &mut BufReader<_> = sock_lock.as_mut().unwrap(); 138 | 139 | // Serialize the body first so we can set the Content-Length header. 140 | let body = serde_json::to_vec(&req)?; 141 | 142 | let mut request_bytes = Vec::new(); 143 | 144 | request_bytes.write_all(b"POST ")?; 145 | request_bytes.write_all(self.path.as_bytes())?; 146 | request_bytes.write_all(b" HTTP/1.1\r\n")?; 147 | // Write headers 148 | request_bytes.write_all(b"host: ")?; 149 | request_bytes.write_all(self.addr.to_string().as_bytes())?; 150 | request_bytes.write_all(b"\r\n")?; 151 | request_bytes.write_all(b"Content-Type: application/json\r\n")?; 152 | request_bytes.write_all(b"Content-Length: ")?; 153 | request_bytes.write_all(body.len().to_string().as_bytes())?; 154 | request_bytes.write_all(b"\r\n")?; 155 | if let Some(ref auth) = self.basic_auth { 156 | request_bytes.write_all(b"Authorization: ")?; 157 | request_bytes.write_all(auth.as_ref())?; 158 | request_bytes.write_all(b"\r\n")?; 159 | } 160 | // Write body 161 | request_bytes.write_all(b"\r\n")?; 162 | request_bytes.write_all(&body)?; 163 | 164 | // Send HTTP request 165 | let write_success = sock.get_mut().write_all(request_bytes.as_slice()).is_ok() 166 | && sock.get_mut().flush().is_ok(); 167 | 168 | // This indicates the socket is broken so let's retry the send once with a fresh socket 169 | if !write_success { 170 | *sock.get_mut() = self.fresh_socket()?; 171 | sock.get_mut().write_all(request_bytes.as_slice())?; 172 | sock.get_mut().flush()?; 173 | } 174 | 175 | // Parse first HTTP response header line 176 | let mut header_buf = String::new(); 177 | let read_success = sock.read_line(&mut header_buf).is_ok(); 178 | 179 | // This is another possible indication that the socket is broken so let's retry the send once 180 | // with a fresh socket IF the write attempt has not already experienced a failure 181 | if (!read_success || header_buf.is_empty()) && write_success { 182 | *sock.get_mut() = self.fresh_socket()?; 183 | sock.get_mut().write_all(request_bytes.as_slice())?; 184 | sock.get_mut().flush()?; 185 | 186 | sock.read_line(&mut header_buf)?; 187 | } 188 | 189 | if header_buf.len() < 12 { 190 | return Err(Error::HttpResponseTooShort { actual: header_buf.len(), needed: 12 }); 191 | } 192 | if !header_buf.as_bytes()[..12].is_ascii() { 193 | return Err(Error::HttpResponseNonAsciiHello(header_buf.as_bytes()[..12].to_vec())); 194 | } 195 | if !header_buf.starts_with("HTTP/1.1 ") { 196 | return Err(Error::HttpResponseBadHello { 197 | actual: header_buf[0..9].into(), 198 | expected: "HTTP/1.1 ".into(), 199 | }); 200 | } 201 | let response_code = match header_buf[9..12].parse::() { 202 | Ok(n) => n, 203 | Err(e) => return Err(Error::HttpResponseBadStatus(header_buf[9..12].into(), e)), 204 | }; 205 | 206 | // Parse response header fields 207 | let mut content_length = None; 208 | loop { 209 | header_buf.clear(); 210 | sock.read_line(&mut header_buf)?; 211 | if header_buf == "\r\n" { 212 | break; 213 | } 214 | header_buf.make_ascii_lowercase(); 215 | 216 | const CONTENT_LENGTH: &str = "content-length: "; 217 | if let Some(s) = header_buf.strip_prefix(CONTENT_LENGTH) { 218 | content_length = Some( 219 | s.trim() 220 | .parse::() 221 | .map_err(|e| Error::HttpResponseBadContentLength(s.into(), e))?, 222 | ); 223 | } 224 | 225 | const TRANSFER_ENCODING: &str = "transfer-encoding: "; 226 | if let Some(s) = header_buf.strip_prefix(TRANSFER_ENCODING) { 227 | const CHUNKED: &str = "chunked"; 228 | if s.trim() == CHUNKED { 229 | return Err(Error::HttpResponseChunked); 230 | } 231 | } 232 | } 233 | 234 | if response_code == 401 { 235 | // There is no body in a 401 response, so don't try to read it 236 | return Err(Error::HttpErrorCode(response_code)); 237 | } 238 | 239 | // Read up to `content_length` bytes. Note that if there is no content-length 240 | // header, we will assume an effectively infinite content length, i.e. we will 241 | // just keep reading from the socket until it is closed. 242 | let mut reader = match content_length { 243 | None => sock.take(FINAL_RESP_ALLOC), 244 | Some(n) if n > FINAL_RESP_ALLOC => { 245 | return Err(Error::HttpResponseContentLengthTooLarge { 246 | length: n, 247 | max: FINAL_RESP_ALLOC, 248 | }); 249 | } 250 | Some(n) => sock.take(n), 251 | }; 252 | 253 | // Attempt to parse the response. Don't check the HTTP error code until 254 | // after parsing, since Bitcoin Core will often return a descriptive JSON 255 | // error structure which is more useful than the error code. 256 | match serde_json::from_reader(&mut reader) { 257 | Ok(s) => { 258 | if content_length.is_some() { 259 | reader.bytes().count(); // consume any trailing bytes 260 | } 261 | Ok(s) 262 | } 263 | Err(e) => { 264 | // If the response was not 200, assume the parse failed because of that 265 | if response_code != 200 { 266 | Err(Error::HttpErrorCode(response_code)) 267 | } else { 268 | // If it was 200 then probably it was legitimately a parse error 269 | Err(e.into()) 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | /// Does some very basic manual URL parsing because the uri/url crates 277 | /// all have unicode-normalization as a dependency and that's broken. 278 | fn check_url(url: &str) -> Result<(SocketAddr, String), Error> { 279 | // The fallback port in case no port was provided. 280 | // This changes when the http or https scheme was provided. 281 | let mut fallback_port = DEFAULT_PORT; 282 | 283 | // We need to get the hostname and the port. 284 | // (1) Split scheme 285 | let after_scheme = { 286 | let mut split = url.splitn(2, "://"); 287 | let s = split.next().unwrap(); 288 | match split.next() { 289 | None => s, // no scheme present 290 | Some(after) => { 291 | // Check if the scheme is http or https. 292 | if s == "http" { 293 | fallback_port = 80; 294 | } else if s == "https" { 295 | fallback_port = 443; 296 | } else { 297 | return Err(Error::url(url, "scheme should be http or https")); 298 | } 299 | after 300 | } 301 | } 302 | }; 303 | // (2) split off path 304 | let (before_path, path) = { 305 | if let Some(slash) = after_scheme.find('/') { 306 | (&after_scheme[0..slash], &after_scheme[slash..]) 307 | } else { 308 | (after_scheme, "/") 309 | } 310 | }; 311 | // (3) split off auth part 312 | let after_auth = { 313 | let mut split = before_path.splitn(2, '@'); 314 | let s = split.next().unwrap(); 315 | split.next().unwrap_or(s) 316 | }; 317 | 318 | // (4) Parse into socket address. 319 | // At this point we either have or : 320 | // `std::net::ToSocketAddrs` requires `&str` to have : format. 321 | let mut addr = match after_auth.to_socket_addrs() { 322 | Ok(addr) => addr, 323 | Err(_) => { 324 | // Invalid socket address. Try to add port. 325 | format!("{}:{}", after_auth, fallback_port).to_socket_addrs()? 326 | } 327 | }; 328 | 329 | match addr.next() { 330 | Some(a) => Ok((a, path.to_owned())), 331 | None => Err(Error::url(url, "invalid hostname: error extracting socket address")), 332 | } 333 | } 334 | 335 | impl Transport for SimpleHttpTransport { 336 | fn send_request(&self, req: Request) -> Result { 337 | Ok(self.request(req)?) 338 | } 339 | 340 | fn send_batch(&self, reqs: &[Request]) -> Result, crate::Error> { 341 | Ok(self.request(reqs)?) 342 | } 343 | 344 | fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { 345 | write!(f, "http://{}:{}{}", self.addr.ip(), self.addr.port(), self.path) 346 | } 347 | } 348 | 349 | /// Builder for simple bitcoind [`SimpleHttpTransport`]. 350 | #[derive(Clone, Debug)] 351 | pub struct Builder { 352 | tp: SimpleHttpTransport, 353 | } 354 | 355 | impl Builder { 356 | /// Constructs a new [`Builder`] with default configuration. 357 | pub fn new() -> Builder { Builder { tp: SimpleHttpTransport::new() } } 358 | 359 | /// Sets the timeout after which requests will abort if they aren't finished. 360 | pub fn timeout(mut self, timeout: Duration) -> Self { 361 | self.tp.timeout = timeout; 362 | self 363 | } 364 | 365 | /// Sets the URL of the server to the transport. 366 | pub fn url(mut self, url: &str) -> Result { 367 | self.tp.set_url(url)?; 368 | Ok(self) 369 | } 370 | 371 | /// Adds authentication information to the transport. 372 | pub fn auth>(mut self, user: S, pass: Option) -> Self { 373 | let mut auth = user.as_ref().to_owned(); 374 | auth.push(':'); 375 | if let Some(ref pass) = pass { 376 | auth.push_str(pass.as_ref()); 377 | } 378 | self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(auth.as_bytes()))); 379 | self 380 | } 381 | 382 | /// Adds authentication information to the transport using a cookie string ('user:pass'). 383 | pub fn cookie_auth>(mut self, cookie: S) -> Self { 384 | self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(cookie.as_ref().as_bytes()))); 385 | self 386 | } 387 | 388 | /// Adds proxy address to the transport for SOCKS5 proxy. 389 | #[cfg(feature = "proxy")] 390 | pub fn proxy_addr>(mut self, proxy_addr: S) -> Result { 391 | // We don't expect path in proxy address. 392 | self.tp.proxy_addr = check_url(proxy_addr.as_ref())?.0; 393 | Ok(self) 394 | } 395 | 396 | /// Adds optional proxy authentication as ('username', 'password'). 397 | #[cfg(feature = "proxy")] 398 | pub fn proxy_auth>(mut self, user: S, pass: S) -> Self { 399 | self.tp.proxy_auth = 400 | Some((user, pass)).map(|(u, p)| (u.as_ref().to_string(), p.as_ref().to_string())); 401 | self 402 | } 403 | 404 | /// Builds the final [`SimpleHttpTransport`]. 405 | pub fn build(self) -> SimpleHttpTransport { self.tp } 406 | } 407 | 408 | impl Default for Builder { 409 | fn default() -> Self { Builder::new() } 410 | } 411 | 412 | impl crate::Client { 413 | /// Creates a new JSON-RPC client using a bare-minimum HTTP transport. 414 | pub fn simple_http( 415 | url: &str, 416 | user: Option, 417 | pass: Option, 418 | ) -> Result { 419 | let mut builder = Builder::new().url(url)?; 420 | if let Some(user) = user { 421 | builder = builder.auth(user, pass); 422 | } 423 | Ok(crate::Client::with_transport(builder.build())) 424 | } 425 | 426 | /// Creates a new JSON_RPC client using a HTTP-Socks5 proxy transport. 427 | #[cfg(feature = "proxy")] 428 | pub fn http_proxy( 429 | url: &str, 430 | user: Option, 431 | pass: Option, 432 | proxy_addr: &str, 433 | proxy_auth: Option<(&str, &str)>, 434 | ) -> Result { 435 | let mut builder = Builder::new().url(url)?; 436 | if let Some(user) = user { 437 | builder = builder.auth(user, pass); 438 | } 439 | builder = builder.proxy_addr(proxy_addr)?; 440 | if let Some((user, pass)) = proxy_auth { 441 | builder = builder.proxy_auth(user, pass); 442 | } 443 | let tp = builder.build(); 444 | Ok(crate::Client::with_transport(tp)) 445 | } 446 | } 447 | 448 | /// Error that can happen when sending requests. 449 | #[derive(Debug)] 450 | pub enum Error { 451 | /// An invalid URL was passed. 452 | InvalidUrl { 453 | /// The URL passed. 454 | url: String, 455 | /// The reason the URL is invalid. 456 | reason: &'static str, 457 | }, 458 | /// An error occurred on the socket layer. 459 | SocketError(io::Error), 460 | /// The HTTP response was too short to even fit a HTTP 1.1 header. 461 | HttpResponseTooShort { 462 | /// The total length of the response. 463 | actual: usize, 464 | /// Minimum length we can parse. 465 | needed: usize, 466 | }, 467 | /// The HTTP response started with a HTTP/1.1 line which was not ASCII. 468 | HttpResponseNonAsciiHello(Vec), 469 | /// The HTTP response did not start with HTTP/1.1 470 | HttpResponseBadHello { 471 | /// Actual HTTP-whatever string. 472 | actual: String, 473 | /// The hello string of the HTTP version we support. 474 | expected: String, 475 | }, 476 | /// Could not parse the status value as a number. 477 | HttpResponseBadStatus(String, num::ParseIntError), 478 | /// Could not parse the status value as a number. 479 | HttpResponseBadContentLength(String, num::ParseIntError), 480 | /// The indicated content-length header exceeded our maximum. 481 | HttpResponseContentLengthTooLarge { 482 | /// The length indicated in the content-length header. 483 | length: u64, 484 | /// Our hard maximum on number of bytes we'll try to read. 485 | max: u64, 486 | }, 487 | /// The server is replying with chunked encoding which is not supported 488 | HttpResponseChunked, 489 | /// Unexpected HTTP error code (non-200). 490 | HttpErrorCode(u16), 491 | /// Received EOF before getting as many bytes as were indicated by the content-length header. 492 | IncompleteResponse { 493 | /// The content-length header. 494 | content_length: u64, 495 | /// The number of bytes we actually read. 496 | n_read: u64, 497 | }, 498 | /// JSON parsing error. 499 | Json(serde_json::Error), 500 | } 501 | 502 | impl Error { 503 | /// Utility method to create [`Error::InvalidUrl`] variants. 504 | fn url>(url: U, reason: &'static str) -> Error { 505 | Error::InvalidUrl { url: url.into(), reason } 506 | } 507 | } 508 | 509 | impl fmt::Display for Error { 510 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 511 | use Error::*; 512 | 513 | match *self { 514 | InvalidUrl { ref url, ref reason } => write!(f, "invalid URL '{}': {}", url, reason), 515 | SocketError(ref e) => write!(f, "Couldn't connect to host: {}", e), 516 | HttpResponseTooShort { ref actual, ref needed } => { 517 | write!(f, "HTTP response too short: length {}, needed {}.", actual, needed) 518 | } 519 | HttpResponseNonAsciiHello(ref bytes) => { 520 | write!(f, "HTTP response started with non-ASCII {:?}", bytes) 521 | } 522 | HttpResponseBadHello { ref actual, ref expected } => { 523 | write!(f, "HTTP response started with `{}`; expected `{}`.", actual, expected) 524 | } 525 | HttpResponseBadStatus(ref status, ref err) => { 526 | write!(f, "HTTP response had bad status code `{}`: {}.", status, err) 527 | } 528 | HttpResponseBadContentLength(ref len, ref err) => { 529 | write!(f, "HTTP response had bad content length `{}`: {}.", len, err) 530 | } 531 | HttpResponseContentLengthTooLarge { length, max } => { 532 | write!(f, "HTTP response content length {} exceeds our max {}.", length, max) 533 | } 534 | HttpErrorCode(c) => write!(f, "unexpected HTTP code: {}", c), 535 | IncompleteResponse { content_length, n_read } => { 536 | write!( 537 | f, 538 | "read {} bytes but HTTP response content-length header was {}.", 539 | n_read, content_length 540 | ) 541 | } 542 | Json(ref e) => write!(f, "JSON error: {}", e), 543 | HttpResponseChunked => { 544 | write!(f, "The server replied with a chunked response which is not supported") 545 | } 546 | } 547 | } 548 | } 549 | 550 | impl error::Error for Error { 551 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 552 | use self::Error::*; 553 | 554 | match *self { 555 | InvalidUrl { .. } 556 | | HttpResponseTooShort { .. } 557 | | HttpResponseNonAsciiHello(..) 558 | | HttpResponseBadHello { .. } 559 | | HttpResponseBadStatus(..) 560 | | HttpResponseBadContentLength(..) 561 | | HttpResponseContentLengthTooLarge { .. } 562 | | HttpErrorCode(_) 563 | | IncompleteResponse { .. } 564 | | HttpResponseChunked => None, 565 | SocketError(ref e) => Some(e), 566 | Json(ref e) => Some(e), 567 | } 568 | } 569 | } 570 | 571 | impl From for Error { 572 | fn from(e: io::Error) -> Self { Error::SocketError(e) } 573 | } 574 | 575 | impl From for Error { 576 | fn from(e: serde_json::Error) -> Self { Error::Json(e) } 577 | } 578 | 579 | impl From for crate::Error { 580 | fn from(e: Error) -> crate::Error { 581 | match e { 582 | Error::Json(e) => crate::Error::Json(e), 583 | e => crate::Error::Transport(Box::new(e)), 584 | } 585 | } 586 | } 587 | 588 | /// Global mutex used by the fuzzing harness to inject data into the read end of the TCP stream. 589 | #[cfg(jsonrpc_fuzz)] 590 | pub static FUZZ_TCP_SOCK: Mutex>>> = Mutex::new(None); 591 | 592 | #[cfg(jsonrpc_fuzz)] 593 | #[derive(Clone, Debug)] 594 | struct TcpStream; 595 | 596 | #[cfg(jsonrpc_fuzz)] 597 | mod impls { 598 | use super::*; 599 | impl Read for TcpStream { 600 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 601 | match *FUZZ_TCP_SOCK.lock().unwrap() { 602 | Some(ref mut cursor) => io::Read::read(cursor, buf), 603 | None => Ok(0), 604 | } 605 | } 606 | } 607 | impl Write for TcpStream { 608 | fn write(&mut self, buf: &[u8]) -> io::Result { io::sink().write(buf) } 609 | fn flush(&mut self) -> io::Result<()> { Ok(()) } 610 | } 611 | 612 | impl TcpStream { 613 | pub fn connect_timeout(_: &SocketAddr, _: Duration) -> io::Result { Ok(TcpStream) } 614 | pub fn set_read_timeout(&self, _: Option) -> io::Result<()> { Ok(()) } 615 | pub fn set_write_timeout(&self, _: Option) -> io::Result<()> { Ok(()) } 616 | } 617 | } 618 | 619 | #[cfg(test)] 620 | mod tests { 621 | use std::net; 622 | #[cfg(feature = "proxy")] 623 | use std::str::FromStr; 624 | 625 | use super::*; 626 | use crate::Client; 627 | 628 | #[test] 629 | fn test_urls() { 630 | let addr: net::SocketAddr = ("localhost", 22).to_socket_addrs().unwrap().next().unwrap(); 631 | let urls = [ 632 | "localhost:22", 633 | "http://localhost:22/", 634 | "https://localhost:22/walletname/stuff?it=working", 635 | "http://me:weak@localhost:22/wallet", 636 | ]; 637 | for u in &urls { 638 | let tp = Builder::new().url(u).unwrap().build(); 639 | assert_eq!(tp.addr, addr); 640 | } 641 | 642 | // Default port and 80 and 443 fill-in. 643 | let addr: net::SocketAddr = ("localhost", 80).to_socket_addrs().unwrap().next().unwrap(); 644 | let tp = Builder::new().url("http://localhost/").unwrap().build(); 645 | assert_eq!(tp.addr, addr); 646 | let addr: net::SocketAddr = ("localhost", 443).to_socket_addrs().unwrap().next().unwrap(); 647 | let tp = Builder::new().url("https://localhost/").unwrap().build(); 648 | assert_eq!(tp.addr, addr); 649 | let addr: net::SocketAddr = 650 | ("localhost", super::DEFAULT_PORT).to_socket_addrs().unwrap().next().unwrap(); 651 | let tp = Builder::new().url("localhost").unwrap().build(); 652 | assert_eq!(tp.addr, addr); 653 | 654 | let valid_urls = [ 655 | "localhost", 656 | "127.0.0.1:8080", 657 | "http://127.0.0.1:8080/", 658 | "http://127.0.0.1:8080/rpc/test", 659 | "https://127.0.0.1/rpc/test", 660 | "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8300", 661 | "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", 662 | ]; 663 | for u in &valid_urls { 664 | let (addr, path) = check_url(u).unwrap(); 665 | let builder = Builder::new().url(u).unwrap_or_else(|_| panic!("error for: {}", u)); 666 | assert_eq!(builder.tp.addr, addr); 667 | assert_eq!(builder.tp.path, path); 668 | assert_eq!(builder.tp.timeout, DEFAULT_TIMEOUT); 669 | assert_eq!(builder.tp.basic_auth, None); 670 | #[cfg(feature = "proxy")] 671 | assert_eq!(builder.tp.proxy_addr, SocketAddr::from_str("127.0.0.1:9050").unwrap()); 672 | } 673 | 674 | let invalid_urls = [ 675 | "127.0.0.1.0:8080", 676 | "httpx://127.0.0.1:8080/", 677 | "ftp://127.0.0.1:8080/rpc/test", 678 | "http://127.0.0./rpc/test", 679 | // NB somehow, Rust's IpAddr accepts "127.0.0" and adds the extra 0.. 680 | ]; 681 | for u in &invalid_urls { 682 | if let Ok(b) = Builder::new().url(u) { 683 | let tp = b.build(); 684 | panic!("expected error for url {}, got {:?}", u, tp); 685 | } 686 | } 687 | } 688 | 689 | #[test] 690 | fn construct() { 691 | let tp = Builder::new() 692 | .timeout(Duration::from_millis(100)) 693 | .url("localhost:22") 694 | .unwrap() 695 | .auth("user", None) 696 | .build(); 697 | let _ = Client::with_transport(tp); 698 | 699 | let _ = Client::simple_http("localhost:22", None, None).unwrap(); 700 | } 701 | 702 | #[cfg(feature = "proxy")] 703 | #[test] 704 | fn construct_with_proxy() { 705 | let tp = Builder::new() 706 | .timeout(Duration::from_millis(100)) 707 | .url("localhost:22") 708 | .unwrap() 709 | .auth("user", None) 710 | .proxy_addr("127.0.0.1:9050") 711 | .unwrap() 712 | .build(); 713 | let _ = Client::with_transport(tp); 714 | 715 | let _ = Client::http_proxy( 716 | "localhost:22", 717 | None, 718 | None, 719 | "127.0.0.1:9050", 720 | Some(("user", "password")), 721 | ) 722 | .unwrap(); 723 | } 724 | 725 | /// Test that the client will detect that a socket is closed and open a fresh one before sending 726 | /// the request 727 | #[cfg(all(not(feature = "proxy"), not(jsonrpc_fuzz)))] 728 | #[test] 729 | fn request_to_closed_socket() { 730 | use std::net::{Shutdown, TcpListener}; 731 | use std::sync::mpsc; 732 | use std::thread; 733 | 734 | use serde_json::{Number, Value}; 735 | 736 | let (tx, rx) = mpsc::sync_channel(1); 737 | 738 | thread::spawn(move || { 739 | let server = TcpListener::bind("localhost:0").expect("Binding a Tcp Listener"); 740 | tx.send(server.local_addr().unwrap().port()).unwrap(); 741 | for (request_id, stream) in server.incoming().enumerate() { 742 | let mut stream = stream.unwrap(); 743 | 744 | let buf_reader = BufReader::new(&mut stream); 745 | 746 | let _http_request: Vec<_> = buf_reader 747 | .lines() 748 | .map(|result| result.unwrap()) 749 | .take_while(|line| !line.is_empty()) 750 | .collect(); 751 | 752 | let response = Response { 753 | result: None, 754 | error: None, 755 | id: Value::Number(Number::from(request_id)), 756 | jsonrpc: Some(String::from("2.0")), 757 | }; 758 | let response_str = serde_json::to_string(&response).unwrap(); 759 | 760 | stream.write_all(b"HTTP/1.1 200\r\n").unwrap(); 761 | stream.write_all(b"Content-Length: ").unwrap(); 762 | stream.write_all(response_str.len().to_string().as_bytes()).unwrap(); 763 | stream.write_all(b"\r\n").unwrap(); 764 | stream.write_all(b"\r\n").unwrap(); 765 | stream.write_all(response_str.as_bytes()).unwrap(); 766 | stream.flush().unwrap(); 767 | 768 | stream.shutdown(Shutdown::Both).unwrap(); 769 | } 770 | }); 771 | 772 | // Give the server thread a second to start up and listen 773 | thread::sleep(Duration::from_secs(1)); 774 | 775 | let port = rx.recv().unwrap(); 776 | let client = 777 | Client::simple_http(format!("localhost:{}", port).as_str(), None, None).unwrap(); 778 | let request = client.build_request("test_request", None); 779 | let result = client.send_request(request).unwrap(); 780 | assert_eq!(result.id, Value::Number(Number::from(0))); 781 | thread::sleep(Duration::from_secs(1)); 782 | let request = client.build_request("test_request2", None); 783 | let result2 = client.send_request(request) 784 | .expect("This second request should not be an Err like `Err(Transport(HttpResponseTooShort { actual: 0, needed: 12 }))`"); 785 | assert_eq!(result2.id, Value::Number(Number::from(1))); 786 | } 787 | } 788 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! # Rust JSON-RPC Library 4 | //! 5 | //! Rust support for the JSON-RPC 2.0 protocol. 6 | 7 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 8 | // Coding conventions 9 | #![warn(missing_docs)] 10 | 11 | /// Re-export `serde` crate. 12 | pub extern crate serde; 13 | /// Re-export `serde_json` crate. 14 | pub extern crate serde_json; 15 | 16 | /// Re-export `base64` crate. 17 | #[cfg(feature = "base64")] 18 | pub extern crate base64; 19 | 20 | /// Re-export `minreq` crate if the feature is set. 21 | #[cfg(feature = "minreq")] 22 | pub extern crate minreq; 23 | 24 | pub mod client; 25 | pub mod error; 26 | pub mod http; 27 | 28 | #[cfg(feature = "minreq_http")] 29 | pub use http::minreq_http; 30 | #[cfg(feature = "simple_http")] 31 | pub use http::simple_http; 32 | 33 | #[cfg(feature = "simple_tcp")] 34 | pub mod simple_tcp; 35 | 36 | #[cfg(all(feature = "simple_uds", not(windows)))] 37 | pub mod simple_uds; 38 | 39 | use serde::{Deserialize, Serialize}; 40 | use serde_json::value::RawValue; 41 | 42 | pub use crate::client::{Client, Transport}; 43 | pub use crate::error::Error; 44 | 45 | /// Shorthand method to convert an argument into a boxed [`serde_json::value::RawValue`]. 46 | /// 47 | /// Since serializers rarely fail, it's probably easier to use [`arg`] instead. 48 | pub fn try_arg(arg: T) -> Result, serde_json::Error> { 49 | RawValue::from_string(serde_json::to_string(&arg)?) 50 | } 51 | 52 | /// Shorthand method to convert an argument into a boxed [`serde_json::value::RawValue`]. 53 | /// 54 | /// This conversion should not fail, so to avoid returning a [`Result`], 55 | /// in case of an error, the error is serialized as the return value. 56 | pub fn arg(arg: T) -> Box { 57 | match try_arg(arg) { 58 | Ok(v) => v, 59 | Err(e) => RawValue::from_string(format!("<>", e)) 60 | .unwrap_or_else(|_| { 61 | RawValue::from_string("<>".to_owned()).unwrap() 62 | }), 63 | } 64 | } 65 | 66 | /// A JSONRPC request object. 67 | #[derive(Debug, Clone, Serialize)] 68 | pub struct Request<'a> { 69 | /// The name of the RPC call. 70 | pub method: &'a str, 71 | /// Parameters to the RPC call. 72 | pub params: Option<&'a RawValue>, 73 | /// Identifier for this request, which should appear in the response. 74 | pub id: serde_json::Value, 75 | /// jsonrpc field, MUST be "2.0". 76 | pub jsonrpc: Option<&'a str>, 77 | } 78 | 79 | /// A JSONRPC response object. 80 | #[derive(Debug, Clone, Deserialize, Serialize)] 81 | pub struct Response { 82 | /// A result if there is one, or [`None`]. 83 | pub result: Option>, 84 | /// An error if there is one, or [`None`]. 85 | pub error: Option, 86 | /// Identifier for this response, which should match that of the request. 87 | pub id: serde_json::Value, 88 | /// jsonrpc field, MUST be "2.0". 89 | pub jsonrpc: Option, 90 | } 91 | 92 | impl Response { 93 | /// Extracts the result from a response. 94 | pub fn result serde::de::Deserialize<'a>>(&self) -> Result { 95 | if let Some(ref e) = self.error { 96 | return Err(Error::Rpc(e.clone())); 97 | } 98 | 99 | if let Some(ref res) = self.result { 100 | serde_json::from_str(res.get()).map_err(Error::Json) 101 | } else { 102 | serde_json::from_value(serde_json::Value::Null).map_err(Error::Json) 103 | } 104 | } 105 | 106 | /// Returns the RPC error, if there was one, but does not check the result. 107 | pub fn check_error(self) -> Result<(), Error> { 108 | if let Some(e) = self.error { 109 | Err(Error::Rpc(e)) 110 | } else { 111 | Ok(()) 112 | } 113 | } 114 | 115 | /// Returns whether or not the `result` field is empty. 116 | pub fn is_none(&self) -> bool { self.result.is_none() } 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use serde_json::json; 122 | use serde_json::value::{to_raw_value, RawValue}; 123 | 124 | use super::*; 125 | 126 | #[test] 127 | fn response_is_none() { 128 | let joanna = Response { 129 | result: Some(RawValue::from_string(serde_json::to_string(&true).unwrap()).unwrap()), 130 | error: None, 131 | id: From::from(81), 132 | jsonrpc: Some(String::from("2.0")), 133 | }; 134 | 135 | let bill = Response { 136 | result: None, 137 | error: None, 138 | id: From::from(66), 139 | jsonrpc: Some(String::from("2.0")), 140 | }; 141 | 142 | assert!(!joanna.is_none()); 143 | assert!(bill.is_none()); 144 | } 145 | 146 | #[test] 147 | fn response_extract() { 148 | let obj = vec!["Mary", "had", "a", "little", "lamb"]; 149 | let response = Response { 150 | result: Some(RawValue::from_string(serde_json::to_string(&obj).unwrap()).unwrap()), 151 | error: None, 152 | id: serde_json::Value::Null, 153 | jsonrpc: Some(String::from("2.0")), 154 | }; 155 | let recovered1: Vec = response.result().unwrap(); 156 | assert!(response.clone().check_error().is_ok()); 157 | let recovered2: Vec = response.result().unwrap(); 158 | assert_eq!(obj, recovered1); 159 | assert_eq!(obj, recovered2); 160 | } 161 | 162 | #[test] 163 | fn null_result() { 164 | let s = r#"{"result":null,"error":null,"id":"test"}"#; 165 | let response: Response = serde_json::from_str(s).unwrap(); 166 | let recovered1: Result<(), _> = response.result(); 167 | let recovered2: Result<(), _> = response.result(); 168 | assert!(recovered1.is_ok()); 169 | assert!(recovered2.is_ok()); 170 | 171 | let recovered1: Result = response.result(); 172 | let recovered2: Result = response.result(); 173 | assert!(recovered1.is_err()); 174 | assert!(recovered2.is_err()); 175 | } 176 | 177 | #[test] 178 | fn batch_response() { 179 | // from the jsonrpc.org spec example 180 | let s = r#"[ 181 | {"jsonrpc": "2.0", "result": 7, "id": "1"}, 182 | {"jsonrpc": "2.0", "result": 19, "id": "2"}, 183 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, 184 | {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}, 185 | {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} 186 | ]"#; 187 | let batch_response: Vec = serde_json::from_str(s).unwrap(); 188 | assert_eq!(batch_response.len(), 5); 189 | } 190 | 191 | #[test] 192 | fn test_arg() { 193 | macro_rules! test_arg { 194 | ($val:expr, $t:ty) => {{ 195 | let val1: $t = $val; 196 | let arg = super::arg(val1.clone()); 197 | let val2: $t = serde_json::from_str(arg.get()).expect(stringify!($val)); 198 | assert_eq!(val1, val2, "failed test for {}", stringify!($val)); 199 | }}; 200 | } 201 | 202 | test_arg!(true, bool); 203 | test_arg!(42, u8); 204 | test_arg!(42, usize); 205 | test_arg!(42, isize); 206 | test_arg!(vec![42, 35], Vec); 207 | test_arg!(String::from("test"), String); 208 | 209 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 210 | struct Test { 211 | v: String, 212 | } 213 | test_arg!(Test { v: String::from("test") }, Test); 214 | } 215 | 216 | #[test] 217 | fn test_request_list() { 218 | let list = json!([0]); 219 | let raw_value = Some(to_raw_value(&list).unwrap()); 220 | 221 | let request = Request { 222 | method: "list", 223 | params: raw_value.as_deref(), 224 | id: serde_json::json!(2), 225 | jsonrpc: Some("2.0"), 226 | }; 227 | assert_eq!( 228 | serde_json::to_string(&request).unwrap(), 229 | r#"{"method":"list","params":[0],"id":2,"jsonrpc":"2.0"}"# 230 | ); 231 | } 232 | 233 | #[test] 234 | fn test_request_object() { 235 | let object = json!({ "height": 0 }); 236 | let raw_value = Some(to_raw_value(&object).unwrap()); 237 | 238 | let request = Request { 239 | method: "object", 240 | params: raw_value.as_deref(), 241 | id: serde_json::json!(2), 242 | jsonrpc: Some("2.0"), 243 | }; 244 | assert_eq!( 245 | serde_json::to_string(&request).unwrap(), 246 | r#"{"method":"object","params":{"height":0},"id":2,"jsonrpc":"2.0"}"# 247 | ); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/simple_tcp.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! This module implements a synchronous transport over a raw [`std::net::TcpListener`]. 4 | //! Note that it does not handle TCP over Unix Domain Sockets, see `simple_uds` for this. 5 | 6 | use std::{error, fmt, io, net, time}; 7 | 8 | use crate::client::Transport; 9 | use crate::{Request, Response}; 10 | 11 | #[derive(Debug, Clone)] 12 | /// Simple synchronous TCP transport. 13 | pub struct TcpTransport { 14 | /// The internet socket address to connect to. 15 | pub addr: net::SocketAddr, 16 | /// The read and write timeout to use for this connection. 17 | pub timeout: Option, 18 | } 19 | 20 | impl TcpTransport { 21 | /// Creates a new `TcpTransport` without timeouts. 22 | pub fn new(addr: net::SocketAddr) -> TcpTransport { TcpTransport { addr, timeout: None } } 23 | 24 | fn request(&self, req: impl serde::Serialize) -> Result 25 | where 26 | R: for<'a> serde::de::Deserialize<'a>, 27 | { 28 | let mut sock = net::TcpStream::connect(self.addr)?; 29 | sock.set_read_timeout(self.timeout)?; 30 | sock.set_write_timeout(self.timeout)?; 31 | 32 | serde_json::to_writer(&mut sock, &req)?; 33 | 34 | // NOTE: we don't check the id there, so it *must* be synchronous 35 | let resp: R = serde_json::Deserializer::from_reader(&mut sock) 36 | .into_iter() 37 | .next() 38 | .ok_or(Error::Timeout)??; 39 | Ok(resp) 40 | } 41 | } 42 | 43 | impl Transport for TcpTransport { 44 | fn send_request(&self, req: Request) -> Result { 45 | Ok(self.request(req)?) 46 | } 47 | 48 | fn send_batch(&self, reqs: &[Request]) -> Result, crate::Error> { 49 | Ok(self.request(reqs)?) 50 | } 51 | 52 | fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.addr) } 53 | } 54 | 55 | /// Error that can occur while using the TCP transport. 56 | #[derive(Debug)] 57 | pub enum Error { 58 | /// An error occurred on the socket layer. 59 | SocketError(io::Error), 60 | /// We didn't receive a complete response till the deadline ran out. 61 | Timeout, 62 | /// JSON parsing error. 63 | Json(serde_json::Error), 64 | } 65 | 66 | impl fmt::Display for Error { 67 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 68 | use Error::*; 69 | 70 | match *self { 71 | SocketError(ref e) => write!(f, "couldn't connect to host: {}", e), 72 | Timeout => f.write_str("didn't receive response data in time, timed out."), 73 | Json(ref e) => write!(f, "JSON error: {}", e), 74 | } 75 | } 76 | } 77 | 78 | impl error::Error for Error { 79 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 80 | use self::Error::*; 81 | 82 | match *self { 83 | SocketError(ref e) => Some(e), 84 | Timeout => None, 85 | Json(ref e) => Some(e), 86 | } 87 | } 88 | } 89 | 90 | impl From for Error { 91 | fn from(e: io::Error) -> Self { Error::SocketError(e) } 92 | } 93 | 94 | impl From for Error { 95 | fn from(e: serde_json::Error) -> Self { Error::Json(e) } 96 | } 97 | 98 | impl From for crate::Error { 99 | fn from(e: Error) -> crate::Error { 100 | match e { 101 | Error::Json(e) => crate::Error::Json(e), 102 | e => crate::Error::Transport(Box::new(e)), 103 | } 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use std::io::{Read, Write}; 110 | use std::thread; 111 | 112 | use super::*; 113 | use crate::Client; 114 | 115 | // Test a dummy request / response over a raw TCP transport 116 | #[test] 117 | fn sanity_check_tcp_transport() { 118 | let addr: net::SocketAddr = 119 | net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into(); 120 | let server = net::TcpListener::bind(addr).unwrap(); 121 | let addr = server.local_addr().unwrap(); 122 | let dummy_req = Request { 123 | method: "arandommethod", 124 | params: None, 125 | id: serde_json::Value::Number(4242242.into()), 126 | jsonrpc: Some("2.0"), 127 | }; 128 | let dummy_req_ser = serde_json::to_vec(&dummy_req).unwrap(); 129 | let dummy_resp = Response { 130 | result: None, 131 | error: None, 132 | id: serde_json::Value::Number(4242242.into()), 133 | jsonrpc: Some("2.0".into()), 134 | }; 135 | let dummy_resp_ser = serde_json::to_vec(&dummy_resp).unwrap(); 136 | 137 | let client_thread = thread::spawn(move || { 138 | let transport = TcpTransport { addr, timeout: Some(time::Duration::from_secs(5)) }; 139 | let client = Client::with_transport(transport); 140 | 141 | client.send_request(dummy_req.clone()).unwrap() 142 | }); 143 | 144 | let (mut stream, _) = server.accept().unwrap(); 145 | stream.set_read_timeout(Some(time::Duration::from_secs(5))).unwrap(); 146 | let mut recv_req = vec![0; dummy_req_ser.len()]; 147 | let mut read = 0; 148 | while read < dummy_req_ser.len() { 149 | read += stream.read(&mut recv_req[read..]).unwrap(); 150 | } 151 | assert_eq!(recv_req, dummy_req_ser); 152 | 153 | stream.write_all(&dummy_resp_ser).unwrap(); 154 | stream.flush().unwrap(); 155 | let recv_resp = client_thread.join().unwrap(); 156 | assert_eq!(serde_json::to_vec(&recv_resp).unwrap(), dummy_resp_ser); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/simple_uds.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | 3 | //! This module implements a synchronous transport over a raw [`std::os::unix::net::UnixStream`]. 4 | 5 | use std::os::unix::net::UnixStream; 6 | use std::{error, fmt, io, path, time}; 7 | 8 | use crate::client::Transport; 9 | use crate::{Request, Response}; 10 | 11 | /// Simple synchronous UDS transport. 12 | #[derive(Debug, Clone)] 13 | pub struct UdsTransport { 14 | /// The path to the Unix Domain Socket. 15 | pub sockpath: path::PathBuf, 16 | /// The read and write timeout to use. 17 | pub timeout: Option, 18 | } 19 | 20 | impl UdsTransport { 21 | /// Creates a new [`UdsTransport`] without timeouts to use. 22 | pub fn new>(sockpath: P) -> UdsTransport { 23 | UdsTransport { sockpath: sockpath.as_ref().to_path_buf(), timeout: None } 24 | } 25 | 26 | fn request(&self, req: impl serde::Serialize) -> Result 27 | where 28 | R: for<'a> serde::de::Deserialize<'a>, 29 | { 30 | let mut sock = UnixStream::connect(&self.sockpath)?; 31 | sock.set_read_timeout(self.timeout)?; 32 | sock.set_write_timeout(self.timeout)?; 33 | 34 | serde_json::to_writer(&mut sock, &req)?; 35 | 36 | // NOTE: we don't check the id there, so it *must* be synchronous 37 | let resp: R = serde_json::Deserializer::from_reader(&mut sock) 38 | .into_iter() 39 | .next() 40 | .ok_or(Error::Timeout)??; 41 | Ok(resp) 42 | } 43 | } 44 | 45 | impl Transport for UdsTransport { 46 | fn send_request(&self, req: Request) -> Result { 47 | Ok(self.request(req)?) 48 | } 49 | 50 | fn send_batch(&self, reqs: &[Request]) -> Result, crate::error::Error> { 51 | Ok(self.request(reqs)?) 52 | } 53 | 54 | fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | write!(f, "{}", self.sockpath.to_string_lossy()) 56 | } 57 | } 58 | 59 | /// Error that can occur while using the UDS transport. 60 | #[derive(Debug)] 61 | pub enum Error { 62 | /// An error occurred on the socket layer. 63 | SocketError(io::Error), 64 | /// We didn't receive a complete response till the deadline ran out. 65 | Timeout, 66 | /// JSON parsing error. 67 | Json(serde_json::Error), 68 | } 69 | 70 | impl fmt::Display for Error { 71 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 72 | use Error::*; 73 | 74 | match *self { 75 | SocketError(ref e) => write!(f, "couldn't connect to host: {}", e), 76 | Timeout => f.write_str("didn't receive response data in time, timed out."), 77 | Json(ref e) => write!(f, "JSON error: {}", e), 78 | } 79 | } 80 | } 81 | 82 | impl error::Error for Error { 83 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 84 | use self::Error::*; 85 | 86 | match *self { 87 | SocketError(ref e) => Some(e), 88 | Timeout => None, 89 | Json(ref e) => Some(e), 90 | } 91 | } 92 | } 93 | 94 | impl From for Error { 95 | fn from(e: io::Error) -> Self { Error::SocketError(e) } 96 | } 97 | 98 | impl From for Error { 99 | fn from(e: serde_json::Error) -> Self { Error::Json(e) } 100 | } 101 | 102 | impl From for crate::error::Error { 103 | fn from(e: Error) -> crate::error::Error { 104 | match e { 105 | Error::Json(e) => crate::error::Error::Json(e), 106 | e => crate::error::Error::Transport(Box::new(e)), 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use std::io::{Read, Write}; 114 | use std::os::unix::net::UnixListener; 115 | use std::{fs, process, thread}; 116 | 117 | use super::*; 118 | use crate::Client; 119 | 120 | // Test a dummy request / response over an UDS 121 | #[test] 122 | fn sanity_check_uds_transport() { 123 | let socket_path: path::PathBuf = format!("uds_scratch_{}.socket", process::id()).into(); 124 | // Any leftover? 125 | fs::remove_file(&socket_path).unwrap_or(()); 126 | 127 | let server = UnixListener::bind(&socket_path).unwrap(); 128 | let dummy_req = Request { 129 | method: "getinfo", 130 | params: None, 131 | id: serde_json::Value::Number(111.into()), 132 | jsonrpc: Some("2.0"), 133 | }; 134 | let dummy_req_ser = serde_json::to_vec(&dummy_req).unwrap(); 135 | let dummy_resp = Response { 136 | result: None, 137 | error: None, 138 | id: serde_json::Value::Number(111.into()), 139 | jsonrpc: Some("2.0".into()), 140 | }; 141 | let dummy_resp_ser = serde_json::to_vec(&dummy_resp).unwrap(); 142 | 143 | let cli_socket_path = socket_path.clone(); 144 | let client_thread = thread::spawn(move || { 145 | let transport = UdsTransport { 146 | sockpath: cli_socket_path, 147 | timeout: Some(time::Duration::from_secs(5)), 148 | }; 149 | let client = Client::with_transport(transport); 150 | 151 | client.send_request(dummy_req.clone()).unwrap() 152 | }); 153 | 154 | let (mut stream, _) = server.accept().unwrap(); 155 | stream.set_read_timeout(Some(time::Duration::from_secs(5))).unwrap(); 156 | let mut recv_req = vec![0; dummy_req_ser.len()]; 157 | let mut read = 0; 158 | while read < dummy_req_ser.len() { 159 | read += stream.read(&mut recv_req[read..]).unwrap(); 160 | } 161 | assert_eq!(recv_req, dummy_req_ser); 162 | 163 | stream.write_all(&dummy_resp_ser).unwrap(); 164 | stream.flush().unwrap(); 165 | let recv_resp = client_thread.join().unwrap(); 166 | assert_eq!(serde_json::to_vec(&recv_resp).unwrap(), dummy_resp_ser); 167 | 168 | // Clean up 169 | drop(server); 170 | fs::remove_file(&socket_path).unwrap(); 171 | } 172 | } 173 | --------------------------------------------------------------------------------