This commit is contained in:
Harry Bairstow
2022-12-22 14:35:50 +00:00
committed by GitHub
parent bfe8ef3551
commit e975867948
20 changed files with 1935 additions and 1033 deletions
+8
View File
@@ -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
+4 -6
View File
@@ -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
Generated
+923
View File
@@ -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"
+17 -12
View File
@@ -4,7 +4,7 @@ version = "0.6.2"
authors = [
"Harry Bairstow <harry@walletconnect.com>",
"Julius de Bruijn <julius@nauk.io>",
"Sergey Tkachenko <seriy.tkachenko@gmail.com>",
"Sergey Tkachenko <seriy.tkachenko@gmail.com>",
]
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"] }
+3 -1
View File
@@ -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
+25 -18
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
ap.parse_args_or_exit();
}
// Read the private key and certificate from the disk
let mut certificate = File::open(certificate_file).unwrap();
// Which service to call, test or production?
let endpoint = if sandbox {
Endpoint::Sandbox
} else {
Endpoint::Production
};
// Connecting to APNs using a client certificate
let client = Client::certificate(&mut certificate, &password, endpoint).unwrap();
let new_client = || -> Result<Client, Box<dyn std::error::Error + Sync + Send>> {
#[cfg(feature = "openssl")]
{
// Which service to call, test or production?
let endpoint = if sandbox {
a2::Endpoint::Sandbox
} else {
a2::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())
}
};
let client = new_client()?;
let options = NotificationOptions {
apns_topic: topic.as_ref().map(|s| &**s),
@@ -53,9 +59,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
};
// 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?;
+6 -6
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
};
// 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?;
+54 -55
View File
@@ -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<Signer>,
@@ -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<R>(certificate: &mut R, password: &str, endpoint: Endpoint) -> Result<Client, Error>
where
R: Read,
@@ -72,7 +74,7 @@ impl Client {
let mut cert_der: Vec<u8> = 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<Output = Result<Response, Error>> + 'static {
#[cfg_attr(feature = "tracing", ::tracing::instrument)]
pub async fn send(&self, payload: Payload<'_>) -> Result<Response, Error> {
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 {
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?;
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());
+14 -2
View File
@@ -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<openssl::error::ErrorStack> for Error {
fn from(e: openssl::error::ErrorStack) -> Self {
Self::SignerError(SignerError::OpenSSL(e))
}
}
+20 -23
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
//! 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};
+2 -6
View File
@@ -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;
+720
View File
@@ -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<Vec<Cow<'a, str>>>,
#[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<Vec<Cow<'a, str>>>,
#[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<u32>,
sound: Option<&'a str>,
category: Option<&'a str>,
mutable_content: u8,
content_available: Option<u8>,
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 thats 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<S>(mut self, args: &'a [S]) -> Self
where
S: Into<Cow<'a, str>> + AsRef<str>,
{
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<S>(mut self, args: &'a [S]) -> Self
where
S: Into<Cow<'a, str>> + AsRef<str>,
{
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());
}
}
-444
View File
@@ -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<Vec<Cow<'a, str>>>,
#[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<Vec<Cow<'a, str>>>,
#[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<u32>,
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<S>(&mut self, args: &'a [S]) -> &mut Self
where
S: Into<Cow<'a, str>> + AsRef<str>,
{
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<S>(&mut self, args: &'a [S]) -> &mut Self
where
S: Into<Cow<'a, str>> + AsRef<str>,
{
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());
}
}
+1 -13
View File
@@ -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<CollapseId<'a>>,
}
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 {
-223
View File
@@ -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<u32>,
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);
}
}
-167
View File
@@ -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());
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ impl<'a> WebNotificationBuilder<'a> {
WebNotificationBuilder {
alert,
sound: None,
url_args: url_args,
url_args,
}
}
+15 -14
View File
@@ -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>),
}
+2 -2
View File
@@ -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,
+120 -40
View File
@@ -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>,
signature: Arc<RwLock<Signature>>,
key_id: String,
team_id: String,
secret: PKey<Private>,
secret: Arc<Secret>,
expire_after_s: Duration,
}
@@ -46,10 +52,55 @@ struct JwtPayload<'a> {
iat: i64,
}
#[derive(Debug)]
enum Secret {
#[cfg(feature = "openssl")]
OpenSSL(PKey<Private>),
#[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<Self, Error> {
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<Self, Error> {
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<R>(mut pk_pem: R) -> Result<Secret, Error>
where
R: Read,
{
let mut pem_key: Vec<u8> = 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<S, T, R>(mut pk_pem: R, key_id: S, team_id: T, signature_ttl: Duration) -> Result<Signer, Error>
pub fn new<S, T, R>(pk_pem: R, key_id: S, team_id: T, signature_ttl: Duration) -> Result<Signer, Error>
where
S: Into<String>,
T: Into<String>,
@@ -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<u8> = 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<Private>, key_id: &str, team_id: &str, issued_at: i64) -> Result<String, Error> {
fn create_signature(secret: &Secret, key_id: &str, team_id: &str, issued_at: i64) -> Result<String, Error> {
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 = secret.sign(&signing_input)?;
let signature_payload = signer.sign_to_vec()?;
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<Vec<u8>, 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() {