diff --git a/Cargo.lock b/Cargo.lock index 4ec35d77a7..f2f4cef478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,10 +18,19 @@ dependencies = [ ] [[package]] -name = "adler32" -version = "1.0.4" +name = "addr2line" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" [[package]] name = "aes-ctr" @@ -169,26 +178,18 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.46" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" +checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" dependencies = [ - "backtrace-sys", + "addr2line", "cfg-if", "libc", + "miniz_oxide", + "object", "rustc-demangle", ] -[[package]] -name = "backtrace-sys" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "base64" version = "0.9.3" @@ -774,9 +775,9 @@ dependencies = [ [[package]] name = "either" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" [[package]] name = "encoding_rs" @@ -815,9 +816,9 @@ dependencies = [ [[package]] name = "error-chain" -version = "0.12.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "version_check 0.9.1", ] @@ -829,9 +830,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9db393664b0e6c6230a14115e7e798f80b70f54038dc21165db24c6b7f28fc" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -850,9 +851,9 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "synstructure", ] @@ -864,9 +865,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "flate2" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" dependencies = [ "cfg-if", "crc32fast", @@ -998,9 +999,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -1102,6 +1103,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" + [[package]] name = "git2" version = "0.10.2" @@ -1467,9 +1474,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055" +checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" dependencies = [ "wasm-bindgen", ] @@ -1658,11 +1665,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.3.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" dependencies = [ - "adler32", + "adler", ] [[package]] @@ -1826,7 +1833,6 @@ dependencies = [ name = "nym-client" version = "0.8.0-dev" dependencies = [ - "bs58", "built", "clap", "client-core", @@ -1840,11 +1846,9 @@ dependencies = [ "gateway-requests", "log 0.4.8", "nymsphinx", - "pem", "pemstore", "pretty_env_logger", "rand 0.7.3", - "reqwest 0.9.24", "serde", "serde_json", "sled 0.33.0", @@ -1853,6 +1857,7 @@ dependencies = [ "tokio-tungstenite", "topology", "url 2.1.1", + "websocket-requests", ] [[package]] @@ -1987,6 +1992,7 @@ version = "0.1.0" dependencies = [ "crypto", "nymsphinx-types", + "serde", ] [[package]] @@ -1999,6 +2005,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "serde", "topology", ] @@ -2052,6 +2059,12 @@ dependencies = [ "sphinx", ] +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -2234,9 +2247,9 @@ checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" dependencies = [ "pest", "pest_meta", - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -2304,9 +2317,9 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -2360,9 +2373,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "version_check 0.9.1", ] @@ -2372,9 +2385,9 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "syn-mid", "version_check 0.9.1", ] @@ -2402,9 +2415,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.10" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" dependencies = [ "unicode-xid 0.2.0", ] @@ -2468,7 +2481,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", ] [[package]] @@ -2881,9 +2894,9 @@ version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -2969,6 +2982,7 @@ dependencies = [ "simple-socks5-requests", "tokio 0.2.16", "tokio-tungstenite", + "websocket-requests", ] [[package]] @@ -3185,11 +3199,11 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.17" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" +checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", "unicode-xid 0.2.0", ] @@ -3200,9 +3214,9 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -3211,9 +3225,9 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "unicode-xid 0.2.0", ] @@ -3372,9 +3386,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", ] [[package]] @@ -3750,9 +3764,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.60" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f" +checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" dependencies = [ "cfg-if", "serde", @@ -3762,16 +3776,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.60" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd" +checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" dependencies = [ "bumpalo", "lazy_static", "log 0.4.8", - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "wasm-bindgen-shared", ] @@ -3802,9 +3816,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.60" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4" +checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" dependencies = [ "quote 1.0.3", "wasm-bindgen-macro-support", @@ -3812,22 +3826,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.60" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931" +checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.60" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639" +checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" [[package]] name = "wasm-bindgen-test" @@ -3856,14 +3870,23 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb" +checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "websocket-requests" +version = "0.1.0" +dependencies = [ + "nymsphinx", + "serde", + "serde_json", +] + [[package]] name = "wee_alloc" version = "0.4.5" @@ -3964,8 +3987,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" dependencies = [ - "proc-macro2 1.0.10", + "proc-macro2 1.0.19", "quote 1.0.3", - "syn 1.0.17", + "syn 1.0.38", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 8dacbdec2e..4e039a6ff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ opt-level = "s" members = [ "clients/native", + "clients/native/websocket-requests", "clients/socks5", "clients/webassembly", "clients/client-core", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 3115486979..6a6385436c 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -12,22 +12,23 @@ name = "nym_client" path = "src/lib.rs" [dependencies] -bs58 = "0.3.0" -clap = "2.33.0" -dirs = "2.0.2" -dotenv = "0.15.0" -futures = "0.3.1" -log = "0.4" -pem = "0.7.0" -pretty_env_logger = "0.3" -rand = { version = "0.7.3", features = ["wasm-bindgen"] } -reqwest = "0.9.22" -serde = { version = "1.0.104", features = ["derive"] } -serde_json = "1.0.44" -sled = "0.33" -tokio = { version = "0.2", features = ["full"] } -tokio-tungstenite = "0.10.1" -url = "2.1" +# dependencies to review: +futures = "0.3" # bunch of futures stuff, however, now that I think about it, it could perhaps be completely removed +# the AsyncRead, AsyncWrite, Stream, Sink, etc. traits could be used from tokio +# channels should really be replaced with crossbeam due to that implementation being more efficient +# and the single instance of abortable we have should really be refactored anyway +url = "2.1" # I think we should just replace it with String + +clap = "2.33.0" # for the command line arguments +dirs = "2.0.2" # for determining default store directories in config +dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being) +log = "0.4" # self explanatory +pretty_env_logger = "0.3" # for formatting log messages +rand = {version = "0.7.3", features = ["wasm-bindgen"]} # rng-related traits + some rng implementation to use +serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization +sled = "0.33" # for storage of replySURB decryption keys +tokio = { version = "0.2", features = ["full"] } # async runtime +tokio-tungstenite = "0.10.1" # websocket ## internal client-core = { path = "../client-core" } @@ -39,9 +40,11 @@ gateway-requests = { path = "../../gateway/gateway-requests" } nymsphinx = { path = "../../common/nymsphinx" } pemstore = { path = "../../common/pemstore" } topology = { path = "../../common/topology" } +websocket-requests = { path = "websocket-requests" } [build-dependencies] built = "0.3.2" [dev-dependencies] -tempfile = "3.1.0" \ No newline at end of file +tempfile = "3.1.0" +serde_json = "1.0" # for the "textsend" example \ No newline at end of file diff --git a/clients/native/examples/go-examples/websocket/binarysend.go b/clients/native/examples/go-examples/websocket/binarysend.go new file mode 100644 index 0000000000..bc9d1cafa9 --- /dev/null +++ b/clients/native/examples/go-examples/websocket/binarysend.go @@ -0,0 +1,218 @@ +package main + +import ( + "encoding/binary" + "fmt" + "io/ioutil" + + "github.com/gorilla/websocket" +) + +// request tags +const SEND_REQUEST_TAG = 0x00 +const REPLY_REQUEST_TAG = 0x01 +const SELF_ADDRESS_REQUEST_TAG = 0x02 + +// response tags +const ERROR_RESPONSE_TAG = 0x00 +const RECEIVED_RESPONSE_TAG = 0x01 +const SELF_ADDRESS_RESPONSE_TAG = 0x02 + +func makeSelfAddressRequest() []byte { + return []byte{SELF_ADDRESS_REQUEST_TAG} +} + +func parseSelfAddressResponse(rawResponse []byte) []byte { + if len(rawResponse) != 97 || rawResponse[0] != SELF_ADDRESS_RESPONSE_TAG { + panic("Received invalid response") + } + return rawResponse[1:] +} + +func makeSendRequest(recipient []byte, message []byte, withReplySurb bool) []byte { + messageLen := make([]byte, 8) + binary.BigEndian.PutUint64(messageLen, uint64(len(message))) + + surbByte := byte(0) + if withReplySurb { + surbByte = 1 + } + + out := []byte{SEND_REQUEST_TAG, surbByte} + out = append(out, recipient...) + out = append(out, messageLen...) + out = append(out, message...) + + fmt.Println("first send req: ", out[:10]) + return out +} + +func makeReplyRequest(message []byte, replySURB []byte) []byte { + messageLen := make([]byte, 8) + binary.BigEndian.PutUint64(messageLen, uint64(len(message))) + + surbLen := make([]byte, 8) + binary.BigEndian.PutUint64(surbLen, uint64(len(replySURB))) + + out := []byte{REPLY_REQUEST_TAG} + out = append(out, surbLen...) + out = append(out, replySURB...) + out = append(out, messageLen...) + out = append(out, message...) + + return out +} + +func parseReceived(rawResponse []byte) ([]byte, []byte) { + if rawResponse[0] != RECEIVED_RESPONSE_TAG { + panic("Received invalid response!") + } + + hasSurb := false + if rawResponse[1] == 1 { + hasSurb = true + } else if rawResponse[1] == 0 { + hasSurb = false + } else { + panic("malformed received response!") + } + + data := rawResponse[2:] + if hasSurb { + surbLen := binary.BigEndian.Uint64(data[:8]) + other := data[8:] + + surb := other[:surbLen] + msgLen := binary.BigEndian.Uint64(other[surbLen : surbLen+8]) + + if len(other[surbLen+8:]) != int(msgLen) { + panic("invalid msg len") + } + + msg := other[surbLen+8:] + return msg, surb + } else { + msgLen := binary.BigEndian.Uint64(data[:8]) + other := data[8:] + + if len(other) != int(msgLen) { + panic("invalid msg len") + } + + msg := other[:msgLen] + return msg, nil + } +} + +func sendWithoutReply() { + uri := "ws://localhost:1977" + + conn, _, err := websocket.DefaultDialer.Dial(uri, nil) + if err != nil { + panic(err) + } + defer conn.Close() + + selfAddressRequest := makeSelfAddressRequest() + if err = conn.WriteMessage(websocket.BinaryMessage, selfAddressRequest); err != nil { + panic(err) + } + _, receivedResponse, err := conn.ReadMessage() + if err != nil { + panic(err) + } + selfAddress := parseSelfAddressResponse(receivedResponse) + + read_data, err := ioutil.ReadFile("dummy_file") + if err != nil { + panic(err) + } + + sendRequest := makeSendRequest(selfAddress, read_data, false) + fmt.Printf("sending content of 'dummy file' over the mix network...\n") + if err = conn.WriteMessage(websocket.BinaryMessage, sendRequest); err != nil { + panic(err) + } + + fmt.Printf("waiting to receive a message from the mix network...\n") + _, receivedResponse, err = conn.ReadMessage() + if err != nil { + panic(err) + } + + fileData, replySURB := parseReceived(receivedResponse) + if replySURB != nil { + panic("did not expect a replySURB!") + } + fmt.Printf("writing the file back to the disk!\n") + ioutil.WriteFile("received_file_noreply", fileData, 0644) +} + +func sendWithReply() { + uri := "ws://localhost:1977" + + conn, _, err := websocket.DefaultDialer.Dial(uri, nil) + if err != nil { + panic(err) + } + defer conn.Close() + + selfAddressRequest := makeSelfAddressRequest() + if err = conn.WriteMessage(websocket.BinaryMessage, selfAddressRequest); err != nil { + panic(err) + } + _, receivedResponse, err := conn.ReadMessage() + if err != nil { + panic(err) + } + selfAddress := parseSelfAddressResponse(receivedResponse) + + readData, err := ioutil.ReadFile("dummy_file") + if err != nil { + panic(err) + } + + sendRequest := makeSendRequest(selfAddress, readData, true) + fmt.Printf("sending content of 'dummy file' over the mix network...\n") + if err = conn.WriteMessage(websocket.BinaryMessage, sendRequest); err != nil { + panic(err) + } + + fmt.Printf("waiting to receive a message from the mix network...\n") + _, receivedResponse, err = conn.ReadMessage() + if err != nil { + panic(err) + } + + fileData, replySURB := parseReceived(receivedResponse) + + fmt.Printf("writing the file back to the disk!\n") + ioutil.WriteFile("received_file_withreply", fileData, 0644) + + replyMessage := []byte("hello from reply SURB! - thanks for sending me the file!") + replyRequest := makeReplyRequest(replyMessage, replySURB) + + fmt.Printf("sending '%v' (using reply SURB) over the mix network...\n", string(replyMessage)) + if err = conn.WriteMessage(websocket.BinaryMessage, replyRequest); err != nil { + panic(err) + } + + fmt.Printf("waiting to receive a message from the mix network...\n") + _, receivedResponse, err = conn.ReadMessage() + if err != nil { + panic(err) + } + + receivedMessage, replySURB := parseReceived(receivedResponse) + if replySURB != nil { + panic("did not expect a replySURB!") + } + + fmt.Printf("received %v from the mix network!\n", string(receivedMessage)) + +} + +func main() { + // sendWithoutReply() + sendWithReply() +} diff --git a/clients/native/examples/go-examples/websocket/filesend.go b/clients/native/examples/go-examples/websocket/filesend.go deleted file mode 100644 index 4b549b2a9b..0000000000 --- a/clients/native/examples/go-examples/websocket/filesend.go +++ /dev/null @@ -1,177 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "strings" - - "github.com/btcsuite/btcutil/base58" - "github.com/gorilla/websocket" -) - -const WITHOUT_REPLY = 0 -const WITH_REPLY = 1 -const REPLY = 2 - -// this is really flaky so ideally will be replaced by some proper serialization -// 16 - length of encryption key -// 32 - length of first hop -// 348 - current sphinx header size -// 192 - size of single payload key -// 4 - number of mix hops (gateway + 3 nodes) -const REPLY_SURB_LEN = 16 + 32 + 348 + 4*192 - -func getSelfAddress(conn *websocket.Conn) string { - selfAddressRequest, err := json.Marshal(map[string]string{"type": "selfAddress"}) - if err != nil { - panic(err) - } - - if err = conn.WriteMessage(websocket.TextMessage, []byte(selfAddressRequest)); err != nil { - panic(err) - } - - responseJSON := make(map[string]interface{}) - err = conn.ReadJSON(&responseJSON) - if err != nil { - panic(err) - } - - return responseJSON["address"].(string) -} - -func sendWithoutReply() { - uri := "ws://localhost:1977" - - conn, _, err := websocket.DefaultDialer.Dial(uri, nil) - if err != nil { - panic(err) - } - defer conn.Close() - - selfAddress := getSelfAddress(conn) - fmt.Printf("our address is: %v\n", selfAddress) - - // we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY - // all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs - splitAddress := strings.Split(selfAddress, "@") - clientHalf := splitAddress[0] - gatewayHalf := splitAddress[1] - split_client_address := strings.Split(clientHalf, ".") - - decodedIdentity := base58.Decode(split_client_address[0]) - decodedEncryption := base58.Decode(split_client_address[1]) - decodedGateway := base58.Decode(gatewayHalf) - - read_data, err := ioutil.ReadFile("dummy_file") - if err != nil { - panic(err) - } - - payload := []byte{WITHOUT_REPLY} - payload = append(payload, decodedIdentity[:]...) - payload = append(payload, decodedEncryption[:]...) - payload = append(payload, decodedGateway[:]...) - payload = append(payload, read_data[:]...) - - fmt.Printf("sending content of 'dummy file' over the mix network...\n") - if err = conn.WriteMessage(websocket.BinaryMessage, payload); err != nil { - panic(err) - } - - fmt.Printf("waiting to receive a message from the mix network...\n") - _, receivedMessage, err := conn.ReadMessage() - if err != nil { - panic(err) - } - if receivedMessage[0] != WITHOUT_REPLY { - panic("incorrect prefix") - } - - fmt.Printf("writing the file back to the disk!\n") - ioutil.WriteFile("received_file_noreply", receivedMessage[1:], 0644) -} - -func sendWithReply() { - uri := "ws://localhost:1977" - - conn, _, err := websocket.DefaultDialer.Dial(uri, nil) - if err != nil { - panic(err) - } - defer conn.Close() - - selfAddress := getSelfAddress(conn) - fmt.Printf("our address is: %v\n", selfAddress) - - // we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY - // all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs - splitAddress := strings.Split(selfAddress, "@") - clientHalf := splitAddress[0] - gatewayHalf := splitAddress[1] - split_client_address := strings.Split(clientHalf, ".") - - decodedIdentity := base58.Decode(split_client_address[0]) - decodedEncryption := base58.Decode(split_client_address[1]) - decodedGateway := base58.Decode(gatewayHalf) - - read_data, err := ioutil.ReadFile("dummy_file") - if err != nil { - panic(err) - } - - payload := []byte{WITH_REPLY} - payload = append(payload, decodedIdentity[:]...) - payload = append(payload, decodedEncryption[:]...) - payload = append(payload, decodedGateway[:]...) - payload = append(payload, read_data[:]...) - - fmt.Printf("sending content of 'dummy file' over the mix network...\n") - if err = conn.WriteMessage(websocket.BinaryMessage, payload); err != nil { - panic(err) - } - - fmt.Printf("waiting to receive a message from the mix network...\n") - _, receivedMessage, err := conn.ReadMessage() - if err != nil { - panic(err) - } - if receivedMessage[0] != WITH_REPLY { - panic("incorrect prefix") - } - - replySurb := receivedMessage[1 : 1+REPLY_SURB_LEN] - outputFileData := receivedMessage[1+REPLY_SURB_LEN:] - - fmt.Printf("writing the file back to the disk!\n") - ioutil.WriteFile("received_file_withreply", outputFileData, 0644) - - replyMessage := []byte("hello from reply SURB! - thanks for sending me the file!") - binaryReply := []byte{REPLY} - binaryReply = append(binaryReply, replySurb[:]...) - binaryReply = append(binaryReply, replyMessage[:]...) - - fmt.Printf("sending '%v' (using reply SURB) over the mix network...\n", string(replyMessage)) - if err = conn.WriteMessage(websocket.BinaryMessage, binaryReply); err != nil { - panic(err) - } - - fmt.Printf("waiting to receive a message from the mix network...\n") - _, receivedReply, err := conn.ReadMessage() - if err != nil { - panic(err) - } - - if receivedReply[0] != WITHOUT_REPLY { - panic("incorrect prefix") - } - - fmt.Printf("received %v from the mix network!\n", string(receivedReply[1:])) - -} - -func main() { - // sendWithoutReply() - sendWithReply() -} diff --git a/clients/native/examples/go-examples/websocket/textsend.go b/clients/native/examples/go-examples/websocket/textsend.go index 602ad720e8..900ac407f1 100644 --- a/clients/native/examples/go-examples/websocket/textsend.go +++ b/clients/native/examples/go-examples/websocket/textsend.go @@ -40,9 +40,9 @@ func sendWithoutReply() { selfAddress := getSelfAddress(conn) fmt.Printf("our address is: %v\n", selfAddress) sendRequest, err := json.Marshal(map[string]interface{}{ - "type": "send", - "recipient": selfAddress, - "message": message, + "type": "send", + "recipient": selfAddress, + "message": message, "withReplySurb": false, }) if err != nil { @@ -76,9 +76,9 @@ func sendWithReply() { selfAddress := getSelfAddress(conn) fmt.Printf("our address is: %v\n", selfAddress) sendRequest, err := json.Marshal(map[string]interface{}{ - "type": "send", - "recipient": selfAddress, - "message": message, + "type": "send", + "recipient": selfAddress, + "message": message, "withReplySurb": true, }) if err != nil { @@ -101,15 +101,15 @@ func sendWithReply() { if err := json.Unmarshal(receivedMessage, &receivedMessageJson); err != nil { panic(err) } - + // use the received surb to send an anonymous reply! - replySurb := receivedMessageJson["replySurb"] + replySURB := receivedMessageJson["replySURB"] replyMessage := "hello from reply SURB!" reply, err := json.Marshal(map[string]interface{}{ - "type": "reply", - "message": replyMessage, - "replySurb": replySurb, + "type": "reply", + "message": replyMessage, + "replySURB": replySURB, }) if err != nil { panic(err) @@ -129,6 +129,6 @@ func sendWithReply() { } func main() { - // sendWithoutReply(); - sendWithReply(); + // sendWithoutReply() + sendWithReply() } diff --git a/clients/native/examples/python-examples/websocket/binarysend.py b/clients/native/examples/python-examples/websocket/binarysend.py new file mode 100644 index 0000000000..f81d29f24d --- /dev/null +++ b/clients/native/examples/python-examples/websocket/binarysend.py @@ -0,0 +1,139 @@ +import asyncio +import websockets +from pathlib import Path +import struct + +# request tags +SEND_REQUEST_TAG = 0x00 +REPLY_REQUEST_TAG = 0x01 +SELF_ADDRESS_REQUEST_TAG = 0x02 + +# response tags +ERROR_RESPONSE_TAG = 0x00 +RECEIVED_RESPONSE_TAG = 0x01 +SELF_ADDRESS_RESPONSE_TAG = 0x02 + + +def make_self_address_request() -> bytes: + return bytes([SELF_ADDRESS_REQUEST_TAG]) + + +def parse_self_address_response(raw_response: bytes) -> bytes: + if len(raw_response) != 97 or raw_response[0] != SELF_ADDRESS_RESPONSE_TAG: + print('Received invalid response!') + raise + + return raw_response[1:] + + +def make_send_request(recipient: bytes, message: bytes, with_reply_surb: bool) -> bytes: + # a big endian uint64 + message_len = len(message).to_bytes(length=8, byteorder='big', signed=False) + + return bytes([SEND_REQUEST_TAG]) + bytes([with_reply_surb]) + recipient + message_len + message + + +def make_reply_request(message: bytes, reply_surb: bytes) -> bytes: + message_len = len(message).to_bytes(length=8, byteorder='big', signed=False) + surb_len = len(reply_surb).to_bytes(length=8, byteorder='big', signed=False) + + return bytes([REPLY_REQUEST_TAG]) + surb_len + reply_surb + message_len + message + + +# it should have structure of RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg +# where surb_len || surb is only present if 'with_reply' is true +def parse_received(raw_response: bytes) -> (bytes, bytes): + if raw_response[0] != RECEIVED_RESPONSE_TAG: + print('Received invalid response!') + raise + + if raw_response[1] == 1: + has_surb = True + elif raw_response[1] == 0: + has_surb = False + else: + print("malformed received response!") + raise + + data = raw_response[2:] + if has_surb: + (surb_len,), other = struct.unpack(">Q", data[:8]), data[8:] + surb = other[:surb_len] + (msg_len,) = struct.unpack(">Q", other[surb_len:surb_len + 8]) + + if len(other[surb_len + 8:]) != msg_len: + print("invalid msg len") + raise + + msg = other[surb_len + 8:] + return msg, surb + else: + (msg_len,), other = struct.unpack(">Q", data[:8]), data[8:] + if len(other) != msg_len: + print("invalid msg len") + raise + + msg = other[:msg_len] + return msg, None + + +async def send_file_with_reply(): + uri = "ws://localhost:1977" + async with websockets.connect(uri) as websocket: + self_address_req = make_self_address_request() + await websocket.send(self_address_req) + + self_address = parse_self_address_response(await websocket.recv()) + file_data = Path('dummy_file').read_bytes() + + send_request = make_send_request(self_address, file_data, True) + print("sending content of 'dummy_file' over the mix network...") + await websocket.send(send_request) + + print("waiting to receive the 'dummy_file' from the mix network...") + received_response = await websocket.recv() + received_file, surb = parse_received(received_response) + + with open("received_file_withreply", "wb") as output_file: + print("writing the file back to the disk!") + output_file.write(received_file) + + reply_message = b"hello from reply SURB! - thanks for sending me the file!" + reply_request = make_reply_request(reply_message, surb) + + print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message)) + await websocket.send(reply_request) + + print("waiting to receive a message from the mix network...") + received_response = await websocket.recv() + received_msg, surb = parse_received(received_response) + assert surb is None # no surbs in replies! + + print("received '{}' from the mix network".format(received_msg)) + + +async def send_file_without_reply(): + uri = "ws://localhost:1977" + async with websockets.connect(uri) as websocket: + self_address_req = make_self_address_request() + await websocket.send(self_address_req) + + self_address = parse_self_address_response(await websocket.recv()) + file_data = Path('dummy_file').read_bytes() + + send_request = make_send_request(self_address, file_data, False) + print("sending content of 'dummy_file' over the mix network...") + await websocket.send(send_request) + + print("waiting to receive the 'dummy_file' from the mix network...") + received_response = await websocket.recv() + received_file, surb = parse_received(received_response) + assert surb is None # we didn't attach a surb so we expect a None here! + + with open("received_file_noreply", "wb") as output_file: + print("writing the file back to the disk!") + output_file.write(received_file) + + +# asyncio.get_event_loop().run_until_complete(send_file_without_reply()) +asyncio.get_event_loop().run_until_complete(send_file_with_reply()) diff --git a/clients/native/examples/python-examples/websocket/filesend.py b/clients/native/examples/python-examples/websocket/filesend.py deleted file mode 100644 index af54ac2cb6..0000000000 --- a/clients/native/examples/python-examples/websocket/filesend.py +++ /dev/null @@ -1,111 +0,0 @@ -import asyncio -import base58 -import json -import websockets - -self_address_request = json.dumps({ - "type": "selfAddress" -}) - -WITHOUT_REPLY: int = 0 -WITH_REPLY: int = 1 -REPLY: int = 2 - -# this is really flaky so ideally will be replaced by some proper serialization -# 16 - length of encryption key -# 32 - length of first hop -# 348 - current sphinx header size -# 192 - size of single payload key -# 4 - number of mix hops (gateway + 3 nodes) -REPLY_SURB_LEN: int = 16 + 32 + 348 + 4 * 192 - -async def send_file_with_reply(): - uri = "ws://localhost:1977" - async with websockets.connect(uri) as websocket: - await websocket.send(self_address_request) - self_address = json.loads(await websocket.recv()) - print("our address is: {}".format(self_address["address"])) - # we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY - # all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs - split_address = self_address["address"].split("@") - client_half = split_address[0] - gateway_half = split_address[1] - - # to DH: I think at this point this should be replaced with some sort of protobuf / flatbuffers / Cap'n Proto, etc - split_client_address = client_half.split(".") - - bin_payload = bytearray([WITH_REPLY]) - bin_payload += base58.b58decode(split_client_address[0]) - bin_payload += base58.b58decode(split_client_address[1]) - bin_payload += base58.b58decode(gateway_half) - - with open("dummy_file", "rb") as input_file: - read_data = input_file.read() - bin_payload += read_data - - print("sending content of 'dummy_file' over the mix network...") - await websocket.send(bin_payload) - - print("waiting to receive the 'dummy_file' from the mix network...") - received_data = await websocket.recv() - assert received_data[0] == WITH_REPLY - reply_surb = received_data[1:1 +REPLY_SURB_LEN] - output_file_data = received_data[1 + REPLY_SURB_LEN:] - - with open("received_file_withreply", "wb") as output_file: - print("writing the file back to the disk!") - output_file.write(output_file_data) - - - reply_message = b"hello from reply SURB! - thanks for sending me the file!" - binary_reply = bytearray([REPLY]) - binary_reply += reply_surb - binary_reply += reply_message - - print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message)) - await websocket.send(binary_reply) - - print("waiting to receive a message from the mix network...") - received_reply = await websocket.recv() - assert received_reply[0] == WITHOUT_REPLY - - print("received '{}' from the mix network".format(received_reply[1:])) - - - -async def send_file_without_reply(): - uri = "ws://localhost:1977" - async with websockets.connect(uri) as websocket: - await websocket.send(self_address_request) - self_address = json.loads(await websocket.recv()) - print("our address is: {}".format(self_address["address"])) - # we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY - # all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs - split_address = self_address["address"].split("@") - client_half = split_address[0] - gateway_half = split_address[1] - - # to DH: I think at this point this should be replaced with some sort of protobuf / flatbuffers / Cap'n Proto, etc - split_client_address = client_half.split(".") - - bin_payload = bytearray([WITHOUT_REPLY]) - bin_payload += base58.b58decode(split_client_address[0]) - bin_payload += base58.b58decode(split_client_address[1]) - bin_payload += base58.b58decode(gateway_half) - - with open("dummy_file", "rb") as input_file: - read_data = input_file.read() - bin_payload += read_data - - print("sending content of 'dummy_file' over the mix network...") - await websocket.send(bin_payload) - - print("waiting to receive the 'dummy_file' from the mix network...") - received_data = await websocket.recv() - assert received_data[0] == WITHOUT_REPLY - with open("received_file_noreply", "wb") as output_file: - print("writing the file back to the disk!") - output_file.write(received_data[1:]) - -# asyncio.get_event_loop().run_until_complete(send_file_without_reply()) -asyncio.get_event_loop().run_until_complete(send_file_with_reply()) diff --git a/clients/native/examples/python-examples/websocket/textsend.py b/clients/native/examples/python-examples/websocket/textsend.py index 35ba8f9852..51ef03adb0 100644 --- a/clients/native/examples/python-examples/websocket/textsend.py +++ b/clients/native/examples/python-examples/websocket/textsend.py @@ -55,13 +55,13 @@ async def send_text_with_reply(): print("received '{}' from the mix network".format(received_message)) # use the received surb to send an anonymous reply! - reply_surb = received_message["replySurb"] + reply_surb = received_message["replySURB"] reply_message = "hello from reply SURB!" reply = json.dumps({ "type": "reply", "message": reply_message, - "replySurb": reply_surb + "replySURB": reply_surb }) print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message)) diff --git a/clients/native/examples/websocket_filesend.rs b/clients/native/examples/websocket_binarysend.rs similarity index 55% rename from clients/native/examples/websocket_filesend.rs rename to clients/native/examples/websocket_binarysend.rs index 1869cc653e..7f0830edb4 100644 --- a/clients/native/examples/websocket_filesend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -1,46 +1,30 @@ use futures::{SinkExt, StreamExt}; -use nym_client::websocket::{ - BinaryClientRequest, BinaryServerResponse, ClientTextRequest, ServerTextResponse, -}; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::receiver::ReconstructedMessage; -use std::convert::TryFrom; use tokio::net::TcpStream; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, WebSocketStream}; +use websocket_requests::{requests::ClientRequest, responses::ServerResponse}; // just helpers functions that work in this very particular context because we are sending to ourselves // and hence will always get a response back (i.e. the message we sent) -async fn send_message_and_get_response>( +async fn send_message_and_get_response( ws_stream: &mut WebSocketStream, - req: M, -) -> ReconstructedMessage { - ws_stream.send(req.into()).await.unwrap(); + req: Vec, +) -> ServerResponse { + ws_stream.send(Message::Binary(req)).await.unwrap(); let raw_message = ws_stream.next().await.unwrap().unwrap(); - let message = match raw_message { - Message::Binary(bin_payload) => BinaryServerResponse::try_from_bytes(&bin_payload).unwrap(), + match raw_message { + Message::Binary(bin_payload) => ServerResponse::deserialize(&bin_payload).unwrap(), _ => panic!("received an unexpected response type!"), - }; - - match message { - BinaryServerResponse::Received(received_message) => received_message, } } async fn get_self_address(ws_stream: &mut WebSocketStream) -> Recipient { - let self_address_request = ClientTextRequest::SelfAddress; - ws_stream.send(self_address_request.into()).await.unwrap(); - - let raw_response = ws_stream.next().await.unwrap().unwrap(); - // what we received now is just a json, but we know it's exact format - // so might as well use that - let response = match raw_response { - Message::Text(txt_msg) => ServerTextResponse::try_from(txt_msg).unwrap(), - _ => panic!("received an unexpected response type!"), - }; + let self_address_request = ClientRequest::SelfAddress.serialize(); + let response = send_message_and_get_response(ws_stream, self_address_request).await; match response { - ServerTextResponse::SelfAddress { address } => Recipient::try_from_string(address).unwrap(), - _ => panic!("received an unexpected response type!"), + ServerResponse::SelfAddress(recipient) => recipient, + _ => panic!("received an unexpected response!"), } } @@ -53,33 +37,42 @@ async fn send_file_with_reply() { let read_data = std::fs::read("examples/dummy_file").unwrap(); - let send_request = BinaryClientRequest::Send { + let send_request = ClientRequest::Send { recipient, - data: read_data, + message: read_data, with_reply_surb: true, }; println!("sending content of 'dummy_file' over the mix network..."); - let response = send_message_and_get_response(&mut ws_stream, send_request).await; + let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; + + let received = match response { + ServerResponse::Received(received) => received, + _ => panic!("received an unexpected response!"), + }; println!("writing the file back to the disk!"); - std::fs::write("examples/received_file_withreply", response.message).unwrap(); + std::fs::write("examples/received_file_withreply", received.message).unwrap(); let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec(); - let reply_request = BinaryClientRequest::Reply { + let reply_request = ClientRequest::Reply { message: reply_message.clone(), - reply_surb: response.reply_SURB.unwrap(), + reply_surb: received.reply_SURB.unwrap(), }; println!( "sending {:?} (using reply SURB!) over the mix network...", String::from_utf8(reply_message).unwrap() ); - let response = send_message_and_get_response(&mut ws_stream, reply_request).await; + let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await; + let received = match response { + ServerResponse::Received(received) => received, + _ => panic!("received an unexpected response!"), + }; println!( "received {:#?} from the mix network!", - String::from_utf8(response.message).unwrap() + String::from_utf8(received.message).unwrap() ); } @@ -92,21 +85,33 @@ async fn send_file_without_reply() { let read_data = std::fs::read("examples/dummy_file").unwrap(); - let send_request = BinaryClientRequest::Send { + let send_request = ClientRequest::Send { recipient, - data: read_data, + message: read_data, with_reply_surb: false, }; println!("sending content of 'dummy_file' over the mix network..."); - let response = send_message_and_get_response(&mut ws_stream, send_request).await; + let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; + + let received = match response { + ServerResponse::Received(received) => received, + _ => panic!("received an unexpected response!"), + }; println!("writing the file back to the disk!"); - std::fs::write("examples/received_file_noreply", response.message).unwrap(); + std::fs::write("examples/received_file_noreply", received.message).unwrap(); } #[tokio::main] async fn main() { - // send_file_without_reply().await; - send_file_with_reply().await + println!("#############################"); + println!("Example without using replies"); + + send_file_without_reply().await; + + println!("\n\n#############################"); + println!("Example using replies"); + + send_file_with_reply().await; } diff --git a/clients/native/examples/websocket_textsend.rs b/clients/native/examples/websocket_textsend.rs index 7d1ea45585..52ecc02640 100644 --- a/clients/native/examples/websocket_textsend.rs +++ b/clients/native/examples/websocket_textsend.rs @@ -1,44 +1,30 @@ use futures::{SinkExt, StreamExt}; -use nym_client::websocket::{ClientTextRequest, ReceivedTextMessage, ServerTextResponse}; -use std::convert::TryFrom; +use serde_json::json; use tokio::net::TcpStream; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, WebSocketStream}; +// PREFACE: in practice I don't see why you would ever want to use text api while in Rust, but example +// is here for the completion sake + // just helpers functions that work in this very particular context because we are sending to ourselves // and hence will always get a response back (i.e. the message we sent) -async fn send_message_and_get_response>( +async fn send_message_and_get_json_response( ws_stream: &mut WebSocketStream, - req: M, -) -> ReceivedTextMessage { - ws_stream.send(req.into()).await.unwrap(); + text_req: String, +) -> serde_json::Value { + ws_stream.send(Message::Text(text_req)).await.unwrap(); let raw_message = ws_stream.next().await.unwrap().unwrap(); - let message = match raw_message { - Message::Text(txt_msg) => ServerTextResponse::try_from(txt_msg).unwrap(), - _ => panic!("received an unexpected response type!"), - }; - - match message { - ServerTextResponse::Received(received_message) => received_message, + match raw_message { + Message::Text(txt_msg) => serde_json::from_str(&txt_msg).unwrap(), _ => panic!("received an unexpected response type!"), } } async fn get_self_address(ws_stream: &mut WebSocketStream) -> String { - let self_address_request = ClientTextRequest::SelfAddress; - ws_stream.send(self_address_request.into()).await.unwrap(); + let self_address_request = json!({ "type": "selfAddress" }).to_string(); + let response = send_message_and_get_json_response(ws_stream, self_address_request).await; - let raw_response = ws_stream.next().await.unwrap().unwrap(); - // what we received now is just a json, but we know it's exact format - // so might as well use that - let response = match raw_response { - Message::Text(txt_msg) => ServerTextResponse::try_from(txt_msg).unwrap(), - _ => panic!("received an unexpected response type!"), - }; - - match response { - ServerTextResponse::SelfAddress { address } => address, - _ => panic!("received an unexpected response type!"), - } + response["address"].as_str().unwrap().to_string() } async fn send_text_with_reply() { @@ -50,32 +36,35 @@ async fn send_text_with_reply() { let recipient = get_self_address(&mut ws_stream).await; println!("our full address is: {}", recipient.to_string()); - let send_request = ClientTextRequest::Send { - message: message.clone(), - recipient, - with_reply_surb: true, - }; + let send_request = json!({ + "type" : "send", + "recipient": recipient, + "message": message, + "withReplySurb": true, + }); + println!( "sending {:?} (*with* reply SURB) over the mix network...", message ); - - let response = send_message_and_get_response(&mut ws_stream, send_request).await; - println!("received {:#?} from the mix network!", response); + let response = + send_message_and_get_json_response(&mut ws_stream, send_request.to_string()).await; let reply_message = "hello from reply SURB!"; - let reply_request = ClientTextRequest::Reply { - message: reply_message.to_string(), - reply_surb: response.reply_surb.unwrap(), - }; + let reply_request = json!({ + "type": "reply", + "message": reply_message, + "replySURB": response["replySURB"] + }); println!( "sending {:?} (using reply SURB!) over the mix network...", reply_message ); - let response = send_message_and_get_response(&mut ws_stream, reply_request).await; - println!("received {:#?} from the mix network!", response); + let response = + send_message_and_get_json_response(&mut ws_stream, reply_request.to_string()).await; + println!("received {:#?} from the mix network!", response.to_string()); } async fn send_text_without_reply() { @@ -87,22 +76,32 @@ async fn send_text_without_reply() { let recipient = get_self_address(&mut ws_stream).await; println!("our full address is: {}", recipient.to_string()); - let send_request = ClientTextRequest::Send { - message: message.clone(), - recipient, - with_reply_surb: false, - }; + let send_request = json!({ + "type" : "send", + "recipient": recipient, + "message": message, + "withReplySurb": false, + }); + println!( - "sending {:?} (*without reply SURB) over the mix network...", + "sending {:?} (*without* reply SURB) over the mix network...", message ); - let response = send_message_and_get_response(&mut ws_stream, send_request).await; + let response = + send_message_and_get_json_response(&mut ws_stream, send_request.to_string()).await; - println!("received {:#?} from the mix network!", response); + println!("received {:#?} from the mix network!", response.to_string()); } #[tokio::main] async fn main() { - // send_text_without_reply().await; + println!("#############################"); + println!("Example without using replies"); + + send_text_without_reply().await; + + println!("\n\n#############################"); + println!("Example using replies"); + send_text_with_reply().await; } diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index df0d8cbaeb..f47383fe85 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::types::{BinaryClientRequest, ClientTextRequest, ServerTextResponse}; -use crate::websocket::types::ReceivedTextMessage; use client_core::client::{ inbound_messages::{InputMessage, InputMessageSender}, received_buffer::{ @@ -26,13 +24,13 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::anonymous_replies::ReplySURB; use nymsphinx::receiver::ReconstructedMessage; -use std::convert::{TryFrom, TryInto}; use tokio::net::TcpStream; use tokio_tungstenite::{ accept_async, - tungstenite::{protocol::Message, Error as WsError}, + tungstenite::{protocol::Message as WsMessage, Error as WsError}, WebSocketStream, }; +use websocket_requests::{requests::ClientRequest, responses::ServerResponse}; enum ReceivedResponseType { Binary, @@ -89,189 +87,127 @@ impl Handler { } } - fn handle_text_send( - &mut self, - msg: String, - full_recipient_address: String, - with_reply_surb: bool, - ) -> Option { - let message_bytes = msg.into_bytes(); - - let recipient = match Recipient::try_from_string(full_recipient_address) { - Ok(address) => address, - Err(err) => { - trace!("failed to parse received Recipient: {:?}", err); - return Some(ServerTextResponse::new_error("malformed recipient address")); - } - }; - - // the ack control is now responsible for chunking, etc. - let input_msg = InputMessage::new_fresh(recipient, message_bytes, with_reply_surb); - self.msg_input.unbounded_send(input_msg).unwrap(); - - self.received_response_type = ReceivedResponseType::Text; - None - } - - fn handle_text_reply(&mut self, msg: String, reply_surb: String) -> Option { - let message_bytes = msg.into_bytes(); - - let reply_surb = match ReplySURB::from_base58_string(reply_surb) { - Ok(reply_surb) => reply_surb, - Err(err) => { - trace!("failed to parse received ReplySURB: {:?}", err); - return Some(ServerTextResponse::new_error("malformed reply surb")); - } - }; - - let input_msg = InputMessage::new_reply(reply_surb, message_bytes); - self.msg_input.unbounded_send(input_msg).unwrap(); - - self.received_response_type = ReceivedResponseType::Text; - None - } - - fn handle_text_self_address(&self) -> ServerTextResponse { - ServerTextResponse::SelfAddress { - address: self.self_full_address.to_string(), - } - } - - async fn handle_text_message(&mut self, msg: String) -> Option { - debug!("Handling text message request"); - trace!("Content: {:?}", msg.clone()); - - match ClientTextRequest::try_from(msg) { - Err(e) => Some( - ServerTextResponse::Error { - message: format!("received invalid request. err: {:?}", e), - } - .into(), - ), - Ok(req) => match req { - ClientTextRequest::Send { - message, - recipient, - with_reply_surb, - } => self - .handle_text_send(message, recipient, with_reply_surb) - .map(|resp| resp.into()), - ClientTextRequest::Reply { - message, - reply_surb, - } => self - .handle_text_reply(message, reply_surb) - .map(|resp| resp.into()), - ClientTextRequest::SelfAddress => Some(self.handle_text_self_address().into()), - }, - } - } - - fn handle_binary_send( + fn handle_send( &mut self, recipient: Recipient, - data: Vec, + message: Vec, with_reply_surb: bool, - ) -> Option { + ) -> Option { // the ack control is now responsible for chunking, etc. - let input_msg = InputMessage::new_fresh(recipient, data, with_reply_surb); + let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb); self.msg_input.unbounded_send(input_msg).unwrap(); - self.received_response_type = ReceivedResponseType::Binary; - None } - fn handle_binary_reply( - &mut self, - reply_surb: ReplySURB, - message: Vec, - ) -> Option { + fn handle_reply(&mut self, reply_surb: ReplySURB, message: Vec) -> Option { if message.len() > ReplySURB::max_msg_len(Default::default()) { - return Some(ServerTextResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySURB::max_msg_len(Default::default())))); + return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySURB::max_msg_len(Default::default())))); } let input_msg = InputMessage::new_reply(reply_surb, message); self.msg_input.unbounded_send(input_msg).unwrap(); - self.received_response_type = ReceivedResponseType::Binary; - None } - // if it's binary we assume it's a sphinx packet formatted the same way as we'd have sent - // it to the gateway - fn handle_binary_message(&mut self, msg: Vec) -> Option { + fn handle_self_address(&self) -> ServerResponse { + ServerResponse::SelfAddress(self.self_full_address.clone()) + } + + fn handle_request(&mut self, request: ClientRequest) -> Option { + match request { + ClientRequest::Send { + recipient, + message, + with_reply_surb, + } => self.handle_send(recipient, message, with_reply_surb), + ClientRequest::Reply { + message, + reply_surb, + } => self.handle_reply(reply_surb, message), + ClientRequest::SelfAddress => Some(self.handle_self_address()), + } + } + + fn handle_text_message(&mut self, msg: String) -> Option { + debug!("Handling text message request"); + trace!("Content: {:?}", msg); + + self.received_response_type = ReceivedResponseType::Text; + let client_request = ClientRequest::try_from_text(msg); + + let response = match client_request { + Err(err) => Some(ServerResponse::Error(err)), + Ok(req) => self.handle_request(req), + }; + + response.map(|resp| WsMessage::text(resp.into_text())) + } + + fn handle_binary_message(&mut self, msg: Vec) -> Option { debug!("Handling binary message request"); self.received_response_type = ReceivedResponseType::Binary; - // make sure it is correctly formatted - let binary_request = BinaryClientRequest::try_from_bytes(&msg); - if binary_request.is_none() { - return Some(ServerTextResponse::new_error("invalid binary request").into()); - } - match binary_request.unwrap() { - BinaryClientRequest::Send { - recipient, - data, - with_reply_surb, - } => self.handle_binary_send(recipient, data, with_reply_surb), - BinaryClientRequest::Reply { - message, - reply_surb, - } => self.handle_binary_reply(reply_surb, message), - } - .map(|resp| resp.into()) + let client_request = ClientRequest::try_from_binary(msg); + + let response = match client_request { + Err(err) => Some(ServerResponse::Error(err)), + Ok(req) => self.handle_request(req), + }; + + response.map(|resp| WsMessage::Binary(resp.into_binary())) } - async fn handle_request(&mut self, raw_request: Message) -> Option { + fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option { // apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore // them and let's test that claim. If that's not the case, just copy code from // old version of this file. match raw_request { - Message::Text(text_message) => self.handle_text_message(text_message).await, - Message::Binary(binary_message) => self.handle_binary_message(binary_message), + WsMessage::Text(text_message) => self.handle_text_message(text_message), + WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message), _ => None, } } + // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but + // let's just play along for now + fn prepare_reconstructed_binary( + &self, + reconstructed_messages: Vec, + ) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Binary(resp.into_binary()))) + .collect() + } + + // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but + // let's just play along for now + fn prepare_reconstructed_text( + &self, + reconstructed_messages: Vec, + ) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Text(resp.into_text()))) + .collect() + } + async fn push_websocket_received_plaintexts( &mut self, reconstructed_messages: Vec, ) -> Result<(), WsError> { - // TODO: later there might be a flag on the reconstructed message itself - - let response_messages: Vec<_> = match self.received_response_type { - ReceivedResponseType::Binary => reconstructed_messages - .into_iter() - .map(|msg| Ok(Message::Binary(msg.into_bytes()))) - .collect(), - ReceivedResponseType::Text => { - let mut decoded_messages: Vec = Vec::new(); - // either all succeed or all fall back - let mut did_fail = false; - for message in reconstructed_messages.iter() { - match message.try_into() { - Ok(msg) => decoded_messages.push(msg), - Err(err) => { - did_fail = true; - warn!("Invalid UTF-8 sequence in response message - {:?}", err); - break; - } - } - } - if did_fail { - reconstructed_messages - .into_iter() - .map(|msg| Ok(Message::Binary(msg.into_bytes()))) - .collect() - } else { - decoded_messages - .into_iter() - .map(|msg| Ok(ServerTextResponse::Received(msg).into())) - .collect() - } + // TODO: later there might be a flag on the reconstructed message itself to tell us + // if it's text or binary, but for time being we use the naive assumption that if + // client is sending Message::Text it expects text back. Same for Message::Binary + let response_messages = match self.received_response_type { + ReceivedResponseType::Binary => { + self.prepare_reconstructed_binary(reconstructed_messages) } + ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages), }; let mut send_stream = futures::stream::iter(response_messages); @@ -282,7 +218,7 @@ impl Handler { .await } - async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> { + async fn send_websocket_response(&mut self, msg: WsMessage) -> Result<(), WsError> { match self.socket { // TODO: more closely investigate difference between `Sink::send` and `Sink::send_all` // it got something to do with batching and flushing - it might be important if it @@ -292,7 +228,7 @@ impl Handler { } } - async fn next_websocket_request(&mut self) -> Option> { + async fn next_websocket_request(&mut self) -> Option> { match self.socket { Some(ref mut ws_stream) => ws_stream.next().await, None => None, @@ -302,6 +238,7 @@ impl Handler { async fn listen_for_requests(&mut self, mut msg_receiver: ReconstructedMessagesReceiver) { loop { tokio::select! { + // we can either get a client request from the websocket socket_msg = self.next_websocket_request() => { if socket_msg.is_none() { break; @@ -309,7 +246,7 @@ impl Handler { let socket_msg = match socket_msg.unwrap() { Ok(socket_msg) => socket_msg, Err(err) => { - error!("failed to obtain message from websocket stream! stopping connection handler: {}", err); + warn!("failed to obtain message from websocket stream! stopping connection handler: {}", err); break; } }; @@ -318,7 +255,7 @@ impl Handler { break; } - if let Some(response) = self.handle_request(socket_msg).await { + if let Some(response) = self.handle_ws_request(socket_msg) { if let Err(err) = self.send_websocket_response(response).await { warn!( "Failed to send message over websocket: {}. Assuming the connection is dead.", @@ -328,6 +265,7 @@ impl Handler { } } } + // or a reconstructed mix message that we need to push back to the client mix_messages = msg_receiver.next() => { let mix_messages = mix_messages.expect( "mix messages sender was unexpectedly closed! this shouldn't have ever happened!", diff --git a/clients/native/src/websocket/mod.rs b/clients/native/src/websocket/mod.rs index 8f5d64b3c4..bff0aa245b 100644 --- a/clients/native/src/websocket/mod.rs +++ b/clients/native/src/websocket/mod.rs @@ -14,12 +14,6 @@ pub(crate) mod handler; pub(crate) mod listener; -pub(crate) mod types; pub(crate) use handler::Handler; pub(crate) use listener::Listener; - -pub use types::{ - BinaryClientRequest, BinaryServerResponse, ClientTextRequest, ReceivedTextMessage, - ServerTextResponse, -}; diff --git a/clients/native/src/websocket/types.rs b/clients/native/src/websocket/types.rs deleted file mode 100644 index ef6a657ea3..0000000000 --- a/clients/native/src/websocket/types.rs +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySURB; -use nymsphinx::params::{MessageType, DEFAULT_NUM_MIX_HOPS}; -use nymsphinx::receiver::ReconstructedMessage; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use tokio_tungstenite::tungstenite::protocol::Message as WsMessage; - -// 'Text' -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum ClientTextRequest { - #[serde(rename_all = "camelCase")] - Send { - message: String, - recipient: String, - // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? - with_reply_surb: bool, - }, - SelfAddress, - #[serde(rename_all = "camelCase")] - Reply { - message: String, - reply_surb: String, - }, -} - -impl TryFrom for ClientTextRequest { - type Error = serde_json::Error; - - fn try_from(msg: String) -> Result { - serde_json::from_str(&msg) - } -} - -impl Into for ClientTextRequest { - fn into(self) -> WsMessage { - let str_req = serde_json::to_string(&self).unwrap(); - WsMessage::Text(str_req) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ReceivedTextMessage { - pub message: String, - pub reply_surb: Option, -} - -impl<'a> TryFrom<&'a ReconstructedMessage> for ReceivedTextMessage { - type Error = std::str::Utf8Error; - - fn try_from(reconstructed_message: &ReconstructedMessage) -> Result { - Ok(ReceivedTextMessage { - message: std::str::from_utf8(&reconstructed_message.message)?.to_string(), - reply_surb: reconstructed_message - .reply_SURB - .as_ref() - .map(|reply_surb| reply_surb.to_base58_string()), - }) - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum ServerTextResponse { - Received(ReceivedTextMessage), - GetClients { clients: Vec }, - SelfAddress { address: String }, - Error { message: String }, -} - -impl ServerTextResponse { - pub fn new_error>(msg: S) -> Self { - ServerTextResponse::Error { - message: msg.into(), - } - } -} - -impl TryFrom for ServerTextResponse { - type Error = serde_json::Error; - - fn try_from(msg: String) -> Result>::Error> { - serde_json::from_str(&msg) - } -} - -impl Into for ServerTextResponse { - fn into(self) -> WsMessage { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved - let str_res = serde_json::to_string(&self).unwrap(); - WsMessage::Text(str_res) - } -} - -// 'Binary' - -// Note that control message, like 'SelfAddress', are not allowed as 'BinaryRequests' - but maybe -// they should be? -// Right now you can only send a `Send` or `Reply` binary message -#[allow(non_snake_case)] -#[derive(Debug)] -pub enum BinaryClientRequest { - Send { - recipient: Recipient, - data: Vec, - with_reply_surb: bool, - }, - Reply { - message: Vec, - reply_surb: ReplySURB, - }, -} - -impl BinaryClientRequest { - // TODO: I really think this should be done with something like protobuf / flatbuffers / Cap'n Proto, - // especially if people using different languages had to use it - // Another reason for some proper schema: messages pushed back to the client which will require - // extra parsing to determine when the actual message starts and which parts are the reply surb - - // TODO2: perhaps do it the proper way and introduce an error type - // TODO3: but if this 'stays' this way, the function could definitely use a clean up - pub fn try_from_bytes(req: &[u8]) -> Option { - if req.is_empty() { - return None; - } - let with_reply_surb = match req[0] { - n if n == MessageType::WithReplySURB as u8 => true, - n if n == MessageType::WithoutReplySURB as u8 => false, - n if n == MessageType::IsReply as u8 => { - // TODO: this is extremely fragile as only works for the very specific network topology - // and number of hops - another reason for some proper serialization library - let surb_len = ReplySURB::serialized_len(DEFAULT_NUM_MIX_HOPS); - - if req.len() < surb_len + 1 { - return None; - } - - // note the extra +1 (due to message prefix) - let surb_bytes = &req[1..1 + surb_len]; - let reply_surb = ReplySURB::from_bytes(surb_bytes).ok()?; - - return Some(BinaryClientRequest::Reply { - message: req[1 + surb_len..].to_vec(), - reply_surb, - }); - } - _ => return None, // no other option is valid in this context - }; - - if req.len() < Recipient::LEN + 1 { - return None; - } - - let mut recipient_bytes = [0u8; Recipient::LEN]; - recipient_bytes.copy_from_slice(&req[1..Recipient::LEN + 1]); - let recipient = Recipient::try_from_bytes(recipient_bytes).ok()?; - - Some(BinaryClientRequest::Send { - recipient, - data: req[1 + Recipient::LEN..].to_vec(), - with_reply_surb, - }) - } - - pub fn into_bytes(self) -> Vec { - match self { - // (MessageType::WithReplySURB OR MessageType::WithoutReplySURB) || RECIPIENT || MESSAGE - BinaryClientRequest::Send { - recipient, - data, - with_reply_surb, - } => std::iter::once(if with_reply_surb { - MessageType::WithReplySURB as u8 - } else { - MessageType::WithoutReplySURB as u8 - }) - .chain(recipient.into_bytes().iter().cloned()) - .chain(data.into_iter()) - .collect(), - - // MessageType::IsReply || REPLY_SURB || MESSAGE - // TODO: this is fragile as reply_SURB length CAN BE variable. however temporarily - // we are making 'unsafe' assumption that it will be constant - BinaryClientRequest::Reply { - message, - reply_surb, - } => std::iter::once(MessageType::IsReply as u8) - .chain(reply_surb.to_bytes().into_iter()) - .chain(message.into_iter()) - .collect(), - } - } -} - -impl Into for BinaryClientRequest { - fn into(self) -> WsMessage { - WsMessage::Binary(self.into_bytes()) - } -} - -pub enum BinaryServerResponse { - Received(ReconstructedMessage), -} - -impl BinaryServerResponse { - pub fn into_bytes(self) -> Vec { - match self { - // this happens to work because right now there's only a single possible binary response - BinaryServerResponse::Received(reconstructed_message) => { - reconstructed_message.into_bytes() - } - } - } - - // TODO: dont be lazy and define error type and change it into Result - pub fn try_from_bytes(b: &[u8]) -> Option { - // this happens to work because right now there's only a single possible binary response - Some(BinaryServerResponse::Received( - ReconstructedMessage::try_from_bytes(b).ok()?, - )) - } -} - -impl Into for BinaryServerResponse { - fn into(self) -> WsMessage { - WsMessage::Binary(self.into_bytes()) - } -} diff --git a/clients/native/websocket-requests/Cargo.toml b/clients/native/websocket-requests/Cargo.toml new file mode 100644 index 0000000000..bd97d7fad6 --- /dev/null +++ b/clients/native/websocket-requests/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "websocket-requests" +version = "0.1.0" +authors = ["Jędrzej Stuczyński "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +nymsphinx = { path = "../../../common/nymsphinx" } \ No newline at end of file diff --git a/clients/native/websocket-requests/src/error.rs b/clients/native/websocket-requests/src/error.rs new file mode 100644 index 0000000000..9758a652c3 --- /dev/null +++ b/clients/native/websocket-requests/src/error.rs @@ -0,0 +1,92 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use std::fmt; + +// no need to go fancy here like we've done in other places. +#[derive(PartialEq, Clone, Serialize, Deserialize)] +pub struct Error { + pub kind: ErrorKind, + pub message: String, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.kind.as_str(), self.message) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl Error { + pub fn new(kind: ErrorKind, message: String) -> Self { + Error { kind, message } + } +} + +#[repr(u8)] +#[derive(PartialEq, Clone, Serialize, Deserialize)] +pub enum ErrorKind { + /// The received request contained no data. + EmptyRequest = 0x01, + + /// The received request did not contain enough data to be fully parsed. + TooShortRequest = 0x02, + + /// The received request tag is not defined. + UnknownRequest = 0x03, + + /// The received request is malformed. + MalformedRequest = 0x04, + + // that's an arbitrary division but lets keep 1-127 (hex 0x01 - 0x7F) values request-specific + // and 128-254 (hex 0x80 - 0xFE) for responses + /// The received response contained no data. + EmptyResponse = 0x80, + + /// The received response did not contain enough data to be fully parsed. + TooShortResponse = 0x81, + + /// The received response tag is not defined. + UnknownResponse = 0x82, + + /// The received response is malformed. + MalformedResponse = 0x83, + + /// The error is due to something else. + Other = 0xFF, +} + +impl ErrorKind { + pub(crate) fn as_str(&self) -> &'static str { + match *self { + ErrorKind::EmptyRequest => "received request contained no data", + ErrorKind::TooShortRequest => "received request did not contain enough data", + ErrorKind::UnknownRequest => "unknown request type", + ErrorKind::MalformedRequest => "malformed request", + + ErrorKind::EmptyResponse => "received response contained no data", + ErrorKind::TooShortResponse => "received response did not contain enough data", + ErrorKind::UnknownResponse => "unknown response type", + ErrorKind::MalformedResponse => "malformed response", + + ErrorKind::Other => "other", + } + } +} diff --git a/common/nymsphinx/params/src/message_types.rs b/clients/native/websocket-requests/src/lib.rs similarity index 65% rename from common/nymsphinx/params/src/message_types.rs rename to clients/native/websocket-requests/src/lib.rs index 86dbb82c21..c3c719dda2 100644 --- a/common/nymsphinx/params/src/message_types.rs +++ b/clients/native/websocket-requests/src/lib.rs @@ -12,12 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Perhaps this idea could be extended with some binary masking to convey more information -// like to help the receiver to know whether the message is expected to be text-based or binary -#[repr(u8)] -#[derive(Clone, Copy, Debug)] -pub enum MessageType { - WithoutReplySURB = 0, - WithReplySURB = 1, - IsReply = 2, -} +pub mod error; +pub mod requests; +pub mod responses; +mod text; diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs new file mode 100644 index 0000000000..27159f1f1a --- /dev/null +++ b/clients/native/websocket-requests/src/requests.rs @@ -0,0 +1,366 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// all variable size data is always prefixed with u64 length +// tags are u8 + +use crate::error::{self, ErrorKind}; +use crate::text::ClientRequestText; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::ReplySURB; +use std::convert::{TryFrom, TryInto}; +use std::mem::size_of; + +/// Value tag representing [`Send`] variant of the [`ClientRequest`] +pub const SEND_REQUEST_TAG: u8 = 0x00; + +/// Value tag representing [`Reply`] variant of the [`ClientRequest`] +pub const REPLY_REQUEST_TAG: u8 = 0x01; + +/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`] +pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; + +#[allow(non_snake_case)] +#[derive(Debug)] +pub enum ClientRequest { + Send { + recipient: Recipient, + message: Vec, + // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? + with_reply_surb: bool, + }, + Reply { + message: Vec, + reply_surb: ReplySURB, + }, + SelfAddress, +} + +// we could have been parsing it directly TryFrom, but we want to retain +// information about whether it came from binary or text to send appropriate response back +impl ClientRequest { + // SEND_REQUEST_TAG || with_surb || recipient || data_len || data + fn serialize_send(recipient: Recipient, data: Vec, with_reply_surb: bool) -> Vec { + let data_len_bytes = (data.len() as u64).to_be_bytes(); + std::iter::once(SEND_REQUEST_TAG) + .chain(std::iter::once(with_reply_surb as u8)) + .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant + .chain(data_len_bytes.iter().cloned()) + .chain(data.into_iter()) + .collect() + } + + // SEND_REQUEST_TAG || with_reply || recipient || data_len || data + fn deserialize_send(b: &[u8]) -> Result { + // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof bytes + if b.len() < 2 + Recipient::LEN + size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortRequest, + "not enough data provided to recover 'send'".to_string(), + )); + } + + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], SEND_REQUEST_TAG); + + let with_reply_surb = match b[1] { + 0 => false, + 1 => true, + n => { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!("invalid reply surb flag {}", n), + )) + } + }; + + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&b[2..2 + Recipient::LEN]); + let recipient = match Recipient::try_from_bytes(recipient_bytes) { + Ok(recipient) => recipient, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!("malformed recipient: {:?}", err), + )) + } + }; + + let data_len_bytes = &b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]; + let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap()); + let data = &b[2 + Recipient::LEN + size_of::()..]; + if data.len() as u64 != data_len { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!( + "data len has inconsistent length. specified: {} got: {}", + data_len, + data.len() + ), + )); + } + + Ok(ClientRequest::Send { + with_reply_surb, + recipient, + message: data.to_vec(), + }) + } + + // REPLY_REQUEST_TAG || surb_len || surb || message_len || message + fn serialize_reply(message: Vec, reply_surb: ReplySURB) -> Vec { + let reply_surb_bytes = reply_surb.to_bytes(); + let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); + let message_len_bytes = (message.len() as u64).to_be_bytes(); + + std::iter::once(REPLY_REQUEST_TAG) + .chain(surb_len_bytes.iter().cloned()) + .chain(reply_surb_bytes.into_iter()) + .chain(message_len_bytes.iter().cloned()) + .chain(message.into_iter()) + .collect() + } + + // REPLY_REQUEST_TAG || surb_len || surb || message_len || message + fn deserialize_reply(b: &[u8]) -> Result { + // we need to have at the very least 2 * sizeof bytes (in case, for some peculiar reason + // message and reply surb were 0 len - the request would still be malformed, but would in theory + // be parse'able) + if b.len() < 1 + 2 * size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortRequest, + "not enough data provided to recover 'reply'".to_string(), + )); + } + + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], REPLY_REQUEST_TAG); + + let reply_surb_len = + u64::from_be_bytes(b[1..1 + size_of::()].as_ref().try_into().unwrap()); + + // make sure we won't go out of bounds here + if reply_surb_len > (b.len() - 1 + 2 * size_of::()) as u64 { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!( + "not enough data to recover reply surb with specified length {}", + reply_surb_len + ), + )); + } + + let surb_bound = 1 + size_of::() + reply_surb_len as usize; + + let reply_surb_bytes = &b[1 + size_of::()..surb_bound]; + let reply_surb = match ReplySURB::from_bytes(reply_surb_bytes) { + Ok(reply_surb) => reply_surb, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!("malformed reply surb: {:?}", err), + )) + } + }; + + let message_len = u64::from_be_bytes( + b[surb_bound..surb_bound + size_of::()] + .as_ref() + .try_into() + .unwrap(), + ); + let message = &b[surb_bound + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + // TODO: should this blow HERE, i.e. during deserialization that the data you're trying + // to send via reply is too long? + + Ok(ClientRequest::Reply { + reply_surb, + message: message.to_vec(), + }) + } + + // SELF_ADDRESS_REQUEST_TAG + fn serialize_self_address() -> Vec { + std::iter::once(SELF_ADDRESS_REQUEST_TAG).collect() + } + + // SELF_ADDRESS_REQUEST_TAG + fn deserialize_self_address(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], SELF_ADDRESS_REQUEST_TAG); + + Ok(ClientRequest::SelfAddress) + } + + pub fn serialize(self) -> Vec { + match self { + ClientRequest::Send { + recipient, + message, + with_reply_surb, + } => Self::serialize_send(recipient, message, with_reply_surb), + + ClientRequest::Reply { + message, + reply_surb, + } => Self::serialize_reply(message, reply_surb), + + ClientRequest::SelfAddress => Self::serialize_self_address(), + } + } + + pub fn deserialize(b: &[u8]) -> Result { + if b.is_empty() { + // technically I'm not even sure this can ever be returned, because reading empty + // request would imply closed socket, but let's include it for completion sake + return Err(error::Error::new( + ErrorKind::EmptyRequest, + "no data provided".to_string(), + )); + } + + if b.len() < size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortRequest, + format!( + "not enough data provided to recover request tag. Provided only {} bytes", + b.len() + ), + )); + } + let request_tag = b[0]; + + // determine what kind of request that is and try to deserialize it + match request_tag { + SEND_REQUEST_TAG => Self::deserialize_send(b), + REPLY_REQUEST_TAG => Self::deserialize_reply(b), + SELF_ADDRESS_REQUEST_TAG => Self::deserialize_self_address(b), + n => Err(error::Error::new( + ErrorKind::UnknownRequest, + format!("type {}", n), + )), + } + } + + pub fn try_from_binary(raw_req: Vec) -> Result { + Self::deserialize(&raw_req) + } + + pub fn try_from_text(raw_req: String) -> Result { + // use the intermediate string structure and let serde do bunch of work for us + let text_req = ClientRequestText::try_from(raw_req).map_err(|json_err| { + error::Error::new(ErrorKind::MalformedRequest, json_err.to_string()) + })?; + + text_req.try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // very basic tests to check for obvious errors like off by one + #[test] + fn send_request_serialization_works() { + let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); + let recipient_string = recipient.to_string(); + + let send_request_no_surb = ClientRequest::Send { + recipient: recipient.clone(), + message: b"foomp".to_vec(), + with_reply_surb: false, + }; + + let bytes = send_request_no_surb.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::Send { + recipient, + message, + with_reply_surb, + } => { + assert_eq!(recipient.to_string(), recipient_string); + assert_eq!(message, b"foomp".to_vec()); + assert!(!with_reply_surb) + } + _ => unreachable!(), + } + + let send_request_surb = ClientRequest::Send { + recipient, + message: b"foomp".to_vec(), + with_reply_surb: true, + }; + + let bytes = send_request_surb.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::Send { + recipient, + message, + with_reply_surb, + } => { + assert_eq!(recipient.to_string(), recipient_string); + assert_eq!(message, b"foomp".to_vec()); + assert!(with_reply_surb) + } + _ => unreachable!(), + } + } + + #[test] + fn reply_request_serialization_works() { + let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; + let reply_surb = ReplySURB::from_base58_string(reply_surb_string).unwrap(); + let reply_request = ClientRequest::Reply { + message: b"foomp".to_vec(), + reply_surb, + }; + + let bytes = reply_request.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::Reply { + reply_surb, + message, + } => { + assert_eq!(reply_surb.to_base58_string(), reply_surb_string); + assert_eq!(message, b"foomp".to_vec()); + } + _ => unreachable!(), + } + } + + #[test] + fn self_address_request_serialization_works() { + let self_address_request = ClientRequest::SelfAddress; + let bytes = self_address_request.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::SelfAddress => (), + _ => unreachable!(), + } + } +} diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs new file mode 100644 index 0000000000..e3ccfc209f --- /dev/null +++ b/clients/native/websocket-requests/src/responses.rs @@ -0,0 +1,401 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// all variable size data is always prefixed with u64 length +// tags are u8 + +use crate::error::{self, ErrorKind}; +use crate::text::ServerResponseText; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::ReplySURB; +use nymsphinx::receiver::ReconstructedMessage; +use std::convert::TryInto; +use std::mem::size_of; + +/// Value tag representing [`Error`] variant of the [`ServerResponse`] +pub const ERROR_RESPONSE_TAG: u8 = 0x00; + +/// Value tag representing [`Received`] variant of the [`ServerResponse`] +pub const RECEIVED_RESPONSE_TAG: u8 = 0x01; + +/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] +pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02; + +#[derive(Debug)] +pub enum ServerResponse { + Received(ReconstructedMessage), + SelfAddress(Recipient), + Error(error::Error), +} + +impl ServerResponse { + pub fn new_error>(message: S) -> Self { + ServerResponse::Error(error::Error { + kind: ErrorKind::Other, + message: message.into(), + }) + } + + // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + fn serialize_received(reconstructed_message: ReconstructedMessage) -> Vec { + let message_len_bytes = (reconstructed_message.message.len() as u64).to_be_bytes(); + if let Some(reply_surb) = reconstructed_message.reply_SURB { + let reply_surb_bytes = reply_surb.to_bytes(); + let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); + + // with_reply || surb_len || surb || msg_len || msg + std::iter::once(RECEIVED_RESPONSE_TAG) + .chain(std::iter::once(true as u8)) + .chain(surb_len_bytes.iter().cloned()) + .chain(reply_surb_bytes.iter().cloned()) + .chain(message_len_bytes.iter().cloned()) + .chain(reconstructed_message.message.into_iter()) + .collect() + } else { + // without_reply || msg_len || msg + std::iter::once(RECEIVED_RESPONSE_TAG) + .chain(std::iter::once(false as u8)) + .chain(message_len_bytes.iter().cloned()) + .chain(reconstructed_message.message.into_iter()) + .collect() + } + } + + // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + fn deserialize_received(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], RECEIVED_RESPONSE_TAG); + + // we must be able to read at the very least if it has a reply_surb and length of some field + if b.len() < 2 + size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + "not enough data provided to recover 'received'".to_string(), + )); + } + + let with_reply_surb = match b[1] { + 0 => false, + 1 => true, + n => { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!("invalid reply flag {}", n), + )) + } + }; + + if with_reply_surb { + let reply_surb_len = + u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); + + // make sure we won't go out of bounds here + if reply_surb_len > (b.len() - 2 + 2 * size_of::()) as u64 { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + "not enough bytes to read reply_surb bytes!".to_string(), + )); + } + + let surb_bound = 2 + size_of::() + reply_surb_len as usize; + + let reply_surb_bytes = &b[2 + size_of::()..surb_bound]; + let reply_surb = match ReplySURB::from_bytes(reply_surb_bytes) { + Ok(reply_surb) => reply_surb, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!("malformed reply SURB: {:?}", err), + )) + } + }; + + let message_len = u64::from_be_bytes( + b[surb_bound..surb_bound + size_of::()] + .as_ref() + .try_into() + .unwrap(), + ); + let message = &b[surb_bound + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + + Ok(ServerResponse::Received(ReconstructedMessage { + message: message.to_vec(), + reply_SURB: Some(reply_surb), + })) + } else { + let message_len = + u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); + let message = &b[2 + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + + Ok(ServerResponse::Received(ReconstructedMessage { + message: message.to_vec(), + reply_SURB: None, + })) + } + } + + // SELF_ADDRESS_RESPONSE_TAG || self_address + fn serialize_self_address(address: Recipient) -> Vec { + std::iter::once(SELF_ADDRESS_RESPONSE_TAG) + .chain(address.to_bytes().iter().cloned()) + .collect() + } + + // SELF_ADDRESS_RESPONSE_TAG || self_address + fn deserialize_self_address(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], SELF_ADDRESS_RESPONSE_TAG); + + if b.len() != 1 + Recipient::LEN { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + "not enough data provided to recover 'self_address'".to_string(), + )); + } + + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]); + + let recipient = match Recipient::try_from_bytes(recipient_bytes) { + Ok(recipient) => recipient, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!("malformed Recipient: {:?}", err), + )) + } + }; + + Ok(ServerResponse::SelfAddress(recipient)) + } + + // ERROR_RESPONSE_TAG || err_code || msg_len || msg + fn serialize_error(error: error::Error) -> Vec { + let message_len_bytes = (error.message.len() as u64).to_be_bytes(); + std::iter::once(ERROR_RESPONSE_TAG) + .chain(std::iter::once(error.kind as u8)) + .chain(message_len_bytes.iter().cloned()) + .chain(error.message.into_bytes().into_iter()) + .collect() + } + + // ERROR_RESPONSE_TAG || err_code || msg_len || msg + fn deserialize_error(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], ERROR_RESPONSE_TAG); + + if b.len() < size_of::() + size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + "not enough data provided to recover 'error'".to_string(), + )); + } + + let error_kind = match b[1] { + _ if b[1] == (ErrorKind::EmptyRequest as u8) => ErrorKind::EmptyRequest, + _ if b[1] == (ErrorKind::TooShortRequest as u8) => ErrorKind::TooShortRequest, + _ if b[1] == (ErrorKind::UnknownRequest as u8) => ErrorKind::UnknownRequest, + _ if b[1] == (ErrorKind::MalformedRequest as u8) => ErrorKind::MalformedRequest, + + _ if b[1] == (ErrorKind::EmptyResponse as u8) => ErrorKind::EmptyResponse, + _ if b[1] == (ErrorKind::TooShortResponse as u8) => ErrorKind::TooShortResponse, + _ if b[1] == (ErrorKind::UnknownResponse as u8) => ErrorKind::UnknownResponse, + _ if b[1] == (ErrorKind::MalformedResponse as u8) => ErrorKind::MalformedResponse, + + _ if b[1] == (ErrorKind::Other as u8) => ErrorKind::Other, + + n => { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!("invalid error code {}", n), + )) + } + }; + + let message_len = + u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); + let message = &b[2 + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + + let err_message = match String::from_utf8(message.to_vec()) { + Ok(msg) => msg, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!("malformed error message: {:?}", err), + )) + } + }; + + Ok(ServerResponse::Error(error::Error::new( + error_kind, + err_message, + ))) + } + + pub fn serialize(self) -> Vec { + match self { + ServerResponse::Received(reconstructed_message) => { + Self::serialize_received(reconstructed_message) + } + ServerResponse::SelfAddress(address) => Self::serialize_self_address(address), + ServerResponse::Error(err) => Self::serialize_error(err), + } + } + + pub fn deserialize(b: &[u8]) -> Result { + if b.is_empty() { + // technically I'm not even sure this can ever be returned, because reading empty + // request would imply closed socket, but let's include it for completion sake + return Err(error::Error::new( + ErrorKind::EmptyResponse, + "no data provided".to_string(), + )); + } + + if b.len() < size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + format!( + "not enough data provided to recover response tag. Provided only {} bytes", + b.len() + ), + )); + } + + let response_tag = b[0]; + + // determine what kind of response that is and try to deserialize it + match response_tag { + RECEIVED_RESPONSE_TAG => Self::deserialize_received(b), + SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b), + ERROR_RESPONSE_TAG => Self::deserialize_error(b), + n => { + return Err(error::Error::new( + ErrorKind::UnknownResponse, + format!("type {}", n), + )) + } + } + } + + pub fn into_binary(self) -> Vec { + self.serialize() + } + + pub fn into_text(self) -> String { + // use the intermediate string structure and let serde do bunch of work for us + let text_resp = ServerResponseText::from(self); + + text_resp.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn received_response_serialization_works() { + let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; + + let received_with_surb = ServerResponse::Received(ReconstructedMessage { + message: b"foomp".to_vec(), + reply_SURB: Some(ReplySURB::from_base58_string(reply_surb_string).unwrap()), + }); + let bytes = received_with_surb.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::Received(reconstructed) => { + assert_eq!(reconstructed.message, b"foomp".to_vec()); + assert_eq!( + reconstructed.reply_SURB.unwrap().to_base58_string(), + reply_surb_string + ) + } + _ => unreachable!(), + } + + let received_without_surb = ServerResponse::Received(ReconstructedMessage { + message: b"foomp".to_vec(), + reply_SURB: None, + }); + let bytes = received_without_surb.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::Received(reconstructed) => { + assert_eq!(reconstructed.message, b"foomp".to_vec()); + assert!(reconstructed.reply_SURB.is_none()) + } + _ => unreachable!(), + } + } + + #[test] + fn self_address_response_serialization_works() { + let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); + let recipient_string = recipient.to_string(); + + let self_address_response = ServerResponse::SelfAddress(recipient); + let bytes = self_address_response.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::SelfAddress(recipient) => { + assert_eq!(recipient.to_string(), recipient_string) + } + _ => unreachable!(), + } + } + + #[test] + fn error_response_serialization_works() { + let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string()); + let error_response = ServerResponse::Error(dummy_error.clone()); + let bytes = error_response.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::Error(error) => assert_eq!(error, dummy_error), + _ => unreachable!(), + } + } +} diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs new file mode 100644 index 0000000000..0a33c29ac5 --- /dev/null +++ b/clients/native/websocket-requests/src/text.rs @@ -0,0 +1,153 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::error::ErrorKind; +use crate::requests::ClientRequest; +use crate::responses::ServerResponse; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::ReplySURB; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + +// local text equivalent of `ClientRequest` for easier serialization + deserialization with serde +// TODO: figure out if there's an easy way to avoid defining it + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub(super) enum ClientRequestText { + #[serde(rename_all = "camelCase")] + Send { + message: String, + recipient: String, + with_reply_surb: bool, + }, + SelfAddress, + #[serde(rename_all = "camelCase")] + Reply { + message: String, + reply_SURB: String, + }, +} + +impl TryFrom for ClientRequestText { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} + +impl TryInto for ClientRequestText { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + match self { + ClientRequestText::Send { + message, + recipient, + with_reply_surb, + } => { + let message_bytes = message.into_bytes(); + let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; + + Ok(ClientRequest::Send { + message: message_bytes, + recipient, + with_reply_surb, + }) + } + ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress), + ClientRequestText::Reply { + message, + reply_SURB, + } => { + let message_bytes = message.into_bytes(); + let reply_surb = ReplySURB::from_base58_string(reply_SURB).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; + + Ok(ClientRequest::Reply { + message: message_bytes, + reply_surb, + }) + } + } + } +} + +// local text equivalent of `ServerResponse` for easier serialization + deserialization with serde +// TODO: figure out if there's an easy way to avoid defining it + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub(super) enum ServerResponseText { + #[serde(rename_all = "camelCase")] + Received { + message: String, + reply_SURB: Option, + }, + SelfAddress { + address: String, + }, + Error { + message: String, + }, +} + +impl TryFrom for ServerResponseText { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result>::Error> { + serde_json::from_str(&msg) + } +} + +impl Into for ServerResponseText { + fn into(self) -> String { + // per serde_json docs: + /* + /// Serialization can fail if `T`'s implementation of `Serialize` decides to + /// fail, or if `T` contains a map with non-string keys. + */ + // this is not the case here. + serde_json::to_string(&self).unwrap() + } +} + +impl From for ServerResponseText { + fn from(resp: ServerResponse) -> Self { + match resp { + ServerResponse::Received(reconstructed) => { + ServerResponseText::Received { + // TODO: ask DH what is more appropriate, lossy utf8 conversion or returning error and then + // pure binary later + message: String::from_utf8_lossy(&reconstructed.message).into_owned(), + reply_SURB: reconstructed + .reply_SURB + .map(|reply_surb| reply_surb.to_base58_string()), + } + } + ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress { + address: recipient.to_string(), + }, + ServerResponse::Error(err) => ServerResponseText::Error { + message: err.to_string(), + }, + } + } +} diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 55e3744f73..6714ab0263 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -38,9 +38,7 @@ impl MixnetResponseListener { } } - async fn on_message(&self, bytes: Vec) { - let reconstructed_message = - ReconstructedMessage::try_from_bytes(&bytes).expect("todo: error handling"); + async fn on_message(&self, reconstructed_message: ReconstructedMessage) { let raw_message = reconstructed_message.message; if reconstructed_message.reply_SURB.is_some() { println!("this message had a surb - we didn't do anything with it"); @@ -70,8 +68,8 @@ impl MixnetResponseListener { pub(crate) async fn run(&mut self) { while let Some(received_responses) = self.mix_response_receiver.next().await { - for received_response in received_responses { - self.on_message(received_response.into_bytes()).await; + for reconstructed_message in received_responses { + self.on_message(reconstructed_message).await; } } println!("We should never see this message"); diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 4add3a8ffe..a23791853b 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -64,7 +64,7 @@ pub struct NodeData { pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> Vec { utils::set_panic_hook(); // nicer js errors. - let recipient = Recipient::try_from_string(recipient).unwrap(); + let recipient = Recipient::try_from_base58_string(recipient).unwrap(); let route = sphinx_route_to(topology_json, &recipient.gateway()).expect("todo: error handling..."); diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index 5ec05dcf7e..5b7c85f039 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -7,5 +7,6 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -crypto = { path = "../../crypto" } -nymsphinx-types = { path = "../types" } +crypto = { path = "../../crypto" } # all addresses are expressed in terms on their crypto keys +nymsphinx-types = { path = "../types" } # we need to be able to refer to some types defined inside sphinx crate +serde = "1.0" # implementing serialization/deserialization for some types, like `Recipient` diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index c59407ec0b..1de9964c5d 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -7,6 +7,9 @@ use crate::nodes::{NodeIdentity, NODE_IDENTITY_SIZE}; use crypto::asymmetric::{encryption, identity}; use nymsphinx_types::Destination; +use serde::de::{Error as SerdeError, Unexpected, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{self, Formatter}; // Not entirely sure whether this is the correct place for those, but let's see how it's going // to work out @@ -24,6 +27,30 @@ pub enum RecipientFormattingError { MalformedGatewayError(identity::SignatureError), } +impl fmt::Display for RecipientFormattingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + RecipientFormattingError::MalformedRecipientError => { + write!(f, "recipient is malformed") + } + RecipientFormattingError::MalformedIdentityError(id_err) => { + write!(f, "recipient's identity key is malformed: {}", id_err) + } + RecipientFormattingError::MalformedEncryptionKeyError(enc_err) => { + write!(f, "recipient's encryption key is malformed: {}", enc_err) + } + RecipientFormattingError::MalformedGatewayError(id_err) => write!( + f, + "recipient gateway's identity key is malformed: {}", + id_err + ), + } + } +} + +// since we have Debug and Display might as well slap Error on top of it too +impl std::error::Error for RecipientFormattingError {} + impl From for RecipientFormattingError { fn from(err: encryption::EncryptionKeyError) -> Self { RecipientFormattingError::MalformedEncryptionKeyError(err) @@ -38,6 +65,56 @@ pub struct Recipient { gateway: NodeIdentity, } +// Serialize + Deserialize is not really used anymore (it was for a CBOR experiment) +// however, if we decided we needed it again, it's already here +impl Serialize for Recipient { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_bytes(&self.to_bytes()) + } +} + +impl<'de> Deserialize<'de> for Recipient { + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + struct RecipientVisitor; + + impl<'de> Visitor<'de> for RecipientVisitor { + type Value = Recipient; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "A recipient is 96-byte sequence containing two ed25519 public keys an one x25519 public key all in compressed forms.") + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: SerdeError, + { + if bytes.len() != Recipient::LEN { + return Err(SerdeError::invalid_length(bytes.len(), &self)); + } + + let mut recipient_bytes = [0u8; Recipient::LEN]; + // this shouldn't panic as we just checked for length + recipient_bytes.copy_from_slice(&bytes); + + Recipient::try_from_bytes(recipient_bytes).or_else(|_| { + Err(SerdeError::invalid_value( + Unexpected::Other("At least one of the curve points was malformed"), + &self, + )) + }) + } + } + + deserializer.deserialize_bytes(RecipientVisitor) + } +} + impl Recipient { pub const LEN: usize = CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE + NODE_IDENTITY_SIZE; @@ -77,7 +154,7 @@ impl Recipient { &self.gateway } - pub fn into_bytes(self) -> [u8; Self::LEN] { + pub fn to_bytes(&self) -> [u8; Self::LEN] { let mut out = [0u8; Self::LEN]; out[..CLIENT_IDENTITY_SIZE].copy_from_slice(&self.client_identity.to_bytes()); out[CLIENT_IDENTITY_SIZE..CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE] @@ -113,7 +190,7 @@ impl Recipient { }) } - pub fn try_from_string>( + pub fn try_from_base58_string>( full_address: S, ) -> Result { let string_address = full_address.into(); @@ -151,7 +228,7 @@ impl Recipient { // ADDRESS . ENCRYPTION @ GATEWAY_ID impl std::fmt::Display for Recipient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}.{}@{}", @@ -179,7 +256,7 @@ mod tests { ); let str_recipient = recipient.to_string(); - let recovered_recipient = Recipient::try_from_string(str_recipient).unwrap(); + let recovered_recipient = Recipient::try_from_base58_string(str_recipient).unwrap(); // as long as byte representation of internal keys are identical, it's all fine assert_eq!( @@ -208,7 +285,7 @@ mod tests { *gateway_id_pair.public_key(), ); - let bytes_recipient = recipient.clone().into_bytes(); + let bytes_recipient = recipient.clone().to_bytes(); let recovered_recipient = Recipient::try_from_bytes(bytes_recipient).unwrap(); // as long as byte representation of internal keys are identical, it's all fine diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index 538267fbdb..7a2e2bdf69 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] rand = {version = "0.7.3", features = ["wasm-bindgen"]} bs58 = "0.3" +serde = "1.0" crypto = { path = "../../crypto" } nymsphinx-addressing = { path = "../addressing" } diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 163f924284..1663e213ec 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -22,18 +22,45 @@ use nymsphinx_params::packet_sizes::PacketSize; use nymsphinx_params::{ReplySURBKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; use nymsphinx_types::{delays, Error as SphinxError, SURBMaterial, SphinxPacket, SURB}; use rand::{CryptoRng, RngCore}; +use serde::de::{Error as SerdeError, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; +use std::fmt::{self, Formatter}; use std::time; use topology::{NymTopology, NymTopologyError}; #[derive(Debug)] pub enum ReplySURBError { - NonPaddedMessageError, - MalformedStringError, + UnpaddedMessageError, + MalformedStringError(bs58::decode::Error), RecoveryError(SphinxError), InvalidEncryptionKeyData(SURBEncryptionKeyError), } +impl fmt::Display for ReplySURBError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + ReplySURBError::UnpaddedMessageError => { + write!(f, "tried to use reply SURB with an unpadded message") + } + ReplySURBError::MalformedStringError(decode_err) => { + write!(f, "reply SURB is incorrectly formatted: {}", decode_err) + } + ReplySURBError::RecoveryError(sphinx_err) => { + write!(f, "failed to recover reply SURB from bytes: {}", sphinx_err) + } + ReplySURBError::InvalidEncryptionKeyData(surb_key_err) => write!( + f, + "failed to recover reply SURB encryption key from bytes: {}", + surb_key_err + ), + } + } +} + +// since we have Debug and Display might as well slap Error on top of it too +impl std::error::Error for ReplySURBError {} + impl From for ReplySURBError { fn from(err: SURBEncryptionKeyError) -> Self { ReplySURBError::InvalidEncryptionKeyData(err) @@ -46,6 +73,44 @@ pub struct ReplySURB { encryption_key: SURBEncryptionKey, } +// Serialize + Deserialize is not really used anymore (it was for a CBOR experiment) +// however, if we decided we needed it again, it's already here +impl Serialize for ReplySURB { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_bytes(&self.to_bytes()) + } +} + +impl<'de> Deserialize<'de> for ReplySURB { + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + struct ReplySURBVisitor; + + impl<'de> Visitor<'de> for ReplySURBVisitor { + type Value = ReplySURB; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "A replySURB must contain a valid symmetric encryption key and a correctly formed sphinx header") + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: SerdeError, + { + ReplySURB::from_bytes(bytes) + .or_else(|_| Err(SerdeError::invalid_length(bytes.len(), &self))) + } + } + + deserializer.deserialize_bytes(ReplySURBVisitor) + } +} + impl ReplySURB { pub fn max_msg_len(packet_size: PacketSize) -> usize { // For detailed explanation (of ack overhead) refer to common\nymsphinx\src\preparer.rs::available_plaintext_per_packet() @@ -126,7 +191,7 @@ impl ReplySURB { pub fn from_base58_string>(val: S) -> Result { let bytes = match bs58::decode(val.into()).into_vec() { Ok(decoded) => decoded, - Err(_) => return Err(ReplySURBError::MalformedStringError), + Err(err) => return Err(ReplySURBError::MalformedStringError(err)), }; Self::from_bytes(&bytes) } @@ -145,7 +210,7 @@ impl ReplySURB { let packet_size = packet_size.unwrap_or_else(Default::default); if message.len() != packet_size.plaintext_size() { - return Err(ReplySURBError::NonPaddedMessageError); + return Err(ReplySURBError::UnpaddedMessageError); } // this can realistically only fail on too long messages and we just checked for that diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index 66b14d3120..afcdcda36d 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. use crypto::blake3; - -pub mod message_types; pub mod packet_sizes; use crypto::aes_ctr::Aes128Ctr; -pub use message_types::MessageType; pub use packet_sizes::PacketSize; // If somebody can provide an argument why it might be reasonable to have more than 255 mix hops, diff --git a/common/nymsphinx/src/preparer.rs b/common/nymsphinx/src/preparer.rs index 34c44275a4..7851a7e0b4 100644 --- a/common/nymsphinx/src/preparer.rs +++ b/common/nymsphinx/src/preparer.rs @@ -26,7 +26,7 @@ use nymsphinx_anonymous_replies::reply_surb::ReplySURB; use nymsphinx_chunking::fragment::{Fragment, FragmentIdentifier}; use nymsphinx_params::packet_sizes::PacketSize; use nymsphinx_params::{ - MessageType, PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySURBEncryptionAlgorithm, + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySURBEncryptionAlgorithm, ReplySURBKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS, }; use nymsphinx_types::builder::SphinxPacketBuilder; @@ -175,7 +175,7 @@ where let reply_key = reply_surb.encryption_key(); // if there's a reply surb, the message takes form of `1 || REPLY_KEY || REPLY_SURB || MSG` Ok(( - std::iter::once(MessageType::WithReplySURB as u8) + std::iter::once(true as u8) .chain(reply_surb.to_bytes().iter().cloned()) .chain(message.into_iter()) .collect(), @@ -184,7 +184,7 @@ where } else { // but if there's no reply surb, the message takes form of `0 || MSG` Ok(( - std::iter::once(MessageType::WithoutReplySURB as u8) + std::iter::once(false as u8) .chain(message.into_iter()) .collect(), None, @@ -405,7 +405,7 @@ where #[cfg(test)] pub(crate) fn test_fixture() -> MessagePreparer { let rng = rand::rngs::OsRng; - let dummy_address = Recipient::try_from_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); + let dummy_address = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); MessagePreparer { rng, diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index fe22074d3b..371f951358 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -18,9 +18,7 @@ use crypto::symmetric::stream_cipher; use nymsphinx_anonymous_replies::reply_surb::{ReplySURB, ReplySURBError}; use nymsphinx_chunking::fragment::Fragment; use nymsphinx_chunking::reconstruction::MessageReconstructor; -use nymsphinx_params::{ - MessageType, PacketEncryptionAlgorithm, PacketHkdfAlgorithm, DEFAULT_NUM_MIX_HOPS, -}; +use nymsphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, DEFAULT_NUM_MIX_HOPS}; // TODO: should this live in this file? #[allow(non_snake_case)] @@ -33,45 +31,6 @@ pub struct ReconstructedMessage { pub reply_SURB: Option, } -impl ReconstructedMessage { - pub fn into_bytes(self) -> Vec { - if let Some(reply_surb) = self.reply_SURB { - std::iter::once(MessageType::WithReplySURB as u8) - .chain(reply_surb.to_bytes().iter().cloned()) - .chain(self.message.into_iter()) - .collect() - } else { - std::iter::once(MessageType::WithoutReplySURB as u8) - .chain(self.message.into_iter()) - .collect() - } - } - - pub fn try_from_bytes(b: &[u8]) -> Result { - if b.is_empty() { - return Err(MessageRecoveryError::TooShortMessageError); - } - - match b[0] { - n if n == MessageType::WithReplySURB as u8 => { - let surb_len = ReplySURB::serialized_len(DEFAULT_NUM_MIX_HOPS); - if b.len() < surb_len + 1 { - return Err(MessageRecoveryError::TooShortMessageError); - } - Ok(ReconstructedMessage { - reply_SURB: Some(ReplySURB::from_bytes(&b[1..1 + surb_len])?), - message: b[1 + surb_len..].to_vec(), - }) - } - n if n == MessageType::WithoutReplySURB as u8 => Ok(ReconstructedMessage { - message: b[1..].to_vec(), - reply_SURB: None, - }), - _ => Err(MessageRecoveryError::InvalidSurbPrefixError), - } - } -} - #[derive(Debug)] pub enum MessageRecoveryError { InvalidSurbPrefixError, @@ -122,11 +81,11 @@ impl MessageReceiver { message: &mut Vec, ) -> Result, MessageRecoveryError> { match message[0] { - n if n == MessageType::WithoutReplySURB as u8 => { + n if n == false as u8 => { message.remove(0); Ok(None) } - n if n == MessageType::WithReplySURB as u8 => { + n if n == true as u8 => { let surb_len: usize = ReplySURB::serialized_len(self.num_mix_hops); // note the extra +1 (due to 0/1 message prefix) let surb_bytes = &message[1..1 + surb_len]; @@ -327,7 +286,7 @@ mod message_receiver { // the actual 'correctness' of the underlying message doesn't matter for this test let message = vec![42; 100]; - let dummy_recipient = Recipient::try_from_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML").unwrap(); + let dummy_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML").unwrap(); let average_delay = Duration::from_millis(500); let topology = topology_fixture(); diff --git a/service-providers/simple-socks5/Cargo.toml b/service-providers/simple-socks5/Cargo.toml index d59def383d..9d1efa1fb3 100644 --- a/service-providers/simple-socks5/Cargo.toml +++ b/service-providers/simple-socks5/Cargo.toml @@ -17,4 +17,5 @@ tokio = { version = "0.2", features = ["full"] } tokio-tungstenite = "0.10.1" nymsphinx = { path = "../../common/nymsphinx" } -simple-socks5-requests = { path = "simple-socks5-requests" } \ No newline at end of file +simple-socks5-requests = { path = "simple-socks5-requests" } +websocket-requests = { path = "../../clients/native/websocket-requests" } \ No newline at end of file diff --git a/service-providers/simple-socks5/src/core.rs b/service-providers/simple-socks5/src/core.rs index 1a36f0181a..2ca19b7eb8 100644 --- a/service-providers/simple-socks5/src/core.rs +++ b/service-providers/simple-socks5/src/core.rs @@ -1,15 +1,14 @@ use crate::{controller::Controller, websocket}; use futures::SinkExt; use futures_util::StreamExt; -use nymsphinx::params::MessageType; -use nymsphinx::receiver::ReconstructedMessage; +use nymsphinx::addressing::clients::Recipient; use simple_socks5_requests::Request; use tokio::net::TcpStream; use tokio::runtime::Runtime; use tokio_tungstenite::tungstenite::protocol::Message; use tokio_tungstenite::WebSocketStream; use websocket::WebsocketConnectionError; - +use websocket_requests::{requests::ClientRequest, responses::ServerResponse}; pub struct ServiceProvider { runtime: Runtime, } @@ -30,25 +29,33 @@ impl ServiceProvider { println!("\nAll systems go. Press CTRL-C to stop the server."); while let Some(msg) = websocket_reader.next().await { let data = msg.unwrap().into_data(); + let received = match ServerResponse::deserialize(&data).expect("todo: error handling") { + ServerResponse::Received(received) => received, + ServerResponse::Error(err) => { + panic!("received error from native client! - {}", err) + }, + _ => unimplemented!("probably should never be reached?") + }; - let reconstructed_message = ReconstructedMessage::try_from_bytes(&data).expect("todo: error handling"); - let raw_message = reconstructed_message.message; + let raw_message = received.message; let request = Request::try_from_bytes(&raw_message).unwrap(); - let response = controller.process_request(request).await.unwrap(); - if response.is_none() { // restart the loop if we got nothing back - continue; - } + let response = match controller.process_request(request).await.unwrap() { + None => continue, // restart the loop if we got nothing back + Some(response) => response, + }; // TODO: wire SURBs in here once they're available let return_address = "7tVXwePpo6SM99sqM1xEp6S4T1TSpxYx97fTpEdvmF7i.GgrN8998SmwvQghNEvqtPPZCgMQqJovWBrzspMnBESsE@e3vUAo6YhB7zq3GH8B4k3iiGT4H2USjdd5ZMZoUsHdF"; - let recipient = nymsphinx::addressing::clients::Recipient::try_from_string(return_address).unwrap(); + let recipient = Recipient::try_from_base58_string(return_address).unwrap(); - let response_message = std::iter::once(MessageType::WithoutReplySURB as u8) - .chain(recipient.into_bytes().iter().cloned()) - .chain(response.unwrap().into_bytes().into_iter()) - .collect(); + // make 'request' to native-websocket client + let response_message = ClientRequest::Send { + recipient, + message: response.into_bytes(), + with_reply_surb: false + }; - let message = Message::Binary(response_message); + let message = Message::Binary(response_message.serialize()); websocket_writer.send(message).await.unwrap(); } });