Compare commits
211 Commits
nonblockin
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2fdc5ccf8 | ||
|
|
df6675dc09 | ||
|
|
4b98565ee2 | ||
|
|
5d7be37136 | ||
|
|
761796c94a | ||
|
|
5dc4bb6360 | ||
|
|
e31cf4773b | ||
|
|
c2656995fc | ||
|
|
b185259561 | ||
|
|
80bf744a70 | ||
|
|
0e28021b3e | ||
|
|
980fa4afd6 | ||
|
|
b20d41ace8 | ||
|
|
24d07e2cac | ||
|
|
fbffdfe65e | ||
|
|
dd13e4e422 | ||
|
|
1372898ff2 | ||
|
|
f24458248a | ||
|
|
74ac97cfe4 | ||
|
|
7de4cb758d | ||
|
|
ed9784ee79 | ||
|
|
e14f66b2f1 | ||
|
|
862280a882 | ||
|
|
a3488f4ff9 | ||
|
|
0ee0a6ad7a | ||
|
|
6721929f22 | ||
|
|
372eda9455 | ||
|
|
eb38c038b2 | ||
|
|
83747b131b | ||
|
|
0e459b6bd6 | ||
|
|
805ea0af30 | ||
|
|
3dc4daceaf | ||
|
|
5a476fdfd9 | ||
|
|
ec90685324 | ||
|
|
15f753f05f | ||
|
|
ed0cdae329 | ||
|
|
7ef3ff6873 | ||
|
|
f00b9998d1 | ||
|
|
b0a1bfb0cb | ||
|
|
6e2a9b4b64 | ||
|
|
43da8e9e6b | ||
|
|
3c20ca18b6 | ||
|
|
d917f891e8 | ||
|
|
1fcddcba9b | ||
|
|
a871b084ec | ||
|
|
6fe96fddae | ||
|
|
05771a81d7 | ||
|
|
0b97659b38 | ||
|
|
4dd7e2135e | ||
|
|
9f09f50217 | ||
|
|
0dd51408a3 | ||
|
|
e7af332684 | ||
|
|
84f4f609b9 | ||
|
|
746a0e6463 | ||
|
|
54797a0be8 | ||
|
|
8d71f9597c | ||
|
|
3a1f1bffb0 | ||
|
|
64c77ee1bc | ||
|
|
4b421084e9 | ||
|
|
1899234f2e | ||
|
|
1bbae7d5a0 | ||
|
|
6cf723504f | ||
|
|
78cb066966 | ||
|
|
28b1aaa0c3 | ||
|
|
898f230081 | ||
|
|
ef9fd6bf50 | ||
|
|
fd81717f8e | ||
|
|
d8554fb550 | ||
|
|
91228df8a5 | ||
|
|
dd3c171a7a | ||
|
|
e4d2b1d194 | ||
|
|
dacd772d5e | ||
|
|
8b31e5fe41 | ||
|
|
54fd52d898 | ||
|
|
fe33e19bda | ||
|
|
dd872d6714 | ||
|
|
a331ae8059 | ||
|
|
5ecb26fd7d | ||
|
|
3f6dd0c796 | ||
|
|
ed9bb09d02 | ||
|
|
2d44350b44 | ||
|
|
84b1435df5 | ||
|
|
7535debbd6 | ||
|
|
87348e5809 | ||
|
|
7c11f208c0 | ||
|
|
dbec56a95b | ||
|
|
20493aa475 | ||
|
|
f5a438cd0a | ||
|
|
9a7cc14c94 | ||
|
|
c84507e45b | ||
|
|
b86f2bb22c | ||
|
|
d2dbab93be | ||
|
|
0f356ef94d | ||
|
|
27eb4d0cb0 | ||
|
|
3a61037733 | ||
|
|
d8ee94c30d | ||
|
|
bd16116f9a | ||
|
|
b989ddb04b | ||
|
|
c925a9179e | ||
|
|
abdbd020e1 | ||
|
|
a04a84299d | ||
|
|
d26dfd7f54 | ||
|
|
80b2adeb4b | ||
|
|
40cecd5010 | ||
|
|
bf2f8ef2e5 | ||
|
|
156e6fc839 | ||
|
|
84d6860144 | ||
|
|
413c964e5c | ||
|
|
3568b60612 | ||
|
|
adb0c7444e | ||
|
|
129081999c | ||
|
|
561db261d3 | ||
|
|
add1b7853e | ||
|
|
97e60bad4b | ||
|
|
3adfbd783f | ||
|
|
d132572c14 | ||
|
|
ad62e7a80f | ||
|
|
f3caf4a279 | ||
|
|
cdfc21bd6a | ||
|
|
fc8ba9de90 | ||
|
|
9994f8a2e1 | ||
|
|
cbda7a22db | ||
|
|
7e40dfd441 | ||
|
|
c34fde5698 | ||
|
|
aa0b957818 | ||
|
|
737cedb74e | ||
|
|
a148dfb9d1 | ||
|
|
195d23accd | ||
|
|
0d77c1860f | ||
|
|
acc05c3666 | ||
|
|
7493630ed8 | ||
|
|
d9668c86f4 | ||
|
|
e9bee5a46d | ||
|
|
e1c20b8a0a | ||
|
|
3d96644a0f | ||
|
|
d8e339c4c1 | ||
|
|
ca48628539 | ||
|
|
29d77de2f5 | ||
|
|
e9a7fba7a9 | ||
|
|
aea9da234b | ||
|
|
866c2424e1 | ||
|
|
2ecd93b9b3 | ||
|
|
0e3e6325e6 | ||
|
|
19f00a576c | ||
|
|
5c8555c2f6 | ||
|
|
4732dcc424 | ||
|
|
8b32726335 | ||
|
|
68d2f23306 | ||
|
|
98e1bf205c | ||
|
|
f012323274 | ||
|
|
47053c0302 | ||
|
|
cfc7bfe037 | ||
|
|
d64aaffb3f | ||
|
|
d5766abb8e | ||
|
|
f30c37b69f | ||
|
|
7776ec0732 | ||
|
|
e7eab4f77a | ||
|
|
88e92af429 | ||
|
|
e3aa6a5a73 | ||
|
|
f30be3bfde | ||
|
|
fa9359cf73 | ||
|
|
25dc3fa5e5 | ||
|
|
d46ba79972 | ||
|
|
078e513f62 | ||
|
|
c8bace9407 | ||
|
|
cb9b51f356 | ||
|
|
2652016f99 | ||
|
|
23c2004f38 | ||
|
|
7db9a1331b | ||
|
|
d927387df3 | ||
|
|
1d59d9605e | ||
|
|
af10aeac99 | ||
|
|
efdc7d2243 | ||
|
|
7d7a3088e6 | ||
|
|
3ac7f6ec6b | ||
|
|
aacb1291a5 | ||
|
|
8b81ad1d90 | ||
|
|
226b12aafa | ||
|
|
89fdf749f1 | ||
|
|
3e266037a1 | ||
|
|
c4735374f9 | ||
|
|
1a0252450c | ||
|
|
b08c59815d | ||
|
|
2477ab4440 | ||
|
|
e4c72ed4bc | ||
|
|
add53ebfa1 | ||
|
|
ef3ddfdabc | ||
|
|
f200b7fc61 | ||
|
|
c1f4e9d76d | ||
|
|
11c2b1b6ce | ||
|
|
6f8cefd1f1 | ||
|
|
5a5fdca348 | ||
|
|
3502a5c7ab | ||
|
|
d5e4bfe3e7 | ||
|
|
335fd49eb5 | ||
|
|
110fff5d4f | ||
|
|
f9b318c1c6 | ||
|
|
db4554f9e2 | ||
|
|
f37bcd59e0 | ||
|
|
7b4e99e80c | ||
|
|
2a9278879f | ||
|
|
33f4ea2e51 | ||
|
|
e1dbfd7ceb | ||
|
|
686aafce41 | ||
|
|
396ab8ea57 | ||
|
|
e26ed29ee8 | ||
|
|
c240adcd1d | ||
|
|
98b70e4791 | ||
|
|
0eb74e6176 | ||
|
|
3a360e4d46 | ||
|
|
cccb808e6f |
71
.github/workflows/cont_integration.yml
vendored
Normal file
71
.github/workflows/cont_integration.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- stable # STABLE
|
||||||
|
- 1.75.0 # MSRV
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||||
|
- name: Install rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --verbose --all-features
|
||||||
|
- name: Setup iptables for the timeout test
|
||||||
|
run: sudo ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP
|
||||||
|
- name: Timeout test
|
||||||
|
run: cargo test -- --ignored test_local_timeout
|
||||||
|
- run: cargo check --verbose --features=use-openssl
|
||||||
|
- run: cargo check --verbose --no-default-features --features=proxy
|
||||||
|
- run: cargo check --verbose --no-default-features --features=minimal
|
||||||
|
- run: cargo check --verbose --no-default-features --features=minimal,debug-calls
|
||||||
|
- run: cargo check --verbose --no-default-features --features=proxy,use-openssl
|
||||||
|
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls
|
||||||
|
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls-ring
|
||||||
|
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls,use-rustls-ring
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: Rust fmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
components: rustfmt
|
||||||
|
- name: Check fmt
|
||||||
|
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||||
|
|
||||||
|
clippy_check:
|
||||||
|
name: Rust clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.90.0
|
||||||
|
components: clippy
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2.2.1
|
||||||
|
- run: cargo clippy --all-features --all-targets -- -D warnings
|
||||||
23
.travis.yml
23
.travis.yml
@ -1,23 +0,0 @@
|
|||||||
language: rust
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
# - 1.31.0 TODO: support the first 2018-edition toolchain
|
|
||||||
before_script:
|
|
||||||
- rustup component add rustfmt
|
|
||||||
# Run tests and check a few combinations of features
|
|
||||||
script:
|
|
||||||
- cargo fmt -- --check --verbose
|
|
||||||
- cargo test --verbose --all
|
|
||||||
- cargo check --verbose --features=use-openssl
|
|
||||||
- cargo check --verbose --no-default-features --features=proxy
|
|
||||||
- cargo check --verbose --no-default-features --features=minimal
|
|
||||||
- cargo check --verbose --no-default-features --features=minimal,debug-calls
|
|
||||||
- cargo check --verbose --no-default-features --features=proxy,use-openssl
|
|
||||||
- cargo check --verbose --no-default-features --features=proxy,use-rustls
|
|
||||||
|
|
||||||
before_cache:
|
|
||||||
- rm -rf "$TRAVIS_HOME/.cargo/registry/src"
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
66
CHANGELOG.md
Normal file
66
CHANGELOG.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project can be found here and in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
|
||||||
|
|
||||||
|
Contributors do not need to change this file but do need to add changelog details in their PR descriptions. The person making the next release will collect changelog details from included PRs and edit this file prior to each release.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.24.1]
|
||||||
|
- Default to `ring` if multiple `rustls` features are set #183
|
||||||
|
|
||||||
|
## [0.24.0]
|
||||||
|
- Use default `CryptoProvider` if available, otherwise install `rustls`'s `CryptoProvider` based on features #171
|
||||||
|
- Add a new batch method for `blockchain.transaction.get_merkle` #170
|
||||||
|
|
||||||
|
## [0.23.1]
|
||||||
|
- Fix batch request to Electrum servers out of order responses #160
|
||||||
|
- Allow types that references to `ElectrumApi` to also implement it #163
|
||||||
|
|
||||||
|
## [0.23.0]
|
||||||
|
|
||||||
|
- Raise MSRV to `1.75` and bump `rustls` to `0.23.21` #159
|
||||||
|
- Enforce min `rustls` version 0.23.19 to support MSRV with fix for RUSTSEC-2024-0399 #158
|
||||||
|
|
||||||
|
## [0.22.0]
|
||||||
|
|
||||||
|
- Updates the NoCertificateVerification implementation for the rustls::client::danger::ServerCertVerifier to use the rustls::SignatureScheme from CryptoProvider in use #150
|
||||||
|
- Add `id_from_pos` support #155
|
||||||
|
|
||||||
|
## [0.21.0]
|
||||||
|
|
||||||
|
- Add use-rustls-ring feature #135
|
||||||
|
- refactor: make validate_merkle_proof more efficient #134
|
||||||
|
- chore: set rust edition to 2021, fix clippy, add ci fmt and clippy checks #139
|
||||||
|
|
||||||
|
## [0.20.0]
|
||||||
|
|
||||||
|
- Upgrade rustls to 0.23 #132
|
||||||
|
- chore(deps): upgrade rust-bitcoin to 0.32.0 #133
|
||||||
|
- ci: add test with MSRV 1.63.0 #128
|
||||||
|
|
||||||
|
## [0.19.0]
|
||||||
|
|
||||||
|
- Add Batch::raw and improve docs #94
|
||||||
|
- Remove webpki and bump webpki-roots to v0.25 #117
|
||||||
|
- Upgrade rust-bitcoin to v0.31.0 #121
|
||||||
|
- Add utility to validate GetMerkleRes #122
|
||||||
|
- Enforce timeout on initial socks5 proxy connection #125
|
||||||
|
|
||||||
|
## [0.18.0]
|
||||||
|
|
||||||
|
- Revert "errors if expecting headers notification but not subscribed" #115
|
||||||
|
|
||||||
|
[0.18.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.17.0...0.18.0
|
||||||
|
[0.19.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.18.0...0.19.0
|
||||||
|
[0.20.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.19.0...0.20.0
|
||||||
|
[0.21.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.20.0...0.21.0
|
||||||
|
[0.22.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.21.0...0.22.0
|
||||||
|
[0.23.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.22.0...0.23.0
|
||||||
|
[0.23.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.0...0.23.1
|
||||||
|
[0.24.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.1...0.24.0
|
||||||
|
[0.24.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.0...0.24.1
|
||||||
|
[Unreleased]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.1...HEAD
|
||||||
36
Cargo.toml
36
Cargo.toml
@ -1,17 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "electrum-client"
|
name = "electrum-client"
|
||||||
version = "0.1.0-beta.5"
|
version = "0.24.1"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
homepage = "https://github.com/MagicalBitcoin/rust-electrum-client"
|
homepage = "https://github.com/bitcoindevkit/rust-electrum-client"
|
||||||
repository = "https://github.com/MagicalBitcoin/rust-electrum-client"
|
repository = "https://github.com/bitcoindevkit/rust-electrum-client"
|
||||||
documentation = "https://docs.rs/electrum-client/"
|
documentation = "https://docs.rs/electrum-client/"
|
||||||
description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers."
|
description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers."
|
||||||
keywords = ["bitcoin", "electrum"]
|
keywords = ["bitcoin", "electrum"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
exclude = [
|
rust-version = "1.75.0"
|
||||||
"test_data/*",
|
edition = "2021"
|
||||||
]
|
|
||||||
|
|
||||||
# loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client
|
# loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client
|
||||||
|
|
||||||
@ -21,21 +20,28 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
bitcoin = { version = "0.23", features = ["use-serde"] }
|
bitcoin = { version = "0.32", features = ["serde"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
socks = { version = "^0.3", optional = true }
|
openssl = { version = "0.10", optional = true }
|
||||||
openssl = { version = "^0.10", optional = true }
|
rustls = { version = "0.23.21", optional = true, default-features = false }
|
||||||
rustls = { version = "0.16.0", optional = true, features = ["dangerous_configuration"] }
|
webpki-roots = { version = "0.25", optional = true }
|
||||||
webpki = { version = "0.21.0", optional = true }
|
|
||||||
webpki-roots = { version = "^0.19", optional = true }
|
byteorder = { version = "1.0", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winapi = { version="0.3.9", features=["winsock2"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["socks", "webpki", "webpki-roots", "rustls"]
|
default = ["proxy", "use-rustls"]
|
||||||
minimal = []
|
minimal = []
|
||||||
debug-calls = []
|
debug-calls = []
|
||||||
proxy = ["socks"]
|
proxy = ["byteorder", "winapi", "libc"]
|
||||||
use-rustls = ["webpki", "webpki-roots", "rustls"]
|
use-rustls = ["webpki-roots", "rustls/default"]
|
||||||
|
use-rustls-ring = ["webpki-roots", "rustls/ring", "rustls/logging", "rustls/std", "rustls/tls12"]
|
||||||
use-openssl = ["openssl"]
|
use-openssl = ["openssl"]
|
||||||
|
|||||||
14
README.md
14
README.md
@ -1,9 +1,15 @@
|
|||||||
# rust-electrum-client [![Build Status]][travis] [![Latest Version]][crates.io]
|
# rust-electrum-client
|
||||||
|
[![Build Status]][GitHub Workflow] [![Latest Version]][crates.io] [![MSRV Badge]][Rust Blog]
|
||||||
|
|
||||||
[Build Status]: https://api.travis-ci.org/MagicalBitcoin/rust-electrum-client.svg?branch=master
|
[Build Status]: https://github.com/bitcoindevkit/rust-electrum-client/actions/workflows/cont_integration.yml/badge.svg
|
||||||
[travis]: https://travis-ci.org/MagicalBitcoin/rust-electrum-client
|
[GitHub Workflow]: https://github.com/bitcoindevkit/rust-electrum-client/actions?query=workflow%3ACI
|
||||||
[Latest Version]: https://img.shields.io/crates/v/electrum-client.svg
|
[Latest Version]: https://img.shields.io/crates/v/electrum-client.svg
|
||||||
[crates.io]: https://crates.io/crates/electrum-client
|
[crates.io]: https://crates.io/crates/electrum-client
|
||||||
|
[MSRV Badge]: https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg
|
||||||
|
[Rust Blog]: https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html
|
||||||
|
|
||||||
Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers.
|
Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers.
|
||||||
|
|
||||||
|
## Minimum Supported Rust Version (MSRV)
|
||||||
|
|
||||||
|
This library should compile with any combination of features with Rust 1.75.0.
|
||||||
|
|||||||
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
msrv="1.75.0"
|
||||||
@ -1,9 +1,9 @@
|
|||||||
extern crate electrum_client;
|
extern crate electrum_client;
|
||||||
|
|
||||||
use electrum_client::Client;
|
use electrum_client::{Client, ElectrumApi};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut client = Client::new("kirsche.emzy.de:50001").unwrap();
|
let client = Client::new("tcp://electrum.blockstream.info:50001").unwrap();
|
||||||
let res = client.server_features();
|
let res = client.server_features();
|
||||||
println!("{:#?}", res);
|
println!("{:#?}", res);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
extern crate electrum_client;
|
extern crate electrum_client;
|
||||||
|
|
||||||
use electrum_client::Client;
|
use electrum_client::{Client, ElectrumApi};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut client = Client::new_ssl("electrum2.hodlister.co:50002", true).unwrap();
|
let client = Client::new("ssl://electrum.blockstream.info:50002").unwrap();
|
||||||
let res = client.server_features();
|
let res = client.server_features();
|
||||||
println!("{:#?}", res);
|
println!("{:#?}", res);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
extern crate electrum_client;
|
extern crate electrum_client;
|
||||||
|
|
||||||
use electrum_client::Client;
|
use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// NOTE: This assumes Tor is running localy, with an unauthenticated Socks5 listening at
|
// NOTE: This assumes Tor is running localy, with an unauthenticated Socks5 listening at
|
||||||
// localhost:9050
|
// localhost:9050
|
||||||
|
let proxy = Socks5Config::new("127.0.0.1:9050");
|
||||||
|
let config = ConfigBuilder::new().socks5(Some(proxy)).build();
|
||||||
|
|
||||||
let mut client = Client::new_proxy("ozahtqwp25chjdjd.onion:50001", "127.0.0.1:9050").unwrap();
|
let client = Client::from_config("tcp://explorernuoc63nb.onion:110", config.clone()).unwrap();
|
||||||
let res = client.server_features();
|
let res = client.server_features();
|
||||||
println!("{:#?}", res);
|
println!("{:#?}", res);
|
||||||
|
|
||||||
// works both with onion v2/v3 (if your Tor supports them)
|
// works both with onion v2/v3 (if your Tor supports them)
|
||||||
let mut client = Client::new_proxy(
|
let client = Client::from_config(
|
||||||
"v7gtzf7nua6hdmb2wtqaqioqmesdb4xrlly4zwr7bvayxv2bpg665pqd.onion:50001",
|
"tcp://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion:110",
|
||||||
"127.0.0.1:9050",
|
config,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = client.server_features();
|
let res = client.server_features();
|
||||||
|
|||||||
34
justfile
Normal file
34
justfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
alias b := build
|
||||||
|
alias c := check
|
||||||
|
alias f := fmt
|
||||||
|
alias t := test
|
||||||
|
alias p := pre-push
|
||||||
|
|
||||||
|
_default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Check code: formatting, compilation, linting, doc comments, and commit signature
|
||||||
|
check:
|
||||||
|
cargo +nightly fmt --all -- --check
|
||||||
|
cargo check --all-features --all-targets
|
||||||
|
cargo clippy --all-features --all-targets -- -D warnings
|
||||||
|
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
|
||||||
|
@[ "$(git log --pretty='format:%G?' -1 HEAD)" = "N" ] && \
|
||||||
|
echo "\n⚠️ Unsigned commit: BDK requires that commits be signed." || \
|
||||||
|
true
|
||||||
|
|
||||||
|
# Format all code
|
||||||
|
fmt:
|
||||||
|
cargo +nightly fmt
|
||||||
|
|
||||||
|
# Run all tests on the workspace with all features
|
||||||
|
test:
|
||||||
|
cargo test --all-features -- --test-threads=1
|
||||||
|
|
||||||
|
# Run pre-push suite: format, check, and test
|
||||||
|
pre-push: fmt check test
|
||||||
|
|
||||||
630
src/api.rs
Normal file
630
src/api.rs
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
//! Electrum APIs
|
||||||
|
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
|
use bitcoin::{block, Script, Transaction, Txid};
|
||||||
|
|
||||||
|
use crate::batch::Batch;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
impl<E: Deref> ElectrumApi for E
|
||||||
|
where
|
||||||
|
E::Target: ElectrumApi,
|
||||||
|
{
|
||||||
|
fn raw_call(
|
||||||
|
&self,
|
||||||
|
method_name: &str,
|
||||||
|
params: impl IntoIterator<Item = Param>,
|
||||||
|
) -> Result<serde_json::Value, Error> {
|
||||||
|
(**self).raw_call(method_name, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
|
||||||
|
(**self).batch_call(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers_subscribe_raw(&self) -> Result<RawHeaderNotification, Error> {
|
||||||
|
(**self).block_headers_subscribe_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers_pop_raw(&self) -> Result<Option<RawHeaderNotification>, Error> {
|
||||||
|
(**self).block_headers_pop_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_header_raw(&self, height: usize) -> Result<Vec<u8>, Error> {
|
||||||
|
(**self).block_header_raw(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error> {
|
||||||
|
(**self).block_headers(start_height, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_fee(&self, number: usize) -> Result<f64, Error> {
|
||||||
|
(**self).estimate_fee(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relay_fee(&self) -> Result<f64, Error> {
|
||||||
|
(**self).relay_fee()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
|
||||||
|
(**self).script_subscribe(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>,
|
||||||
|
{
|
||||||
|
(**self).batch_script_subscribe(scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error> {
|
||||||
|
(**self).script_unsubscribe(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_pop(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
|
||||||
|
(**self).script_pop(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes, Error> {
|
||||||
|
(**self).script_get_balance(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>,
|
||||||
|
{
|
||||||
|
(**self).batch_script_get_balance(scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error> {
|
||||||
|
(**self).script_get_history(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>,
|
||||||
|
{
|
||||||
|
(**self).batch_script_get_history(scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error> {
|
||||||
|
(**self).script_list_unspent(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_list_unspent<'s, I>(
|
||||||
|
&self,
|
||||||
|
scripts: I,
|
||||||
|
) -> Result<Vec<Vec<ListUnspentRes>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>,
|
||||||
|
{
|
||||||
|
(**self).batch_script_list_unspent(scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error> {
|
||||||
|
(**self).transaction_get_raw(txid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'t Txid>,
|
||||||
|
{
|
||||||
|
(**self).batch_transaction_get_raw(txids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_block_header_raw<I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<u32>,
|
||||||
|
{
|
||||||
|
(**self).batch_block_header_raw(heights)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_estimate_fee<I>(&self, numbers: I) -> Result<Vec<f64>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<usize>,
|
||||||
|
{
|
||||||
|
(**self).batch_estimate_fee(numbers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error> {
|
||||||
|
(**self).transaction_broadcast_raw(raw_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error> {
|
||||||
|
(**self).transaction_get_merkle(txid, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_transaction_get_merkle<I>(
|
||||||
|
&self,
|
||||||
|
txids_and_heights: I,
|
||||||
|
) -> Result<Vec<GetMerkleRes>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<(Txid, usize)>,
|
||||||
|
{
|
||||||
|
(**self).batch_transaction_get_merkle(txids_and_heights)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
|
||||||
|
(**self).txid_from_pos(height, tx_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txid_from_pos_with_merkle(
|
||||||
|
&self,
|
||||||
|
height: usize,
|
||||||
|
tx_pos: usize,
|
||||||
|
) -> Result<TxidFromPosRes, Error> {
|
||||||
|
(**self).txid_from_pos_with_merkle(height, tx_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
|
||||||
|
(**self).server_features()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ping(&self) -> Result<(), Error> {
|
||||||
|
(**self).ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "debug-calls")]
|
||||||
|
fn calls_made(&self) -> Result<usize, Error> {
|
||||||
|
(**self).calls_made()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API calls exposed by an Electrum client
|
||||||
|
pub trait ElectrumApi {
|
||||||
|
/// Gets the block header for height `height`.
|
||||||
|
fn block_header(&self, height: usize) -> Result<block::Header, Error> {
|
||||||
|
Ok(deserialize(&self.block_header_raw(height)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call.
|
||||||
|
fn block_headers_subscribe(&self) -> Result<HeaderNotification, Error> {
|
||||||
|
self.block_headers_subscribe_raw()?.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to pop one queued notification for a new block header that we might have received.
|
||||||
|
/// Returns `None` if there are no items in the queue.
|
||||||
|
fn block_headers_pop(&self) -> Result<Option<HeaderNotification>, Error> {
|
||||||
|
self.block_headers_pop_raw()?
|
||||||
|
.map(|raw| raw.try_into())
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the transaction with `txid`. Returns an error if not found.
|
||||||
|
fn transaction_get(&self, txid: &Txid) -> Result<Transaction, Error> {
|
||||||
|
Ok(deserialize(&self.transaction_get_raw(txid)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch version of [`transaction_get`](#method.transaction_get).
|
||||||
|
///
|
||||||
|
/// Takes a list of `txids` and returns a list of transactions.
|
||||||
|
fn batch_transaction_get<'t, I>(&self, txids: I) -> Result<Vec<Transaction>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'t Txid>,
|
||||||
|
{
|
||||||
|
self.batch_transaction_get_raw(txids)?
|
||||||
|
.iter()
|
||||||
|
.map(|s| Ok(deserialize(s)?))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch version of [`block_header`](#method.block_header).
|
||||||
|
///
|
||||||
|
/// Takes a list of `heights` of blocks and returns a list of headers.
|
||||||
|
fn batch_block_header<I>(&self, heights: I) -> Result<Vec<block::Header>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<u32>,
|
||||||
|
{
|
||||||
|
self.batch_block_header_raw(heights)?
|
||||||
|
.iter()
|
||||||
|
.map(|s| Ok(deserialize(s)?))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Broadcasts a transaction to the network.
|
||||||
|
fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
|
||||||
|
let buffer: Vec<u8> = serialize(tx);
|
||||||
|
self.transaction_broadcast_raw(&buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the requested API call returning the raw answer.
|
||||||
|
fn raw_call(
|
||||||
|
&self,
|
||||||
|
method_name: &str,
|
||||||
|
params: impl IntoIterator<Item = Param>,
|
||||||
|
) -> Result<serde_json::Value, Error>;
|
||||||
|
|
||||||
|
/// Execute a queue of calls stored in a [`Batch`](../batch/struct.Batch.html) struct. Returns
|
||||||
|
/// `Ok()` **only if** all of the calls are successful. The order of the JSON `Value`s returned
|
||||||
|
/// reflects the order in which the calls were made on the `Batch` struct.
|
||||||
|
fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error>;
|
||||||
|
|
||||||
|
/// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call and
|
||||||
|
/// returns the current tip as raw bytes instead of deserializing them.
|
||||||
|
fn block_headers_subscribe_raw(&self) -> Result<RawHeaderNotification, Error>;
|
||||||
|
|
||||||
|
/// Tries to pop one queued notification for a new block header that we might have received.
|
||||||
|
/// Returns a the header in raw bytes if a notification is found in the queue, None otherwise.
|
||||||
|
fn block_headers_pop_raw(&self) -> Result<Option<RawHeaderNotification>, Error>;
|
||||||
|
|
||||||
|
/// Gets the raw bytes of block header for height `height`.
|
||||||
|
fn block_header_raw(&self, height: usize) -> Result<Vec<u8>, Error>;
|
||||||
|
|
||||||
|
/// Tries to fetch `count` block headers starting from `start_height`.
|
||||||
|
fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error>;
|
||||||
|
|
||||||
|
/// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks.
|
||||||
|
fn estimate_fee(&self, number: usize) -> Result<f64, Error>;
|
||||||
|
|
||||||
|
/// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**.
|
||||||
|
fn relay_fee(&self) -> Result<f64, Error>;
|
||||||
|
|
||||||
|
/// Subscribes to notifications for activity on a specific *scriptPubKey*.
|
||||||
|
///
|
||||||
|
/// Returns a [`ScriptStatus`](../types/type.ScriptStatus.html) when successful that represents
|
||||||
|
/// the current status for the requested script.
|
||||||
|
///
|
||||||
|
/// Returns [`Error::AlreadySubscribed`](../types/enum.Error.html#variant.AlreadySubscribed) if
|
||||||
|
/// already subscribed to the script.
|
||||||
|
fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`script_subscribe`](#method.script_subscribe).
|
||||||
|
///
|
||||||
|
/// Takes a list of scripts and returns a list of script status responses.
|
||||||
|
///
|
||||||
|
/// Note you should pass a reference to a collection because otherwise an expensive clone is made
|
||||||
|
fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>;
|
||||||
|
|
||||||
|
/// Subscribes to notifications for activity on a specific *scriptPubKey*.
|
||||||
|
///
|
||||||
|
/// Returns a `bool` with the server response when successful.
|
||||||
|
///
|
||||||
|
/// Returns [`Error::NotSubscribed`](../types/enum.Error.html#variant.NotSubscribed) if
|
||||||
|
/// not subscribed to the script.
|
||||||
|
fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error>;
|
||||||
|
|
||||||
|
/// Tries to pop one queued notification for a the requested script. Returns `None` if there are no items in the queue.
|
||||||
|
fn script_pop(&self, script: &Script) -> Result<Option<ScriptStatus>, Error>;
|
||||||
|
|
||||||
|
/// Returns the balance for a *scriptPubKey*.
|
||||||
|
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`script_get_balance`](#method.script_get_balance).
|
||||||
|
///
|
||||||
|
/// Takes a list of scripts and returns a list of balance responses.
|
||||||
|
fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>;
|
||||||
|
|
||||||
|
/// Returns the history for a *scriptPubKey*
|
||||||
|
fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`script_get_history`](#method.script_get_history).
|
||||||
|
///
|
||||||
|
/// Takes a list of scripts and returns a list of history responses.
|
||||||
|
fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>;
|
||||||
|
|
||||||
|
/// Returns the list of unspent outputs for a *scriptPubKey*
|
||||||
|
fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`script_list_unspent`](#method.script_list_unspent).
|
||||||
|
///
|
||||||
|
/// Takes a list of scripts and returns a list of a list of utxos.
|
||||||
|
fn batch_script_list_unspent<'s, I>(
|
||||||
|
&self,
|
||||||
|
scripts: I,
|
||||||
|
) -> Result<Vec<Vec<ListUnspentRes>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'s Script>;
|
||||||
|
|
||||||
|
/// Gets the raw bytes of a transaction with `txid`. Returns an error if not found.
|
||||||
|
fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`transaction_get_raw`](#method.transaction_get_raw).
|
||||||
|
///
|
||||||
|
/// Takes a list of `txids` and returns a list of transactions raw bytes.
|
||||||
|
fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<&'t Txid>;
|
||||||
|
|
||||||
|
/// Batch version of [`block_header_raw`](#method.block_header_raw).
|
||||||
|
///
|
||||||
|
/// Takes a list of `heights` of blocks and returns a list of block header raw bytes.
|
||||||
|
fn batch_block_header_raw<I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<u32>;
|
||||||
|
|
||||||
|
/// Batch version of [`estimate_fee`](#method.estimate_fee).
|
||||||
|
///
|
||||||
|
/// Takes a list of `numbers` of blocks and returns a list of fee required in
|
||||||
|
/// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks.
|
||||||
|
fn batch_estimate_fee<I>(&self, numbers: I) -> Result<Vec<f64>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<usize>;
|
||||||
|
|
||||||
|
/// Broadcasts the raw bytes of a transaction to the network.
|
||||||
|
fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error>;
|
||||||
|
|
||||||
|
/// Returns the merkle path for the transaction `txid` confirmed in the block at `height`.
|
||||||
|
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error>;
|
||||||
|
|
||||||
|
/// Batch version of [`transaction_get_merkle`](#method.transaction_get_merkle).
|
||||||
|
///
|
||||||
|
/// Take a list of `(txid, height)`, for transactions with `txid` confirmed in the block at `height`.
|
||||||
|
fn batch_transaction_get_merkle<I>(
|
||||||
|
&self,
|
||||||
|
txids_and_heights: I,
|
||||||
|
) -> Result<Vec<GetMerkleRes>, Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: Borrow<(Txid, usize)>;
|
||||||
|
|
||||||
|
/// Returns a transaction hash, given a block `height` and a `tx_pos` in the block.
|
||||||
|
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error>;
|
||||||
|
|
||||||
|
/// Returns a transaction hash and a merkle path, given a block `height` and a `tx_pos` in the
|
||||||
|
/// block.
|
||||||
|
fn txid_from_pos_with_merkle(
|
||||||
|
&self,
|
||||||
|
height: usize,
|
||||||
|
tx_pos: usize,
|
||||||
|
) -> Result<TxidFromPosRes, Error>;
|
||||||
|
|
||||||
|
/// Returns the capabilities of the server.
|
||||||
|
fn server_features(&self) -> Result<ServerFeaturesRes, Error>;
|
||||||
|
|
||||||
|
/// Pings the server. This method can also be used as a "dummy" call to trigger the processing
|
||||||
|
/// of incoming block header or script notifications.
|
||||||
|
fn ping(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
#[cfg(feature = "debug-calls")]
|
||||||
|
/// Returns the number of network calls made since the creation of the client.
|
||||||
|
fn calls_made(&self) -> Result<usize, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
use super::ElectrumApi;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FakeApi;
|
||||||
|
|
||||||
|
impl ElectrumApi for FakeApi {
|
||||||
|
fn raw_call(
|
||||||
|
&self,
|
||||||
|
_: &str,
|
||||||
|
_: impl IntoIterator<Item = super::Param>,
|
||||||
|
) -> Result<serde_json::Value, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_call(&self, _: &crate::Batch) -> Result<Vec<serde_json::Value>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers_subscribe_raw(
|
||||||
|
&self,
|
||||||
|
) -> Result<super::RawHeaderNotification, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers_pop_raw(
|
||||||
|
&self,
|
||||||
|
) -> Result<Option<super::RawHeaderNotification>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_header_raw(&self, _: usize) -> Result<Vec<u8>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_headers(&self, _: usize, _: usize) -> Result<super::GetHeadersRes, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_fee(&self, _: usize) -> Result<f64, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relay_fee(&self) -> Result<f64, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_subscribe(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Script,
|
||||||
|
) -> Result<Option<super::ScriptStatus>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_subscribe<'s, I>(
|
||||||
|
&self,
|
||||||
|
_: I,
|
||||||
|
) -> Result<Vec<Option<super::ScriptStatus>>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_unsubscribe(&self, _: &bitcoin::Script) -> Result<bool, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_pop(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Script,
|
||||||
|
) -> Result<Option<super::ScriptStatus>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_get_balance(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Script,
|
||||||
|
) -> Result<super::GetBalanceRes, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_get_balance<'s, I>(
|
||||||
|
&self,
|
||||||
|
_: I,
|
||||||
|
) -> Result<Vec<super::GetBalanceRes>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_get_history(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Script,
|
||||||
|
) -> Result<Vec<super::GetHistoryRes>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_get_history<'s, I>(
|
||||||
|
&self,
|
||||||
|
_: I,
|
||||||
|
) -> Result<Vec<Vec<super::GetHistoryRes>>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_list_unspent(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Script,
|
||||||
|
) -> Result<Vec<super::ListUnspentRes>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_script_list_unspent<'s, I>(
|
||||||
|
&self,
|
||||||
|
_: I,
|
||||||
|
) -> Result<Vec<Vec<super::ListUnspentRes>>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_get_raw(&self, _: &bitcoin::Txid) -> Result<Vec<u8>, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_transaction_get_raw<'t, I>(&self, _: I) -> Result<Vec<Vec<u8>>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<&'t bitcoin::Txid>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_block_header_raw<I>(&self, _: I) -> Result<Vec<Vec<u8>>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<u32>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_estimate_fee<I>(&self, _: I) -> Result<Vec<f64>, super::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<usize>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_broadcast_raw(&self, _: &[u8]) -> Result<bitcoin::Txid, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_get_merkle(
|
||||||
|
&self,
|
||||||
|
_: &bitcoin::Txid,
|
||||||
|
_: usize,
|
||||||
|
) -> Result<super::GetMerkleRes, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_transaction_get_merkle<I>(
|
||||||
|
&self,
|
||||||
|
_: I,
|
||||||
|
) -> Result<Vec<crate::GetMerkleRes>, crate::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator + Clone,
|
||||||
|
I::Item: std::borrow::Borrow<(bitcoin::Txid, usize)>,
|
||||||
|
{
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txid_from_pos(&self, _: usize, _: usize) -> Result<bitcoin::Txid, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txid_from_pos_with_merkle(
|
||||||
|
&self,
|
||||||
|
_: usize,
|
||||||
|
_: usize,
|
||||||
|
) -> Result<super::TxidFromPosRes, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_features(&self) -> Result<super::ServerFeaturesRes, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ping(&self) -> Result<(), super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "debug-calls")]
|
||||||
|
fn calls_made(&self) -> Result<usize, super::Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_impl<A: ElectrumApi>() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deref() {
|
||||||
|
is_impl::<FakeApi>();
|
||||||
|
is_impl::<&FakeApi>();
|
||||||
|
is_impl::<Arc<FakeApi>>();
|
||||||
|
is_impl::<Box<FakeApi>>();
|
||||||
|
is_impl::<Cow<FakeApi>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/batch.rs
61
src/batch.rs
@ -2,10 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! This module contains definitions and helper functions used when making batch calls.
|
//! This module contains definitions and helper functions used when making batch calls.
|
||||||
|
|
||||||
use bitcoin::hashes::hex::ToHex;
|
|
||||||
use bitcoin::{Script, Txid};
|
use bitcoin::{Script, Txid};
|
||||||
|
|
||||||
use types::{Param, ToElectrumScriptHash};
|
use crate::types::{Call, Param, ToElectrumScriptHash};
|
||||||
|
|
||||||
/// Helper structure that caches all the requests before they are actually sent to the server.
|
/// Helper structure that caches all the requests before they are actually sent to the server.
|
||||||
///
|
///
|
||||||
@ -17,11 +16,17 @@ use types::{Param, ToElectrumScriptHash};
|
|||||||
/// [`Client`](../client/struct.Client.html), like
|
/// [`Client`](../client/struct.Client.html), like
|
||||||
/// [`batch_script_get_balance`](../client/struct.Client.html#method.batch_script_get_balance) to ask the
|
/// [`batch_script_get_balance`](../client/struct.Client.html#method.batch_script_get_balance) to ask the
|
||||||
/// server for the balance of multiple scripts with a single request.
|
/// server for the balance of multiple scripts with a single request.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Batch {
|
pub struct Batch {
|
||||||
calls: Vec<(String, Vec<Param>)>,
|
calls: Vec<Call>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Batch {
|
impl Batch {
|
||||||
|
/// Add a raw request to the batch queue
|
||||||
|
pub fn raw(&mut self, method: String, params: Vec<Param>) {
|
||||||
|
self.calls.push((method, params));
|
||||||
|
}
|
||||||
|
|
||||||
/// Add one `blockchain.scripthash.listunspent` request to the batch queue
|
/// Add one `blockchain.scripthash.listunspent` request to the batch queue
|
||||||
pub fn script_list_unspent(&mut self, script: &Script) {
|
pub fn script_list_unspent(&mut self, script: &Script) {
|
||||||
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
|
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
|
||||||
@ -43,19 +48,52 @@ impl Batch {
|
|||||||
.push((String::from("blockchain.scripthash.get_balance"), params));
|
.push((String::from("blockchain.scripthash.get_balance"), params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add one `blockchain.scripthash.listunspent` request to the batch queue
|
||||||
|
pub fn script_subscribe(&mut self, script: &Script) {
|
||||||
|
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
|
||||||
|
self.calls
|
||||||
|
.push((String::from("blockchain.scripthash.subscribe"), params));
|
||||||
|
}
|
||||||
|
|
||||||
/// Add one `blockchain.transaction.get` request to the batch queue
|
/// Add one `blockchain.transaction.get` request to the batch queue
|
||||||
pub fn transaction_get(&mut self, tx_hash: &Txid) {
|
pub fn transaction_get(&mut self, tx_hash: &Txid) {
|
||||||
let params = vec![Param::String(tx_hash.to_hex())];
|
let params = vec![Param::String(format!("{:x}", tx_hash))];
|
||||||
self.calls
|
self.calls
|
||||||
.push((String::from("blockchain.transaction.get"), params));
|
.push((String::from("blockchain.transaction.get"), params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add one `blockchain.transaction.get_merkle` request to the batch queue
|
||||||
|
pub fn transaction_get_merkle(&mut self, tx_hash_and_height: &(Txid, usize)) {
|
||||||
|
let (tx_hash, height) = tx_hash_and_height;
|
||||||
|
let params = vec![
|
||||||
|
Param::String(format!("{:x}", tx_hash)),
|
||||||
|
Param::Usize(*height),
|
||||||
|
];
|
||||||
|
self.calls
|
||||||
|
.push((String::from("blockchain.transaction.get_merkle"), params));
|
||||||
|
}
|
||||||
|
|
||||||
/// Add one `blockchain.estimatefee` request to the batch queue
|
/// Add one `blockchain.estimatefee` request to the batch queue
|
||||||
pub fn estimate_fee(&mut self, number: usize) {
|
pub fn estimate_fee(&mut self, number: usize) {
|
||||||
let params = vec![Param::Usize(number)];
|
let params = vec![Param::Usize(number)];
|
||||||
self.calls
|
self.calls
|
||||||
.push((String::from("blockchain.estimatefee"), params));
|
.push((String::from("blockchain.estimatefee"), params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add one `blockchain.block.get_header` request to the batch queue
|
||||||
|
pub fn block_header(&mut self, height: u32) {
|
||||||
|
let params = vec![Param::U32(height)];
|
||||||
|
self.calls
|
||||||
|
.push((String::from("blockchain.block.header"), params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator on the batch
|
||||||
|
pub fn iter(&self) -> BatchIter<'_> {
|
||||||
|
BatchIter {
|
||||||
|
batch: self,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::iter::IntoIterator for Batch {
|
impl std::iter::IntoIterator for Batch {
|
||||||
@ -67,8 +105,17 @@ impl std::iter::IntoIterator for Batch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for Batch {
|
pub struct BatchIter<'a> {
|
||||||
fn default() -> Self {
|
batch: &'a Batch,
|
||||||
Batch { calls: Vec::new() }
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> std::iter::Iterator for BatchIter<'a> {
|
||||||
|
type Item = &'a (String, Vec<Param>);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let val = self.batch.calls.get(self.index);
|
||||||
|
self.index += 1;
|
||||||
|
val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1232
src/client.rs
1232
src/client.rs
File diff suppressed because it is too large
Load Diff
149
src/config.rs
Normal file
149
src/config.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Configuration for an electrum client
|
||||||
|
///
|
||||||
|
/// Refer to [`Client::from_config`] and [`ClientType::from_config`].
|
||||||
|
///
|
||||||
|
/// [`Client::from_config`]: crate::Client::from_config
|
||||||
|
/// [`ClientType::from_config`]: crate::ClientType::from_config
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
/// Proxy socks5 configuration, default None
|
||||||
|
socks5: Option<Socks5Config>,
|
||||||
|
/// timeout in seconds, default None (depends on TcpStream default)
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
/// number of retry if any error, default 1
|
||||||
|
retry: u8,
|
||||||
|
/// when ssl, validate the domain, default true
|
||||||
|
validate_domain: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for Socks5
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Socks5Config {
|
||||||
|
/// The address of the socks5 service
|
||||||
|
pub addr: String,
|
||||||
|
/// Optional credential for the service
|
||||||
|
pub credentials: Option<Socks5Credential>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Credential for the proxy
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Socks5Credential {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [Config] Builder
|
||||||
|
pub struct ConfigBuilder {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigBuilder {
|
||||||
|
/// Create a builder with a default config, equivalent to [ConfigBuilder::default()]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ConfigBuilder {
|
||||||
|
config: Config::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the socks5 config if Some, it accept an `Option` because it's easier for the caller to use
|
||||||
|
/// in a method chain
|
||||||
|
pub fn socks5(mut self, socks5_config: Option<Socks5Config>) -> Self {
|
||||||
|
self.config.socks5 = socks5_config;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the timeout
|
||||||
|
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||||
|
self.config.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the retry attempts number
|
||||||
|
pub fn retry(mut self, retry: u8) -> Self {
|
||||||
|
self.config.retry = retry;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets if the domain has to be validated
|
||||||
|
pub fn validate_domain(mut self, validate_domain: bool) -> Self {
|
||||||
|
self.config.validate_domain = validate_domain;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the config and consume the builder
|
||||||
|
pub fn build(self) -> Config {
|
||||||
|
self.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConfigBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socks5Config {
|
||||||
|
/// Socks5Config constructor without credentials
|
||||||
|
pub fn new(addr: impl ToString) -> Self {
|
||||||
|
let addr = addr.to_string().replacen("socks5://", "", 1);
|
||||||
|
Socks5Config {
|
||||||
|
addr,
|
||||||
|
credentials: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Socks5Config constructor if we have credentials
|
||||||
|
pub fn with_credentials(addr: impl ToString, username: String, password: String) -> Self {
|
||||||
|
let mut config = Socks5Config::new(addr);
|
||||||
|
config.credentials = Some(Socks5Credential { username, password });
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Get the configuration for `socks5`
|
||||||
|
///
|
||||||
|
/// Set this with [`ConfigBuilder::socks5`]
|
||||||
|
pub fn socks5(&self) -> &Option<Socks5Config> {
|
||||||
|
&self.socks5
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the configuration for `retry`
|
||||||
|
///
|
||||||
|
/// Set this with [`ConfigBuilder::retry`]
|
||||||
|
pub fn retry(&self) -> u8 {
|
||||||
|
self.retry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the configuration for `timeout`
|
||||||
|
///
|
||||||
|
/// Set this with [`ConfigBuilder::timeout`]
|
||||||
|
pub fn timeout(&self) -> Option<Duration> {
|
||||||
|
self.timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the configuration for `validate_domain`
|
||||||
|
///
|
||||||
|
/// Set this with [`ConfigBuilder::validate_domain`]
|
||||||
|
pub fn validate_domain(&self) -> bool {
|
||||||
|
self.validate_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience method for calling [`ConfigBuilder::new`]
|
||||||
|
pub fn builder() -> ConfigBuilder {
|
||||||
|
ConfigBuilder::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
socks5: None,
|
||||||
|
timeout: None,
|
||||||
|
retry: 1,
|
||||||
|
validate_domain: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/lib.rs
64
src/lib.rs
@ -12,38 +12,72 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use electrum_client::Client;
|
//! use electrum_client::{Client, ElectrumApi};
|
||||||
//!
|
//!
|
||||||
//! let mut client = Client::new("kirsche.emzy.de:50001")?;
|
//! let mut client = Client::new("tcp://electrum.blockstream.info:50001")?;
|
||||||
//! let response = client.server_features()?;
|
//! let response = client.server_features()?;
|
||||||
//! # Ok::<(), electrum_client::Error>(())
|
//! # Ok::<(), electrum_client::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
pub extern crate bitcoin;
|
pub extern crate bitcoin;
|
||||||
|
extern crate core;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[cfg(feature = "use-openssl")]
|
#[cfg(feature = "use-openssl")]
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
any(feature = "default", feature = "use-rustls"),
|
any(
|
||||||
|
feature = "default",
|
||||||
|
feature = "use-rustls",
|
||||||
|
feature = "use-rustls-ring"
|
||||||
|
),
|
||||||
not(feature = "use-openssl")
|
not(feature = "use-openssl")
|
||||||
))]
|
))]
|
||||||
extern crate rustls;
|
extern crate rustls;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[cfg(any(feature = "default", feature = "proxy"))]
|
|
||||||
extern crate socks;
|
#[cfg(any(
|
||||||
#[cfg(any(feature = "use-rustls", feature = "default"))]
|
feature = "default",
|
||||||
extern crate webpki;
|
feature = "use-rustls",
|
||||||
#[cfg(any(feature = "use-rustls", feature = "default"))]
|
feature = "use-rustls-ring"
|
||||||
|
))]
|
||||||
extern crate webpki_roots;
|
extern crate webpki_roots;
|
||||||
|
|
||||||
pub mod batch;
|
#[cfg(any(feature = "default", feature = "proxy"))]
|
||||||
pub mod client;
|
extern crate byteorder;
|
||||||
mod stream;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test_stream;
|
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
|
#[cfg(all(unix, any(feature = "default", feature = "proxy")))]
|
||||||
|
extern crate libc;
|
||||||
|
#[cfg(all(windows, any(feature = "default", feature = "proxy")))]
|
||||||
|
extern crate winapi;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "default", feature = "proxy"))]
|
||||||
|
pub mod socks;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod batch;
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
all(feature = "proxy", feature = "use-openssl"),
|
||||||
|
all(feature = "proxy", feature = "use-rustls"),
|
||||||
|
all(feature = "proxy", feature = "use-rustls-ring")
|
||||||
|
))]
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
pub mod raw_client;
|
||||||
|
mod stream;
|
||||||
|
mod types;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use api::ElectrumApi;
|
||||||
pub use batch::Batch;
|
pub use batch::Batch;
|
||||||
pub use client::Client;
|
#[cfg(any(
|
||||||
|
all(feature = "proxy", feature = "use-openssl"),
|
||||||
|
all(feature = "proxy", feature = "use-rustls"),
|
||||||
|
all(feature = "proxy", feature = "use-rustls-ring")
|
||||||
|
))]
|
||||||
|
pub use client::*;
|
||||||
|
pub use config::{Config, ConfigBuilder, Socks5Config};
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
1678
src/raw_client.rs
Normal file
1678
src/raw_client.rs
Normal file
File diff suppressed because it is too large
Load Diff
162
src/socks/mod.rs
Normal file
162
src/socks/mod.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
//! SOCKS proxy clients
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
pub use self::v4::{Socks4Listener, Socks4Stream};
|
||||||
|
pub use self::v5::{Socks5Datagram, Socks5Listener, Socks5Stream};
|
||||||
|
|
||||||
|
mod v4;
|
||||||
|
mod v5;
|
||||||
|
mod writev;
|
||||||
|
|
||||||
|
/// A description of a connection target.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TargetAddr {
|
||||||
|
/// Connect to an IP address.
|
||||||
|
Ip(SocketAddr),
|
||||||
|
/// Connect to a fully qualified domain name.
|
||||||
|
///
|
||||||
|
/// The domain name will be passed along to the proxy server and DNS lookup
|
||||||
|
/// will happen there.
|
||||||
|
Domain(String, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSocketAddrs for TargetAddr {
|
||||||
|
type Iter = Iter;
|
||||||
|
|
||||||
|
fn to_socket_addrs(&self) -> io::Result<Iter> {
|
||||||
|
let inner = match *self {
|
||||||
|
TargetAddr::Ip(addr) => IterInner::Ip(Some(addr)),
|
||||||
|
TargetAddr::Domain(ref domain, port) => {
|
||||||
|
let it = (&**domain, port).to_socket_addrs()?;
|
||||||
|
IterInner::Domain(it)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Iter(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IterInner {
|
||||||
|
Ip(Option<SocketAddr>),
|
||||||
|
Domain(vec::IntoIter<SocketAddr>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over `SocketAddr`s associated with a `TargetAddr`.
|
||||||
|
pub struct Iter(IterInner);
|
||||||
|
|
||||||
|
impl Iterator for Iter {
|
||||||
|
type Item = SocketAddr;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<SocketAddr> {
|
||||||
|
match self.0 {
|
||||||
|
IterInner::Ip(ref mut addr) => addr.take(),
|
||||||
|
IterInner::Domain(ref mut it) => it.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for objects that can be converted to `TargetAddr`.
|
||||||
|
pub trait ToTargetAddr {
|
||||||
|
/// Converts the value of `self` to a `TargetAddr`.
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for TargetAddr {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddr {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
Ok(TargetAddr::Ip(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddrV4 {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddr::V4(*self).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddrV6 {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddr::V6(*self).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (Ipv4Addr, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddrV4::new(self.0, self.1).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (&str, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
// try to parse as an IP first
|
||||||
|
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
|
||||||
|
return (addr, self.1).to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
|
||||||
|
return (addr, self.1).to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TargetAddr::Domain(self.0.to_owned(), self.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for &str {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
// try to parse as an IP first
|
||||||
|
if let Ok(addr) = self.parse::<SocketAddrV4>() {
|
||||||
|
return addr.to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(addr) = self.parse::<SocketAddrV6>() {
|
||||||
|
return addr.to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the string by ':' and convert the second part to u16
|
||||||
|
let mut parts_iter = self.rsplitn(2, ':');
|
||||||
|
let port_str = match parts_iter.next() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid socket address",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let host = match parts_iter.next() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid socket address",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let port: u16 = match port_str.parse() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid port value",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(host, port).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
280
src/socks/v4.rs
Normal file
280
src/socks/v4.rs
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs};
|
||||||
|
|
||||||
|
use super::{TargetAddr, ToTargetAddr};
|
||||||
|
|
||||||
|
fn read_response(socket: &mut TcpStream) -> io::Result<SocketAddrV4> {
|
||||||
|
let mut response = [0u8; 8];
|
||||||
|
socket.read_exact(&mut response)?;
|
||||||
|
let mut response = &response[..];
|
||||||
|
|
||||||
|
if response.read_u8()? != 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid response version",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match response.read_u8()? {
|
||||||
|
90 => {}
|
||||||
|
91 => return Err(io::Error::other("request rejected or failed")),
|
||||||
|
92 => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"request rejected because SOCKS server cannot connect to \
|
||||||
|
idnetd on the client",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
93 => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"request rejected because the client program and identd \
|
||||||
|
report different user-ids",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid response code",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = response.read_u16::<BigEndian>()?;
|
||||||
|
let ip = Ipv4Addr::from(response.read_u32::<BigEndian>()?);
|
||||||
|
|
||||||
|
Ok(SocketAddrV4::new(ip, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SOCKS4 client.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Socks4Stream {
|
||||||
|
socket: TcpStream,
|
||||||
|
proxy_addr: SocketAddrV4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socks4Stream {
|
||||||
|
/// Connects to a target server through a SOCKS4 proxy.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// If `target` is a `TargetAddr::Domain`, the domain name will be forwarded
|
||||||
|
/// to the proxy server using the SOCKS4A protocol extension. If the proxy
|
||||||
|
/// server does not support SOCKS4A, consider performing the DNS lookup
|
||||||
|
/// locally and passing a `TargetAddr::Ip`.
|
||||||
|
pub fn connect<T, U>(proxy: T, target: U, userid: &str) -> io::Result<Socks4Stream>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
Self::connect_raw(1, proxy, target, userid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_raw<T, U>(command: u8, proxy: T, target: U, userid: &str) -> io::Result<Socks4Stream>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
let mut socket = TcpStream::connect(proxy)?;
|
||||||
|
|
||||||
|
let target = target.to_target_addr()?;
|
||||||
|
|
||||||
|
let mut packet = vec![];
|
||||||
|
let _ = packet.write_u8(4); // version
|
||||||
|
let _ = packet.write_u8(command); // command code
|
||||||
|
match target.to_target_addr()? {
|
||||||
|
TargetAddr::Ip(addr) => {
|
||||||
|
let addr = match addr {
|
||||||
|
SocketAddr::V4(addr) => addr,
|
||||||
|
SocketAddr::V6(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SOCKS4 does not support IPv6",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = packet.write_u16::<BigEndian>(addr.port());
|
||||||
|
let _ = packet.write_u32::<BigEndian>((*addr.ip()).into());
|
||||||
|
let _ = packet.write_all(userid.as_bytes());
|
||||||
|
let _ = packet.write_u8(0);
|
||||||
|
}
|
||||||
|
TargetAddr::Domain(ref host, port) => {
|
||||||
|
let _ = packet.write_u16::<BigEndian>(port);
|
||||||
|
let _ = packet.write_u32::<BigEndian>(Ipv4Addr::new(0, 0, 0, 1).into());
|
||||||
|
let _ = packet.write_all(userid.as_bytes());
|
||||||
|
let _ = packet.write_u8(0);
|
||||||
|
packet.extend(host.as_bytes());
|
||||||
|
let _ = packet.write_u8(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.write_all(&packet)?;
|
||||||
|
let proxy_addr = read_response(&mut socket)?;
|
||||||
|
|
||||||
|
Ok(Socks4Stream { socket, proxy_addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the proxy-side address of the connection between the proxy and
|
||||||
|
/// target server.
|
||||||
|
pub fn proxy_addr(&self) -> SocketAddrV4 {
|
||||||
|
self.proxy_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a shared reference to the inner `TcpStream`.
|
||||||
|
pub fn get_ref(&self) -> &TcpStream {
|
||||||
|
&self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the inner `TcpStream`.
|
||||||
|
pub fn get_mut(&mut self) -> &mut TcpStream {
|
||||||
|
&mut self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the `Socks4Stream`, returning the inner `TcpStream`.
|
||||||
|
pub fn into_inner(self) -> TcpStream {
|
||||||
|
self.socket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for Socks4Stream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.socket.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for &Socks4Stream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
(&self.socket).read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Socks4Stream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.socket.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.socket.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for &Socks4Stream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
(&self.socket).write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
(&self.socket).flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SOCKS4 BIND client.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Socks4Listener(Socks4Stream);
|
||||||
|
|
||||||
|
impl Socks4Listener {
|
||||||
|
/// Initiates a BIND request to the specified proxy.
|
||||||
|
///
|
||||||
|
/// The proxy will filter incoming connections based on the value of
|
||||||
|
/// `target`.
|
||||||
|
pub fn bind<T, U>(proxy: T, target: U, userid: &str) -> io::Result<Socks4Listener>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
Socks4Stream::connect_raw(2, proxy, target, userid).map(Socks4Listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address of the proxy-side TCP listener.
|
||||||
|
///
|
||||||
|
/// This should be forwarded to the remote process, which should open a
|
||||||
|
/// connection to it.
|
||||||
|
pub fn proxy_addr(&self) -> io::Result<SocketAddr> {
|
||||||
|
if self.0.proxy_addr.ip().octets() != [0, 0, 0, 0] {
|
||||||
|
Ok(SocketAddr::V4(self.0.proxy_addr()))
|
||||||
|
} else {
|
||||||
|
let port = self.0.proxy_addr.port();
|
||||||
|
let peer = match self.0.socket.peer_addr()? {
|
||||||
|
SocketAddr::V4(addr) => SocketAddr::V4(SocketAddrV4::new(*addr.ip(), port)),
|
||||||
|
SocketAddr::V6(addr) => SocketAddr::V6(SocketAddrV6::new(*addr.ip(), port, 0, 0)),
|
||||||
|
};
|
||||||
|
Ok(peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for the remote process to connect to the proxy server.
|
||||||
|
///
|
||||||
|
/// The value of `proxy_addr` should be forwarded to the remote process
|
||||||
|
/// before this method is called.
|
||||||
|
pub fn accept(mut self) -> io::Result<Socks4Stream> {
|
||||||
|
self.0.proxy_addr = read_response(&mut self.0.socket)?;
|
||||||
|
Ok(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::{SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn google_ip() -> SocketAddrV4 {
|
||||||
|
"google.com:80"
|
||||||
|
.to_socket_addrs()
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|a| match a {
|
||||||
|
SocketAddr::V4(a) => Some(a),
|
||||||
|
SocketAddr::V6(_) => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn google() {
|
||||||
|
let mut socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap();
|
||||||
|
|
||||||
|
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
|
||||||
|
let mut result = vec![];
|
||||||
|
socket.read_to_end(&mut result).unwrap();
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&result));
|
||||||
|
assert!(result.starts_with(b"HTTP/1.0"));
|
||||||
|
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore] // dante doesn't support SOCKS4A
|
||||||
|
fn google_dns() {
|
||||||
|
let mut socket = Socks4Stream::connect("127.0.0.1:8080", "google.com:80", "").unwrap();
|
||||||
|
|
||||||
|
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
|
||||||
|
let mut result = vec![];
|
||||||
|
socket.read_to_end(&mut result).unwrap();
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&result));
|
||||||
|
assert!(result.starts_with(b"HTTP/1.0"));
|
||||||
|
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn bind() {
|
||||||
|
// First figure out our local address that we'll be connecting from
|
||||||
|
let socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap();
|
||||||
|
let addr = socket.proxy_addr();
|
||||||
|
|
||||||
|
let listener = Socks4Listener::bind("127.0.0.1:1080", addr, "").unwrap();
|
||||||
|
let addr = listener.proxy_addr().unwrap();
|
||||||
|
let mut end = TcpStream::connect(addr).unwrap();
|
||||||
|
let mut conn = listener.accept().unwrap();
|
||||||
|
conn.write_all(b"hello world").unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let mut result = vec![];
|
||||||
|
end.read_to_end(&mut result).unwrap();
|
||||||
|
assert_eq!(result, b"hello world");
|
||||||
|
}
|
||||||
|
}
|
||||||
798
src/socks/v5.rs
Normal file
798
src/socks/v5.rs
Normal file
@ -0,0 +1,798 @@
|
|||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use std::cmp;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::net::{
|
||||||
|
Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs, UdpSocket,
|
||||||
|
};
|
||||||
|
use std::ptr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use super::writev::WritevExt;
|
||||||
|
use super::{TargetAddr, ToTargetAddr};
|
||||||
|
|
||||||
|
const MAX_ADDR_LEN: usize = 260;
|
||||||
|
|
||||||
|
fn read_addr<R: Read>(socket: &mut R) -> io::Result<TargetAddr> {
|
||||||
|
match socket.read_u8()? {
|
||||||
|
1 => {
|
||||||
|
let ip = Ipv4Addr::from(socket.read_u32::<BigEndian>()?);
|
||||||
|
let port = socket.read_u16::<BigEndian>()?;
|
||||||
|
Ok(TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(ip, port))))
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let len = socket.read_u8()?;
|
||||||
|
let mut domain = vec![0; len as usize];
|
||||||
|
socket.read_exact(&mut domain)?;
|
||||||
|
let domain = String::from_utf8(domain)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
let port = socket.read_u16::<BigEndian>()?;
|
||||||
|
Ok(TargetAddr::Domain(domain, port))
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
let mut ip = [0; 16];
|
||||||
|
socket.read_exact(&mut ip)?;
|
||||||
|
let ip = Ipv6Addr::from(ip);
|
||||||
|
let port = socket.read_u16::<BigEndian>()?;
|
||||||
|
Ok(TargetAddr::Ip(SocketAddr::V6(SocketAddrV6::new(
|
||||||
|
ip, port, 0, 0,
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
_ => Err(io::Error::other("unsupported address type")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_response(socket: &mut TcpStream) -> io::Result<TargetAddr> {
|
||||||
|
if socket.read_u8()? != 5 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid response version",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match socket.read_u8()? {
|
||||||
|
0 => {}
|
||||||
|
1 => return Err(io::Error::other("general SOCKS server failure")),
|
||||||
|
2 => return Err(io::Error::other("connection not allowed by ruleset")),
|
||||||
|
3 => return Err(io::Error::other("network unreachable")),
|
||||||
|
4 => return Err(io::Error::other("host unreachable")),
|
||||||
|
5 => return Err(io::Error::other("connection refused")),
|
||||||
|
6 => return Err(io::Error::other("TTL expired")),
|
||||||
|
7 => return Err(io::Error::other("command not supported")),
|
||||||
|
8 => return Err(io::Error::other("address kind not supported")),
|
||||||
|
_ => return Err(io::Error::other("unknown error")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if socket.read_u8()? != 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid reserved byte",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
read_addr(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_addr(mut packet: &mut [u8], target: &TargetAddr) -> io::Result<usize> {
|
||||||
|
let start_len = packet.len();
|
||||||
|
match *target {
|
||||||
|
TargetAddr::Ip(SocketAddr::V4(addr)) => {
|
||||||
|
packet.write_u8(1).unwrap();
|
||||||
|
packet.write_u32::<BigEndian>((*addr.ip()).into()).unwrap();
|
||||||
|
packet.write_u16::<BigEndian>(addr.port()).unwrap();
|
||||||
|
}
|
||||||
|
TargetAddr::Ip(SocketAddr::V6(addr)) => {
|
||||||
|
packet.write_u8(4).unwrap();
|
||||||
|
packet.write_all(&addr.ip().octets()).unwrap();
|
||||||
|
packet.write_u16::<BigEndian>(addr.port()).unwrap();
|
||||||
|
}
|
||||||
|
TargetAddr::Domain(ref domain, port) => {
|
||||||
|
packet.write_u8(3).unwrap();
|
||||||
|
if domain.len() > u8::MAX as usize {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"domain name too long",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
packet.write_u8(domain.len() as u8).unwrap();
|
||||||
|
packet.write_all(domain.as_bytes()).unwrap();
|
||||||
|
packet.write_u16::<BigEndian>(port).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(start_len - packet.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authentication methods
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Authentication<'a> {
|
||||||
|
Password {
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a str,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Authentication<'_> {
|
||||||
|
fn id(&self) -> u8 {
|
||||||
|
match *self {
|
||||||
|
Authentication::Password { .. } => 2,
|
||||||
|
Authentication::None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_no_auth(&self) -> bool {
|
||||||
|
matches!(*self, Authentication::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SOCKS5 client.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Socks5Stream {
|
||||||
|
socket: TcpStream,
|
||||||
|
proxy_addr: TargetAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socks5Stream {
|
||||||
|
/// Connects to a target server through a SOCKS5 proxy.
|
||||||
|
pub fn connect<T, U>(proxy: T, target: U, timeout: Option<Duration>) -> io::Result<Socks5Stream>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
Self::connect_raw(1, proxy, target, &Authentication::None, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a target server through a SOCKS5 proxy using given
|
||||||
|
/// username and password.
|
||||||
|
pub fn connect_with_password<T, U>(
|
||||||
|
proxy: T,
|
||||||
|
target: U,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> io::Result<Socks5Stream>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
let auth = Authentication::Password { username, password };
|
||||||
|
Self::connect_raw(1, proxy, target, &auth, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_raw<T, U>(
|
||||||
|
command: u8,
|
||||||
|
proxy: T,
|
||||||
|
target: U,
|
||||||
|
auth: &Authentication,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> io::Result<Socks5Stream>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
let mut socket = if let Some(timeout) = timeout {
|
||||||
|
let addr = proxy.to_socket_addrs()?.next().unwrap();
|
||||||
|
TcpStream::connect_timeout(&addr, timeout)?
|
||||||
|
} else {
|
||||||
|
TcpStream::connect(proxy)?
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.set_read_timeout(timeout)?;
|
||||||
|
socket.set_write_timeout(timeout)?;
|
||||||
|
|
||||||
|
let target = target.to_target_addr()?;
|
||||||
|
|
||||||
|
let packet_len = if auth.is_no_auth() { 3 } else { 4 };
|
||||||
|
let packet = [
|
||||||
|
5, // protocol version
|
||||||
|
if auth.is_no_auth() { 1 } else { 2 }, // method count
|
||||||
|
auth.id(), // method
|
||||||
|
0, // no auth (always offered)
|
||||||
|
];
|
||||||
|
socket.write_all(&packet[..packet_len])?;
|
||||||
|
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
socket.read_exact(&mut buf)?;
|
||||||
|
let response_version = buf[0];
|
||||||
|
let selected_method = buf[1];
|
||||||
|
|
||||||
|
if response_version != 5 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid response version",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected_method == 0xff {
|
||||||
|
return Err(io::Error::other("no acceptable auth methods"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected_method != auth.id() && selected_method != Authentication::None.id() {
|
||||||
|
return Err(io::Error::other("unknown auth method"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match *auth {
|
||||||
|
Authentication::Password { username, password } if selected_method == auth.id() => {
|
||||||
|
Self::password_authentication(&mut socket, username, password)?
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packet = [0; MAX_ADDR_LEN + 3];
|
||||||
|
packet[0] = 5; // protocol version
|
||||||
|
packet[1] = command; // command
|
||||||
|
packet[2] = 0; // reserved
|
||||||
|
let len = write_addr(&mut packet[3..], &target)?;
|
||||||
|
socket.write_all(&packet[..len + 3])?;
|
||||||
|
|
||||||
|
let proxy_addr = read_response(&mut socket)?;
|
||||||
|
|
||||||
|
Ok(Socks5Stream { socket, proxy_addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn password_authentication(
|
||||||
|
socket: &mut TcpStream,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if username.is_empty() || username.len() > 255 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid username",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if password.is_empty() || password.len() > 255 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"invalid password",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packet = [0; 515];
|
||||||
|
let packet_size = 3 + username.len() + password.len();
|
||||||
|
packet[0] = 1; // version
|
||||||
|
packet[1] = username.len() as u8;
|
||||||
|
packet[2..2 + username.len()].copy_from_slice(username.as_bytes());
|
||||||
|
packet[2 + username.len()] = password.len() as u8;
|
||||||
|
packet[3 + username.len()..packet_size].copy_from_slice(password.as_bytes());
|
||||||
|
socket.write_all(&packet[..packet_size])?;
|
||||||
|
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
socket.read_exact(&mut buf)?;
|
||||||
|
if buf[0] != 1 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid response version",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if buf[1] != 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"password authentication failed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the proxy-side address of the connection between the proxy and
|
||||||
|
/// target server.
|
||||||
|
pub fn proxy_addr(&self) -> &TargetAddr {
|
||||||
|
&self.proxy_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a shared reference to the inner `TcpStream`.
|
||||||
|
pub fn get_ref(&self) -> &TcpStream {
|
||||||
|
&self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the inner `TcpStream`.
|
||||||
|
pub fn get_mut(&mut self) -> &mut TcpStream {
|
||||||
|
&mut self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the `Socks5Stream`, returning the inner `TcpStream`.
|
||||||
|
pub fn into_inner(self) -> TcpStream {
|
||||||
|
self.socket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for Socks5Stream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.socket.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for &Socks5Stream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
(&self.socket).read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Socks5Stream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.socket.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.socket.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for &Socks5Stream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
(&self.socket).write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
(&self.socket).flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SOCKS5 BIND client.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Socks5Listener(Socks5Stream);
|
||||||
|
|
||||||
|
impl Socks5Listener {
|
||||||
|
/// Initiates a BIND request to the specified proxy.
|
||||||
|
///
|
||||||
|
/// The proxy will filter incoming connections based on the value of
|
||||||
|
/// `target`.
|
||||||
|
pub fn bind<T, U>(proxy: T, target: U, timeout: Option<Duration>) -> io::Result<Socks5Listener>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
Socks5Stream::connect_raw(2, proxy, target, &Authentication::None, timeout)
|
||||||
|
.map(Socks5Listener)
|
||||||
|
}
|
||||||
|
/// Initiates a BIND request to the specified proxy using given username
|
||||||
|
/// and password.
|
||||||
|
///
|
||||||
|
/// The proxy will filter incoming connections based on the value of
|
||||||
|
/// `target`.
|
||||||
|
pub fn bind_with_password<T, U>(
|
||||||
|
proxy: T,
|
||||||
|
target: U,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> io::Result<Socks5Listener>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToTargetAddr,
|
||||||
|
{
|
||||||
|
let auth = Authentication::Password { username, password };
|
||||||
|
Socks5Stream::connect_raw(2, proxy, target, &auth, timeout).map(Socks5Listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address of the proxy-side TCP listener.
|
||||||
|
///
|
||||||
|
/// This should be forwarded to the remote process, which should open a
|
||||||
|
/// connection to it.
|
||||||
|
pub fn proxy_addr(&self) -> &TargetAddr {
|
||||||
|
&self.0.proxy_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for the remote process to connect to the proxy server.
|
||||||
|
///
|
||||||
|
/// The value of `proxy_addr` should be forwarded to the remote process
|
||||||
|
/// before this method is called.
|
||||||
|
pub fn accept(mut self) -> io::Result<Socks5Stream> {
|
||||||
|
self.0.proxy_addr = read_response(&mut self.0.socket)?;
|
||||||
|
Ok(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SOCKS5 UDP client.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Socks5Datagram {
|
||||||
|
socket: UdpSocket,
|
||||||
|
// keeps the session alive
|
||||||
|
stream: Socks5Stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socks5Datagram {
|
||||||
|
/// Creates a UDP socket bound to the specified address which will have its
|
||||||
|
/// traffic routed through the specified proxy.
|
||||||
|
pub fn bind<T, U>(proxy: T, addr: U, timeout: Option<Duration>) -> io::Result<Socks5Datagram>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToSocketAddrs,
|
||||||
|
{
|
||||||
|
Self::bind_internal(proxy, addr, &Authentication::None, timeout)
|
||||||
|
}
|
||||||
|
/// Creates a UDP socket bound to the specified address which will have its
|
||||||
|
/// traffic routed through the specified proxy. The given username and password
|
||||||
|
/// is used to authenticate to the SOCKS proxy.
|
||||||
|
pub fn bind_with_password<T, U>(
|
||||||
|
proxy: T,
|
||||||
|
addr: U,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> io::Result<Socks5Datagram>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToSocketAddrs,
|
||||||
|
{
|
||||||
|
let auth = Authentication::Password { username, password };
|
||||||
|
Self::bind_internal(proxy, addr, &auth, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_internal<T, U>(
|
||||||
|
proxy: T,
|
||||||
|
addr: U,
|
||||||
|
auth: &Authentication,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> io::Result<Socks5Datagram>
|
||||||
|
where
|
||||||
|
T: ToSocketAddrs,
|
||||||
|
U: ToSocketAddrs,
|
||||||
|
{
|
||||||
|
// we don't know what our IP is from the perspective of the proxy, so
|
||||||
|
// don't try to pass `addr` in here.
|
||||||
|
let dst = TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::new(0, 0, 0, 0),
|
||||||
|
0,
|
||||||
|
)));
|
||||||
|
let stream = Socks5Stream::connect_raw(3, proxy, dst, auth, timeout)?;
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind(addr)?;
|
||||||
|
socket.connect(&stream.proxy_addr)?;
|
||||||
|
|
||||||
|
Ok(Socks5Datagram { socket, stream })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `UdpSocket::send_to`.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The SOCKS protocol inserts a header at the beginning of the message. The
|
||||||
|
/// header will be 10 bytes for an IPv4 address, 22 bytes for an IPv6
|
||||||
|
/// address, and 7 bytes plus the length of the domain for a domain address.
|
||||||
|
pub fn send_to<A>(&self, buf: &[u8], addr: A) -> io::Result<usize>
|
||||||
|
where
|
||||||
|
A: ToTargetAddr,
|
||||||
|
{
|
||||||
|
let addr = addr.to_target_addr()?;
|
||||||
|
|
||||||
|
let mut header = [0; MAX_ADDR_LEN + 3];
|
||||||
|
// first two bytes are reserved at 0
|
||||||
|
// third byte is the fragment id at 0
|
||||||
|
let len = write_addr(&mut header[3..], &addr)?;
|
||||||
|
|
||||||
|
self.socket.writev([&header[..len + 3], buf])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `UdpSocket::recv_from`.
|
||||||
|
pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, TargetAddr)> {
|
||||||
|
let mut header = [0; MAX_ADDR_LEN + 3];
|
||||||
|
let len = self.socket.readv([&mut header, buf])?;
|
||||||
|
|
||||||
|
let overflow = len.saturating_sub(header.len());
|
||||||
|
|
||||||
|
let header_len = cmp::min(header.len(), len);
|
||||||
|
let mut header = &mut &header[..header_len];
|
||||||
|
|
||||||
|
if header.read_u16::<BigEndian>()? != 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid reserved bytes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if header.read_u8()? != 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid fragment id",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let addr = read_addr(&mut header)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ptr::copy(buf.as_ptr(), buf.as_mut_ptr().add(header.len()), overflow);
|
||||||
|
}
|
||||||
|
buf[..header.len()].copy_from_slice(header);
|
||||||
|
|
||||||
|
Ok((header.len() + overflow, addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the address of the proxy-side UDP socket through which all
|
||||||
|
/// messages will be routed.
|
||||||
|
pub fn proxy_addr(&self) -> &TargetAddr {
|
||||||
|
&self.stream.proxy_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a shared reference to the inner socket.
|
||||||
|
pub fn get_ref(&self) -> &UdpSocket {
|
||||||
|
&self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the inner socket.
|
||||||
|
pub fn get_mut(&mut self) -> &mut UdpSocket {
|
||||||
|
&mut self.socket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::{TcpStream, ToSocketAddrs, UdpSocket};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const SOCKS_PROXY_NO_AUTH_ONLY: &str = "127.0.0.1:1080";
|
||||||
|
const SOCKS_PROXY_PASSWD_ONLY: &str = "127.0.0.1:1081";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn google_no_auth() {
|
||||||
|
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
let socket = Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap();
|
||||||
|
google(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn google_with_password() {
|
||||||
|
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
let socket = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
"testuser",
|
||||||
|
"testpass",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
google(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn google(mut socket: Socks5Stream) {
|
||||||
|
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
|
||||||
|
let mut result = vec![];
|
||||||
|
socket.read_to_end(&mut result).unwrap();
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&result));
|
||||||
|
assert!(result.starts_with(b"HTTP/1.0"));
|
||||||
|
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn google_dns() {
|
||||||
|
let mut socket =
|
||||||
|
Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap();
|
||||||
|
|
||||||
|
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
|
||||||
|
let mut result = vec![];
|
||||||
|
socket.read_to_end(&mut result).unwrap();
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&result));
|
||||||
|
assert!(result.starts_with(b"HTTP/1.0"));
|
||||||
|
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn bind_no_auth() {
|
||||||
|
let addr = find_address();
|
||||||
|
let listener = Socks5Listener::bind(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap();
|
||||||
|
bind(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn bind_with_password_supported_but_no_auth_used() {
|
||||||
|
let addr = find_address();
|
||||||
|
let listener = Socks5Listener::bind_with_password(
|
||||||
|
SOCKS_PROXY_NO_AUTH_ONLY,
|
||||||
|
addr,
|
||||||
|
"unused_and_invalid_username",
|
||||||
|
"unused_and_invalid_password",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
bind(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn bind_with_password() {
|
||||||
|
let addr = find_address();
|
||||||
|
let listener = Socks5Listener::bind_with_password(
|
||||||
|
"127.0.0.1:1081",
|
||||||
|
addr,
|
||||||
|
"testuser",
|
||||||
|
"testpass",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
bind(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind(listener: Socks5Listener) {
|
||||||
|
let addr = listener.proxy_addr().clone();
|
||||||
|
let mut end = TcpStream::connect(addr).unwrap();
|
||||||
|
let mut conn = listener.accept().unwrap();
|
||||||
|
conn.write_all(b"hello world").unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let mut result = vec![];
|
||||||
|
end.read_to_end(&mut result).unwrap();
|
||||||
|
assert_eq!(result, b"hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
// First figure out our local address that we'll be connecting from
|
||||||
|
fn find_address() -> TargetAddr {
|
||||||
|
let socket =
|
||||||
|
Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap();
|
||||||
|
socket.proxy_addr().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn associate_no_auth() {
|
||||||
|
let socks =
|
||||||
|
Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15410", None).unwrap();
|
||||||
|
associate(socks, "127.0.0.1:15411");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn associate_with_password() {
|
||||||
|
let socks = Socks5Datagram::bind_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
"127.0.0.1:15414",
|
||||||
|
"testuser",
|
||||||
|
"testpass",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
associate(socks, "127.0.0.1:15415");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn associate(socks: Socks5Datagram, socket_addr: &str) {
|
||||||
|
let socket = UdpSocket::bind(socket_addr).unwrap();
|
||||||
|
|
||||||
|
socks.send_to(b"hello world!", socket_addr).unwrap();
|
||||||
|
let mut buf = [0; 13];
|
||||||
|
let (len, addr) = socket.recv_from(&mut buf).unwrap();
|
||||||
|
assert_eq!(len, 12);
|
||||||
|
assert_eq!(&buf[..12], b"hello world!");
|
||||||
|
|
||||||
|
socket.send_to(b"hello world!", addr).unwrap();
|
||||||
|
|
||||||
|
let len = socks.recv_from(&mut buf).unwrap().0;
|
||||||
|
assert_eq!(len, 12);
|
||||||
|
assert_eq!(&buf[..12], b"hello world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn associate_long() {
|
||||||
|
let socks =
|
||||||
|
Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15412", None).unwrap();
|
||||||
|
let socket_addr = "127.0.0.1:15413";
|
||||||
|
let socket = UdpSocket::bind(socket_addr).unwrap();
|
||||||
|
|
||||||
|
let mut msg = vec![];
|
||||||
|
for i in 0..(MAX_ADDR_LEN + 100) {
|
||||||
|
msg.push(i as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
socks.send_to(&msg, socket_addr).unwrap();
|
||||||
|
let mut buf = vec![0; msg.len() + 1];
|
||||||
|
let (len, addr) = socket.recv_from(&mut buf).unwrap();
|
||||||
|
assert_eq!(len, msg.len());
|
||||||
|
assert_eq!(msg, &buf[..msg.len()]);
|
||||||
|
|
||||||
|
socket.send_to(&msg, addr).unwrap();
|
||||||
|
|
||||||
|
let mut buf = vec![0; msg.len() + 1];
|
||||||
|
let len = socks.recv_from(&mut buf).unwrap().0;
|
||||||
|
assert_eq!(len, msg.len());
|
||||||
|
assert_eq!(msg, &buf[..msg.len()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn incorrect_password() {
|
||||||
|
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
"testuser",
|
||||||
|
"invalid",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
|
||||||
|
assert_eq!(err.to_string(), "password authentication failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn auth_method_not_supported() {
|
||||||
|
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
let err = Socks5Stream::connect(SOCKS_PROXY_PASSWD_ONLY, addr, None).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::Other);
|
||||||
|
assert_eq!(err.to_string(), "no acceptable auth methods");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn username_and_password_length() {
|
||||||
|
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(1),
|
||||||
|
&string_of_size(1),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
|
||||||
|
assert_eq!(err.to_string(), "password authentication failed");
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(255),
|
||||||
|
&string_of_size(255),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
|
||||||
|
assert_eq!(err.to_string(), "password authentication failed");
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(0),
|
||||||
|
&string_of_size(255),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||||
|
assert_eq!(err.to_string(), "invalid username");
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(256),
|
||||||
|
&string_of_size(255),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||||
|
assert_eq!(err.to_string(), "invalid username");
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(255),
|
||||||
|
&string_of_size(0),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||||
|
assert_eq!(err.to_string(), "invalid password");
|
||||||
|
|
||||||
|
let err = Socks5Stream::connect_with_password(
|
||||||
|
SOCKS_PROXY_PASSWD_ONLY,
|
||||||
|
addr,
|
||||||
|
&string_of_size(255),
|
||||||
|
&string_of_size(256),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||||
|
assert_eq!(err.to_string(), "invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_of_size(size: usize) -> String {
|
||||||
|
(0..size).map(|_| 'x').collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/socks/writev.rs
Normal file
133
src/socks/writev.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::net::UdpSocket;
|
||||||
|
|
||||||
|
pub trait WritevExt {
|
||||||
|
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize>;
|
||||||
|
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod imp {
|
||||||
|
use libc;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl WritevExt for UdpSocket {
|
||||||
|
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize> {
|
||||||
|
unsafe {
|
||||||
|
let iovecs = [
|
||||||
|
libc::iovec {
|
||||||
|
iov_base: bufs[0].as_ptr() as *const _ as *mut _,
|
||||||
|
iov_len: bufs[0].len(),
|
||||||
|
},
|
||||||
|
libc::iovec {
|
||||||
|
iov_base: bufs[1].as_ptr() as *const _ as *mut _,
|
||||||
|
iov_len: bufs[1].len(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let r = libc::writev(self.as_raw_fd(), iovecs.as_ptr(), 2);
|
||||||
|
if r < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(r as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize> {
|
||||||
|
unsafe {
|
||||||
|
let mut iovecs = [
|
||||||
|
libc::iovec {
|
||||||
|
iov_base: bufs[0].as_mut_ptr() as *mut _,
|
||||||
|
iov_len: bufs[0].len(),
|
||||||
|
},
|
||||||
|
libc::iovec {
|
||||||
|
iov_base: bufs[1].as_mut_ptr() as *mut _,
|
||||||
|
iov_len: bufs[1].len(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let r = libc::readv(self.as_raw_fd(), iovecs.as_mut_ptr(), 2);
|
||||||
|
if r < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(r as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod imp {
|
||||||
|
use std::os::windows::io::AsRawSocket;
|
||||||
|
use std::ptr;
|
||||||
|
use winapi::shared::minwindef;
|
||||||
|
use winapi::shared::ws2def;
|
||||||
|
use winapi::um::winsock2;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl WritevExt for UdpSocket {
|
||||||
|
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize> {
|
||||||
|
unsafe {
|
||||||
|
let mut wsabufs = [
|
||||||
|
ws2def::WSABUF {
|
||||||
|
len: bufs[0].len() as winsock2::u_long,
|
||||||
|
buf: bufs[0].as_ptr() as *const _ as *mut _,
|
||||||
|
},
|
||||||
|
ws2def::WSABUF {
|
||||||
|
len: bufs[1].len() as winsock2::u_long,
|
||||||
|
buf: bufs[1].as_ptr() as *const _ as *mut _,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut sent = 0;
|
||||||
|
let r = winsock2::WSASend(
|
||||||
|
self.as_raw_socket() as usize,
|
||||||
|
wsabufs.as_mut_ptr(),
|
||||||
|
bufs.len() as minwindef::DWORD,
|
||||||
|
&mut sent,
|
||||||
|
0,
|
||||||
|
ptr::null_mut(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if r == 0 {
|
||||||
|
Ok(sent as usize)
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize> {
|
||||||
|
unsafe {
|
||||||
|
let mut wsabufs = [
|
||||||
|
ws2def::WSABUF {
|
||||||
|
len: bufs[0].len() as winsock2::u_long,
|
||||||
|
buf: bufs[0].as_mut_ptr() as *mut _,
|
||||||
|
},
|
||||||
|
ws2def::WSABUF {
|
||||||
|
len: bufs[1].len() as winsock2::u_long,
|
||||||
|
buf: bufs[1].as_mut_ptr() as *mut _,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut recved = 0;
|
||||||
|
let mut flags = 0;
|
||||||
|
let r = winsock2::WSARecv(
|
||||||
|
self.as_raw_socket() as usize,
|
||||||
|
wsabufs.as_mut_ptr(),
|
||||||
|
bufs.len() as minwindef::DWORD,
|
||||||
|
&mut recved,
|
||||||
|
&mut flags,
|
||||||
|
ptr::null_mut(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if r == 0 {
|
||||||
|
Ok(recved as usize)
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
use log::error;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@ -6,17 +7,35 @@ pub struct ClonableStream<T: Read + Write>(Arc<Mutex<T>>);
|
|||||||
|
|
||||||
impl<T: Read + Write> Read for ClonableStream<T> {
|
impl<T: Read + Write> Read for ClonableStream<T> {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
self.0.lock().unwrap().read(buf)
|
self.0
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| {
|
||||||
|
error!("Unable to acquire lock on ClonableStream read operation");
|
||||||
|
io::Error::from(io::ErrorKind::BrokenPipe)
|
||||||
|
})?
|
||||||
|
.read(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Write> Write for ClonableStream<T> {
|
impl<T: Read + Write> Write for ClonableStream<T> {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
self.0.lock().unwrap().write(buf)
|
self.0
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| {
|
||||||
|
error!("Unable to acquire lock on ClonableStream write operation");
|
||||||
|
io::Error::from(io::ErrorKind::BrokenPipe)
|
||||||
|
})?
|
||||||
|
.write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
self.0.lock().unwrap().flush()
|
self.0
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| {
|
||||||
|
error!("Unable to acquire lock on ClonableStream flush operation");
|
||||||
|
io::Error::from(io::ErrorKind::BrokenPipe)
|
||||||
|
})?
|
||||||
|
.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,10 +50,3 @@ impl<T: Read + Write> Clone for ClonableStream<T> {
|
|||||||
ClonableStream(Arc::clone(&self.0))
|
ClonableStream(Arc::clone(&self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl<T: Read + Write> ClonableStream<T> {
|
|
||||||
pub fn stream(&self) -> Arc<Mutex<T>> {
|
|
||||||
Arc::clone(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
use std::io::{Read, Result, Write};
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
|
|
||||||
pub struct TestStream {
|
|
||||||
pub file: File,
|
|
||||||
pub buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestStream {
|
|
||||||
pub fn new(file: File) -> Self {
|
|
||||||
TestStream {
|
|
||||||
file,
|
|
||||||
buffer: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read for TestStream {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
|
||||||
self.file.read(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for TestStream {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
|
||||||
self.buffer.write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<()> {
|
|
||||||
self.buffer.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
214
src/types.rs
214
src/types.rs
@ -2,25 +2,37 @@
|
|||||||
//!
|
//!
|
||||||
//! This module contains definitions of all the complex data structures that are returned by calls
|
//! This module contains definitions of all the complex data structures that are returned by calls
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bitcoin::blockdata::block;
|
use bitcoin::blockdata::block;
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::consensus::encode::deserialize;
|
||||||
use bitcoin::hashes::{sha256, Hash};
|
use bitcoin::hashes::{sha256, Hash};
|
||||||
|
use bitcoin::hex::{DisplayHex, FromHex};
|
||||||
use bitcoin::{Script, Txid};
|
use bitcoin::{Script, Txid};
|
||||||
|
|
||||||
use serde::{de, Deserialize, Serialize};
|
use serde::{de, Deserialize, Serialize};
|
||||||
|
|
||||||
static JSONRPC_2_0: &str = "2.0";
|
static JSONRPC_2_0: &str = "2.0";
|
||||||
|
|
||||||
|
pub(crate) type Call = (String, Vec<Param>);
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
/// A single parameter of a [`Request`](struct.Request.html)
|
/// A single parameter of a [`Request`](struct.Request.html)
|
||||||
pub enum Param {
|
pub enum Param {
|
||||||
|
/// Integer parameter
|
||||||
|
U32(u32),
|
||||||
/// Integer parameter
|
/// Integer parameter
|
||||||
Usize(usize),
|
Usize(usize),
|
||||||
/// String parameter
|
/// String parameter
|
||||||
String(String),
|
String(String),
|
||||||
/// Boolean parameter
|
/// Boolean parameter
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
|
/// Bytes array parameter
|
||||||
|
Bytes(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
@ -38,7 +50,7 @@ pub struct Request<'a> {
|
|||||||
|
|
||||||
impl<'a> Request<'a> {
|
impl<'a> Request<'a> {
|
||||||
/// Creates a new request with a default id
|
/// Creates a new request with a default id
|
||||||
pub fn new(method: &'a str, params: Vec<Param>) -> Self {
|
fn new(method: &'a str, params: Vec<Param>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: 0,
|
id: 0,
|
||||||
jsonrpc: JSONRPC_2_0,
|
jsonrpc: JSONRPC_2_0,
|
||||||
@ -56,12 +68,37 @@ impl<'a> Request<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub struct Hex32Bytes(#[serde(deserialize_with = "from_hex", serialize_with = "to_hex")] [u8; 32]);
|
||||||
|
|
||||||
|
impl Deref for Hex32Bytes {
|
||||||
|
type Target = [u8; 32];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 32]> for Hex32Bytes {
|
||||||
|
fn from(other: [u8; 32]) -> Hex32Bytes {
|
||||||
|
Hex32Bytes(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hex32Bytes {
|
||||||
|
pub(crate) fn to_hex(self) -> String {
|
||||||
|
self.0.to_lower_hex_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Format used by the Electrum server to identify an address. The reverse sha256 hash of the
|
/// Format used by the Electrum server to identify an address. The reverse sha256 hash of the
|
||||||
/// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes).
|
/// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes).
|
||||||
pub type ScriptHash = [u8; 32];
|
pub type ScriptHash = Hex32Bytes;
|
||||||
|
|
||||||
/// Binary blob that condenses all the activity of an address. Used to detect changes without
|
/// Binary blob that condenses all the activity of an address. Used to detect changes without
|
||||||
/// having to compare potentially long lists of transactions.
|
/// having to compare potentially long lists of transactions.
|
||||||
pub type ScriptStatus = [u8; 32];
|
pub type ScriptStatus = Hex32Bytes;
|
||||||
|
|
||||||
/// Trait used to convert a struct into the Electrum representation of an address
|
/// Trait used to convert a struct into the Electrum representation of an address
|
||||||
pub trait ToElectrumScriptHash {
|
pub trait ToElectrumScriptHash {
|
||||||
@ -71,10 +108,10 @@ pub trait ToElectrumScriptHash {
|
|||||||
|
|
||||||
impl ToElectrumScriptHash for Script {
|
impl ToElectrumScriptHash for Script {
|
||||||
fn to_electrum_scripthash(&self) -> ScriptHash {
|
fn to_electrum_scripthash(&self) -> ScriptHash {
|
||||||
let mut result = sha256::Hash::hash(self.as_bytes()).into_inner();
|
let mut result = sha256::Hash::hash(self.as_bytes()).to_byte_array();
|
||||||
result.reverse();
|
result.reverse();
|
||||||
|
|
||||||
result
|
result.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +124,13 @@ where
|
|||||||
T::from_hex(&s).map_err(de::Error::custom)
|
T::from_hex(&s).map_err(de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_hex<S>(bytes: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&bytes.to_lower_hex_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn from_hex_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
fn from_hex_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||||
where
|
where
|
||||||
T: FromHex + std::fmt::Debug,
|
T: FromHex + std::fmt::Debug,
|
||||||
@ -107,28 +151,28 @@ where
|
|||||||
Ok(answer)
|
Ok(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_hex_header<'de, D>(deserializer: D) -> Result<block::BlockHeader, D::Error>
|
fn from_hex_header<'de, D>(deserializer: D) -> Result<block::Header, D::Error>
|
||||||
where
|
where
|
||||||
D: de::Deserializer<'de>,
|
D: de::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
use bitcoin::consensus::deserialize;
|
|
||||||
|
|
||||||
let vec: Vec<u8> = from_hex(deserializer)?;
|
let vec: Vec<u8> = from_hex(deserializer)?;
|
||||||
deserialize(&vec).map_err(de::Error::custom)
|
deserialize(&vec).map_err(de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request.
|
/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct GetHistoryRes {
|
pub struct GetHistoryRes {
|
||||||
/// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
|
/// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
|
||||||
/// its inputs are unconfirmed too.
|
/// its inputs are unconfirmed too.
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
/// Txid of the transaction.
|
/// Txid of the transaction.
|
||||||
pub tx_hash: Txid,
|
pub tx_hash: Txid,
|
||||||
|
/// Fee of the transaction.
|
||||||
|
pub fee: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request.
|
/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct ListUnspentRes {
|
pub struct ListUnspentRes {
|
||||||
/// Confirmation height of the transaction that created this output.
|
/// Confirmation height of the transaction that created this output.
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
@ -141,7 +185,7 @@ pub struct ListUnspentRes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct ServerFeaturesRes {
|
pub struct ServerFeaturesRes {
|
||||||
/// Server version reported.
|
/// Server version reported.
|
||||||
pub server_version: String,
|
pub server_version: String,
|
||||||
@ -159,7 +203,7 @@ pub struct ServerFeaturesRes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct GetHeadersRes {
|
pub struct GetHeadersRes {
|
||||||
/// Maximum number of headers returned in a single response.
|
/// Maximum number of headers returned in a single response.
|
||||||
pub max: usize,
|
pub max: usize,
|
||||||
@ -170,20 +214,22 @@ pub struct GetHeadersRes {
|
|||||||
pub raw_headers: Vec<u8>,
|
pub raw_headers: Vec<u8>,
|
||||||
/// Array of block headers.
|
/// Array of block headers.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub headers: Vec<block::BlockHeader>,
|
pub headers: Vec<block::Header>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request.
|
/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct GetBalanceRes {
|
pub struct GetBalanceRes {
|
||||||
/// Confirmed balance in Satoshis for the address.
|
/// Confirmed balance in Satoshis for the address.
|
||||||
pub confirmed: u64,
|
pub confirmed: u64,
|
||||||
/// Unconfirmed balance in Satoshis for the address.
|
/// Unconfirmed balance in Satoshis for the address.
|
||||||
pub unconfirmed: u64,
|
///
|
||||||
|
/// Some servers (e.g. `electrs`) return this as a negative value.
|
||||||
|
pub unconfirmed: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request.
|
/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct GetMerkleRes {
|
pub struct GetMerkleRes {
|
||||||
/// Height of the block that confirmed the transaction
|
/// Height of the block that confirmed the transaction
|
||||||
pub block_height: usize,
|
pub block_height: usize,
|
||||||
@ -194,18 +240,50 @@ pub struct GetMerkleRes {
|
|||||||
pub merkle: Vec<[u8; 32]>,
|
pub merkle: Vec<[u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response to a [`txid_from_pos_with_merkle`](../client/struct.Client.html#method.txid_from_pos_with_merkle)
|
||||||
|
/// request.
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct TxidFromPosRes {
|
||||||
|
/// Txid of the transaction.
|
||||||
|
pub tx_hash: Txid,
|
||||||
|
/// The merkle path of the transaction.
|
||||||
|
#[serde(deserialize_with = "from_hex_array")]
|
||||||
|
pub merkle: Vec<[u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Notification of a new block header
|
/// Notification of a new block header
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct HeaderNotification {
|
pub struct HeaderNotification {
|
||||||
/// New block height.
|
/// New block height.
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
/// Newly added header.
|
/// Newly added header.
|
||||||
#[serde(rename = "hex", deserialize_with = "from_hex_header")]
|
#[serde(rename = "hex", deserialize_with = "from_hex_header")]
|
||||||
pub header: block::BlockHeader,
|
pub header: block::Header,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notification of a new block header with the header encoded as raw bytes
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct RawHeaderNotification {
|
||||||
|
/// New block height.
|
||||||
|
pub height: usize,
|
||||||
|
/// Newly added header.
|
||||||
|
#[serde(rename = "hex", deserialize_with = "from_hex")]
|
||||||
|
pub header: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<RawHeaderNotification> for HeaderNotification {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(raw: RawHeaderNotification) -> Result<Self, Self::Error> {
|
||||||
|
Ok(HeaderNotification {
|
||||||
|
height: raw.height,
|
||||||
|
header: deserialize(&raw.header)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notification of the new status of a script
|
/// Notification of the new status of a script
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct ScriptNotification {
|
pub struct ScriptNotification {
|
||||||
/// Address that generated this notification.
|
/// Address that generated this notification.
|
||||||
pub scripthash: ScriptHash,
|
pub scripthash: ScriptHash,
|
||||||
@ -220,8 +298,8 @@ pub enum Error {
|
|||||||
IOError(std::io::Error),
|
IOError(std::io::Error),
|
||||||
/// Wraps `serde_json::error::Error`
|
/// Wraps `serde_json::error::Error`
|
||||||
JSON(serde_json::error::Error),
|
JSON(serde_json::error::Error),
|
||||||
/// Wraps `bitcoin::hashes::hex::Error`
|
/// Wraps `bitcoin::hex::HexToBytesError`
|
||||||
Hex(bitcoin::hashes::hex::Error),
|
Hex(bitcoin::hex::HexToBytesError),
|
||||||
/// Error returned by the Electrum server
|
/// Error returned by the Electrum server
|
||||||
Protocol(serde_json::Value),
|
Protocol(serde_json::Value),
|
||||||
/// Error during the deserialization of a Bitcoin data structure
|
/// Error during the deserialization of a Bitcoin data structure
|
||||||
@ -238,6 +316,19 @@ pub enum Error {
|
|||||||
InvalidDNSNameError(String),
|
InvalidDNSNameError(String),
|
||||||
/// Missing domain while it was explicitly asked to validate it
|
/// Missing domain while it was explicitly asked to validate it
|
||||||
MissingDomain,
|
MissingDomain,
|
||||||
|
/// Made one or multiple attempts, always in Error
|
||||||
|
AllAttemptsErrored(Vec<Error>),
|
||||||
|
/// There was an io error reading the socket, to be shared between threads
|
||||||
|
SharedIOError(Arc<std::io::Error>),
|
||||||
|
|
||||||
|
/// Couldn't take a lock on the reader mutex. This means that there's already another reader
|
||||||
|
/// thread running
|
||||||
|
CouldntLockReader,
|
||||||
|
/// Broken IPC communication channel: the other thread probably has exited
|
||||||
|
Mpsc,
|
||||||
|
#[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))]
|
||||||
|
/// Could not create a rustls client connection
|
||||||
|
CouldNotCreateConnection(rustls::Error),
|
||||||
|
|
||||||
#[cfg(feature = "use-openssl")]
|
#[cfg(feature = "use-openssl")]
|
||||||
/// Invalid OpenSSL method used
|
/// Invalid OpenSSL method used
|
||||||
@ -247,11 +338,55 @@ pub enum Error {
|
|||||||
SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>),
|
SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::IOError(e) => Display::fmt(e, f),
|
||||||
|
Error::JSON(e) => Display::fmt(e, f),
|
||||||
|
Error::Hex(e) => Display::fmt(e, f),
|
||||||
|
Error::Bitcoin(e) => Display::fmt(e, f),
|
||||||
|
Error::SharedIOError(e) => Display::fmt(e, f),
|
||||||
|
#[cfg(feature = "use-openssl")]
|
||||||
|
Error::SslHandshakeError(e) => Display::fmt(e, f),
|
||||||
|
#[cfg(feature = "use-openssl")]
|
||||||
|
Error::InvalidSslMethod(e) => Display::fmt(e, f),
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "use-rustls",
|
||||||
|
feature = "use-rustls-ring",
|
||||||
|
))]
|
||||||
|
Error::CouldNotCreateConnection(e) => Display::fmt(e, f),
|
||||||
|
|
||||||
|
Error::Message(e) => f.write_str(e),
|
||||||
|
Error::InvalidDNSNameError(domain) => write!(f, "Invalid domain name {} not matching SSL certificate", domain),
|
||||||
|
Error::AllAttemptsErrored(errors) => {
|
||||||
|
f.write_str("Made one or multiple attempts, all errored:\n")?;
|
||||||
|
for err in errors {
|
||||||
|
writeln!(f, "\t- {}", err)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Error::Protocol(e) => write!(f, "Electrum server error: {}", e.clone().take()),
|
||||||
|
Error::InvalidResponse(e) => write!(f, "Error during the deserialization of a response from the server: {}", e.clone().take()),
|
||||||
|
|
||||||
|
// TODO: Print out addresses once `ScriptHash` will implement `Display`
|
||||||
|
Error::AlreadySubscribed(_) => write!(f, "Already subscribed to the notifications of an address"),
|
||||||
|
Error::NotSubscribed(_) => write!(f, "Not subscribed to the notifications of an address"),
|
||||||
|
|
||||||
|
Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"),
|
||||||
|
Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"),
|
||||||
|
Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
macro_rules! impl_error {
|
macro_rules! impl_error {
|
||||||
( $from:ty, $to:ident ) => {
|
( $from:ty, $to:ident ) => {
|
||||||
impl std::convert::From<$from> for Error {
|
impl std::convert::From<$from> for Error {
|
||||||
fn from(err: $from) -> Self {
|
fn from(err: $from) -> Self {
|
||||||
Error::$to(err)
|
Error::$to(err.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -259,5 +394,36 @@ macro_rules! impl_error {
|
|||||||
|
|
||||||
impl_error!(std::io::Error, IOError);
|
impl_error!(std::io::Error, IOError);
|
||||||
impl_error!(serde_json::Error, JSON);
|
impl_error!(serde_json::Error, JSON);
|
||||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
impl_error!(bitcoin::hex::HexToBytesError, Hex);
|
||||||
impl_error!(bitcoin::consensus::encode::Error, Bitcoin);
|
impl_error!(bitcoin::consensus::encode::Error, Bitcoin);
|
||||||
|
|
||||||
|
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||||
|
fn from(_: std::sync::PoisonError<T>) -> Self {
|
||||||
|
Error::IOError(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<std::sync::mpsc::SendError<T>> for Error {
|
||||||
|
fn from(_: std::sync::mpsc::SendError<T>) -> Self {
|
||||||
|
Error::Mpsc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::sync::mpsc::RecvError> for Error {
|
||||||
|
fn from(_: std::sync::mpsc::RecvError) -> Self {
|
||||||
|
Error::Mpsc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::ScriptStatus;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn script_status_roundtrip() {
|
||||||
|
let script_status: ScriptStatus = [1u8; 32].into();
|
||||||
|
let script_status_json = serde_json::to_string(&script_status).unwrap();
|
||||||
|
let script_status_back = serde_json::from_str(&script_status_json).unwrap();
|
||||||
|
assert_eq!(script_status, script_status_back);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
src/utils.rs
Normal file
43
src/utils.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//! Utilities helping to handle Electrum-related data.
|
||||||
|
|
||||||
|
use crate::types::GetMerkleRes;
|
||||||
|
use bitcoin::hash_types::TxMerkleNode;
|
||||||
|
use bitcoin::hashes::sha256d::Hash as Sha256d;
|
||||||
|
use bitcoin::hashes::{Hash, HashEngine};
|
||||||
|
use bitcoin::Txid;
|
||||||
|
|
||||||
|
/// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the
|
||||||
|
/// given `txid` and `merkle_root` as included in the [`BlockHeader`].
|
||||||
|
///
|
||||||
|
/// Returns `true` if the transaction is included in the corresponding block, and `false`
|
||||||
|
/// otherwise.
|
||||||
|
///
|
||||||
|
/// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle
|
||||||
|
/// [`BlockHeader`]: bitcoin::block::Header
|
||||||
|
pub fn validate_merkle_proof(
|
||||||
|
txid: &Txid,
|
||||||
|
merkle_root: &TxMerkleNode,
|
||||||
|
merkle_res: &GetMerkleRes,
|
||||||
|
) -> bool {
|
||||||
|
let mut index = merkle_res.pos;
|
||||||
|
let mut cur = txid.to_raw_hash();
|
||||||
|
for mut bytes in merkle_res.merkle.iter().cloned() {
|
||||||
|
bytes.reverse();
|
||||||
|
let next_hash = Sha256d::from_byte_array(bytes);
|
||||||
|
|
||||||
|
cur = Sha256d::from_engine({
|
||||||
|
let mut engine = Sha256d::engine();
|
||||||
|
if index % 2 == 0 {
|
||||||
|
engine.input(cur.as_ref());
|
||||||
|
engine.input(next_hash.as_ref());
|
||||||
|
} else {
|
||||||
|
engine.input(next_hash.as_ref());
|
||||||
|
engine.input(cur.as_ref());
|
||||||
|
};
|
||||||
|
engine
|
||||||
|
});
|
||||||
|
index /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur == merkle_root.to_raw_hash()
|
||||||
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":10.0}
|
|
||||||
{"id":0,"jsonrpc":"2.0","result":20.0}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.estimatefee","params":[10]}
|
|
||||||
{"jsonrpc":"2.0","id":1,"method":"blockchain.estimatefee","params":[20]}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{"id":1,"jsonrpc":"2.0","result":[{"height":0,"tx_hash":"83f5de2e6d7dfd5b582a5b2a3de4a5adb32c9cdca91473cf1fbcba76d56e4486","tx_pos":1,"value":100000000}]}
|
|
||||||
{"id":0,"jsonrpc":"2.0","result":[{"height":0,"tx_hash":"eb07e5d67565bad5231fd5aeeb16d0c2c53371265690642b943aa24c83ecae1d","tx_pos":0,"value":300000000},{"height":0,"tx_hash":"824f9a426d4b6a9b23c52901754a01017f3113ffdf3ed20c02747db85b161a40","tx_pos":0,"value":100000000}]}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.scripthash.listunspent","params":["c60b02f19c2053efedddb804024edd3f05f181ac2f828384dff40d072d25d962"]}
|
|
||||||
{"jsonrpc":"2.0","id":1,"method":"blockchain.scripthash.listunspent","params":["97897cdd5b98fab0b99aa5f861cee45c597a1ab2fe90ea1a7cf234b029eb5883"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":"000000207a8eb5cf562c0b013f03bf4be90318770510bcc57b918491b07f29f15a6433416fe34a556424483dad983f24f906a77638b4583688a0308c75d5bb9f31561e20c6e7105effff7f2000000000"}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.block.header","params":[500]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":{"count":4,"hex":"00000020a6b63c802e0bdeccfd6f4e132dfbad5822c563ef705b57267b1c05c07fb4bb066a95320ef101fba9911c3f4870cc8c3f8900cfa57384379635cba0466fb42bf36517345dffff7f200000000000000020cfa3201d443e007ec5edebbb2600c11ba04f07bd88056ad1ac402573ffe63473937fa56356c956cc4320cd8d98a3db17b845cffaf07158b5abcc7f914c85dcc66517345dffff7f200100000000000020a20ed7c06c55db6ec785d5578c32fb57c3db0bf1d3ef6c90a7af647d88add90df34090f37280af8b2e31f7857350947b1698a1a1742117eb1eff7d87c7fa9b986517345dffff7f2001000000000000207d475add1706bb3fceb22a45a9816ce156a8292a515bf4eeecf60e5aa9dc3007edf508bf9eb08dfa3d73d758e05e9f6730552277c5a249e6ba89ad7b50b4c93d6517345dffff7f2000000000","max":2016}}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.block.headers","params":[100,4]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":10.0}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.estimatefee","params":[10]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":123.4}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.relayfee","params":[]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":{"confirmed":0,"unconfirmed":130000000}}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.scripthash.get_balance","params":["c60b02f19c2053efedddb804024edd3f05f181ac2f828384dff40d072d25d962"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":[{"height":0,"tx_hash":"a1aa2b52fb79641f918d44a27f51781c3c0c49f7ee0e4b14dbb37c722853f046"},{"height":0,"tx_hash":"f9b4649764b9e9b53641d8bad750b1e40329937f79ae192f9e84e4a7978267bc"}]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.scripthash.get_history","params":["c60b02f19c2053efedddb804024edd3f05f181ac2f828384dff40d072d25d962"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":[{"height":0,"tx_hash":"a1aa2b52fb79641f918d44a27f51781c3c0c49f7ee0e4b14dbb37c722853f046","tx_pos":1,"value":30000000},{"height":0,"tx_hash":"f9b4649764b9e9b53641d8bad750b1e40329937f79ae192f9e84e4a7978267bc","tx_pos":1,"value":100000000}]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.scripthash.listunspent","params":["c60b02f19c2053efedddb804024edd3f05f181ac2f828384dff40d072d25d962"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id": 0, "method":"server.features", "result": {"genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943","hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},"protocol_max": "1.0","protocol_min": "1.0","pruning": null,"server_version": "ElectrumX 1.0.17","hash_function": "sha256"}}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"server.features","params":[]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":"a1aa2b52fb79641f918d44a27f51781c3c0c49f7ee0e4b14dbb37c722853f046"}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.transaction.broadcast","params":["02000000000101f6cd5873d669cc2de550453623d9d10ed5b5ba906d81160ee3ab853ebcfffa0c0100000000feffffff02e22f82000000000017a914e229870f3af1b1a3aefc3452a4d2939b443e6eba8780c3c9010000000017a9145f859501ff79211aeb972633b782743dd3b31dab8702473044022046ff3b0618107e08bd25fb753e31542b8c23575d7e9faf43dd17f59727cfb9c902200a4f3837105808d810de01fcd63fb18e66a69026090dc72b66840d41e55c6bf3012103e531113bbca998f8d164235e3395db336d3ba03552d1bfaa83fd7cffe6e5c6c960050000"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id":0,"jsonrpc":"2.0","result":"02000000000101f6cd5873d669cc2de550453623d9d10ed5b5ba906d81160ee3ab853ebcfffa0c0100000000feffffff02e22f82000000000017a914e229870f3af1b1a3aefc3452a4d2939b443e6eba8780c3c9010000000017a9145f859501ff79211aeb972633b782743dd3b31dab8702473044022046ff3b0618107e08bd25fb753e31542b8c23575d7e9faf43dd17f59727cfb9c902200a4f3837105808d810de01fcd63fb18e66a69026090dc72b66840d41e55c6bf3012103e531113bbca998f8d164235e3395db336d3ba03552d1bfaa83fd7cffe6e5c6c960050000"}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.transaction.get","params":["a1aa2b52fb79641f918d44a27f51781c3c0c49f7ee0e4b14dbb37c722853f046"]}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"id": 0, "method": "blockchain.transaction.get_merkle", "result": {"merkle": ["713d6c7e6ce7bbea708d61162231eaa8ecb31c4c5dd84f81c20409a90069cb24", "03dbaec78d4a52fbaf3c7aa5d3fccd9d8654f323940716ddf5ee2e4bda458fde", "e670224b23f156c27993ac3071940c0ff865b812e21e0a162fe7a005d6e57851", "369a1619a67c3108a8850118602e3669455c70cdcdb89248b64cc6325575b885", "4756688678644dcb27d62931f04013254a62aeee5dec139d1aac9f7b1f318112", "7b97e73abc043836fd890555bfce54757d387943a6860e5450525e8e9ab46be5", "61505055e8b639b7c64fd58bce6fc5c2378b92e025a02583303f69930091b1c3", "27a654ff1895385ac14a574a0415d3bbba9ec23a8774f22ec20d53dd0b5386ff", "5312ed87933075e60a9511857d23d460a085f3b6e9e5e565ad2443d223cfccdc", "94f60b14a9f106440a197054936e6fb92abbd69d6059b38fdf79b33fc864fca0", "2d64851151550e8c4d337f335ee28874401d55b358a66f1bafab2c3e9f48773d"], "block_height": 450538, "pos": 710}}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"jsonrpc":"2.0","id":0,"method":"blockchain.transaction.get_merkle","params":["2d64851151550e8c4d337f335ee28874401d55b358a66f1bafab2c3e9f48773d",1234]}
|
|
||||||
Loading…
Reference in New Issue
Block a user