diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 706f8128..97c6e191 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,10 @@ jobs: cmd: clippy args: -- -D clippy::all cache: {} + - name: "Clippy (ring feature)" + cmd: clippy + args: --no-default-features --features ring -- -D clippy::all + cache: {} - name: "Formatting" cmd: fmt args: -- --check @@ -44,6 +48,10 @@ jobs: cmd: test args: --all-features cache: { sharedKey: "tests" } + - name: "Unit Tests (ring feature)" + cmd: test + args: --no-default-features --features ring + cache: { sharedKey: "tests-ring" } include: - os: ubuntu-latest sccache-path: /home/runner/.cache/sccache diff --git a/.gitignore b/.gitignore index fee1f314..08dbe03d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,11 @@ # Generated by Cargo /target/ -# IntelliJ IDEA -/.idea/ +# Editors +.idea *.iml - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock +.vscode +.atom # Certificate files *.der diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..a50a762c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,923 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "a2" +version = "0.6.2" +dependencies = [ + "argparse", + "base64 0.20.0", + "erased-serde", + "http", + "hyper", + "hyper-alpn", + "openssl", + "pem", + "ring", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "argparse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "erased-serde" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d" +dependencies = [ + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-alpn" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f485f87658b5ac391dbed0379d50928de16b54bffee0a85234dc6a92fbe534f6" +dependencies = [ + "hyper", + "log", + "rustls", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "webpki-roots", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pem" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 0ce2ba73..251901a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.6.2" authors = [ "Harry Bairstow ", "Julius de Bruijn ", - "Sergey Tkachenko ", + "Sergey Tkachenko ", ] license = "MIT" readme = "README.md" @@ -13,24 +13,29 @@ keywords = ["apns", "apple", "push", "async", "http2"] repository = "https://github.com/walletconnect/a2.git" homepage = "https://github.com/walletconnect/a2" documentation = "https://docs.rs/a2" -edition = "2018" +edition = "2021" + +[features] +default = ["openssl"] +tracing = ["dep:tracing"] +ring = ["dep:ring", "pem"] [dependencies] -serde = "1" +serde = { version = "1", features = ["derive"] } erased-serde = "0.3" -serde_derive = "1" serde_json = "1" thiserror = "1" -openssl = "0.10" -futures = "0.3" +openssl = { version = "0.10", optional = true } +hyper = { version = "0.14", default-features = false, features = ["client", "http2"] } +hyper-alpn = "0.4" http = "0.2" -base64 = "0.13" -log = "0.4" -hyper = { version = "0.14", features = ["client", "http2", "tcp"] } -hyper-alpn = "0.3" +base64 = "0.20" +tracing = { version = "0.1", optional = true } +pem = { version = "1.0", optional = true } +ring = { version = "0.16", features = ["std"], optional = true } [dev-dependencies] argparse = "0.2" -pretty_env_logger = "0.4" -indoc = "1" +tracing-subscriber = "0.3" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +hyper = { version = "0.14", features = ["client", "http2", "tcp"] } diff --git a/README.md b/README.md index 70a40add..9b902b66 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ HTTP/2 Apple Push Notification Service for Rust using Tokio and async sending. ## Requirements -Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or later. +Needs a Tokio executor version 1.0 or later and Rust compiler version 1.60.0 or later. ## Documentation @@ -27,6 +27,8 @@ Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or * Supports `.p8` private keys to connect using authentication tokens. * If using authentication tokens, handles signature renewing for Apple's guidelines and caching for maximum performance. +* Cryptography primitives are provided either by openssl or + [ring](https://github.com/briansmith/ring). ## Examples diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index 3fcc2a64..f0fdad87 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -1,13 +1,11 @@ -use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; +use a2::{Client, DefaultNotificationBuilder, NotificationBuilder, NotificationOptions}; use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; -use pretty_env_logger; -use std::fs::File; use tokio; // An example client connectiong to APNs with a certificate and key #[tokio::main] async fn main() -> Result<(), Box> { - pretty_env_logger::init(); + tracing_subscriber::fmt().init(); let mut certificate_file = String::new(); let mut password = String::new(); @@ -34,18 +32,26 @@ async fn main() -> Result<(), Box> { ap.parse_args_or_exit(); } - // Read the private key and certificate from the disk - let mut certificate = File::open(certificate_file).unwrap(); + // Connecting to APNs using a client certificate + let new_client = || -> Result> { + #[cfg(feature = "openssl")] + { + // Which service to call, test or production? + let endpoint = if sandbox { + a2::Endpoint::Sandbox + } else { + a2::Endpoint::Production + }; - // Which service to call, test or production? - let endpoint = if sandbox { - Endpoint::Sandbox - } else { - Endpoint::Production + let mut certificate = std::fs::File::open(certificate_file)?; + Ok(Client::certificate(&mut certificate, &password, endpoint)?) + } + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + { + Err("ring does not support loading of certificates".into()) + } }; - - // Connecting to APNs using a client certificate - let client = Client::certificate(&mut certificate, &password, endpoint).unwrap(); + let client = new_client()?; let options = NotificationOptions { apns_topic: topic.as_ref().map(|s| &**s), @@ -53,9 +59,10 @@ async fn main() -> Result<(), Box> { }; // Notification payload - let mut builder = PlainNotificationBuilder::new(message.as_ref()); - builder.set_sound("default"); - builder.set_badge(1u32); + let builder = DefaultNotificationBuilder::new() + .set_body(message.as_ref()) + .set_sound("default") + .set_badge(1u32); let payload = builder.build(device_token.as_ref(), options); let response = client.send(payload).await?; diff --git a/examples/token_client.rs b/examples/token_client.rs index c24652aa..cbe5c859 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -1,14 +1,13 @@ use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; -use pretty_env_logger; use std::fs::File; use tokio; -use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; +use a2::{Client, DefaultNotificationBuilder, Endpoint, NotificationBuilder, NotificationOptions}; // An example client connectiong to APNs with a JWT token #[tokio::main] async fn main() -> Result<(), Box> { - pretty_env_logger::init(); + tracing_subscriber::fmt().init(); let mut key_file = String::new(); let mut team_id = String::new(); @@ -57,9 +56,10 @@ async fn main() -> Result<(), Box> { }; // Notification payload - let mut builder = PlainNotificationBuilder::new(message.as_ref()); - builder.set_sound("default"); - builder.set_badge(1u32); + let builder = DefaultNotificationBuilder::new() + .set_body(message.as_ref()) + .set_sound("default") + .set_badge(1u32); let payload = builder.build(device_token.as_ref(), options); let response = client.send(payload).await?; diff --git a/src/client.rs b/src/client.rs index ab929055..d44b3cef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,11 +9,9 @@ use crate::request::payload::Payload; use crate::response::Response; use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::{self, Body, Client as HttpClient, StatusCode}; -use openssl::pkcs12::Pkcs12; -use std::future::Future; +use std::fmt; use std::io::Read; use std::time::Duration; -use std::{fmt, str}; /// The APNs service endpoint to connect. #[derive(Debug, Clone)] @@ -43,6 +41,7 @@ impl fmt::Display for Endpoint { /// the notification and responds with a status OK. In any other case the future /// fails. If APNs gives a reason for the failure, the returned `Err` /// holds the response for handling. +#[derive(Debug, Clone)] pub struct Client { endpoint: Endpoint, signer: Option, @@ -65,6 +64,9 @@ impl Client { /// Create a connection to APNs using the provider client certificate which /// you obtain from your [Apple developer /// account](https://developer.apple.com/account/). + /// + /// Only works with the `openssl` feature. + #[cfg(feature = "openssl")] pub fn certificate(certificate: &mut R, password: &str, endpoint: Endpoint) -> Result where R: Read, @@ -72,7 +74,7 @@ impl Client { let mut cert_der: Vec = Vec::new(); certificate.read_to_end(&mut cert_der)?; - let pkcs = Pkcs12::from_der(&cert_der)?.parse(password)?; + let pkcs = openssl::pkcs12::Pkcs12::from_der(&cert_der)?.parse(password)?; let connector = AlpnConnector::with_client_cert(&pkcs.cert.to_pem()?, &pkcs.pkey.private_key_to_pem_pkcs8()?)?; Ok(Self::new(connector, None, endpoint)) @@ -98,34 +100,33 @@ impl Client { /// Send a notification payload. /// /// See [ErrorReason](enum.ErrorReason.html) for possible errors. - pub fn send(&self, payload: Payload<'_>) -> impl Future> + 'static { + #[cfg_attr(feature = "tracing", ::tracing::instrument)] + pub async fn send(&self, payload: Payload<'_>) -> Result { let request = self.build_request(payload); let requesting = self.http_client.request(request); - async move { - let response = requesting.await?; + let response = requesting.await?; + + let apns_id = response + .headers() + .get("apns-id") + .and_then(|s| s.to_str().ok()) + .map(String::from); - let apns_id = response - .headers() - .get("apns-id") - .and_then(|s| s.to_str().ok()) - .map(String::from); + match response.status() { + StatusCode::OK => Ok(Response { + apns_id, + error: None, + code: response.status().as_u16(), + }), + status => { + let body = hyper::body::to_bytes(response).await?; - match response.status() { - StatusCode::OK => Ok(Response { + Err(ResponseError(Response { apns_id, - error: None, - code: response.status().as_u16(), - }), - status => { - let body = hyper::body::to_bytes(response).await?; - - Err(ResponseError(Response { - apns_id, - error: serde_json::from_slice(&body).ok(), - code: status.as_u16(), - })) - } + error: serde_json::from_slice(&body).ok(), + code: status.as_u16(), + })) } } } @@ -141,7 +142,7 @@ impl Client { if let Some(ref apns_priority) = payload.options.apns_priority { builder = builder.header("apns-priority", apns_priority.to_string().as_bytes()); } - if let Some(ref apns_id) = payload.options.apns_id { + if let Some(apns_id) = payload.options.apns_id { builder = builder.header("apns-id", apns_id.as_bytes()); } if let Some(ref apns_expiration) = payload.options.apns_expiration { @@ -150,7 +151,7 @@ impl Client { if let Some(ref apns_collapse_id) = payload.options.apns_collapse_id { builder = builder.header("apns-collapse-id", apns_collapse_id.value.to_string().as_bytes()); } - if let Some(ref apns_topic) = payload.options.apns_topic { + if let Some(apns_topic) = payload.options.apns_topic { builder = builder.header("apns-topic", apns_topic.as_bytes()); } if let Some(ref signer) = self.signer { @@ -172,25 +173,23 @@ impl Client { #[cfg(test)] mod tests { use super::*; + use crate::request::notification::DefaultNotificationBuilder; use crate::request::notification::NotificationBuilder; - use crate::request::notification::PlainNotificationBuilder; use crate::request::notification::{CollapseId, NotificationOptions, Priority}; use crate::signer::Signer; use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::Method; use hyper_alpn::AlpnConnector; - const PRIVATE_KEY: &'static str = indoc!( - "-----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu - lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 - jDwmlD1Gg0yJt1e38djFwsxsfr5q2hv0Rj9fTEqAPr8H7mGm0wKxZ7iQ - -----END PRIVATE KEY-----" - ); + const PRIVATE_KEY: &'static str = "-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu +lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 +jDwmlD1Gg0yJt1e38djFwsxsfr5q2hv0Rj9fTEqAPr8H7mGm0wKxZ7iQ +-----END PRIVATE KEY-----"; #[test] fn test_production_request_uri() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -201,7 +200,7 @@ mod tests { #[test] fn test_sandbox_request_uri() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Sandbox); let request = client.build_request(payload); @@ -212,7 +211,7 @@ mod tests { #[test] fn test_request_method() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -222,7 +221,7 @@ mod tests { #[test] fn test_request_content_type() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -232,7 +231,7 @@ mod tests { #[test] fn test_request_content_length() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload.clone()); @@ -244,7 +243,7 @@ mod tests { #[test] fn test_request_authorization_with_no_signer() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -262,7 +261,7 @@ mod tests { ) .unwrap(); - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), Some(signer), Endpoint::Production); let request = client.build_request(payload); @@ -272,7 +271,7 @@ mod tests { #[test] fn test_request_with_default_priority() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -283,7 +282,7 @@ mod tests { #[test] fn test_request_with_normal_priority() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -302,7 +301,7 @@ mod tests { #[test] fn test_request_with_high_priority() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -321,7 +320,7 @@ mod tests { #[test] fn test_request_with_default_apns_id() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); @@ -334,7 +333,7 @@ mod tests { #[test] fn test_request_with_an_apns_id() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -353,7 +352,7 @@ mod tests { #[test] fn test_request_with_default_apns_expiration() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); @@ -366,7 +365,7 @@ mod tests { #[test] fn test_request_with_an_apns_expiration() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -385,7 +384,7 @@ mod tests { #[test] fn test_request_with_default_apns_collapse_id() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); @@ -398,7 +397,7 @@ mod tests { #[test] fn test_request_with_an_apns_collapse_id() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -417,7 +416,7 @@ mod tests { #[test] fn test_request_with_default_apns_topic() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); @@ -430,7 +429,7 @@ mod tests { #[test] fn test_request_with_an_apns_topic() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build( "a_test_id", @@ -449,7 +448,7 @@ mod tests { #[tokio::test] async fn test_request_body() { - let builder = PlainNotificationBuilder::new("test"); + let builder = DefaultNotificationBuilder::new(); let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload.clone()); diff --git a/src/error.rs b/src/error.rs index fe91d8f2..5f167ba2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ //! Error and result module -use crate::response::Response; +use crate::{response::Response, signer::SignerError}; use std::io; use thiserror::Error; @@ -16,7 +16,7 @@ pub enum Error { /// Couldn't generate an APNs token with the given key. #[error("Error creating a signature: {0}")] - SignerError(#[from] openssl::error::ErrorStack), + SignerError(#[from] SignerError), /// APNs couldn't accept the notification. Contains /// [Response](response/struct.Response.html) with additional @@ -38,4 +38,16 @@ pub enum Error { /// Error reading the certificate or private key. #[error("Error in reading a certificate file: {0}")] ReadError(#[from] io::Error), + + /// Unexpected private key (only EC keys are supported). + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + #[error("Unexpected private key: {0}")] + UnexpectedKey(#[from] ring::error::KeyRejected), +} + +#[cfg(feature = "openssl")] +impl From for Error { + fn from(e: openssl::error::ErrorStack) -> Self { + Self::SignerError(SignerError::OpenSSL(e)) + } } diff --git a/src/lib.rs b/src/lib.rs index 11a8585f..adc97d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,8 @@ //! //! ## Payload //! -//! Building the notification payload should be done with the corresponding builders: -//! -//! * [PlainNotificationBuilder](request/notification/struct.PlainNotificationBuilder.html) for text only messages. -//! * [SilentNotificationBuilder](request/notification/struct.SilentNotificationBuilder.html) for silent notifications with custom data. -//! * [LocalizedNotificationBuilder](request/notification/struct.LocalizedNotificationBuilder.html) for localized rich notifications. +//! Building the notification payload should be done with the [DefaultNotificationBuilder](request/notification/struct.DefaultNotificationBuilder.html) for most use-cases. +//! There is also the [WebNotificationBuilder](request/notification/struct.WebNotificationBuilder.html) in the case you need to send notifications to safari //! //! The payload generated by the builder [can hold a custom data //! section](request/payload/struct.Payload.html#method.add_custom_data), @@ -34,14 +31,15 @@ //! ## Example sending a plain notification using token authentication: //! //! ```no_run -//! # use a2::{PlainNotificationBuilder, NotificationBuilder, Client, Endpoint}; +//! # use a2::{DefaultNotificationBuilder, NotificationBuilder, Client, Endpoint}; //! # use std::fs::File; //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { -//! let mut builder = PlainNotificationBuilder::new("Hi there"); -//! builder.set_badge(420); -//! builder.set_category("cat1"); -//! builder.set_sound("ping.flac"); +//! let mut builder = DefaultNotificationBuilder::new() +//! .set_body("Hi there") +//! .set_badge(420) +//! .set_category("cat1") +//! .set_sound("ping.flac"); //! //! let payload = builder.build("device-token-from-the-user", Default::default()); //! let mut file = File::open("/path/to/private_key.p8")?; @@ -61,10 +59,12 @@ //! ## Example sending a silent notification with custom data using certificate authentication: //! //! ```no_run -//! #[macro_use] extern crate serde_derive; +//! #[macro_use] extern crate serde; +//! # #[cfg(all(feature = "openssl", not(feature = "ring")))] +//! # { //! //! use a2::{ -//! Client, Endpoint, SilentNotificationBuilder, NotificationBuilder, NotificationOptions, +//! Client, Endpoint, DefaultNotificationBuilder, NotificationBuilder, NotificationOptions, //! Priority, //! }; //! use std::fs::File; @@ -82,7 +82,8 @@ //! is_paying_user: false, //! }; //! -//! let mut payload = SilentNotificationBuilder::new() +//! let mut payload = DefaultNotificationBuilder::new() +//! .set_content_available() //! .build("device-token-from-the-user", //! NotificationOptions { //! apns_priority: Some(Priority::Normal), @@ -103,22 +104,18 @@ //! //! Ok(()) //! } +//! # } //! ``` +#[cfg(not(any(feature = "openssl", feature = "ring")))] +compile_error!("either feature \"openssl\" or feature \"ring\" has to be enabled"); #[macro_use] -extern crate serde_derive; +extern crate serde; #[allow(unused_imports)] #[macro_use] extern crate serde_json; -#[cfg(test)] -#[macro_use] -extern crate indoc; - -#[macro_use] -extern crate log; - pub mod client; pub mod error; pub mod request; @@ -126,8 +123,8 @@ pub mod response; mod signer; pub use crate::request::notification::{ - CollapseId, LocalizedNotificationBuilder, NotificationBuilder, NotificationOptions, PlainNotificationBuilder, - Priority, SilentNotificationBuilder, WebNotificationBuilder, WebPushAlert, + CollapseId, DefaultNotificationBuilder, NotificationBuilder, NotificationOptions, Priority, WebNotificationBuilder, + WebPushAlert, }; pub use crate::response::{ErrorBody, ErrorReason, Response}; diff --git a/src/request/notification.rs b/src/request/notification.rs index f57b66b7..717c0717 100644 --- a/src/request/notification.rs +++ b/src/request/notification.rs @@ -1,15 +1,11 @@ //! The `aps` notification content builders -mod localized; +mod default; mod options; -mod plain; -mod silent; mod web; -pub use self::localized::{LocalizedAlert, LocalizedNotificationBuilder}; +pub use self::default::{DefaultAlert, DefaultNotificationBuilder}; pub use self::options::{CollapseId, NotificationOptions, Priority}; -pub use self::plain::PlainNotificationBuilder; -pub use self::silent::SilentNotificationBuilder; pub use self::web::{WebNotificationBuilder, WebPushAlert}; use crate::request::payload::Payload; diff --git a/src/request/notification/default.rs b/src/request/notification/default.rs new file mode 100644 index 00000000..ab3834eb --- /dev/null +++ b/src/request/notification/default.rs @@ -0,0 +1,720 @@ +use crate::request::notification::{NotificationBuilder, NotificationOptions}; +use crate::request::payload::{APSAlert, Payload, APS}; + +use std::{borrow::Cow, collections::BTreeMap}; + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct DefaultAlert<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + subtitle: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + body: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + title_loc_key: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + title_loc_args: Option>>, + + #[serde(skip_serializing_if = "Option::is_none")] + action_loc_key: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + loc_key: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + loc_args: Option>>, + + #[serde(skip_serializing_if = "Option::is_none")] + launch_image: Option<&'a str>, +} + +/// A builder to create an APNs payload. +/// +/// # Example +/// +/// ```rust +/// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; +/// # fn main() { +/// let mut builder = DefaultNotificationBuilder::new() +/// .set_title("Hi there") +/// .set_subtitle("From bob") +/// .set_body("What's up?") +/// .set_badge(420) +/// .set_category("cat1") +/// .set_sound("prööt") +/// .set_mutable_content() +/// .set_action_loc_key("PLAY") +/// .set_launch_image("foo.jpg") +/// .set_loc_args(&["argh", "narf"]) +/// .set_title_loc_key("STOP") +/// .set_title_loc_args(&["herp", "derp"]) +/// .set_loc_key("PAUSE") +/// .set_loc_args(&["narf", "derp"]); +/// let payload = builder.build("device_id", Default::default()) +/// .to_json_string().unwrap(); +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct DefaultNotificationBuilder<'a> { + alert: DefaultAlert<'a>, + badge: Option, + sound: Option<&'a str>, + category: Option<&'a str>, + mutable_content: u8, + content_available: Option, + has_edited_alert: bool, +} + +impl<'a> DefaultNotificationBuilder<'a> { + /// Creates a new builder with the minimum amount of content. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let payload = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_body("a body") + /// .build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn new() -> DefaultNotificationBuilder<'a> { + DefaultNotificationBuilder { + alert: DefaultAlert { + title: None, + subtitle: None, + body: None, + title_loc_key: None, + title_loc_args: None, + action_loc_key: None, + loc_key: None, + loc_args: None, + launch_image: None, + }, + badge: None, + sound: None, + category: None, + mutable_content: 0, + content_available: None, + has_edited_alert: false, + } + } + + /// Set the title of the notification. + /// Apple Watch displays this string in the short look notification interface. + /// Specify a string that’s quickly understood by the user. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_title(mut self, title: &'a str) -> Self { + self.alert.title = Some(title); + self.has_edited_alert = true; + self + } + + /// Used to set the subtitle which should provide additional information that explains the purpose of the notification. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_subtitle("a subtitle"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"subtitle\":\"a subtitle\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_subtitle(mut self, subtitle: &'a str) -> Self { + self.alert.subtitle = Some(subtitle); + self.has_edited_alert = true; + self + } + + /// Sets the content of the alert message. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_body("a body"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"body\":\"a body\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_body(mut self, body: &'a str) -> Self { + self.alert.body = Some(body); + self.has_edited_alert = true; + self + } + + /// A number to show on a badge on top of the app icon. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_badge(4); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"badge\":4,\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_badge(mut self, badge: u32) -> Self { + self.badge = Some(badge); + self + } + + /// File name of the custom sound to play when receiving the notification. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_sound("ping"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"sound\":\"ping\"}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_sound(mut self, sound: &'a str) -> Self { + self.sound = Some(sound); + self + } + + /// When a notification includes the category key, the system displays the + /// actions for that category as buttons in the banner or alert interface. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_category("cat1"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"category\":\"cat1\",\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_category(mut self, category: &'a str) -> Self { + self.category = Some(category); + self + } + + /// The localization key for the notification title. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_title_loc_key("play"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-key\":\"play\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_title_loc_key(mut self, key: &'a str) -> Self { + self.alert.title_loc_key = Some(key); + self.has_edited_alert = true; + self + } + + /// Arguments for the title localization. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_title_loc_args(&["foo", "bar"]); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_title_loc_args(mut self, args: &'a [S]) -> Self + where + S: Into> + AsRef, + { + let converted = args.iter().map(|a| a.as_ref().into()).collect(); + + self.alert.title_loc_args = Some(converted); + self.has_edited_alert = true; + self + } + + /// The localization key for the action. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_action_loc_key("stop"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"action-loc-key\":\"stop\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_action_loc_key(mut self, key: &'a str) -> Self { + self.alert.action_loc_key = Some(key); + self.has_edited_alert = true; + self + } + + /// The localization key for the push message body. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_loc_key("lol"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"loc-key\":\"lol\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_loc_key(mut self, key: &'a str) -> Self { + self.alert.loc_key = Some(key); + self.has_edited_alert = true; + self + } + + /// Arguments for the content localization. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_loc_args(&["omg", "foo"]); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"loc-args\":[\"omg\",\"foo\"],\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_loc_args(mut self, args: &'a [S]) -> Self + where + S: Into> + AsRef, + { + let converted = args.iter().map(|a| a.as_ref().into()).collect(); + + self.alert.loc_args = Some(converted); + self.has_edited_alert = true; + self + } + + /// Image to display in the rich notification. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_launch_image("cat.png"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"launch-image\":\"cat.png\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_launch_image(mut self, image: &'a str) -> Self { + self.alert.launch_image = Some(image); + self.has_edited_alert = true; + self + } + + /// Allow client to modify push content before displaying. + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_mutable_content(); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":1}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_mutable_content(mut self) -> Self { + self.mutable_content = 1; + self + } + + /// Used for adding custom data to push notifications + /// + /// ```rust + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// # fn main() { + /// let mut builder = DefaultNotificationBuilder::new() + /// .set_title("a title") + /// .set_content_available(); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"content-available\":1,\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_content_available(mut self) -> Self { + self.content_available = Some(1); + self + } +} + +impl<'a> NotificationBuilder<'a> for DefaultNotificationBuilder<'a> { + fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { + Payload { + aps: APS { + alert: match self.has_edited_alert { + true => Some(APSAlert::Default(self.alert)), + false => None, + }, + badge: self.badge, + sound: self.sound, + content_available: self.content_available, + category: self.category, + mutable_content: Some(self.mutable_content), + url_args: None, + }, + device_token, + options, + data: BTreeMap::new(), + } + } +} + +impl<'a> Default for DefaultNotificationBuilder<'a> { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_notification_with_minimal_required_values() { + let payload = DefaultNotificationBuilder::new() + .set_title("the title") + .set_body("the body") + .build("device-token", Default::default()) + .to_json_string() + .unwrap(); + + let expected_payload = json!({ + "aps": { + "alert": { + "title": "the title", + "body": "the body", + }, + "mutable-content": 0 + } + }) + .to_string(); + + assert_eq!(expected_payload, payload); + } + + #[test] + fn test_default_notification_with_full_data() { + let builder = DefaultNotificationBuilder::new() + .set_title("the title") + .set_body("the body") + .set_badge(420) + .set_category("cat1") + .set_sound("prööt") + .set_mutable_content() + .set_action_loc_key("PLAY") + .set_launch_image("foo.jpg") + .set_loc_args(&["argh", "narf"]) + .set_title_loc_key("STOP") + .set_title_loc_args(&["herp", "derp"]) + .set_loc_key("PAUSE") + .set_loc_args(&["narf", "derp"]); + + let payload = builder + .build("device-token", Default::default()) + .to_json_string() + .unwrap(); + + let expected_payload = json!({ + "aps": { + "alert": { + "action-loc-key": "PLAY", + "body": "the body", + "launch-image": "foo.jpg", + "loc-args": ["narf", "derp"], + "loc-key": "PAUSE", + "title": "the title", + "title-loc-args": ["herp", "derp"], + "title-loc-key": "STOP" + }, + "badge": 420, + "category": "cat1", + "mutable-content": 1, + "sound": "prööt" + } + }) + .to_string(); + + assert_eq!(expected_payload, payload); + } + + #[test] + fn test_notification_with_custom_data_1() { + #[derive(Serialize, Debug)] + struct SubData { + nothing: &'static str, + } + + #[derive(Serialize, Debug)] + struct TestData { + key_str: &'static str, + key_num: u32, + key_bool: bool, + key_struct: SubData, + } + + let test_data = TestData { + key_str: "foo", + key_num: 42, + key_bool: false, + key_struct: SubData { nothing: "here" }, + }; + + let mut payload = DefaultNotificationBuilder::new() + .set_title("the title") + .set_body("the body") + .build("device-token", Default::default()); + + payload.add_custom_data("custom", &test_data).unwrap(); + + let expected_payload = json!({ + "custom": { + "key_str": "foo", + "key_num": 42, + "key_bool": false, + "key_struct": { + "nothing": "here" + } + }, + "aps": { + "alert": { + "title": "the title", + "body": "the body", + }, + "mutable-content": 0, + }, + }) + .to_string(); + + assert_eq!(expected_payload, payload.to_json_string().unwrap()); + } + + #[test] + fn test_notification_with_custom_data_2() { + #[derive(Serialize, Debug)] + struct SubData { + nothing: &'static str, + } + + #[derive(Serialize, Debug)] + struct TestData { + key_str: &'static str, + key_num: u32, + key_bool: bool, + key_struct: SubData, + } + + let test_data = TestData { + key_str: "foo", + key_num: 42, + key_bool: false, + key_struct: SubData { nothing: "here" }, + }; + + let mut payload = DefaultNotificationBuilder::new() + .set_body("kulli") + .build("device-token", Default::default()); + + payload.add_custom_data("custom", &test_data).unwrap(); + + let payload_json = payload.to_json_string().unwrap(); + + let expected_payload = json!({ + "custom": { + "key_str": "foo", + "key_num": 42, + "key_bool": false, + "key_struct": { + "nothing": "here" + } + }, + "aps": { + "alert": { + "body": "kulli" + }, + "mutable-content": 0 + } + }) + .to_string(); + + assert_eq!(expected_payload, payload_json); + } + + #[test] + fn test_silent_notification_with_no_content() { + let payload = DefaultNotificationBuilder::new() + .set_content_available() + .build("device-token", Default::default()) + .to_json_string() + .unwrap(); + + let expected_payload = json!({ + "aps": { + "content-available": 1, + "mutable-content": 0 + } + }) + .to_string(); + + assert_eq!(expected_payload, payload); + } + + #[test] + fn test_silent_notification_with_custom_data() { + #[derive(Serialize, Debug)] + struct SubData { + nothing: &'static str, + } + + #[derive(Serialize, Debug)] + struct TestData { + key_str: &'static str, + key_num: u32, + key_bool: bool, + key_struct: SubData, + } + + let test_data = TestData { + key_str: "foo", + key_num: 42, + key_bool: false, + key_struct: SubData { nothing: "here" }, + }; + + let mut payload = DefaultNotificationBuilder::new() + .set_content_available() + .build("device-token", Default::default()); + + payload.add_custom_data("custom", &test_data).unwrap(); + + let expected_payload = json!({ + "aps": { + "content-available": 1, + "mutable-content": 0 + }, + "custom": { + "key_str": "foo", + "key_num": 42, + "key_bool": false, + "key_struct": { + "nothing": "here" + } + } + }) + .to_string(); + + assert_eq!(expected_payload, payload.to_json_string().unwrap()); + } + + #[test] + fn test_silent_notification_with_custom_hashmap() { + let mut test_data = BTreeMap::new(); + test_data.insert("key_str", "foo"); + test_data.insert("key_str2", "bar"); + + let mut payload = DefaultNotificationBuilder::new() + .set_content_available() + .build("device-token", Default::default()); + + payload.add_custom_data("custom", &test_data).unwrap(); + + let expected_payload = json!({ + "aps": { + "content-available": 1, + "mutable-content": 0, + }, + "custom": { + "key_str": "foo", + "key_str2": "bar" + } + }) + .to_string(); + + assert_eq!(expected_payload, payload.to_json_string().unwrap()); + } +} diff --git a/src/request/notification/localized.rs b/src/request/notification/localized.rs deleted file mode 100644 index 213314c1..00000000 --- a/src/request/notification/localized.rs +++ /dev/null @@ -1,444 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{APSAlert, Payload, APS}; - -use std::{borrow::Cow, collections::BTreeMap}; - -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct LocalizedAlert<'a> { - title: &'a str, - body: &'a str, - - #[serde(skip_serializing_if = "Option::is_none")] - title_loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - title_loc_args: Option>>, - - #[serde(skip_serializing_if = "Option::is_none")] - action_loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - loc_args: Option>>, - - #[serde(skip_serializing_if = "Option::is_none")] - launch_image: Option<&'a str>, -} - -/// A builder to create a localized APNs payload. -/// -/// # Example -/// -/// ```rust -/// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; -/// # fn main() { -/// let mut builder = LocalizedNotificationBuilder::new("Hi there", "What's up?"); -/// builder.set_badge(420); -/// builder.set_category("cat1"); -/// builder.set_sound("prööt"); -/// builder.set_mutable_content(); -/// builder.set_action_loc_key("PLAY"); -/// builder.set_launch_image("foo.jpg"); -/// builder.set_loc_args(&["argh", "narf"]); -/// builder.set_title_loc_key("STOP"); -/// builder.set_title_loc_args(&["herp", "derp"]); -/// builder.set_loc_key("PAUSE"); -/// builder.set_loc_args(&["narf", "derp"]); -/// let payload = builder.build("device_id", Default::default()) -/// .to_json_string().unwrap(); -/// # } -/// ``` -pub struct LocalizedNotificationBuilder<'a> { - alert: LocalizedAlert<'a>, - badge: Option, - sound: Option<&'a str>, - category: Option<&'a str>, - mutable_content: u8, -} - -impl<'a> LocalizedNotificationBuilder<'a> { - /// Creates a new builder with the minimum amount of content. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = LocalizedNotificationBuilder::new("a title", "a body") - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new(title: &'a str, body: &'a str) -> LocalizedNotificationBuilder<'a> { - LocalizedNotificationBuilder { - alert: LocalizedAlert { - title, - body, - title_loc_key: None, - title_loc_args: None, - action_loc_key: None, - loc_key: None, - loc_args: None, - launch_image: None, - }, - badge: None, - sound: None, - category: None, - mutable_content: 0, - } - } - - /// A number to show on a badge on top of the app icon. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_badge(4); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"badge\":4,\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self { - self.badge = Some(badge); - self - } - - /// File name of the custom sound to play when receiving the notification. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_sound("ping"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0,\"sound\":\"ping\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { - self.sound = Some(sound); - self - } - - /// When a notification includes the category key, the system displays the - /// actions for that category as buttons in the banner or alert interface. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_category("cat1"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"category\":\"cat1\",\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self { - self.category = Some(category); - self - } - - /// The localization key for the notification title. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_title_loc_key("play"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\",\"title-loc-key\":\"play\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_title_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.title_loc_key = Some(key); - self - } - - /// Arguments for the title localization. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_title_loc_args(&["foo", "bar"]); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\",\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_title_loc_args(&mut self, args: &'a [S]) -> &mut Self - where - S: Into> + AsRef, - { - let converted = args.iter().map(|a| a.as_ref().into()).collect(); - - self.alert.title_loc_args = Some(converted); - self - } - - /// The localization key for the action. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_action_loc_key("stop"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"action-loc-key\":\"stop\",\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_action_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.action_loc_key = Some(key); - self - } - - /// The localization key for the push message body. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_loc_key("lol"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"loc-key\":\"lol\",\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.loc_key = Some(key); - self - } - - /// Arguments for the content localization. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_loc_args(&["omg", "foo"]); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"loc-args\":[\"omg\",\"foo\"],\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_loc_args(&mut self, args: &'a [S]) -> &mut Self - where - S: Into> + AsRef, - { - let converted = args.iter().map(|a| a.as_ref().into()).collect(); - - self.alert.loc_args = Some(converted); - self - } - - /// Image to display in the rich notification. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_launch_image("cat.png"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"launch-image\":\"cat.png\",\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_launch_image(&mut self, image: &'a str) -> &mut Self { - self.alert.launch_image = Some(image); - self - } - - /// Allow client to modify push content before displaying. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_mutable_content(); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":1}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_mutable_content(&mut self) -> &mut Self { - self.mutable_content = 1; - self - } -} - -impl<'a> NotificationBuilder<'a> for LocalizedNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: Some(APSAlert::Localized(self.alert)), - badge: self.badge, - sound: self.sound, - content_available: None, - category: self.category, - mutable_content: Some(self.mutable_content), - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_localized_notification_with_minimal_required_values() { - let payload = LocalizedNotificationBuilder::new("the title", "the body") - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": { - "title": "the title", - "body": "the body", - }, - "mutable-content": 0 - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_localized_notification_with_full_data() { - let mut builder = LocalizedNotificationBuilder::new("the title", "the body"); - - builder.set_badge(420); - builder.set_category("cat1"); - builder.set_sound("prööt"); - builder.set_mutable_content(); - builder.set_action_loc_key("PLAY"); - builder.set_launch_image("foo.jpg"); - builder.set_loc_args(&["argh", "narf"]); - builder.set_title_loc_key("STOP"); - builder.set_title_loc_args(&["herp", "derp"]); - builder.set_loc_key("PAUSE"); - builder.set_loc_args(&["narf", "derp"]); - - let payload = builder - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": { - "action-loc-key": "PLAY", - "body": "the body", - "launch-image": "foo.jpg", - "loc-args": ["narf", "derp"], - "loc-key": "PAUSE", - "title": "the title", - "title-loc-args": ["herp", "derp"], - "title-loc-key": "STOP" - }, - "badge": 420, - "category": "cat1", - "mutable-content": 1, - "sound": "prööt" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let mut payload = - LocalizedNotificationBuilder::new("the title", "the body").build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - }, - "aps": { - "alert": { - "title": "the title", - "body": "the body", - }, - "mutable-content": 0 - }, - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } -} diff --git a/src/request/notification/options.rs b/src/request/notification/options.rs index 91688b06..d821c5aa 100644 --- a/src/request/notification/options.rs +++ b/src/request/notification/options.rs @@ -20,7 +20,7 @@ impl<'a> CollapseId<'a> { } /// Headers to specify options to the notification. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct NotificationOptions<'a> { /// A canonical UUID that identifies the notification. If there is an error /// sending the notification, APNs uses this value to identify the @@ -62,18 +62,6 @@ pub struct NotificationOptions<'a> { pub apns_collapse_id: Option>, } -impl<'a> Default for NotificationOptions<'a> { - fn default() -> NotificationOptions<'a> { - NotificationOptions { - apns_id: None, - apns_expiration: None, - apns_priority: None, - apns_topic: None, - apns_collapse_id: None, - } - } -} - /// The importance how fast to bring the notification for the user.. #[derive(Debug, Clone)] pub enum Priority { diff --git a/src/request/notification/plain.rs b/src/request/notification/plain.rs deleted file mode 100644 index 675d535b..00000000 --- a/src/request/notification/plain.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{APSAlert, Payload, APS}; -use std::collections::BTreeMap; - -/// A builder to create a simple APNs notification payload. -/// -/// # Example -/// -/// ```rust -/// # use a2::request::notification::{NotificationBuilder, PlainNotificationBuilder}; -/// # fn main() { -/// let mut builder = PlainNotificationBuilder::new("Hi there"); -/// builder.set_badge(420); -/// builder.set_category("cat1"); -/// builder.set_sound("prööt"); -/// let payload = builder.build("device_id", Default::default()) -/// .to_json_string().unwrap(); -/// # } -/// ``` -pub struct PlainNotificationBuilder<'a> { - body: &'a str, - badge: Option, - sound: Option<&'a str>, - category: Option<&'a str>, -} - -impl<'a> PlainNotificationBuilder<'a> { - /// Creates a new builder with the minimum amount of content. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = PlainNotificationBuilder::new("a body") - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new(body: &'a str) -> PlainNotificationBuilder<'a> { - PlainNotificationBuilder { - body, - badge: None, - sound: None, - category: None, - } - } - - /// A number to show on a badge on top of the app icon. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_badge(4); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"badge\":4}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self { - self.badge = Some(badge); - self - } - - /// File name of the custom sound to play when receiving the notification. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_sound("meow"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"sound\":\"meow\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { - self.sound = Some(sound); - self - } - - /// When a notification includes the category key, the system displays the - /// actions for that category as buttons in the banner or alert interface. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_category("cat1"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"category\":\"cat1\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self { - self.category = Some(category); - self - } -} - -impl<'a> NotificationBuilder<'a> for PlainNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: Some(APSAlert::Plain(self.body)), - badge: self.badge, - sound: self.sound, - content_available: None, - category: self.category, - mutable_content: None, - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_plain_notification_with_text_only() { - let payload = PlainNotificationBuilder::new("kulli") - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": "kulli", - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_full_data() { - let mut builder = PlainNotificationBuilder::new("Hi there"); - builder.set_badge(420); - builder.set_category("cat1"); - builder.set_sound("prööt"); - - let payload = builder - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": "Hi there", - "badge": 420, - "category": "cat1", - "sound": "prööt" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let mut payload = PlainNotificationBuilder::new("kulli").build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let payload_json = payload.to_json_string().unwrap(); - - let expected_payload = json!({ - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - }, - "aps": { - "alert": "kulli", - } - }) - .to_string(); - - assert_eq!(expected_payload, payload_json); - } -} diff --git a/src/request/notification/silent.rs b/src/request/notification/silent.rs deleted file mode 100644 index 9e9dde23..00000000 --- a/src/request/notification/silent.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{Payload, APS}; -use std::collections::BTreeMap; - -/// A builder to create an APNs silent notification payload which can be used to -/// send custom data to the user's phone if the user hasn't been running the app -/// for a while. The custom data needs to be implementing `Serialize` from Serde. -/// -/// # Example -/// -/// ```rust -/// # use std::collections::HashMap; -/// # use a2::request::notification::{NotificationBuilder, SilentNotificationBuilder}; -/// # fn main() { -/// let mut test_data = HashMap::new(); -/// test_data.insert("a", "value"); -/// -/// let mut payload = SilentNotificationBuilder::new() -/// .build("device_id", Default::default()); -/// -/// payload.add_custom_data("custom", &test_data); -/// -/// assert_eq!( -/// "{\"aps\":{\"content-available\":1},\"custom\":{\"a\":\"value\"}}", -/// &payload.to_json_string().unwrap() -/// ); -/// # } -/// ``` -pub struct SilentNotificationBuilder { - content_available: u8, -} - -impl SilentNotificationBuilder { - /// Creates a new builder. - /// - /// ```rust - /// # use a2::request::notification::{SilentNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = SilentNotificationBuilder::new() - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"content-available\":1}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new() -> SilentNotificationBuilder { - SilentNotificationBuilder { content_available: 1 } - } -} - -impl Default for SilentNotificationBuilder { - fn default() -> Self { - Self::new() - } -} - -impl<'a> NotificationBuilder<'a> for SilentNotificationBuilder { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: None, - badge: None, - sound: None, - content_available: Some(self.content_available), - category: None, - mutable_content: None, - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - #[test] - fn test_silent_notification_with_no_content() { - let payload = SilentNotificationBuilder::new() - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_silent_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - }, - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - } - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } - - #[test] - fn test_silent_notification_with_custom_hashmap() { - let mut test_data = BTreeMap::new(); - test_data.insert("key_str", "foo"); - test_data.insert("key_str2", "bar"); - - let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - }, - "custom": { - "key_str": "foo", - "key_str2": "bar" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } -} diff --git a/src/request/notification/web.rs b/src/request/notification/web.rs index 1cc567f8..2cc86bcd 100644 --- a/src/request/notification/web.rs +++ b/src/request/notification/web.rs @@ -48,7 +48,7 @@ impl<'a> WebNotificationBuilder<'a> { WebNotificationBuilder { alert, sound: None, - url_args: url_args, + url_args, } } diff --git a/src/request/payload.rs b/src/request/payload.rs index 3bf2d9ca..dcb013e4 100644 --- a/src/request/payload.rs +++ b/src/request/payload.rs @@ -1,7 +1,7 @@ //! Payload with `aps` and custom data use crate::error::Error; -use crate::request::notification::{LocalizedAlert, NotificationOptions, WebPushAlert}; +use crate::request::notification::{DefaultAlert, NotificationOptions, WebPushAlert}; use erased_serde::Serialize; use serde_json::{self, Value}; use std::collections::BTreeMap; @@ -30,10 +30,11 @@ impl<'a> Payload<'a> { /// Using a `HashMap`: /// /// ```rust - /// # use a2::request::notification::{SilentNotificationBuilder, NotificationBuilder}; + /// # use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; /// # use std::collections::HashMap; /// # fn main() { - /// let mut payload = SilentNotificationBuilder::new() + /// let mut payload = DefaultNotificationBuilder::new() + /// .set_content_available() /// .build("token", Default::default()); /// let mut custom_data = HashMap::new(); /// @@ -41,7 +42,7 @@ impl<'a> Payload<'a> { /// payload.add_custom_data("foo_data", &custom_data).unwrap(); /// /// assert_eq!( - /// "{\"aps\":{\"content-available\":1},\"foo_data\":{\"foo\":\"bar\"}}", + /// "{\"aps\":{\"content-available\":1,\"mutable-content\":0},\"foo_data\":{\"foo\":\"bar\"}}", /// &payload.to_json_string().unwrap() /// ); /// # } @@ -50,24 +51,26 @@ impl<'a> Payload<'a> { /// Using a custom struct: /// /// ```rust - /// # #[macro_use] extern crate serde_derive; - /// # use a2::request::notification::{SilentNotificationBuilder, NotificationBuilder}; - /// # fn main() { + /// #[macro_use] extern crate serde; + /// use a2::request::notification::{DefaultNotificationBuilder, NotificationBuilder}; + /// fn main() { /// #[derive(Serialize)] /// struct CompanyData { /// foo: &'static str, /// } /// - /// let mut payload = SilentNotificationBuilder::new().build("token", Default::default()); + /// let mut payload = DefaultNotificationBuilder::new() + /// .set_content_available() + /// .build("token", Default::default()); /// let mut custom_data = CompanyData { foo: "bar" }; /// /// payload.add_custom_data("foo_data", &custom_data).unwrap(); /// /// assert_eq!( - /// "{\"aps\":{\"content-available\":1},\"foo_data\":{\"foo\":\"bar\"}}", + /// "{\"aps\":{\"content-available\":1,\"mutable-content\":0},\"foo_data\":{\"foo\":\"bar\"}}", /// &payload.to_json_string().unwrap() /// ); - /// # } + /// } /// ``` pub fn add_custom_data(&mut self, root_key: &'a str, data: &dyn Serialize) -> Result<&mut Self, Error> { self.data.insert(root_key, serde_json::to_value(data)?); @@ -126,10 +129,8 @@ pub struct APS<'a> { #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum APSAlert<'a> { - /// Text-only notification. - Plain(&'a str), - /// A rich localized notification. - Localized(LocalizedAlert<'a>), + /// A notification that supports all of the iOS features + Default(DefaultAlert<'a>), /// Safari web push notification WebPush(WebPushAlert<'a>), } diff --git a/src/response.rs b/src/response.rs index 7218720a..ae8ba091 100644 --- a/src/response.rs +++ b/src/response.rs @@ -27,7 +27,7 @@ pub struct Response { } /// The response body from APNs. Only available for errors. -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Deserialize, Debug, PartialEq, Eq)] pub struct ErrorBody { /// The error indicating the reason for the failure. pub reason: ErrorReason, @@ -42,7 +42,7 @@ pub struct ErrorBody { } /// A description what went wrong with the push notification. -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Deserialize, Debug, PartialEq, Eq)] pub enum ErrorReason { /// The collapse identifier exceeds the maximum allowed size. BadCollapseId, diff --git a/src/signer.rs b/src/signer.rs index bc6300c2..df93fb5f 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -1,19 +1,24 @@ use crate::error::Error; use base64::encode; use std::io::Read; +use std::sync::Arc; use std::{ sync::RwLock, time::{Duration, SystemTime, UNIX_EPOCH}, }; +#[cfg(feature = "openssl")] use openssl::{ ec::EcKey, hash::MessageDigest, pkey::{PKey, Private}, sign::Signer as SslSigner, }; +#[cfg(all(not(feature = "openssl"), feature = "ring"))] +use ring::{rand, signature}; +use thiserror::Error; -#[derive(Debug)] +#[derive(Debug, Clone)] struct Signature { key: String, issued_at: i64, @@ -21,11 +26,12 @@ struct Signature { /// For signing requests when using token-based authentication. Re-uses the same /// signature for a certain amount of time. +#[derive(Debug, Clone)] pub struct Signer { - signature: RwLock, + signature: Arc>, key_id: String, team_id: String, - secret: PKey, + secret: Arc, expire_after_s: Duration, } @@ -46,10 +52,55 @@ struct JwtPayload<'a> { iat: i64, } +#[derive(Debug)] +enum Secret { + #[cfg(feature = "openssl")] + OpenSSL(PKey), + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + Ring { + signing_key: signature::EcdsaKeyPair, + rng: rand::SystemRandom, + }, +} + +impl Secret { + #[cfg(feature = "openssl")] + fn new_openssl(pem_key: &[u8]) -> Result { + let ec_key = EcKey::private_key_from_pem(pem_key)?; + let secret = PKey::from_ec_key(ec_key)?; + Ok(Self::OpenSSL(secret)) + } + + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + fn new_ring(pem_key: &[u8]) -> Result { + let der = pem::parse(pem_key).map_err(SignerError::Pem)?; + let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, &der.contents)?; + let rng = rand::SystemRandom::new(); + Ok(Self::Ring { signing_key, rng }) + } + + fn from_pem(mut pk_pem: R) -> Result + where + R: Read, + { + let mut pem_key: Vec = Vec::new(); + pk_pem.read_to_end(&mut pem_key)?; + #[cfg(feature = "openssl")] + { + Self::new_openssl(&pem_key) + } + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + { + Self::new_ring(&pem_key) + } + } +} + impl Signer { /// Creates a signer with a pkcs8 private key, APNs key id and team id. /// Can fail if the key is not valid or there is a problem with system OpenSSL. - pub fn new(mut pk_pem: R, key_id: S, team_id: T, signature_ttl: Duration) -> Result + pub fn new(pk_pem: R, key_id: S, team_id: T, signature_ttl: Duration) -> Result where S: Into, T: Into, @@ -58,24 +109,19 @@ impl Signer { let key_id: String = key_id.into(); let team_id: String = team_id.into(); - let mut pem_key: Vec = Vec::new(); - pk_pem.read_to_end(&mut pem_key)?; - - let ec_key = EcKey::private_key_from_pem(&pem_key)?; + let secret = Secret::from_pem(pk_pem)?; let issued_at = get_time(); - let secret = PKey::from_ec_key(ec_key)?; - let signature = RwLock::new(Signature { key: Self::create_signature(&secret, &key_id, &team_id, issued_at)?, issued_at, }); let signer = Signer { - signature, + signature: Arc::new(signature), key_id, team_id, - secret, + secret: Arc::new(secret), expire_after_s: signature_ttl, }; @@ -94,17 +140,20 @@ impl Signer { let signature = self.signature.read().unwrap(); - trace!( - "Signer::with_signature found signature for {}/{} valid for {}s", - self.key_id, - self.team_id, - self.expire_after_s.as_secs(), - ); + #[cfg(feature = "tracing")] + { + tracing::trace!( + "Signer::with_signature found signature for {}/{} valid for {}s", + self.key_id, + self.team_id, + self.expire_after_s.as_secs(), + ); + } Ok(f(&signature.key)) } - fn create_signature(secret: &PKey, key_id: &str, team_id: &str, issued_at: i64) -> Result { + fn create_signature(secret: &Secret, key_id: &str, team_id: &str, issued_at: i64) -> Result { let headers = JwtHeader { alg: JwtAlg::ES256, kid: key_id, @@ -115,28 +164,28 @@ impl Signer { iat: issued_at, }; - let encoded_header = encode(&serde_json::to_string(&headers)?); - let encoded_payload = encode(&serde_json::to_string(&payload)?); + let encoded_header = encode(serde_json::to_string(&headers)?); + let encoded_payload = encode(serde_json::to_string(&payload)?); let signing_input = format!("{}.{}", encoded_header, encoded_payload); - let mut signer = SslSigner::new(MessageDigest::sha256(), secret)?; - signer.update(signing_input.as_bytes())?; - - let signature_payload = signer.sign_to_vec()?; + let signature_payload = secret.sign(&signing_input)?; - Ok(format!("{}.{}", signing_input, encode(&signature_payload))) + Ok(format!("{}.{}", signing_input, encode(signature_payload))) } fn renew(&self) -> Result<(), Error> { let issued_at = get_time(); - trace!( - "Signer::renew for k_id {} t_id {} issued {} valid for {}s", - self.key_id, - self.team_id, - issued_at, - self.expire_after_s.as_secs(), - ); + #[cfg(feature = "tracing")] + { + tracing::trace!( + "Signer::renew for k_id {} t_id {} issued {} valid for {}s", + self.key_id, + self.team_id, + issued_at, + self.expire_after_s.as_secs(), + ); + } let mut signature = self.signature.write().unwrap(); @@ -155,6 +204,39 @@ impl Signer { } } +impl Secret { + fn sign(&self, signing_input: &String) -> Result, SignerError> { + match self { + #[cfg(feature = "openssl")] + Secret::OpenSSL(key) => { + let mut signer = SslSigner::new(MessageDigest::sha256(), key)?; + signer.update(signing_input.as_bytes())?; + let signature_payload = signer.sign_to_vec()?; + Ok(signature_payload) + } + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + Secret::Ring { signing_key, rng } => { + let signature_payload = signing_key.sign(rng, signing_input.as_bytes())?; + Ok(signature_payload.as_ref().to_vec()) + } + } + } +} + +/// Failed to sign payload +#[derive(Debug, Error)] +pub enum SignerError { + #[cfg(feature = "openssl")] + #[error(transparent)] + OpenSSL(#[from] openssl::error::ErrorStack), + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + #[error(transparent)] + Pem(#[from] pem::PemError), + #[cfg(all(not(feature = "openssl"), feature = "ring"))] + #[error(transparent)] + Ring(#[from] ring::error::Unspecified), +} + fn get_time() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -166,13 +248,11 @@ fn get_time() -> i64 { mod tests { use super::*; - const PRIVATE_KEY: &'static str = indoc!( - "-----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu - lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 - jDwmlD1Gg0yJt1e38djFwsxsfr5q2hv0Rj9fTEqAPr8H7mGm0wKxZ7iQ - -----END PRIVATE KEY-----" - ); + const PRIVATE_KEY: &'static str = "-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu +lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 +jDwmlD1Gg0yJt1e38djFwsxsfr5q2hv0Rj9fTEqAPr8H7mGm0wKxZ7iQ +-----END PRIVATE KEY-----"; #[test] fn test_signature_caching() {