--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "aead"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
+dependencies = [
+ "aes-soft",
+ "aesni",
+ "cipher",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "aes-soft"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
+dependencies = [
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aesni"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
+dependencies = [
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
+
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59740d83946db6a5af71ae25ddf9562c2b176b2ca42cf99a455f09f4a220d6b9"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-dup"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c"
+dependencies = [
+ "futures-io",
+ "simple-mutex",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb877970c7b440ead138f6321a3b5395d6061183af779340b65e20c0fede9146"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "vec-arena",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-mutex",
+ "blocking",
+ "futures-lite",
+ "num_cpus",
+ "once_cell",
+]
+
+[[package]]
+name = "async-h1"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c68a75f812ff0f299e142c06dd0c34e3295a594d935e61eeb6c77041d1d4dc"
+dependencies = [
+ "async-channel",
+ "async-dup",
+ "async-std",
+ "byte-pool",
+ "futures-core",
+ "http-types",
+ "httparse",
+ "lazy_static",
+ "log",
+ "pin-project",
+]
+
+[[package]]
+name = "async-io"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd"
+dependencies = [
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "libc",
+ "log",
+ "nb-connect",
+ "once_cell",
+ "parking",
+ "polling",
+ "vec-arena",
+ "waker-fn",
+ "winapi",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1996609732bde4a9988bc42125f55f2af5f3c36370e27c778d5191a4a1b63bfb"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-mutex"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-process"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8cea09c1fb10a317d1b5af8024eeba256d6554763e85ecd90ff8df31c7bbda"
+dependencies = [
+ "async-io",
+ "blocking",
+ "cfg-if 0.1.10",
+ "event-listener",
+ "futures-lite",
+ "once_cell",
+ "signal-hook 0.1.17",
+ "winapi",
+]
+
+[[package]]
+name = "async-sse"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10"
+dependencies = [
+ "async-channel",
+ "async-std",
+ "http-types",
+ "log",
+ "memchr",
+ "pin-project-lite 0.1.11",
+]
+
+[[package]]
+name = "async-std"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341"
+dependencies = [
+ "async-attributes",
+ "async-channel",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils 0.8.1",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "num_cpus",
+ "once_cell",
+ "pin-project-lite 0.2.4",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
+
+[[package]]
+name = "async-tls"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "rustls",
+ "webpki",
+ "webpki-roots",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-tungstenite"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39eca8dd578b18e557361e50ca767df55c5e62f690a5e53868c3c7a8123145b7"
+dependencies = [
+ "futures-io",
+ "futures-util",
+ "log",
+ "pin-project",
+ "tungstenite",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base-x"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9"
+
+[[package]]
+name = "byte-pool"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38e98299d518ec351ca016363e0cbfc77059dcd08dfa9700d15e405536097a"
+dependencies = [
+ "crossbeam-queue",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
+
+[[package]]
+name = "bytes"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+
+[[package]]
+name = "bytes"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
+
+[[package]]
+name = "cache-padded"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
+
+[[package]]
+name = "cc"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "combine"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e"
+dependencies = [
+ "bytes 1.0.1",
+ "futures-util",
+ "memchr",
+ "pin-project-lite 0.2.4",
+ "tokio",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
+dependencies = [
+ "cache-padded",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
+
+[[package]]
+name = "cookie"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f"
+dependencies = [
+ "aes-gcm",
+ "base64 0.12.3",
+ "hkdf",
+ "hmac",
+ "percent-encoding",
+ "rand 0.7.3",
+ "sha2",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cpuid-bool"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
+
+[[package]]
+name = "cpuid-bool"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
+dependencies = [
+ "cfg-if 0.1.10",
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg",
+ "cfg-if 0.1.10",
+ "lazy_static",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+dependencies = [
+ "autocfg",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10bcb9d7dcbf7002aaffbb53eac22906b64cdcc127971dcc387d8eb7c95d5560"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ctr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "dtoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
+
+[[package]]
+name = "env_logger"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
+
+[[package]]
+name = "fastrand"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500"
+
+[[package]]
+name = "futures-lite"
+version = "1.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite 0.2.4",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6"
+
+[[package]]
+name = "futures-task"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "futures-util"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite 0.2.4",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "ghash"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747"
+dependencies = [
+ "bytes 1.0.1",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-client"
+version = "6.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "010092b71b94ee49293995625ce7a607778b8b4099c8088fa84fd66bd3e0f21c"
+dependencies = [
+ "async-trait",
+ "http-types",
+ "log",
+]
+
+[[package]]
+name = "http-types"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32613ebb139d1d430ef5783676f84abfa06fc5f2b4b5a25220cdeeff7e16ef5c"
+dependencies = [
+ "anyhow",
+ "async-channel",
+ "async-std",
+ "base64 0.13.0",
+ "cookie",
+ "futures-lite",
+ "infer",
+ "pin-project-lite 0.2.4",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "serde_urlencoded",
+ "url",
+]
+
+[[package]]
+name = "httparse"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "idna"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "infer"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
+
+[[package]]
+name = "input_buffer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754"
+dependencies = [
+ "bytes 0.5.6",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "js-sys"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[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.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if 1.0.0",
+ "value-bag",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "nb-connect"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pin-project"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95b70b68509f17aa2857863b6fa00bf21fc93674c7a8893de2f469f6aa7ca2f2"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa25a6393f22ce819b0f50e0be89287292fda8d425be38ee0ca14c4931d9e71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pokerwave"
+version = "0.1.0"
+dependencies = [
+ "async-std",
+ "env_logger",
+ "futures",
+ "log",
+ "rand 0.8.3",
+ "redis",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "signal-hook 0.3.4",
+ "signal-hook-async-std",
+ "tide",
+ "tide-rustls",
+ "tide-websockets",
+]
+
+[[package]]
+name = "polling"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "log",
+ "wepoll-sys",
+ "winapi",
+]
+
+[[package]]
+name = "polyval"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
+dependencies = [
+ "cpuid-bool 0.2.0",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro-nested"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.0",
+ "rand_core 0.6.1",
+ "rand_hc 0.3.0",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
+dependencies = [
+ "getrandom 0.2.2",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
+dependencies = [
+ "rand_core 0.6.1",
+]
+
+[[package]]
+name = "redis"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a6ddfecac9391fed21cce10e83c65fa4abafd77df05c98b1c647c65374ce9b3"
+dependencies = [
+ "async-std",
+ "async-trait",
+ "bytes 1.0.1",
+ "combine",
+ "dtoa",
+ "futures-util",
+ "itoa",
+ "percent-encoding",
+ "pin-project-lite 0.2.4",
+ "sha1",
+ "tokio",
+ "tokio-util",
+ "url",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+
+[[package]]
+name = "ring"
+version = "0.16.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "route-recognizer"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b"
+dependencies = [
+ "base64 0.13.0",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "sct"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5af82de3c6549b001bec34961ff2d6a54339a87bab37ce901b693401f27de6cb"
+dependencies = [
+ "data-encoding",
+ "percent-encoding",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c"
+dependencies = [
+ "block-buffer",
+ "cfg-if 1.0.0",
+ "cpuid-bool 0.1.2",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
+
+[[package]]
+name = "sha2"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
+dependencies = [
+ "block-buffer",
+ "cfg-if 1.0.0",
+ "cpuid-bool 0.1.2",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780f5e3fe0c66f67197236097d89de1e86216f1f6fdeaf47c442f854ab46c240"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-async-std"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90526e74631c69a79b38212e3d4fda4b00de9d6be56b3cead133bf67ad371af1"
+dependencies = [
+ "async-io",
+ "futures-lite",
+ "libc",
+ "signal-hook 0.3.4",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simple-mutex"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "standback"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "stdweb"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_derive",
+ "syn",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "syn",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+
+[[package]]
+name = "subtle"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
+
+[[package]]
+name = "sval"
+version = "1.0.0-alpha.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08"
+
+[[package]]
+name = "syn"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tide"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0"
+dependencies = [
+ "async-h1",
+ "async-sse",
+ "async-std",
+ "async-trait",
+ "futures-util",
+ "http-client",
+ "http-types",
+ "kv-log-macro",
+ "log",
+ "pin-project-lite 0.2.4",
+ "route-recognizer",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tide-rustls"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf8521961215f0ebbb27e10c9d610ff2a64f853dc3707c03748350c6dbe022"
+dependencies = [
+ "async-dup",
+ "async-h1",
+ "async-std",
+ "async-tls",
+ "rustls",
+ "tide",
+]
+
+[[package]]
+name = "tide-websockets"
+version = "0.1.0"
+source = "git+https://github.com/http-rs/tide-websockets?branch=main#c7f5fc6fd81bb592d6f55f09bddc62cc7a60bdeb"
+dependencies = [
+ "async-dup",
+ "async-std",
+ "async-tungstenite",
+ "base64 0.13.0",
+ "futures-util",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "sha-1",
+ "tide",
+]
+
+[[package]]
+name = "time"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7"
+dependencies = [
+ "const_fn",
+ "libc",
+ "standback",
+ "stdweb",
+ "time-macros",
+ "version_check",
+ "winapi",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
+dependencies = [
+ "proc-macro-hack",
+ "time-macros-impl",
+]
+
+[[package]]
+name = "time-macros-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "standback",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6714d663090b6b0acb0fa85841c6d66233d150cdb2602c8f9b8abb03370beb3f"
+dependencies = [
+ "autocfg",
+ "bytes 1.0.1",
+ "memchr",
+ "pin-project-lite 0.2.4",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b"
+dependencies = [
+ "bytes 1.0.1",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite 0.2.4",
+ "tokio",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23"
+dependencies = [
+ "base64 0.12.3",
+ "byteorder",
+ "bytes 0.5.6",
+ "http",
+ "httparse",
+ "input_buffer",
+ "log",
+ "rand 0.7.3",
+ "sha-1",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "universal-hash"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
+
+[[package]]
+name = "value-bag"
+version = "1.0.0-alpha.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b676010e055c99033117c2343b33a40a30b91fecd6c49055ac9cd2d6c305ab1"
+dependencies = [
+ "ctor",
+ "sval",
+]
+
+[[package]]
+name = "vec-arena"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d"
+
+[[package]]
+name = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
+
+[[package]]
+name = "web-sys"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "wepoll-sys"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff"
+dependencies = [
+ "cc",
+]
+
+[[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-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
--- /dev/null
+[package]
+name = "pokerwave"
+version = "0.1.0"
+authors = ["Geoffrey Allott <geoffrey@allott.email>"]
+edition = "2018"
+
+[dependencies]
+async-std = { version = "1", features = ["attributes"] }
+env_logger = "0.8"
+log = "0.4"
+futures = "0.3"
+rand = "0.8"
+redis = { version = "0.19", features = ["async-std-comp"] }
+serde = "1"
+serde_derive = "1"
+serde_json = "1"
+signal-hook = "0.3"
+signal-hook-async-std = "0.2"
+tide = { version = "0.16.0", default-features = false, features = ["h1-server"] }
+tide-rustls = "0.2"
+tide-websockets = { git = "https://github.com/http-rs/tide-websockets", branch = "main" }
--- /dev/null
+-----BEGIN CERTIFICATE-----
+MIIEKTCCApGgAwIBAgIRANSpSYVi2aOvByhI0RgGwRAwDQYJKoZIhvcNAQELBQAw
+VzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMRYwFAYDVQQLDA1nZW9m
+ZkBHZW9mZlBDMR0wGwYDVQQDDBRta2NlcnQgZ2VvZmZAR2VvZmZQQzAeFw0yMTAx
+MzEyMzIxMDhaFw0yMzA1MDEyMjIxMDhaMEExJzAlBgNVBAoTHm1rY2VydCBkZXZl
+bG9wbWVudCBjZXJ0aWZpY2F0ZTEWMBQGA1UECwwNZ2VvZmZAR2VvZmZQQzCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMzdzX/aPttLL+WGa5E1T3d6jmGQ
+TLElkRUZtGbYbR0PmVj2uBbIPhwe9n7R4QcY6MroGZiem5/tO+/bAvz4x6dDHFww
+//wCzsBZaO+KgqC5839JRyRsHyoLVZfcv/IbJDh52Nubs7ursiDDCo2Vtv+Y0lV4
+iMKxzy882g13Jkfhv7Of+kWlABA/LS+CEjKbbCKI9W7zIl5RvdNXN0dPNws6p5gN
+G96ZIhHWQ0Ah+RYw5bcOzu5AkSlFpUfKBaSyXi0wlCjdglFe4hwCEHU0SwM8qzgX
+WXcp6U+QfDmWg1vb9Vfd7tUUzk6LjhIqR72slMCtmm2AhXkk0nTTnViFLUUCAwEA
+AaOBhTCBgjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYD
+VR0TAQH/BAIwADAfBgNVHSMEGDAWgBQle3YBraPadyDsV2cbZUG6rxEYEDAsBgNV
+HREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZI
+hvcNAQELBQADggGBAJ+mFs0I2/TDv+6Ni19++I2D3qUGcb3nM7RXUOHMrzcVcQHq
+eE+MoNLtyVjZZFqBzZox+RlCCWbHAfqFYblpL27crLXcttDyXIdeQGjuGwE4NGe9
+xi1rlvt0A9IPfrli6SKdH24vijorryXF+GOToBXM7dS8bg1fra5g7uiQgoFhToQp
+TH6eEO8y9X3qgCtY1EcWkGIchxfM0qPhwP+zqe7ZTU8XdCQZqBL53DTVj5HmvOd5
+rGB/zJ1C4X6QJ+WivM6228bSm6vyxSj4zx2RM1Fs4LfPBEzd14vyxXJrbIjDk6Be
+9V8lY3taYhw0GCslGLtUua8rBrvec+XpMrViJ361pMNvG7BhIseg4BET0DA9Er2/
+gZ67hI9gAfHHs8M2SeZUPJdMHMU3FMtpfA3fPBiHKn1gx0/CRx1Yt47VBobVHth9
+con+wkUOne/kXPDLzWiUyOAdjREN84dM/nwmTcKjPJbzoAUzAF/Zs8ZWUH60Tn7T
+eaU0yb10XMxl40b05w==
+-----END CERTIFICATE-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDM3c1/2j7bSy/l
+hmuRNU93eo5hkEyxJZEVGbRm2G0dD5lY9rgWyD4cHvZ+0eEHGOjK6BmYnpuf7Tvv
+2wL8+MenQxxcMP/8As7AWWjvioKgufN/SUckbB8qC1WX3L/yGyQ4edjbm7O7q7Ig
+wwqNlbb/mNJVeIjCsc8vPNoNdyZH4b+zn/pFpQAQPy0vghIym2wiiPVu8yJeUb3T
+VzdHTzcLOqeYDRvemSIR1kNAIfkWMOW3Ds7uQJEpRaVHygWksl4tMJQo3YJRXuIc
+AhB1NEsDPKs4F1l3KelPkHw5loNb2/VX3e7VFM5Oi44SKke9rJTArZptgIV5JNJ0
+051YhS1FAgMBAAECggEANlXluTFDVppce73pLbw79W518P56uMlD75q/tD9WwUxv
+2Z6mOLSndIHp0r0Lt/wtUhO5Jjd7AzHCod0ur7W0DnLIRNNiMY71EyMjp9TOzJ5F
+LM9cpRZgNaCH9R1Bc48rTidTEvv68Z2aQwI3/FFnFeeMccrkSu8qCldTZhrh9lI5
+6of9yedK5Vv6tcbEvW4MRSWS9E9QuqWogDKklAYJggX3sEsDHt6cEeqkUxGIayHm
+kUrYUEFAj+pLktGALSLO7ByrfPMtnZVLMfu366y5vYIUbhf79sWdMGsGI+/g/2bm
+0NGTBpnWnLwpFQKbhYe09CKsmXqmsdhch6/yK+N9CQKBgQDWF7yGPjJqBepn79jq
+Q0IcvoRCt6JO0729jTm57Gu1NZSiM71dmylOy6JMxmRMcYnmEfk+xmm/3ANifo70
+iX1zw8gyrDKYHp0t0kr5BfaMph1W2OYgP1YzV97KlN0+FiZmaLjq9FQUsS+Tr/aR
+5E0h3HQXCIuM/go3+j6OrINxMwKBgQD097uf1NOhjEbs3snYIuoPZStfPXuUruu7
+nVinpMi9IMxCRecf/9b+tranidNrmFXLIxrIV0x7NwHRt+ykFMVWHi8JTVKKCyAR
+rGDnyLfgNHngozqeedBpjxBqWKstTk/Qn+chggKonDqE3gngFi5PORAO5QMbWWQX
+C+jzC3pXpwKBgH3U76pbLhhgp7g1IWH27Amm9GeeNNht3SxmsBIV4exctKJtFJTK
+2ImGaDsFs/e2F93QnJUH2ym374lZZz5U/RYocSdPTGFaRPmkiwo4NgPOspnT29YC
+Q/DjaX/Z5PtT23f/fwghWLXcumDsYFi5PZh0UQskq5jPy0PBYjlLLeo/AoGBAN+z
+BqRLTsM8fVw7iqkUIRfT9Epxs9Ov9NcMNfRJV4LWW5C9kvU2xVcu9ReDlgywPNBX
+C1Md3Vq7fa7MvY2M5jPhNmTRNmmXBT7+YjPnqHpWkWGgzZwAc2Ch44Sp2g9ybxJ9
+oyyHM/RTTmEc/nmi58Eyw8ZGPZMRC4S8PIsN1TgLAoGAdc8i9zGBp+PwjTIeLZry
+byP9GQV6dsBcCFswuGICqO98SdWPVzdTQj7j/9Jnv48FKVEli/KR27POrbIQ+EZY
+20IRywZLe+QB+dAs0SapLRJjq7jABoGgF12ol6R04as2ZgIBBz/cdndhFghjEO4d
+Unvj9zDM6L67LknR+PyaD2c=
+-----END PRIVATE KEY-----
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <h1>404 Not Found</h1>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="main.js"></script>
+ </head>
+ <body>
+ <h1>Pokerwave</h1>
+ </body>
+</html>
--- /dev/null
+"use strict";
+
+var socket;
+
+function socket_open() {
+ let proto = window.location.protocol === "https:" ? "wss:" : "ws:";
+ let uri = proto + "//" + window.location.host + "/api";
+ socket = new WebSocket(uri);
+ socket.onopen = socket_onopen;
+ socket.onmessage = socket_onmessage;
+}
+
+function socket_onopen() {
+ console.log("WebSocket connected");
+ socket_send({type: "Login", username: "geoff"});
+}
+
+function socket_send(object) {
+ console.log(">>", object);
+ socket.send(JSON.stringify(object));
+}
+
+function socket_onmessage(msg) {
+ var message = JSON.parse(msg.data);
+ console.log("<<", message);
+ switch (message.type) {
+ case "LoginAuthChallenge":
+ socket_send({type: "LoginAuthResponse", signature: "hunter2"});
+ break;
+ }
+}
+
+window.onload = function() {
+ socket_open();
+};
--- /dev/null
+use crate::auth::Auth;
+use crate::game::{Action, Game, GameSettings, GameSummary, UserAction};
+
+#[derive(Debug, Clone, Deserialize)]
+enum Scope {
+ Global,
+ Game { id: u32 },
+ Hand { id: u32 },
+ Player { username: String },
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(tag = "type")]
+pub enum ClientMessage {
+ CreateUser { username: String, auth: Auth, nickname: String },
+ Login { username: String },
+ LoginAuthResponse { signature: String },
+ ChangeAuth { auth: Auth },
+ ChangeNickname { nickname: String },
+ Logout,
+ CreateGame { settings: GameSettings },
+ GetGameList { tags: Vec<String> },
+ JoinGame { id: u32 },
+ TakeAction { action: Action },
+ SendMessage { scope: Scope, message: String },
+ LeaveGame,
+ GetHandHistory { scope: Scope },
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(tag = "type")]
+pub enum ServerMessage {
+ CreateUserSuccess,
+ CreateUserFailure { reason: String },
+ LoginAuthChallenge { challenge: String },
+ LoginSuccess,
+ LoginFailure { reason: String },
+ ChangeAuthSuccess,
+ ChangeAuthFailure { reason: String },
+ ChangeNicknameSuccess,
+ ChangeNicknameFailure { reason: String },
+ LogoutSuccess,
+ CreateGameSuccess { id: u32 },
+ CreateGameFailure { reason: String },
+ GameList { games: Vec<GameSummary> },
+ GameListFailure { reason: String },
+ JoinGameSuccess { game: Game },
+ JoinGameFailure { reason: String },
+ NewAction { action: UserAction },
+ TakeActionSuccess,
+ TakeActionFailure { reason: String },
+ NewMessage { username: String, message: String },
+ LeaveGameSuccess,
+ LeaveGameFailure { reason: String },
+ HandHistory { games: Vec<Game> },
+ ProtocolError { reason: String },
+}
--- /dev/null
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "method")]
+pub enum Auth {
+ NoLogin,
+ Plain { password: String },
+}
+
+impl Auth {
+ pub fn verify(&self, challenge: &str, signature: &str) -> bool {
+ match self {
+ Auth::NoLogin => false,
+ Auth::Plain{password} => signature == password,
+ }
+ }
+}
--- /dev/null
+#![allow(non_upper_case_globals)]
+
+use std::fmt::{self, Display, Formatter};
+
+use self::Rank::*;
+use self::Suit::*;
+
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+pub enum Rank {
+ Two = 2,
+ Three = 3,
+ Four = 4,
+ Five = 5,
+ Six = 6,
+ Seven = 7,
+ Eight = 8,
+ Nine = 9,
+ Ten = 10,
+ Jack = 11,
+ Queen = 12,
+ King = 13,
+ Ace = 14,
+}
+
+impl Display for Rank {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Two => "2",
+ Three => "3",
+ Four => "4",
+ Five => "5",
+ Six => "6",
+ Seven => "7",
+ Eight => "8",
+ Nine => "9",
+ Ten => "T",
+ Jack => "J",
+ Queen => "Q",
+ King => "K",
+ Ace => "A",
+ })
+ }
+}
+
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+pub enum Suit {
+ Clubs,
+ Diamonds,
+ Hearts,
+ Spades,
+}
+
+impl Display for Suit {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Clubs => "♣",
+ Diamonds => "♢",
+ Hearts => "♡",
+ Spades => "â™ ",
+ })
+ }
+}
+
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+pub struct Card {
+ pub rank: Rank,
+ pub suit: Suit,
+}
+
+impl Display for Card {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}{}", self.rank, self.suit)
+ }
+}
+
+pub const TwoOfClubs: Card = Card{rank: Two, suit: Clubs};
+pub const ThreeOfClubs: Card = Card{rank: Three, suit: Clubs};
+pub const FourOfClubs: Card = Card{rank: Four, suit: Clubs};
+pub const FiveOfClubs: Card = Card{rank: Five, suit: Clubs};
+pub const SixOfClubs: Card = Card{rank: Six, suit: Clubs};
+pub const SevenOfClubs: Card = Card{rank: Seven, suit: Clubs};
+pub const EightOfClubs: Card = Card{rank: Eight, suit: Clubs};
+pub const NineOfClubs: Card = Card{rank: Nine, suit: Clubs};
+pub const TenOfClubs: Card = Card{rank: Ten, suit: Clubs};
+pub const JackOfClubs: Card = Card{rank: Jack, suit: Clubs};
+pub const QueenOfClubs: Card = Card{rank: Queen, suit: Clubs};
+pub const KingOfClubs: Card = Card{rank: King, suit: Clubs};
+pub const AceOfClubs: Card = Card{rank: Ace, suit: Clubs};
+
+pub const TwoOfDiamonds: Card = Card{rank: Two, suit: Diamonds};
+pub const ThreeOfDiamonds: Card = Card{rank: Three, suit: Diamonds};
+pub const FourOfDiamonds: Card = Card{rank: Four, suit: Diamonds};
+pub const FiveOfDiamonds: Card = Card{rank: Five, suit: Diamonds};
+pub const SixOfDiamonds: Card = Card{rank: Six, suit: Diamonds};
+pub const SevenOfDiamonds: Card = Card{rank: Seven, suit: Diamonds};
+pub const EightOfDiamonds: Card = Card{rank: Eight, suit: Diamonds};
+pub const NineOfDiamonds: Card = Card{rank: Nine, suit: Diamonds};
+pub const TenOfDiamonds: Card = Card{rank: Ten, suit: Diamonds};
+pub const JackOfDiamonds: Card = Card{rank: Jack, suit: Diamonds};
+pub const QueenOfDiamonds: Card = Card{rank: Queen, suit: Diamonds};
+pub const KingOfDiamonds: Card = Card{rank: King, suit: Diamonds};
+pub const AceOfDiamonds: Card = Card{rank: Ace, suit: Diamonds};
+
+pub const TwoOfHearts: Card = Card{rank: Two, suit: Hearts};
+pub const ThreeOfHearts: Card = Card{rank: Three, suit: Hearts};
+pub const FourOfHearts: Card = Card{rank: Four, suit: Hearts};
+pub const FiveOfHearts: Card = Card{rank: Five, suit: Hearts};
+pub const SixOfHearts: Card = Card{rank: Six, suit: Hearts};
+pub const SevenOfHearts: Card = Card{rank: Seven, suit: Hearts};
+pub const EightOfHearts: Card = Card{rank: Eight, suit: Hearts};
+pub const NineOfHearts: Card = Card{rank: Nine, suit: Hearts};
+pub const TenOfHearts: Card = Card{rank: Ten, suit: Hearts};
+pub const JackOfHearts: Card = Card{rank: Jack, suit: Hearts};
+pub const QueenOfHearts: Card = Card{rank: Queen, suit: Hearts};
+pub const KingOfHearts: Card = Card{rank: King, suit: Hearts};
+pub const AceOfHearts: Card = Card{rank: Ace, suit: Hearts};
+
+pub const TwoOfSpades: Card = Card{rank: Two, suit: Spades};
+pub const ThreeOfSpades: Card = Card{rank: Three, suit: Spades};
+pub const FourOfSpades: Card = Card{rank: Four, suit: Spades};
+pub const FiveOfSpades: Card = Card{rank: Five, suit: Spades};
+pub const SixOfSpades: Card = Card{rank: Six, suit: Spades};
+pub const SevenOfSpades: Card = Card{rank: Seven, suit: Spades};
+pub const EightOfSpades: Card = Card{rank: Eight, suit: Spades};
+pub const NineOfSpades: Card = Card{rank: Nine, suit: Spades};
+pub const TenOfSpades: Card = Card{rank: Ten, suit: Spades};
+pub const JackOfSpades: Card = Card{rank: Jack, suit: Spades};
+pub const QueenOfSpades: Card = Card{rank: Queen, suit: Spades};
+pub const KingOfSpades: Card = Card{rank: King, suit: Spades};
+pub const AceOfSpades: Card = Card{rank: Ace, suit: Spades};
+
+pub const FIFTY_TWO_CARD_DECK: [Card; 52] = [
+ AceOfSpades,
+ AceOfHearts,
+ AceOfDiamonds,
+ AceOfClubs,
+ KingOfSpades,
+ KingOfHearts,
+ KingOfDiamonds,
+ KingOfClubs,
+ QueenOfSpades,
+ QueenOfHearts,
+ QueenOfDiamonds,
+ QueenOfClubs,
+ JackOfSpades,
+ JackOfHearts,
+ JackOfDiamonds,
+ JackOfClubs,
+ TenOfSpades,
+ TenOfHearts,
+ TenOfDiamonds,
+ TenOfClubs,
+ NineOfSpades,
+ NineOfHearts,
+ NineOfDiamonds,
+ NineOfClubs,
+ EightOfSpades,
+ EightOfHearts,
+ EightOfDiamonds,
+ EightOfClubs,
+ SevenOfSpades,
+ SevenOfHearts,
+ SevenOfDiamonds,
+ SevenOfClubs,
+ SixOfSpades,
+ SixOfHearts,
+ SixOfDiamonds,
+ SixOfClubs,
+ FiveOfSpades,
+ FiveOfHearts,
+ FiveOfDiamonds,
+ FiveOfClubs,
+ FourOfSpades,
+ FourOfHearts,
+ FourOfDiamonds,
+ FourOfClubs,
+ ThreeOfSpades,
+ ThreeOfHearts,
+ ThreeOfDiamonds,
+ ThreeOfClubs,
+ TwoOfSpades,
+ TwoOfHearts,
+ TwoOfDiamonds,
+ TwoOfClubs,
+];
--- /dev/null
+use std::sync::{Arc, RwLock};
+
+use futures::stream::{Stream, StreamExt, empty, iter, once};
+
+use crate::api::{ClientMessage, ServerMessage};
+use crate::game::{Game, UserAction};
+use crate::server::{ActionStatus, ServerState};
+
+pub struct ConnectionState {
+ server: ServerState,
+ client: ClientState,
+}
+
+#[derive(Debug, Clone)]
+pub enum ClientState {
+ Connected,
+ LoginAuthIssued { username: String, challenge: String },
+ LoggedIn {
+ username: String,
+ state: LoggedInState,
+ },
+}
+
+pub enum ClientUpdate {
+ GameList,
+ Game,
+ Users,
+}
+
+#[derive(Debug, Clone)]
+pub enum LoggedInState {
+ InLobby,
+ InGame { game: Game },
+}
+
+impl ConnectionState {
+ pub fn new(server: ServerState) -> Self {
+ Self {
+ server,
+ client: ClientState::Connected,
+ }
+ }
+
+ pub async fn retrieve_updates(&mut self, update: ClientUpdate) -> impl Stream<Item=ServerMessage> {
+ match update {
+ ClientUpdate::GameList => empty().boxed(), // TODO
+ ClientUpdate::Game => match &mut self.client {
+ ClientState::LoggedIn{ref username, state: LoggedInState::InGame{ref mut game}} => {
+ let from = game.actions_len();
+ match self.server.update_game_state(game).await {
+ Ok(()) => iter(game.update_view_for(username, from)).map(|action| ServerMessage::NewAction{action}).boxed(),
+ Err(err) => once(async move { ServerMessage::ProtocolError{reason: err.to_string()} }).boxed(),
+ }
+ }
+ _ => empty().boxed(),
+ }
+ ClientUpdate::Users => empty().boxed(), // TODO
+ }
+ }
+
+ pub async fn apply_message(&mut self, message: ClientMessage) -> ServerMessage {
+ match (&mut self.client, message) {
+ (_, ClientMessage::CreateUser{username, auth, nickname}) => {
+ match self.server.create_user(&username, auth, &nickname).await {
+ Ok(()) => ServerMessage::CreateUserSuccess,
+ Err(_) => ServerMessage::CreateUserFailure{reason: "User already exists".to_string()},
+ }
+ }
+ (ClientState::Connected, ClientMessage::Login{username}) => {
+ let challenge = format!("{:032x}{:032x}", rand::random::<u128>(), rand::random::<u128>());
+ self.client = ClientState::LoginAuthIssued{username, challenge: challenge.clone()};
+ ServerMessage::LoginAuthChallenge{challenge}
+ }
+ (ClientState::LoginAuthIssued{username, challenge}, ClientMessage::LoginAuthResponse{signature}) => {
+ if self.server.verify(&username, &challenge, &signature).await {
+ self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::InLobby};
+ ServerMessage::LoginSuccess
+ } else {
+ self.client = ClientState::Connected;
+ ServerMessage::LoginFailure{reason: "Invalid username or password".to_string()}
+ }
+ }
+ (ClientState::LoggedIn{username, ..}, ClientMessage::GetGameList{tags}) => {
+ match self.server.get_game_list().await {
+ Ok(games) => ServerMessage::GameList{games},
+ Err(err) => ServerMessage::GameListFailure{reason: err.to_string()},
+ }
+ }
+ (ClientState::LoggedIn{username, ..}, ClientMessage::CreateGame{settings}) => {
+ match self.server.create_game(settings).await {
+ Ok(id) => ServerMessage::CreateGameSuccess{id},
+ Err(err) => ServerMessage::CreateGameFailure{reason: err.to_string()},
+ }
+ }
+ (ClientState::LoggedIn{username, state: LoggedInState::InLobby}, ClientMessage::JoinGame{id}) => {
+ match self.server.get_game(id).await {
+ Ok(game) => {
+ let game_view = game.view_for(&username);
+ self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::InGame{game}};
+ ServerMessage::JoinGameSuccess{game: game_view}
+ }
+ Err(err) => ServerMessage::JoinGameFailure{reason: err.to_string()},
+ }
+ }
+ (ClientState::LoggedIn{ref username, state: LoggedInState::InGame{ref mut game}}, ClientMessage::TakeAction{action}) => {
+ let action = UserAction{username: username.clone(), action};
+ loop {
+ let len = game.actions_len();
+ if let Err(err) = game.verify(&action) {
+ return ServerMessage::TakeActionFailure{reason: err.to_string()};
+ }
+ match self.server.take_action(game.id(), len, &action).await {
+ Ok(ActionStatus::Committed) => match game.take_action(action) {
+ Ok(()) => return ServerMessage::TakeActionSuccess,
+ Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
+ }
+ Ok(ActionStatus::Interrupted) => {
+ if let Err(err) = self.server.update_game_state(game).await {
+ return ServerMessage::TakeActionFailure{reason: err.to_string()};
+ }
+ }
+ Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
+ }
+ }
+ }
+ (ClientState::LoggedIn{username, state: LoggedInState::InGame{..}}, ClientMessage::LeaveGame) => {
+ self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::InLobby};
+ ServerMessage::LeaveGameSuccess
+ }
+ (ClientState::LoggedIn{username, ..}, ClientMessage::Logout) => {
+ self.client = ClientState::Connected;
+ ServerMessage::LogoutSuccess
+ }
+ (ClientState::LoggedIn{username, ..}, ClientMessage::ChangeAuth{auth}) => {
+ match self.server.set_user_auth(&username, auth).await {
+ Ok(()) => ServerMessage::ChangeAuthSuccess,
+ Err(err) => ServerMessage::ChangeAuthFailure{reason: err.to_string()},
+ }
+ }
+ (ClientState::LoggedIn{username, ..}, ClientMessage::ChangeNickname{nickname}) => {
+ match self.server.set_user_nickname(&username, &nickname).await {
+ Ok(()) => ServerMessage::ChangeNicknameSuccess,
+ Err(err) => ServerMessage::ChangeNicknameFailure{reason: err.to_string()},
+ }
+ }
+ (_, _) => {
+ ServerMessage::ProtocolError{reason: "Protocol error".to_string() }
+ }
+ }
+ }
+}
--- /dev/null
+use std::fmt::Debug;
+
+use crate::card::Card;
+use crate::gamestate::GameState;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "format")]
+pub enum GameSettings {
+ Chatroom {max_users: u32},
+ TexasHoldEm {
+ // TODO
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "action")]
+pub enum Action {
+ Join { seat: u32, chips: u64 },
+ AddOn { chips: u64 },
+ NextToAct,
+ CommunityCard { card: Card },
+ ReceiveCard { card: Option<Card> },
+ RevealCard { card: Card },
+ Fold,
+ Bet { chips: u64 },
+ WinPot { chips: u64 },
+ Message { message: String },
+ Leave,
+}
+
+impl Action {
+ pub fn anonymise(&self) -> Self {
+ match self {
+ Action::ReceiveCard{..} => Action::ReceiveCard{card: None},
+ action => action.clone(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UserAction {
+ pub username: String,
+ pub action: Action,
+}
+
+impl UserAction {
+ pub fn view_for(&self, username: &str) -> Self {
+ Self {
+ username: self.username.clone(),
+ action: if username == self.username { self.action.clone() } else { self.action.anonymise() },
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub enum ActionError {
+ NotAuthorised,
+ AlreadyJoined,
+ NoSeatAvailable,
+ OutOfTurn,
+ InvalidActionForGameType,
+}
+
+use std::fmt::{Display, Formatter};
+
+impl Display for ActionError {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ match self {
+ ActionError::NotAuthorised => f.write_str("NotAuthorised"),
+ ActionError::AlreadyJoined => f.write_str("AlreadyJoined"),
+ ActionError::NoSeatAvailable => f.write_str("NoSeatAvailable"),
+ ActionError::OutOfTurn => f.write_str("OutOfTurn"),
+ ActionError::InvalidActionForGameType => f.write_str("InvalidActionForGameType"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GameSummary {
+ id: u32,
+ settings: GameSettings,
+}
+
+impl GameSummary {
+ pub fn new(id: u32, settings: GameSettings) -> Self {
+ Self{id, settings}
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Game {
+ summary: GameSummary,
+ state: GameState,
+}
+
+impl Game {
+ pub fn new(id: u32, settings: GameSettings) -> Self {
+ Self {
+ summary: GameSummary{id, settings},
+ state: GameState::new(),
+ }
+ }
+
+ pub fn id(&self) -> u32 {
+ self.summary.id
+ }
+
+ pub fn actions_len(&self) -> usize {
+ self.state.actions_len()
+ }
+
+ pub fn view_for(&self, username: &str) -> Game {
+ Game {
+ summary: self.summary.clone(),
+ state: self.state.view_for(username),
+ }
+ }
+
+ pub fn update_view_for(&self, username: &str, from: usize) -> Vec<UserAction> {
+ self.state.update_view_for(username, from)
+ }
+
+ pub fn verify(&self, UserAction{ref username, ref action}: &UserAction) -> Result<(), ActionError> {
+ debug!("Verifying action: UserAction {{ username: {:?}, action: {:?} }}", username, action);
+ match self.summary.settings {
+ GameSettings::Chatroom{max_users} => match action {
+ Action::Join{seat: 0, chips: 0} if self.state.user_has_joined(username) => Err(ActionError::AlreadyJoined),
+ Action::Join{seat: 0, chips: 0} if self.state.num_players() + 1 > max_users => Err(ActionError::NoSeatAvailable),
+ Action::Join{seat: 0, chips: 0} => Ok(()),
+ Action::Message{ref message} if self.state.user_has_joined(username) => Ok(()),
+ Action::Message{..} => Err(ActionError::NotAuthorised),
+ Action::Leave if self.state.user_has_joined(username) => Ok(()),
+ Action::Leave => Err(ActionError::NotAuthorised),
+ _ => Err(ActionError::InvalidActionForGameType),
+ },
+ GameSettings::TexasHoldEm{..} => {
+ // TODO
+ Err(ActionError::NotAuthorised)
+ }
+ }
+ }
+
+ pub fn take_action(&mut self, user_action: UserAction) -> Result<(), ActionError> {
+ self.verify(&user_action)?;
+ debug!("Taking action: {:?}", user_action);
+ debug!("State before: {:?}", self.state);
+ self.state.take_action(user_action);
+ debug!("State after: {:?}", self.state);
+ Ok(())
+ }
+}
--- /dev/null
+use crate::game::{Action, UserAction};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GameState {
+ actions: Vec<UserAction>,
+}
+
+impl GameState {
+ pub fn new() -> Self {
+ Self {
+ actions: Vec::new(),
+ }
+ }
+
+ pub fn actions_len(&self) -> usize {
+ self.actions.len()
+ }
+
+ pub fn view_for(&self, username: &str) -> Self {
+ Self {
+ actions: self.actions.iter().map(|action| action.view_for(username)).collect(),
+ }
+ }
+
+ pub fn update_view_for(&self, username: &str, from: usize) -> Vec<UserAction> {
+ self.actions.iter().skip(from).map(|action| action.view_for(username)).collect()
+ }
+
+ pub fn user_has_joined(&self, user: &str) -> bool {
+ let mut logged_in = false;
+ for action in &self.actions {
+ match action {
+ UserAction{ref username, action: Action::Join{..}} if user == username => logged_in = true,
+ UserAction{ref username, action: Action::Leave} if user == username => logged_in = false,
+ _ => continue,
+ }
+ }
+ logged_in
+ }
+
+ pub fn num_players(&self) -> u32 {
+ let mut num_players = 0;
+ for action in &self.actions {
+ match action.action {
+ Action::Join{..} => num_players += 1,
+ Action::Leave => num_players -= 1,
+ _ => continue,
+ }
+ }
+ num_players
+ }
+
+ pub fn take_action(&mut self, action: UserAction) {
+ self.actions.push(action);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn user_has_joined() {
+ let mut state = GameState::new();
+ state.take_action(UserAction{username: "user".to_string(), action: Action::Join{seat: 0, chips: 10000}});
+ assert!(state.user_has_joined("user"));
+ }
+
+ #[test]
+ fn num_players() {
+ let mut state = GameState::new();
+ state.take_action(UserAction{username: "user".to_string(), action: Action::Join{seat: 0, chips: 10000}});
+ assert_eq!(1, state.num_players());
+ state.take_action(UserAction{username: "user2".to_string(), action: Action::Join{seat: 1, chips: 10000}});
+ assert_eq!(2, state.num_players());
+ state.take_action(UserAction{username: "user".to_string(), action: Action::Leave});
+ assert_eq!(1, state.num_players());
+ }
+}
--- /dev/null
+#[macro_use] extern crate log;
+#[macro_use] extern crate serde_derive;
+
+use std::pin::Pin;
+
+use async_std::prelude::*;
+use futures::{future::select, future::Either, pin_mut, Stream, StreamExt, stream};
+use signal_hook::consts::signal::*;
+use signal_hook_async_std::Signals;
+use tide::{Body, Request, Error, Response, StatusCode};
+use tide::utils::After;
+use tide_rustls::TlsListener;
+use tide_websockets::{Message, WebSocket, WebSocketConnection};
+
+mod api;
+mod auth;
+mod card;
+mod client;
+mod game;
+mod gamestate;
+mod server;
+
+use crate::api::ServerMessage;
+use crate::server::ServerState;
+use crate::client::{ClientUpdate, ConnectionState};
+
+pub async fn handle_websocket_request(request: Request<ServerState>, mut stream: WebSocketConnection) -> Result<(), Error> {
+ let mut client = ConnectionState::new(request.state().clone());
+ let update_stream: Pin<Box<dyn Stream<Item=ClientUpdate> + Send>>;
+ update_stream = stream::empty().boxed(); // TODO
+ let mut combined = stream::select(stream.clone().map(Either::Left), update_stream.map(Either::Right));
+ while let Some(message) = combined.next().await {
+ let message: Either<Result<Message, tide_websockets::Error>, ClientUpdate> = message;
+ match message {
+ Either::Left(Ok(Message::Text(input))) => {
+ let response = match serde_json::from_str(&input) {
+ Ok(message) => client.apply_message(message).await,
+ Err(err) => ServerMessage::ProtocolError{reason: err.to_string()},
+ };
+ stream.send_json(&response).await?;
+ }
+ Either::Right(update) => {
+ let mut responses = client.retrieve_updates(update).await;
+ while let Some(response) = responses.next().await {
+ stream.send_json(&response).await?
+ }
+ }
+ Either::Left(Ok(_)) => {
+ error!("Websocket received non-text");
+ break;
+ }
+ Either::Left(Err(err)) => {
+ error!("Websocket error: {}", err);
+ break;
+ }
+ }
+ }
+ Ok(())
+}
+
+async fn handle_signals(mut signals: Signals) -> Result<(), std::io::Error> {
+ signals.next().await;
+ info!("Shutting down...");
+ Ok(())
+}
+
+async fn serve_404(response: Response) -> Result<Response, Error> {
+ match response.status() {
+ StatusCode::NotFound =>
+ Ok(Response::builder(404)
+ .body(Body::from_file("site/404.html").await?)
+ .content_type("text/html")
+ .build()),
+ _ => Ok(response)
+ }
+}
+
+#[async_std::main]
+async fn main() -> Result<(), Error> {
+ env_logger::init();
+
+ let mut app = tide::with_state(ServerState::new("redis://localhost/").await?);
+
+ app.at("/").serve_dir("site/")?;
+ app.at("/").serve_file("site/index.html")?;
+ app.at("/api").get(WebSocket::new(handle_websocket_request));
+ app.with(After(serve_404));
+
+ let signals = Signals::new(&[SIGINT])?;
+ let signal_handler = handle_signals(signals);
+
+ let listener = TlsListener::build()
+ .addrs("localhost:4433")
+ .cert("cert/cert.pem")
+ .key("cert/key.pem");
+
+ let app = app.listen(listener);
+ pin_mut!(signal_handler, app);
+
+ select(app, signal_handler).await.factor_first().0?;
+
+ info!("Pokerwave shut down gracefully.");
+
+ Ok(())
+}
--- /dev/null
+use futures::Stream;
+use redis::{AsyncCommands, Client, ErrorKind, FromRedisValue, IntoConnectionInfo, Msg, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value, aio::MultiplexedConnection};
+use serde::{Serialize, Deserialize};
+
+use crate::auth::Auth;
+use crate::game::{Game, GameSettings, GameSummary, UserAction};
+
+#[derive(Clone)]
+pub struct ServerState {
+ client: Client,
+ redis: MultiplexedConnection,
+}
+
+fn user_key(username: &str) -> String {
+ format!("user:{}", username)
+}
+
+fn game_key(id: u32) -> String {
+ format!("game:{}", id)
+}
+
+const TAKE_ACTION_LUA_SCRIPT: &'static str = r#"
+ local len = redis.call('llen', KEYS[1])
+ local expected = tonumber(ARGV[1])
+ if (len == expected) then
+ return redis.call('rpush', KEYS[1], ARGV[2])
+ elseif (len > expected) then
+ return 0
+ else
+ local error = string.format('Inconsistent game state - database reports %d actions but expected at least %d', len, expected)
+ return redis.error_reply(error)
+ end
+"#;
+
+impl ServerState {
+ pub async fn new<T: IntoConnectionInfo>(params: T) -> RedisResult<Self> {
+ let client = Client::open(params)?;
+ let redis = client.get_multiplexed_async_std_connection().await?;
+ Ok(Self{client, redis})
+ }
+
+ pub async fn create_user(&mut self, username: &str, auth: Auth, nickname: &str) -> RedisResult<()> {
+ let key = user_key(username);
+ if self.redis.hset_nx::<_, _, _, i32>(&key, "auth", AsJson(auth)).await? == 0 {
+ return Err(RedisError::from((ErrorKind::ResponseError, "User already exists")));
+ }
+ self.redis.hset(&key, "nickname", username).await
+ }
+
+ pub async fn set_user_auth(&mut self, username: &str, auth: Auth) -> RedisResult<()> {
+ let key = user_key(username);
+ self.redis.hset(key, "auth", AsJson(auth)).await
+ }
+
+ async fn get_user_auth(&mut self, username: &str) -> Option<Auth> {
+ let key = user_key(username);
+ self.redis.hget(key, "auth").await.ok().map(AsJson::get)
+ }
+
+ pub async fn set_user_nickname(&mut self, username: &str, nickname: &str) -> RedisResult<()> {
+ let key = user_key(username);
+ self.redis.hset(key, "nickname", nickname).await
+ }
+
+ pub async fn verify(&mut self, username: &str, challenge: &str, signature: &str) -> bool {
+ match self.get_user_auth(username).await {
+ None => false,
+ Some(auth) => auth.verify(challenge, signature),
+ }
+ }
+
+ pub async fn create_game(&mut self, settings: GameSettings) -> RedisResult<u32> {
+ self.redis.rpush("games", AsJson(settings)).await.map(|i: u32| i - 1)
+ }
+
+ pub async fn get_game_list(&mut self) -> RedisResult<Vec<GameSummary>> {
+ const GAME_LIST_BLOCK_SIZE: isize = 1024;
+ let mut ret = Vec::new();
+ for i in (0..).step_by(GAME_LIST_BLOCK_SIZE as usize) {
+ let games: Vec<AsJson<GameSettings>> = self.redis.lrange("games", i, i + GAME_LIST_BLOCK_SIZE).await?;
+ if games.is_empty() { break; }
+ for (j, AsJson(settings)) in games.into_iter().enumerate() {
+ ret.push(GameSummary::new(i as u32 + j as u32, settings));
+ }
+ }
+ Ok(ret)
+ }
+
+ pub async fn update_game_state(&mut self, game: &mut Game) -> RedisResult<()> {
+ const GAME_ACTION_BLOCK_SIZE: isize = 1024;
+ let key = game_key(game.id());
+ for i in (game.actions_len() as isize..).step_by(GAME_ACTION_BLOCK_SIZE as usize) {
+ let actions: Vec<AsJson<UserAction>> = self.redis.lrange(&key, i, i + GAME_ACTION_BLOCK_SIZE).await?;
+ if actions.is_empty() { break; }
+ for AsJson(action) in actions {
+ game.take_action(action).map_err(|err| RedisError::from((ErrorKind::ResponseError, "Invalid action")))?;
+ }
+ }
+ Ok(())
+ }
+
+ pub async fn get_game(&mut self, id: u32) -> RedisResult<Game> {
+ let settings = self.redis.lindex("games", id as isize).await.map(AsJson::get)?;
+ let mut game = Game::new(id, settings);
+ self.update_game_state(&mut game).await?;
+ Ok(game)
+ }
+
+ pub async fn take_action(&mut self, id: u32, len: usize, action: &UserAction) -> RedisResult<ActionStatus> {
+ let key = game_key(id);
+ debug!("redis: executing: EVAL {:?} 1 {:?} {} {:?}", TAKE_ACTION_LUA_SCRIPT, key, len, serde_json::to_string(&action).unwrap());
+ redis::cmd("EVAL").arg(TAKE_ACTION_LUA_SCRIPT).arg(1).arg(key).arg(len).arg(AsJson(action)).query_async(&mut self.redis).await
+ }
+}
+
+pub enum ActionStatus {
+ Committed,
+ Interrupted,
+}
+
+impl FromRedisValue for ActionStatus {
+ fn from_redis_value(value: &Value) -> RedisResult<Self> {
+ match value {
+ Value::Int(0) => Ok(ActionStatus::Interrupted),
+ Value::Int(_) => Ok(ActionStatus::Committed),
+ _ => Err(RedisError::from((ErrorKind::TypeError, "ActionStatus may only be converted from an integer")))
+ }
+ }
+}
+
+struct AsJson<T>(T);
+
+impl<T> AsJson<T> {
+ pub fn get(self) -> T {
+ self.0
+ }
+}
+
+impl<T> FromRedisValue for AsJson<T>
+ where T: for<'a> Deserialize<'a>
+{
+ fn from_redis_value(value: &Value) -> RedisResult<Self> {
+ match value {
+ Value::Data(ref bytes) => serde_json::from_slice(bytes).map(AsJson)
+ .map_err(|err| RedisError::from((
+ ErrorKind::TypeError,
+ "Failed to parse as JSON",
+ err.to_string()
+ ))),
+ _ => Err(RedisError::from((ErrorKind::TypeError, "Value was not Value::Data"))),
+ }
+ }
+}
+
+impl<T> ToRedisArgs for AsJson<T>
+ where T: Serialize
+{
+ fn write_redis_args<W>(&self, out: &mut W)
+ where W: ?Sized + RedisWrite
+ {
+ match serde_json::to_vec(&self.0) {
+ Ok(bytes) => out.write_arg(&bytes),
+ Err(_) => return,
+ }
+ }
+
+ fn is_single_arg(&self) -> bool {
+ true
+ }
+}