diff --git a/.dockerignore b/.dockerignore index 6b8710a..d129be9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ .git +target +kcov \ No newline at end of file diff --git a/.gitignore b/.gitignore index dc6e246..e31d24e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ target +*.bk *.rdb +*.sqlite +kcov \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 442e483..3b8dae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,14 @@ +sudo: false language: rust + +# run builds for all the trains (and more) rust: - - nightly-2016-12-16 + - nightly + +# the main build +script: + - | + cd server && + cargo build && + cargo test + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4ae79c6..c69b32d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,18 +1,9 @@ [root] -name = "registration_server" +name = "registration_types" version = "0.1.0" dependencies = [ - "docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "iron-cors 0.1.0 (git+https://github.com/fxbox/iron-cors.git?rev=a58fa6d7)", - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "mount 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "params 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "redis 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "router 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rusqlite 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -23,21 +14,59 @@ dependencies = [ "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ansi_term" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "atty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "bitflags" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bitflags" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bodyparser" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "persistent 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "persistent 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.21 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -46,33 +75,45 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "conduit-mime-types" -version = "0.7.3" +name = "bufstream" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "cookie" -version = "0.2.5" +name = "clap" +version = "2.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "openssl 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "docopt" -version = "0.6.86" +name = "conduit-mime-types" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -80,12 +121,88 @@ name = "dtoa" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "dtoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "email" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "env_logger" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -94,13 +211,18 @@ name = "error" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "foreign-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "gcc" -version = "0.3.41" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -112,82 +234,98 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "hpack" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "httparse" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "hyper" -version = "0.9.14" +version = "0.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", - "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-openssl" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "idna" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "iron" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "iron-cors" version = "0.1.0" -source = "git+https://github.com/fxbox/iron-cors.git?rev=a58fa6d7#a58fa6d7921b03c894e1834778bf673dcf93613c" +source = "git+https://github.com/fxbox/iron-cors.git#550e766872baf6c05a326ca840316a210536a507" dependencies = [ - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "iron-test" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hyper 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itoa" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itoa" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -204,52 +342,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lazy_static" -version = "0.1.16" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "lazy_static" -version = "0.2.2" +name = "lettre" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "email 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "libc" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "libressl-pnacl-sys" -version = "2.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "libsqlite3-sys" -version = "0.5.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "linked-hash-map" -version = "0.0.9" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "log" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lru-cache" -version = "0.0.7" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "linked-hash-map 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -262,15 +403,15 @@ name = "memchr" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "mime" -version = "0.2.2" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -278,9 +419,9 @@ name = "mime_guess" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_codegen 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -291,11 +432,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "mount" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sequence_trie 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -304,10 +445,9 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "buf_redux 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -315,190 +455,164 @@ dependencies = [ [[package]] name = "num" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "num-complex 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "num-rational 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-bigint" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-complex" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-integer" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-iter" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-rational" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-traits" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "num_cpus" -version = "0.2.13" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num_cpus" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "openssl" -version = "0.7.14" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gcc 0.3.41 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.7.17 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys-extras 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "openssl-sys" -version = "0.7.17" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "gcc 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)", "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "openssl-sys-extras" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "gcc 0.3.41 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.7.17 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "openssl-verify" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "openssl 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "params" -version = "0.4.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bodyparser 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bodyparser 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "multipart 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "urlencoded 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "persistent" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_shared 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_codegen" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_generator 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_shared 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_generator" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "phf_shared 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "phf_shared" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "pkg-config" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -510,30 +624,42 @@ dependencies = [ ] [[package]] -name = "pnacl-build-helper" -version = "1.4.10" +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "r2d2" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "scheduled-thread-pool 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "rand" -version = "0.3.15" +name = "r2d2_sqlite" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rusqlite 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "redis" -version = "0.7.1" +name = "rand" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "redox_syscall" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.80" @@ -551,36 +677,76 @@ name = "regex-syntax" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "registration_server" +version = "0.1.0" +dependencies = [ + "clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)", + "email 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-openssl 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iron-cors 0.1.0 (git+https://github.com/fxbox/iron-cors.git)", + "iron-test 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lettre 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "params 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2_sqlite 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "registration_types 0.1.0", + "router 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rusqlite 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "route-recognizer" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "router" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "route-recognizer 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rusqlite" -version = "0.7.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "libsqlite3-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lru-cache 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rustc-serialize" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -591,6 +757,14 @@ dependencies = [ "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "semver" version = "0.1.20" @@ -598,43 +772,87 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "sequence_trie" -version = "0.0.13" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "0.8.21" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "serde_json" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.21 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "sha1" -version = "0.2.0" +name = "serde_json" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "solicit" -version = "0.4.4" +name = "strsim" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "strsim" -version = "0.5.2" +name = "synom" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "tempdir" @@ -644,13 +862,23 @@ dependencies = [ "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread-id" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -663,22 +891,26 @@ dependencies = [ [[package]] name = "time" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "traitobject" -version = "0.0.1" +name = "toml" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "traitobject" -version = "0.0.3" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -691,7 +923,7 @@ name = "typemap" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -704,7 +936,7 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.2.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -712,35 +944,50 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.3" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-segmentation" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "unsafe-any" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "url" -version = "1.2.4" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "urlencoded" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bodyparser 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bodyparser 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -757,6 +1004,32 @@ name = "utf8-ranges" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uuid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vcpkg" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.2.8" @@ -769,94 +1042,127 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" +"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" +"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" +"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" +"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" -"checksum bodyparser 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "07b171b407e583dc8f01011a713f20575a81ac60acecf3b8153012709aeb1fd6" +"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" +"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +"checksum bodyparser 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6928e817538b74a73d1dd6e9a942a2a35c632a597b6bb14fd009480f859a6bf5" "checksum buf_redux 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "861b9d19b9f5cb40647242d10d0cb0a13de0a96d5ff8c8a01ea324fa3956eb7d" +"checksum bufstream 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f382711e76b9de6c744cc00d0497baba02fb00a787f088c879f01d09468e32" +"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8" +"checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056" +"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f" "checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8" -"checksum cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0e3d6405328b6edb412158b3b7710e2634e23f3614b9bb1c412df7952489a626" -"checksum docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)" = "4a7ef30445607f6fc8720f0a0a2c7442284b629cf0d049286860fae23e71c4d9" "checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d" +"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" +"checksum email 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "88560dfb4e8eb3403e6402dfed790d0ef1c16f6d9f80028243a4f09826772f4f" +"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +"checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +"checksum encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +"checksum encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +"checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +"checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" "checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f" "checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc" -"checksum gcc 0.3.41 (registry+https://github.com/rust-lang/crates.io-index)" = "3689e1982a563af74960ae3a4758aa632bb8fd984cfc3cc3b60ee6109477ab6e" +"checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d" +"checksum gcc 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)" = "9be730064c122681712957ba1a9abaf082150be8aaf94526a805d900015b65b9" "checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" -"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58" -"checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" -"checksum hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)" = "bcb3fc65554155980167fb821d05c7c66177f92464976c0b676a19d9e03387a7" -"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11" -"checksum iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fb1b2d809f84bf347e472d5758762b5c804e0c622970235f156d82673e4d334" -"checksum iron-cors 0.1.0 (git+https://github.com/fxbox/iron-cors.git?rev=a58fa6d7)" = "" +"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07" +"checksum hyper 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cb7031283266d12f2d4bf30b624bc2b2fd21bbcc00863c9928e87dc5e1699d2e" +"checksum hyper-openssl 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "85a372eb692590b3fe014c196c30f9f52d4c42f58cd49dd94caeee1593c9cc37" +"checksum idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2233d4940b1f19f0418c158509cd7396b8d70a5db5705ce410914dc8fa603b37" +"checksum iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2440ae846e7a8c7f9b401db8f6e31b4ea5e7d3688b91761337da7e054520c75b" +"checksum iron-cors 0.1.0 (git+https://github.com/fxbox/iron-cors.git)" = "" +"checksum iron-test 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "865d519985bc0a4fb64b5c44b8538b5252d716c6a6369ea3fc3bd035a15b6a16" "checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1" +"checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" -"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" -"checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b" -"checksum libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "a51822fc847e7a8101514d1d44e354ba2ffa7d4c194dcab48870740e327cac70" -"checksum libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "cbc058951ab6a3ef35ca16462d7642c4867e6403520811f28537a4e2f2db3e71" -"checksum libsqlite3-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "663508cb9c1e23363aea1a8b1f7d6340394ebc3bc3a6daebfb9cc99b8feaf2ec" -"checksum linked-hash-map 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "83f7ff3baae999fdf921cccf54b61842bb3b26868d50d02dff48052ebec8dd79" -"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" -"checksum lru-cache 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "42d50dcb5d9f145df83b1043207e1ac0c37c9c779c4e128ca4655abc3f3cbf8c" +"checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf" +"checksum lettre 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "062777c2e39d4ccf5a1f30bb308d6464341e7587a5e140f79887d522ca906844" +"checksum libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e7eb6b826bfc1fdea7935d46556250d1799b7fe2d9f7951071f4291710665e3e" +"checksum libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "370090ad578ba845a3ad4f383ceb3deba7abd51ab1915ad1f2c982cc6035e31c" +"checksum linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7860ec297f7008ff7a1e3382d7f7e1dcd69efc94751a2284bafc3d013c2aa939" +"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b" +"checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21" "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" -"checksum mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5c93a4bd787ddc6e7833c519b73a50883deb5863d76d9b71eb8216fb7f94e66" +"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" "checksum mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76da6df85047af8c0edfa53f48eb1073012ce1cc95c8fedc0a374f659a89dd65" "checksum modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" -"checksum mount 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c518ef1edf5da3aa1cdd5160c08d1781995ccb74b5669c2315ce29fe6cf6c1f2" +"checksum mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "32245731923cd096899502fc4c4317cfd09f121e80e73f7f576cf3777a824256" "checksum multipart 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b16d6498fe5b0c2f6d973fd9753da099948834f96584d628e44a75f0d2955b03" -"checksum num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "bde7c03b09e7c6a301ee81f6ddf66d7a28ec305699e3d3b056d2fc56470e3120" -"checksum num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "88b14378471f7c2adc5262f05b4701ef53e8da376453a8d8fee48e51db745e49" -"checksum num-complex 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c78e054dd19c3fd03419ade63fa661e9c49bb890ce3beb4eee5b7baf93f92f" -"checksum num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "fb24d9bfb3f222010df27995441ded1e954f8f69cd35021f6bef02ca9552fb92" -"checksum num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "287a1c9969a847055e1122ec0ea7a5c5d6f72aad97934e131c83d5c08ab4e45c" -"checksum num-rational 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "54ff603b8334a72fbb27fe66948aac0abaaa40231b3cecd189e76162f6f38aaf" -"checksum num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "a16a42856a256b39c6d3484f097f6713e14feacd9bfb02290917904fae46c81c" -"checksum num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "cee7e88156f3f9e19bdd598f8d6c9db7bf4078f99f8381f43a55b09648d1a6e3" -"checksum num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "55aabf4e2d6271a2e4e4c0f2ea1f5b07cc589cc1a9e9213013b54a76678ca4f3" -"checksum openssl 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "c4117b6244aac42ed0150a6019b4d953d28247c5dd6ae6f46ae469b5f2318733" -"checksum openssl-sys 0.7.17 (registry+https://github.com/rust-lang/crates.io-index)" = "89c47ee94c352eea9ddaf8e364be7f978a3bb6d66d73176572484238dd5a5c3f" -"checksum openssl-sys-extras 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "11c5e1dba7d3d03d80f045bf0d60111dc69213b67651e7c889527a3badabb9fa" -"checksum openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed86cce894f6b0ed4572e21eb34026f1dc8869cb9ee3869029131bc8c3feb2d" -"checksum params 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4b3d9eae22a78f0c0404b6b30f06d8ad7e017182807eac6c43ec8a6d9ce6f12f" -"checksum persistent 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0c0aea7e6e026f9090c56aa7cda9d4ad6f182c717f0640cb03beace1f75a43d2" -"checksum phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "0c6afb2057bb5f846a7b75703f90bc1cef4970c35209f712925db7768e999202" -"checksum phf_codegen 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "6b63f121bf9a128f2172a65d8313a8e0e79d63874eeb4b4b7d82e6dda6b62f7c" -"checksum phf_generator 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "50ffbd7970f75afa083c5dd7b6830c97b72b81579c7a92d8134ef2ee6c0c7eb0" -"checksum phf_shared 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "286385a0e50d4147bce15b2c19f0cf84c395b0e061aaf840898a7bf664c2cfb7" -"checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa" +"checksum num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "98b15ba84e910ea7a1973bccd3df7b31ae282bf9d8bd2897779950c9b8303d40" +"checksum num-bigint 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "ba6d838b16e56da1b6c383d065ff1ec3c7d7797f65a3e8f6ba7092fd87820bac" +"checksum num-complex 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "148eb324ca772230853418731ffdf13531738b50f89b30692a01fcdcb0a64677" +"checksum num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "ef1a4bf6f9174aa5783a9b4cc892cacd11aebad6c69ad027a0b65c6ca5f8aa37" +"checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e" +"checksum num-rational 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "c2dc5ea04020a8f18318ae485c751f8cfa1c0e69dcf465c29ddaaa64a313cc44" +"checksum num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "e1cbfa3781f3fe73dc05321bed52a06d2d491eaa764c52335cf4399f046ece99" +"checksum num_cpus 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f6e850c7f35c3de263e6094e819f6b4b9c09190ff4438fc6dec1aef1568547bc" +"checksum openssl 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b34cd77cf91301fff3123fbd46b065c3b728b17a392835de34c397315dce5586" +"checksum openssl-sys 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e035022a50faa380bd7ccdbd184d946ce539ebdb0a358780de92a995882af97a" +"checksum params 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "421e9f2c30e80365c9672709be664bfc84f73b088720d1cc1f4e99675814bb37" +"checksum persistent 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c9c94f2ef72dc272c6bcc8157ccf2bc7da14f4c58c69059ac2fc48492d6916" +"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" +"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" +"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" +"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" +"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" "checksum plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" -"checksum pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "61c9231d31aea845007443d62fcbb58bb6949ab9c18081ee1e09920e0cf1118b" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum r2d2 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1dd448c29d0ed83cfe187ffb8608fa07c47abdd7997f3f478f3a6223ad3f97fb" +"checksum r2d2_sqlite 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bc1d9bf11be9fd5a45e7be46690f15b7909bdd4faa48b0206b619a6dde20374" "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" -"checksum redis 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "feba37ee5a3a2703f97dddf30edae24e0df0c0ed6310ef078b497f020e7c054b" +"checksum redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b" "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" -"checksum route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4f0a750d020adb1978f5964ea7bca830585899b09da7cbb3f04961fc2400122d" -"checksum router 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b94397bfa5b772b4375be4da12560a7c1c1e74b2e35c46ed312958aad56df726" -"checksum rusqlite 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e9b3854687228334d8a579cd2f666ddd7fb46a5f68ac0460da2898394c4679d2" -"checksum rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "237546c689f20bb44980270c73c3b9edd0891c1be49cc1274406134a66d3957b" +"checksum route-recognizer 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3255338088df8146ba63d60a9b8e3556f1146ce2973bc05a75181a42ce2256" +"checksum router 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9b1797ff166029cb632237bb5542696e54961b4cf75a324c6f05c9cf0584e4e" +"checksum rusqlite 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28df6ae60019445d95d7a5be417ae16923c156d90f7051727fe0a15eaa70fae0" +"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" +"checksum scheduled-thread-pool 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d9fbe48ead32343b76f544c85953bf260ed39219a8bbbb62cd85f6a00f9644f" "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" -"checksum sequence_trie 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "d5b4eb0f7d1ff9b9666d8b8ff543f3705dd464025269a5b0e1988ffa60ca1be8" -"checksum serde 0.8.21 (registry+https://github.com/rust-lang/crates.io-index)" = "7b7c6bf11cf766473ea1d53eb4e3bc4e80f31f50082fc24077cf06f600279a66" -"checksum serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7d3c184d35801fb8b32b46a7d58d57dbcc150b0eb2b46a1eb79645e8ecfd5b" -"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" -"checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2" -"checksum strsim 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "67f84c44fbb2f91db7fef94554e6b2ac05909c9c0b0bc23bb98d3a1aebfe7f7c" +"checksum sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c915714ca833b1d4d6b8f6a9d72a3ff632fe45b40a8d184ef79c81bec6327eed" +"checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" +"checksum serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f530d36fb84ec48fb7146936881f026cdbf4892028835fd9398475f82c1bb4" +"checksum serde_derive 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "10552fad5500771f3902d0c5ba187c5881942b811b7ba0d8fbbfbf84d80806d3" +"checksum serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37aee4e0da52d801acfbc0cc219eb1eda7142112339726e427926a6f6ee65d3a" +"checksum serde_json 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "67f7d2e9edc3523a9c8ec8cd6ec481b3a27810aafee3e625d311febd3e656b4c" +"checksum serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "48b04779552e92037212c3615370f6bd57a40ebba7f20e554ff9f55e41a69a7b" +"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" +"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" +"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209" "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" "checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" -"checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" -"checksum traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "07eaeb7689bb7fca7ce15628319635758eda769fed481ecfe6686ddef2600616" -"checksum traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9dc23794ff47c95882da6f9d15de9a6be14987760a28cc0aafb40b7675ef09d8" +"checksum time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "ffd7ccbf969a892bf83f1e441126968a07a3941c24ff522a26af9f9f4585d1a3" +"checksum toml 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4cc5dbfb20a481e64b99eb7ae280859ec76730c7191570ba5edaa962394edb0a" +"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" "checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" "checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" "checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" -"checksum unicode-bidi 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b61814f3e7fd0e0f15370f767c7c943e08bc2e3214233ae8f88522b334ceb778" -"checksum unicode-normalization 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5e94e9f6961090fcc75180629c4ef33e5310d6ed2c0dd173f4ca63c9043b669e" -"checksum unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b351086021ebc264aea3ab4f94d61d889d98e5e9ec2d985d993f50133537fd3a" -"checksum url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f024e241a55f5c88401595adc1d4af0c9649e91da82d0e190fe55950231ae575" -"checksum urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5ddcf2d3a0beedb5cdf50cabc521ab76a994907877a1d91d996c251d42c70e2e" +"checksum unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a6a2c4e3710edd365cd7e78383153ed739fa31af19f9172f72d3575060f5a43a" +"checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff" +"checksum unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8083c594e02b8ae1654ae26f0ade5158b119bd88ad0e8227a5d8fcd72407946" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +"checksum url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2ba3456fbe5c0098cb877cf08b92b76c3e18e0be9e47c35b487220d377d24e" +"checksum urlencoded 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8c28708636d6f7298a53b1cdb6af40f1ab523209a7cb83cf4d41b3ebc671d319" "checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum uuid 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "885acc3b17fdef6230d1f7765dff1106dfd5e75a93c2f26459fbf600ed6dcc14" +"checksum uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7cfec50b0842181ba6e713151b72f4ec84a6a7e2c9c8a8a3ffc37bb1cd16b231" +"checksum vcpkg 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df74ff70e2ced9607f67e06640f89a6a6374b459b51bdef290a5cfa657fe4fcc" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml index 6266e0f..a75adca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,5 @@ -[package] -name = "registration_server" -version = "0.1.0" -authors = ["fabrice "] - -[features] -ssl = [] - -[dependencies] -docopt = "0.6.83" -env_logger = "0.3.5" -iron-cors = { git = "https://github.com/fxbox/iron-cors.git", rev = "a58fa6d7" } -log = "0.3" -params = "0.4.0" -mount = "0.2.1" -redis = "0.7.0" -router = "0.4.0" -rusqlite = "0.7.3" -rustc-serialize = "0.3" - -[dependencies.iron] -version = "0.4.0" -default-features = true -features = ["ssl"] +[workspace] +members = [ + "server/", + "api/", +] diff --git a/Dockerfile b/Dockerfile index 9c18746..f0bf41a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM debian:jessie -# Adapted from https://raw.githubusercontent.com/Scorpil/docker-rust/master/nightly/Dockerfile +FROM debian:stretch ENV DEBIAN_FRONTEND=noninteractive ENV SHELL=/bin/bash RUN apt-get update && \ + apt-get dist-upgrade -qqy && \ apt-get install \ ca-certificates \ curl \ @@ -13,21 +13,39 @@ RUN apt-get update && \ libc6-dev \ libssl-dev \ libsqlite3-dev \ - redis-server \ + python \ + pkgconf \ + pdns-server \ + pdns-backend-remote \ + sqlite \ -qqy \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* +# Install PageKite +RUN curl -s https://pagekite.net/pk/ | bash + +# Create a non privileged user to build the Rust code. RUN useradd -m -d /home/user -p user user USER user WORKDIR /home/user -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly-2016-12-16 +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly ENV PATH=/home/user/.cargo/bin:/home/user/bin:$PATH COPY . /home/user +WORKDIR /home/user/server RUN cargo build --release USER root -CMD service redis-server start > /certdir/registration_server.log && RUST_LOG=info ./target/release/registration_server -h 0.0.0.0 -p 4443 --cert-directory /certdir + +# Stop the default install of PowerDNS. +CMD service pdns stop + +# We expect to find the configuration mounted in /home/user/config +# and to find the following files: +# - pdns.conf : PowerDNS configuration. +# - config.json : registration server configuration. +# - env : used to source environment variables. +CMD ./run_from_docker.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index edd4cf3..e6c1678 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,34 @@ # Registration server -[![Build Status](https://travis-ci.org/fxbox/registration_server.svg?branch=master)](https://travis-ci.org/fxbox/registration_server) +[![Build Status](https://travis-ci.org/moziot/registration_server.svg?branch=master)](https://travis-ci.org/moziot/registration_server) This server exposes a http(s) API that lets you post messages from your home network and discover them later on. ## Usage ```bash -cargo run -- -h 0.0.0.0 -p 4242 --cert-dir /etc/letsencrypt/live/knilxof.org +USAGE: + registration_server [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + --config-file Path to a toml configuration file. ``` -## Urls +See the `config.toml.sample` for an example of configuration file. + + +## Building & testing + +Just run `cargo build` and `cargo test` ! + +## Deploying + +Deployment details are provided in the [deployment guide](deployment.md). -Two endpoints are provided: +## Api -1. /register?message=XXX will publish `message` to other clients who also connect from the same outgoing IP address as you. -2. /ping will return a json representation of the messages that are published from the same outgoing IP address. +The REST api is documented [here](api.md). Its usage is describe in [this document](flow.md). diff --git a/api.md b/api.md new file mode 100644 index 0000000..13cfc8d --- /dev/null +++ b/api.md @@ -0,0 +1,134 @@ +# REST API + +The REST api follows these general rules: +* All the requests are GET requests. +* CORS is enabled on endpoints that are meant to be queried by web browsers. +* 400 is returned for any client error (missing parameter, incorrect parameter value). +* 501 is returned for internal errors (typically database issues). + +# /subscribe + +This endpoint reserves a new name for the gateway, as a subdomain managed by the registration server. + +*Parameters:* +* `name`: the requested name to use as part of the subdomain assigned to the gateway. +* `desc`: optional, a friendly description of this gateway. If this parameter is not present, a default description is generated including the gateway's name. + +*Returns:* + +A json document: `{ "name": "demo", "token": "asd34q343krj3" }` + +The token is a secret identifier for this gateway, that must not be transmitted to any third party. + +# /unsubscribe + +This endpoint let you remove from the registration server a previously subscribed gateway. + +*Parameters:* +* `token`: the secret token assigned to this gateway. + +*Returns:* + +An empty HTTP 200 response. + +# /register + +This needs to be called on a regular basis to let the system know what the local ip of the gateway is to assist with discovery. The server will evict old entries on a regular basis so the gateway needs to register itself often enough. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `local_ip`: the local ip address of the gateway. + +*Returns:* + +An empty HTTP 200 response. + +The local ip is used to return results of the `/ping` endpoint. + +# /dnsconfig + +This endpoint is used to set the Let's Encrypt DNS challenge value when you need to retrieve or review certificates. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `challenge`: the value of the challenge which will be return in TXT DNS requests with an _acme_challenge prefix. + +*Returns:* + +An empty HTTP 200 response. + +# /info + +*Parameters:* +* `token`: the secret token assigned to this gateway. + +*Returns:* + +A json representation of the database content for the gateway matching this token. + +# /ping + +*No parameters* + +*Returns:* + +An array of `{ "href": "...", "desc": "..." }` objects each describing a gateway that can be reached on this local network. + +# /adddiscovery + +This endpoints binds a gateway to a discovery token. Discovery tokens are meant to only be shared by one 3rd party that can use them to discover the gateway without knowing its secret token. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `disco`: a secret discovery token. + +*Returns:* + +An empty HTTP 200 response. + +# /revokediscovery + +Remove a token <-> disco binding, making the gateway undiscoverable by a 3rd party holding on this discovery token. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `disco`: a secret discovery token. + +*Returns:* + +An empty HTTP 200 response. + +# /discovery + +This endpoint will provide the best way to reach a gateway for a 3rd party application. + +*Parameters:* +* `disco`: a secret discovery token. + +*Returns:* + +An array of `{ "href": "...", "desc": "..." }` objects each describing a way to reach the gateway. + +# /setemail + +Adds a pending verification email to a gateway. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `email`: the email to verify. + +*Returns:* + +An empty HTTP 200 response. This will trigger an email verification flow by sending a message to the email address with a link to follow in order to associate the email address with the gateway. + +# /revokeemail + +Calling this endpoint will cancel an ongoing email verification flow. + +*Parameters:* +* `token`: the secret token assigned to this gateway. +* `email`: the email being verified. + +*Returns:* + +An empty HTTP 200 response. diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..12bd02a --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "registration_types" +version = "0.1.0" +authors = ["fabrice "] + +[dependencies] +serde = "1.0" +serde_derive = "1.0" \ No newline at end of file diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..ece08ff --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Types used by the REST api responses. + +#[macro_use] +extern crate serde_derive; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ServerInfo { + pub token: String, + pub local_name: String, + pub remote_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub dns_challenge: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub local_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_ip: Option, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + pub timestamp: i64, +} + +unsafe impl Send for ServerInfo {} +unsafe impl Sync for ServerInfo {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NameAndToken { + pub name: String, + pub token: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Discovered { + pub href: String, + pub desc: String, +} diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 9668322..0000000 --- a/client/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - FoxBox discovery - - - - -

FoxBox Discovery

- -
-
- - \ No newline at end of file diff --git a/client/main.js b/client/main.js deleted file mode 100644 index e6f3543..0000000 --- a/client/main.js +++ /dev/null @@ -1,58 +0,0 @@ -let loadJSON = function(method, url, content = '') { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.open(method, url, true); - xhr.responseType = 'json'; - xhr.timeout = 3000; - xhr.overrideMimeType('application/json'); - xhr.setRequestHeader('Accept', 'application/json,text/javascript,*/*;q=0.01'); - xhr.addEventListener('load', () => { - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - resolve(xhr.response); - } else { - reject('Could not complete the operation.'); - } - }); - xhr.addEventListener('error', reject); - xhr.send(JSON.stringify(content)); - }); -}; - -function doDiscovery() { - console.log("Starting discovery."); - loadJSON("GET", "http://cloud.desre.org:4242/ping").then(data => { - console.log("Got data: " + JSON.stringify(data)); - var node = document.getElementById("content"); - - if (!data) { - return; - } - // Reset while we wait for an answer. - node.innerHTML = ""; - - var content = ""; - if (data.length == 0) { - content = "

No FoxBox available!

"; - } else { - data.forEach(item => { - content += `

FoxBox found at ${item.local_ip}

`; - loadJSON("GET", `http://${item.local_ip}:3000/services/list.json`).then(list => { - console.log("Service list: " + JSON.stringify(list)); - let html = "
    "; - for (let s in list) { - console.log("We have " + s); - html += `
  • ${list[s].name}
  • `; - } - html += "
      "; - node.innerHTML += html; - }); - }); - } - - node.innerHTML = content; - }); -} - -window.addEventListener("DOMContentLoaded", () => { - document.getElementById("go").addEventListener("click", doDiscovery); -}); diff --git a/config/config.toml b/config/config.toml new file mode 100644 index 0000000..e774731 --- /dev/null +++ b/config/config.toml @@ -0,0 +1,36 @@ +# Configuration used for tests. + +[general] +host = "127.0.1.1" +port = 4141 +domain = "knilxof.org" +data_directory = "/tmp" +tunnel_ip = "1.2.3.4" +eviction_delay = 2 + +[pdns] +dns_ttl = 89 +soa_content = "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800" +socket_path = "/tmp/powerdns_tunnel.sock" + +[email] +server = "mail.gandi.net" +user = "accounts@knilxof.org" +password = "******" +sender = "accounts@knilxof.org" +confirmation_title = "Welcome to your MozIot Gateway" +confirmation_body = "Hello,\n\nTo confirm your email address, follow this link: {link}" +success_page = """ + + Email Confirmation Successful! + +

      Thank you for verifying your email, {email}.

      + +""" +error_page = """ + + Email Confirmation Error! + +

      An error happened while verifiying your email.

      + +""" diff --git a/config/env b/config/env new file mode 100644 index 0000000..0b1a95d --- /dev/null +++ b/config/env @@ -0,0 +1,7 @@ +# Domain specific configuration for pagekite. + +DOMAIN=box.knilxof.org +SECRET=moziot + +# Other variables useful for other purposes. +export RUST_LOG=info \ No newline at end of file diff --git a/config/pdns.conf b/config/pdns.conf new file mode 100644 index 0000000..bcf8437 --- /dev/null +++ b/config/pdns.conf @@ -0,0 +1,14 @@ +# Testing configuration of PowerDNS, running on a non-privileged port +# and not as a daemon. + +daemon=no +local-port=5300 +local-address=0.0.0.0 +socket-dir=. +launch=remote +#remote-connection-string=http:url=http://localhost:4242/pdns,post=true,post_json=true +remote-connection-string=unix:path=/tmp/pdns_tunnel.sock +write-pid=no +log-dns-details=yes +log-dns-queries=yes +loglevel=5 \ No newline at end of file diff --git a/deployment.md b/deployment.md new file mode 100644 index 0000000..61e3b57 --- /dev/null +++ b/deployment.md @@ -0,0 +1,119 @@ +# Deploying the tunnel server + +The setup relies on 3 components: +- this registration server. +- a [PowerDNS](https://powerdns.com/) server. +- [PageKite](https://pagekite.net/). + +To make it easier to deploy a working environment, a Docker file is provided which will build an image including all the needed dependencies. + +Getting a full setup ready involves the following: +- build a Docker image. +- configure the DNS zone for the domain you want to use. +- run the Docker image with the proper configuration. + +## Docker configuration + +First, build the docker image with `docker build -t tunnel_server .` from the source directory. + +## DNS Zone configuration + +The PowerDNS server is only used to answer queries for the `*.box.yourdomain.com` qnames. This means that you need to have access to the configuration of `yourdomain.com` in order to delegate the DNS queries appropriately. + +If `1.2.3.4` is the public IP of the server, add the following to your DNS zone configuration: +``` +* 10800 IN A 1.2.3.4 +@ 10800 IN A 1.2.3.4 +box 10800 IN NS yourdomain.com. +``` + +## Running the Docker image + +You will have to mount a couple of directories and relay some ports for the Docker image to run properly: +- mount `/home/user/config` to a directory where you will store the configuration files. +- mount `/home/user/data` to a directory where the database will be stored. + +Port 53 over tcp and udp needs to be forwarded for PowerDNS. The ports used for the http server and the tunnel also need to be forwarded. + +## Configuration files + +* The `$CONFIG_DIR/env` file is used to set any environment variable need. It is mandatory to declare DOMAIN and SECRET to configure PageKite. For instance, set DOMAIN to `box.yourdomain.com`. Here's a full example: +``` +# Domain specific configuration for pagekite. + +DOMAIN=box.yourdomain.com +SECRET=moziot + +# Other variables useful for other purposes. +export RUST_LOG=debug +``` + +* The `CONFIG_DIR/pdns.conf` is the PowerDNS configuration file. It needs to be consistent with the registration configuration to connect on the correct socket for the remote queries: +``` +daemon=yes +local-port=53 +local-address=0.0.0.0 +socket-dir=. +launch=remote +remote-connection-string=unix:path=/tmp/pdns_tunnel.sock +write-pid=no +log-dns-details=yes +log-dns-queries=yes +loglevel=5 + +``` + +* The `CONFIG_DIR/config.toml` file holds the registration server configuration. Here's a sample consistent with the `pdns.conf` showed above: +``` +# Configuration used for tests. + +[general] +host = "0.0.0.0" +port = 80 +domain = "yourdomain.org" +data_directory = "/home/user/data" +# Uncomment to use TLS (recommended) +# cert_directory = "/home/user/config" +tunnel_ip = "1.2.3.4" +# Evict entries every 5 minutes +eviction_delay = 300 + +[pdns] +dns_ttl = 1203 +# Check your DNS configuration to fill in this field. +soa_content = "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800" +socket_path = "/tmp/powerdns_tunnel.sock" + +[email] +server = "mail.gandi.net" +user = "accounts@knilxof.org" +password = "******" +sender = "accounts@knilxof.org" +confirmation_title = "Welcome to your MozIot Gateway" +confirmation_body = "Hello,\n\nTo confirm your email address, follow this link: {link}" +success_page = """ + + Email Confirmation Successful! + +

      Thank you for verifying your email, {email}.

      + +""" +error_page = """ + + Email Confirmation Error! + +

      An error happened while verifiying your email.

      + +""" + +``` + +By default the PageKite tunnel listen on port 4443 +Once you have all your configuration files ready, you can use such a shell script to start it: + +``` +#!/bin/bash +set -x -e +docker run -d -v /home/ec2-user/moziot/config:/home/user/config -v /home/ec2-user/moziot/data:/home/user/data -p 80:80 -p 4443:4443 -p 53:53 -p 53:53/udp tunnel_server +``` +This script relays port 80 for the server, but it is recommended to instead relay port 443 and to setup TLS certificates. The gateway will be available on port 4443 from the public endpoint, over https. diff --git a/flow.md b/flow.md new file mode 100644 index 0000000..ea66a91 --- /dev/null +++ b/flow.md @@ -0,0 +1,56 @@ +# Actors + +[gateway] the IoT gateway running in the local network. + +[cloud] is the registration+dns+tunnel combo. + +[app] is a 3rd party web application that will access [gateway] on behalf of the end user. + +[browser] is the user's web browser. + +# Setup + +1. [gateway <-> browser] Start the server on http and load the setup UI at http://localhost:8080 +2. [gateway <-> cloud] Find an available DNS name, receive the api token using `/subscribe`. +3. [gateway <-> cloud] Run the LE DNS challenge, use `/dnsconfig`, retrieve the certificates, restart the server on https. +4. [gateway <-> browser] Create the admin account with an email address, verify email using `/setemail`. +5. [gateway <-> cloud] periodically register the server local ip with the cloud service using `/register`. + +# Discovery + +The registration server supports two kinds of discovery mechanism: + +* A simple one, using a single DNS name per gateway, that doesn't provide shortest path in the local network out of the box. +* A more evolved one combining two DNS names, that let 3rd party applications discover the shortest path to access the gateway apis. + +In each case, we consider that the gateway has been subscribed successfully and that the relevant TLS certificates are installed. + +The gateway is then in nominal working mode, in which it periodically calls the `/register` endpoint. + +## Simple discovery + +In this mode, the browser calls the `/ping` endpoint and will either receive a list of local gateways or an empty answer if the user is browsing from outside the local network. This means that the web application must first be used in the local network and remember the gateway url. The DNS will also always resolve this url as a public ip, forcing a round trip through the tunnel. + +## Full discovery + +This mode relies on the set of `discovery` endpoints and works as follows: + +When the [app] at https://example.com/ wants to access the services on the gateway: + +First use: +1. [browser] needs to be connected on the local network. +2. [app <-> cloud] triggers a discovery and presents choices to the user if there are several options (using `/ping`). +3. [app] Opens an iframe to https://server/auth?redirect=https://example.com/auth_done +4. [app] Retrieves the app specific tokens (authentication and discovery ones) to present for REST requests to [server] and discovery requests to [cloud] (using `/adddiscovery` from the gateway to the cloud). + +Subsequent uses: +1. [app] uses its discovery token to discover the [server] location. +2. [app] calls [server] apis using its authentication token. + +In this case, the `/discovery` endpoint called from the browser will provide a different DNS name depending on whether the call is made from within the local network or not. + +# Email verification + +1. The admin UI calls `/setemail`, which triggers the sending of a verification email. +2. When the user clicks on the link from the email, this validates the email as being associated with the gateway. +3. It is possible to check if an email is setup by calling the `/info` endpoint. diff --git a/run_from_docker.sh b/run_from_docker.sh new file mode 100755 index 0000000..b290b08 --- /dev/null +++ b/run_from_docker.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -x -e + +ROOT_DIR=/home/user/config + +source $ROOT_DIR/env + +pdns_server --config-dir=$ROOT_DIR + +pagekite.py --isfrontend --ports=4443 --protos=https --domain=https:*.$DOMAIN:$SECRET --authdomain=$DOMAIN & + +./server/target/release/main --config-file=$ROOT_DIR/config.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..55e2aa6 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +reorder_imported_names = true \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..08ca67f --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "registration_server" +version = "0.1.0" +authors = ["fabrice "] + +[dependencies] +clap = "2" +email = "0.0" +env_logger = "0.3" +hyper-openssl = "0.2" +iron = "0.5" +iron-cors = { git = "https://github.com/fxbox/iron-cors.git" } +lettre = "0.6" +log = "0.3" +params = "0.6" +mount = "0.3" +registration_types = { path = "../api" } +r2d2 = "0.7" +r2d2_sqlite = "0.2" +rusqlite = "0.11" +router = "0.5" +rust-crypto = "0.2" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +toml = "0.4" +uuid = { version = "0.4", features = ["v4"] } + +[dev-dependencies] +iron-test = "0.5" +hyper = "0.10" diff --git a/server/config.toml.sample b/server/config.toml.sample new file mode 100644 index 0000000..ccad3e1 --- /dev/null +++ b/server/config.toml.sample @@ -0,0 +1,36 @@ +# Configuration sample. + +[general] +host = "127.0.1.1" +port = 4141 +domain = "knilxof.org" +data_directory = "/tmp" +tunnel_ip = "1.2.3.4" +eviction_delay = 2 + +[pdns] +dns_ttl = 89 +soa_content = "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800" +socket_path = "/tmp/powerdns_tunnel.sock" + +[email] +server = "mail.gandi.net" +user = "accounts@knilxof.org" +password = "******" +sender = "accounts@knilxof.org" +confirmation_title = "Welcome to MozIot" +confirmation_body = "To confirm your email address, follow this link: {link}" +success_page = """ + + Email Confirmation Successful! + +

      Thank you for verifying your email, {email}.

      + +""" +error_page = """ + + Email Confirmation Error! + +

      An error happened while verifiying your email.

      + +""" diff --git a/server/config.toml.test b/server/config.toml.test new file mode 100644 index 0000000..e774731 --- /dev/null +++ b/server/config.toml.test @@ -0,0 +1,36 @@ +# Configuration used for tests. + +[general] +host = "127.0.1.1" +port = 4141 +domain = "knilxof.org" +data_directory = "/tmp" +tunnel_ip = "1.2.3.4" +eviction_delay = 2 + +[pdns] +dns_ttl = 89 +soa_content = "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800" +socket_path = "/tmp/powerdns_tunnel.sock" + +[email] +server = "mail.gandi.net" +user = "accounts@knilxof.org" +password = "******" +sender = "accounts@knilxof.org" +confirmation_title = "Welcome to your MozIot Gateway" +confirmation_body = "Hello,\n\nTo confirm your email address, follow this link: {link}" +success_page = """ + + Email Confirmation Successful! + +

      Thank you for verifying your email, {email}.

      + +""" +error_page = """ + + Email Confirmation Error! + +

      An error happened while verifiying your email.

      + +""" diff --git a/server/src/args.rs b/server/src/args.rs new file mode 100644 index 0000000..0f80dbd --- /dev/null +++ b/server/src/args.rs @@ -0,0 +1,174 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use clap::{App, ArgMatches}; +use config::{Args, EmailOptions, GeneralOptions, PdnsOptions}; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use toml; + +const USAGE: &'static str = "--config-file=[path] 'Path to a toml configuration file.' +--data-directory=[dir] 'The directory where the persistent data will be saved.' +--host=[host] 'Set local hostname.' +--port=[port] 'Set port to listen on for http connections.' +--cert-directory=[dir] 'Certificate directory.' +--domain=[domain] 'The domain that will be tied to this registration server.' +--dns-ttl=[ttl] 'TTL of the DNS records, in seconds.' +--eviction-delay=[secs] 'How often we purge old records.' +--tunnel-ip=[ip] 'The ip address of the tunnel endpoint.' +--soa-content=[dns] 'The content of the SOA record for this tunnel.' +--socket-path=[path] 'The path to the socket used to communicate with PowerDNS' +--email-server=[name] 'The name of the smpt server' +--email-user=[username] 'The username to authenticate with' +--email-password=[pass] 'The password for this email account' +--email-sender=[email] 'The email identity to use as a sender' +--confirmation-title=[s] 'The title of the confirmation email' +--confirmation-body=[s] 'The body of the confirmation email' +--success-page=[s] 'HTML content of the email confirmation success page' +--error-page=[s] 'HTML content of the email confirmation error page'"; + +const DEFAULT_EVICTION_DELAY: u32 = 120; // In seconds. + +pub struct ArgsParser; + +impl ArgsParser { + fn from_file(path: &PathBuf) -> Args { + let mut file = File::open(path).expect("Can't open config file"); + let mut source = String::new(); + file.read_to_string(&mut source) + .expect("Unable to read config file"); + toml::from_str(&source).expect("Invalid config file") + } + + fn from_matches(matches: ArgMatches) -> Args { + if matches.is_present("config-file") { + return ArgsParser::from_file(&PathBuf::from(matches.value_of("config-file").unwrap())); + } + + macro_rules! optional { + ($var:ident, $name:expr) => ( + let $var = if matches.is_present($name) { + Some(matches.value_of($name).unwrap().to_owned()) + } else { + None + }; + ) + } + + optional!(cert_dir, "cert-directory"); + let cert_directory = match cert_dir { + Some(dir) => Some(PathBuf::from(dir)), + None => None, + }; + + optional!(email_server, "email-server"); + optional!(email_user, "email-user"); + optional!(email_password, "email-password"); + optional!(email_sender, "email-sender"); + optional!(confirmation_title, "confirmation-title"); + optional!(confirmation_body, "confirmation-body"); + optional!(success_page, "success-page"); + optional!(error_page, "error-page"); + + Args { + general: GeneralOptions { + host: matches.value_of("host").unwrap_or("0.0.0.0").to_owned(), + port: value_t!(matches, "port", u16).unwrap_or(4242), + cert_directory: cert_directory, + data_directory: String::from(matches.value_of("data-directory").unwrap_or(".")), + domain: matches + .value_of("domain") + .unwrap_or("knilxof.org") + .to_owned(), + tunnel_ip: matches + .value_of("tunnel-ip") + .unwrap_or("0.0.0.0") + .to_owned(), + eviction_delay: value_t!(matches, "eviction-delay", u32) + .unwrap_or(DEFAULT_EVICTION_DELAY), + }, + pdns: PdnsOptions { + soa_content: matches + .value_of("soa-content") + .unwrap_or("_soa_not_configured_") + .to_owned(), + socket_path: matches.value_of("soa-content").map(|s| s.to_owned()), + dns_ttl: value_t!(matches, "dns-ttl", u32).unwrap_or(60), + }, + email: EmailOptions { + server: email_server, + user: email_user, + password: email_password, + sender: email_sender, + confirmation_title: confirmation_title, + confirmation_body: confirmation_body, + success_page: success_page, + error_page: error_page, + }, + } + } + + // Gets the args from the default command line. + pub fn from_env() -> Args { + ArgsParser::from_matches(App::new("registration_server") + .args_from_usage(USAGE) + .get_matches()) + } + + // Gets the args from a string array. + #[cfg(test)] + pub fn from_vec(params: Vec<&str>) -> Args { + ArgsParser::from_matches(App::new("registration_server") + .args_from_usage(USAGE) + .get_matches_from(params)) + } +} + +#[test] +fn test_args() { + let args = ArgsParser::from_vec(vec!["registration_server", "--tunnel-ip=1.2.3.4"]); + + assert_eq!(args.general.port, 4242); + assert_eq!(args.general.host, "0.0.0.0"); + assert_eq!(args.general.domain, "knilxof.org"); + assert_eq!(args.general.cert_directory, None); + assert_eq!(args.general.tunnel_ip, "1.2.3.4"); + assert_eq!(args.pdns.dns_ttl, 60); + assert_eq!(args.general.eviction_delay, DEFAULT_EVICTION_DELAY); + assert_eq!(args.pdns.socket_path, None); + + let args = ArgsParser::from_vec(vec!["registration_server", + "--host=127.0.1.1", + "--port=4343", + "--domain=example.com", + "--cert-directory=/tmp/certs", + "--dns-ttl=120", + "--tunnel-ip=1.2.3.4", + "--eviction-delay=60"]); + + assert_eq!(args.general.port, 4343); + assert_eq!(args.general.host, "127.0.1.1"); + assert_eq!(args.general.domain, "example.com"); + assert_eq!(args.general.cert_directory, + Some(PathBuf::from("/tmp/certs"))); + assert_eq!(args.general.tunnel_ip, "1.2.3.4"); + assert_eq!(args.pdns.dns_ttl, 120); + assert_eq!(args.general.eviction_delay, 60); + assert_eq!(args.pdns.socket_path, None); + + let soa = "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"; + let args = ArgsParser::from_vec(vec!["registration_server", + "--config-file=./config.toml.sample"]); + assert_eq!(args.general.port, 4141); + assert_eq!(args.general.host, "127.0.1.1"); + assert_eq!(args.general.domain, "knilxof.org"); + assert_eq!(args.general.cert_directory, None); + assert_eq!(args.general.tunnel_ip, "1.2.3.4"); + assert_eq!(args.pdns.dns_ttl, 89); + assert_eq!(args.general.eviction_delay, 2); + assert_eq!(args.pdns.soa_content, soa); + assert_eq!(args.pdns.socket_path, + Some("/tmp/powerdns_tunnel.sock".to_owned())); +} diff --git a/server/src/bin/main.rs b/server/src/bin/main.rs new file mode 100644 index 0000000..b7fda7b --- /dev/null +++ b/server/src/bin/main.rs @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Server that manages foxbox registrations. + +extern crate env_logger; +extern crate hyper_openssl; +extern crate iron; +#[macro_use] +extern crate log; +extern crate mount; +extern crate registration_server; + +use hyper_openssl::OpensslServer; +use iron::Iron; + +use registration_server::args::ArgsParser; +use registration_server::config::Config; +use registration_server::eviction; +use registration_server::routes; +use registration_server::pdns; + +fn main() { + env_logger::init().unwrap(); + + let args = ArgsParser::from_env(); + + info!("Managing the domain {}", args.general.domain); + + let config = Config::from_args(args.clone()); + + eviction::evict_old_entries(&config); + + let iron_server = Iron::new(routes::create_chain("/", &config)); + info!("Starting server on {}:{}", + args.general.host, + args.general.port); + let addr = format!("{}:{}", args.general.host, args.general.port); + + pdns::start_socket_endpoint(&config); + + if args.general.cert_directory.is_none() { + iron_server.http(addr.as_ref() as &str).unwrap(); + } else { + info!("Starting TLS server"); + let certificate_directory = args.general.cert_directory.unwrap(); + + let mut private_key = certificate_directory.clone(); + private_key.push("privkey.pem"); + + let mut cert = certificate_directory.clone(); + cert.push("fullchain.pem"); + + info!("Using cert: '{:?}' pk: '{:?}'", cert, private_key); + let ssl = OpensslServer::from_files(private_key, cert).unwrap(); + iron_server.https(addr.as_ref() as &str, ssl).unwrap(); + } +} diff --git a/server/src/config.rs b/server/src/config.rs new file mode 100644 index 0000000..da71c28 --- /dev/null +++ b/server/src/config.rs @@ -0,0 +1,64 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use database::Database; +use std::path::PathBuf; + +#[derive(Clone, Deserialize)] +pub struct GeneralOptions { + pub host: String, + pub port: u16, + pub data_directory: String, + pub cert_directory: Option, + pub domain: String, + pub tunnel_ip: String, + pub eviction_delay: u32, +} + +#[derive(Clone, Deserialize)] +pub struct PdnsOptions { + pub soa_content: String, + pub socket_path: Option, + pub dns_ttl: u32, +} + +#[derive(Clone, Deserialize)] +pub struct EmailOptions { + pub server: Option, + pub user: Option, + pub password: Option, + pub sender: Option, + pub confirmation_title: Option, + pub confirmation_body: Option, + pub success_page: Option, + pub error_page: Option, +} + +#[derive(Clone, Deserialize)] +pub struct Args { + pub general: GeneralOptions, + pub pdns: PdnsOptions, + pub email: EmailOptions, +} + +#[derive(Clone)] +pub struct Config { + pub db: Database, + pub options: Args, +} + +impl Config { + pub fn from_args(args: Args) -> Self { + Config { + db: Database::new(&format!("{}/domains.sqlite", args.general.data_directory)), + options: args, + } + } + + #[cfg(test)] + pub fn with_db(&mut self, db: Database) -> &mut Self { + self.db = db; + self + } +} diff --git a/server/src/database.rs b/server/src/database.rs new file mode 100644 index 0000000..3a47448 --- /dev/null +++ b/server/src/database.rs @@ -0,0 +1,640 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Manages the SQL database that holds the list of registered domain names. +// Each records is made of the name, the private token and the Let's Encrypt +// challenge value. + +use types::ServerInfo; +use r2d2_sqlite::SqliteConnectionManager; +use r2d2; +use rusqlite::Row; +use rusqlite::Result as SqlResult; +use rusqlite::types::{ToSql, ToSqlOutput}; +use std::sync::mpsc::{Receiver, channel}; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::thread; + +macro_rules! sqlstr { + ($row:ident, $index:expr) => ( + { + let raw = $row.get::($index); + if raw.is_empty() { + None + } else { + Some(raw) + } + } + ) +} + +pub struct DomainRecord; + +impl DomainRecord { + fn from_sql(row: Row) -> ServerInfo { + ServerInfo { + token: row.get(0), + local_name: row.get(1), + remote_name: row.get(2), + dns_challenge: sqlstr!(row, 3), + local_ip: sqlstr!(row, 4), + public_ip: sqlstr!(row, 5), + description: row.get(6), + email: sqlstr!(row, 7), + timestamp: row.get(8), + } + } + + pub fn new(token: &str, + local_name: &str, + remote_name: &str, + dns_challenge: Option<&str>, + local_ip: Option<&str>, + public_ip: Option<&str>, + description: &str, + email: Option<&str>, + timestamp: i64) + -> ServerInfo { + macro_rules! str2sql { + ($val:expr) => ( + if $val.is_some() { + Some($val.unwrap().to_owned()) + } else { + None + } + ) + } + + ServerInfo { + local_name: local_name.to_owned(), + remote_name: remote_name.to_owned(), + token: token.to_owned(), + dns_challenge: str2sql!(dns_challenge), + local_ip: str2sql!(local_ip), + public_ip: str2sql!(public_ip), + description: description.to_owned(), + email: str2sql!(email), + timestamp: timestamp, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum DatabaseError { + DbUnavailable, + SQLError(String), + NoRecord, +} + +#[derive(Clone)] +pub struct Database { + pool: r2d2::Pool, +} + +// try! like macro that sends back the error of a SQLite operation +// over the mpsc channel if needed. +macro_rules! sqltry { + ($sql:expr, $tx:ident, $err:expr) => ( + match $sql { + Err(_) => { + $tx.send(Err($err)).unwrap(); + return; + } + Ok(value) => value, + } + ); + + ($sql:expr, $tx:ident) => ( + match $sql { + Err(err) => { + $tx.send(Err(DatabaseError::SQLError(format!("{}", err)))).unwrap(); + return; + } + Ok(value) => value, + } + ); +} + +#[derive(Clone)] +pub enum SqlParam { + Text(String), + Integer(i64), +} + +impl ToSql for SqlParam { + fn to_sql(&self) -> SqlResult { + match *self { + SqlParam::Text(ref text) => text.to_sql(), + SqlParam::Integer(ref number) => number.to_sql(), + } + } +} + +impl Database { + pub fn new(path: &str) -> Self { + debug!("Opening database at {}", path); + let config = r2d2::Config::default(); + let manager = SqliteConnectionManager::new(path); + let pool = r2d2::Pool::new(config, manager) + .expect(&format!("Unable to open database at {}", path)); + + let conn = pool.get().unwrap(); + + macro_rules! index { + ($table:expr, $index:expr) => ( + conn.execute(&format!("CREATE UNIQUE INDEX IF NOT EXISTS {}_{} ON {}({})", + $table, $index, $table, $index), &[]).unwrap_or_else(|err| { + panic!("Unable to create the {}_{} index: {}", $table, $index, err); + }); + ) + } + + // Create the domains table if needed. + conn.execute("CREATE TABLE IF NOT EXISTS domains ( + token TEXT NOT NULL PRIMARY KEY, + local_name TEXT NOT NULL, + remote_name TEXT NOT NULL, + dns_challenge TEXT NOT NULL, + local_ip TEXT NOT NULL, + public_ip TEXT NOT NULL, + description TEXT NOT NULL, + email TEXT NOT NULL, + timestamp INTEGER)", + &[]) + .unwrap_or_else(|err| { + panic!("Unable to create the domains table: {}", err); + }); + + index!("domains", "local_name"); + index!("domains", "remote_name"); + index!("domains", "timestamp"); + index!("domains", "public_ip"); + index!("domains", "email"); + + // Create the email management table if needed. + conn.execute("CREATE TABLE IF NOT EXISTS emails ( + email TEXT NOT NULL PRIMARY KEY, + token TEXT NOT NULL, + link TEXT NOT NULL)", + &[]) + .unwrap_or_else(|err| { + panic!("Unable to create the email table: {}", err); + }); + index!("emails", "link"); + + // Create the discovery table if needed. + conn.execute("CREATE TABLE IF NOT EXISTS discovery ( + disco TEXT NOT NULL PRIMARY KEY, + token TEXT NOT NULL)", + &[]) + .unwrap_or_else(|err| { + panic!("Unable to create the email table: {}", err); + }); + index!("discovery", "token"); + + Database { pool: pool } + } + + pub fn add_discovery(&self, token: &str, disco: &str) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let token = token.to_owned(); + let disco = disco.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + sqltry!(conn.execute("INSERT INTO discovery VALUES ($1, $2)", + &[&disco, &token]), + tx); + tx.send(Ok(())).unwrap(); + }); + + rx + } + + pub fn get_token_for_discovery(&self, disco: &str) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let disco = disco.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let mut stmt = sqltry!(conn.prepare("SELECT token from discovery WHERE disco=$1"), + tx); + let mut rows = sqltry!(stmt.query(&[&disco]), tx); + if let Some(result_row) = rows.next() { + let row = sqltry!(result_row, tx); + tx.send(Ok(row.get(0))).unwrap(); + } else { + tx.send(Err(DatabaseError::NoRecord)).unwrap(); + } + + }); + + rx + } + + pub fn delete_discovery(&self, disco: &str) -> Receiver> { + self.execute_1param_sql("DELETE FROM discovery WHERE disco=$1", + SqlParam::Text(disco.to_owned())) + } + + // Add a new email. + // TODO: ensure that the token matches a domain token? + pub fn add_email(&self, + email: &str, + token: &str, + link: &str) + -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let email = email.to_owned(); + let token = token.to_owned(); + let link = link.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + sqltry!(conn.execute("INSERT INTO emails VALUES ($1, $2, $3)", + &[&email, &token, &link]), + tx); + tx.send(Ok(())).unwrap(); + }); + + rx + } + + pub fn delete_email(&self, email: &str) -> Receiver> { + self.execute_1param_sql("DELETE FROM emails WHERE email=$1", + SqlParam::Text(email.to_owned())) + } + + pub fn get_email_by_link(&self, + link: &str) + -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let link = link.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let mut stmt = sqltry!(conn.prepare("SELECT email, token from emails WHERE link=$1"), + tx); + let mut rows = sqltry!(stmt.query(&[&link]), tx); + if let Some(result_row) = rows.next() { + let row = sqltry!(result_row, tx); + tx.send(Ok((row.get(0), row.get(1)))).unwrap(); + } else { + tx.send(Err(DatabaseError::NoRecord)).unwrap(); + } + + }); + + rx + } + + #[cfg(test)] + pub fn get_email_by_token(&self, + token: &str) + -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let token = token.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let mut stmt = sqltry!(conn.prepare("SELECT email, link from emails WHERE token=$1"), + tx); + let mut rows = sqltry!(stmt.query(&[&token]), tx); + if let Some(result_row) = rows.next() { + let row = sqltry!(result_row, tx); + tx.send(Ok((row.get(0), row.get(1)))).unwrap(); + } else { + tx.send(Err(DatabaseError::NoRecord)).unwrap(); + } + + }); + + rx + } + + fn select_record(&self, + request: &str, + value: &str) + -> Receiver> { + let (tx, rx) = channel(); + + // Run the sql command on a pooled thread. + let pool = self.pool.clone(); + let value = value.to_owned(); + let request = request.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let mut stmt = sqltry!(conn.prepare(&request), tx); + let mut rows = sqltry!(stmt.query(&[&value]), tx); + if let Some(result_row) = rows.next() { + let row = sqltry!(result_row, tx); + tx.send(Ok(DomainRecord::from_sql(row))).unwrap(); + } else { + tx.send(Err(DatabaseError::NoRecord)).unwrap(); + } + }); + + rx + } + + fn select_records(&self, + request: &str, + value: &str) + -> Receiver, DatabaseError>> { + let (tx, rx) = channel(); + + // Run the sql command on a pooled thread. + let pool = self.pool.clone(); + let value = value.to_owned(); + let request = request.to_owned(); + thread::spawn(move || { + let mut result = Vec::new(); + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let mut stmt = sqltry!(conn.prepare(&request), tx); + let mut rows = sqltry!(stmt.query(&[&value]), tx); + while let Some(result_row) = rows.next() { + let row = sqltry!(result_row, tx); + result.push(DomainRecord::from_sql(row)); + } + tx.send(Ok(result)).unwrap(); + }); + + rx + } + + pub fn get_records_by_public_ip(&self, + public_ip: &str) + -> Receiver, DatabaseError>> { + self.select_records("SELECT token, local_name, remote_name, dns_challenge, \ + local_ip, public_ip, description, email, timestamp \ + FROM domains WHERE public_ip=$1", + public_ip) + } + + pub fn get_record_by_name(&self, name: &str) -> Receiver> { + self.select_record("SELECT token, local_name, remote_name, dns_challenge, \ + local_ip, public_ip, description, email, timestamp \ + FROM domains WHERE local_name=$1 or remote_name=$1", + name) + } + + pub fn get_record_by_token(&self, + token: &str) + -> Receiver> { + self.select_record("SELECT token, local_name, remote_name, dns_challenge, \ + local_ip, public_ip, description, email, timestamp \ + FROM domains WHERE token=$1", + token) + } + + pub fn add_record(&self, record: ServerInfo) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let record = record.clone(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + sqltry!(conn.execute("INSERT INTO domains VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + &[&record.token, + &record.local_name, + &record.remote_name, + &record.dns_challenge.unwrap_or("".to_owned()), + &record.local_ip.unwrap_or("".to_owned()), + &record.public_ip.unwrap_or("".to_owned()), + &record.description, + &record.email.unwrap_or("".to_owned()), + &record.timestamp]), + tx); + tx.send(Ok(())).unwrap(); + }); + + rx + } + + pub fn update_record(&self, record: ServerInfo) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let record = record.clone(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + + sqltry!(conn.execute("UPDATE domains SET dns_challenge=$1, local_ip=$2, \ + public_ip=$3, timestamp=$4, email=$5, description=$6 \ + WHERE (local_name=$7 OR remote_name=$8) AND token=$9", + &[&record.dns_challenge.unwrap_or("".to_owned()), + &record.local_ip.unwrap_or("".to_owned()), + &record.public_ip.unwrap_or("".to_owned()), + &record.timestamp, + &record.email.unwrap_or("".to_owned()), + &record.description, + &record.local_name, + &record.remote_name, + &record.token]), + tx); + tx.send(Ok(())).unwrap(); + }); + + rx + } + + // Evict records older than a given timestamp. + // Returns the number of evicted records. + // Eviction means that we loose the local <-> public ip binding, + // *not* that we remove the record from the database. + pub fn evict_records(&self, timestamp: SqlParam) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let res = sqltry!(conn.execute("UPDATE domains SET local_ip=$1, \ + public_ip=$2, timestamp=$3 where timestamp<$4", + &[&"", &"", &now, ×tamp]), + tx); + tx.send(Ok(res)).unwrap(); + }); + + rx + } + + // Returns the number of rows affected. + pub fn execute_1param_sql(&self, + request: &str, + value: SqlParam) + -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + let value = value.to_owned(); + let request = request.to_owned(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + let res = sqltry!(conn.execute(&request, &[&value]), tx); + tx.send(Ok(res)).unwrap(); + }); + + rx + } + + pub fn delete_record_by_token(&self, token: &str) -> Receiver> { + self.execute_1param_sql("DELETE FROM domains WHERE token=$1", + SqlParam::Text(token.to_owned())) + } + + #[cfg(test)] + pub fn flush(&self) -> Receiver> { + let (tx, rx) = channel(); + + let pool = self.pool.clone(); + thread::spawn(move || { + let conn = sqltry!(pool.get(), tx, DatabaseError::DbUnavailable); + sqltry!(conn.execute("DELETE FROM domains", &[]), tx); + sqltry!(conn.execute("DELETE FROM emails", &[]), tx); + sqltry!(conn.execute("DELETE FROM discovery", &[]), tx); + tx.send(Ok(())).unwrap(); + }); + rx + } +} + +#[test] +fn test_domain_store() { + let db = Database::new("domain_db_test_domains.sqlite"); + + // Start with an empty db. + db.flush().recv().unwrap().expect("Flushing the db"); + + // Check that we don't find any record. + assert_eq!(db.get_record_by_name("test.example.org").recv().unwrap(), + Err(DatabaseError::NoRecord)); + + assert_eq!(db.get_record_by_token("test-token").recv().unwrap(), + Err(DatabaseError::NoRecord)); + + // Add a record without a dns challenge. + let no_challenge_record = DomainRecord::new("test-token", + "local.test.example.org", + "test.example.org", + None, + None, + None, + "Test Server", + None, + 0); + assert_eq!(db.add_record(no_challenge_record.clone()).recv().unwrap(), + Ok(())); + + // Check that we can find it and that it matches our record. + assert_eq!(db.get_record_by_name("test.example.org").recv().unwrap(), + Ok(no_challenge_record.clone())); + + assert_eq!(db.get_record_by_token("test-token").recv().unwrap(), + Ok(no_challenge_record.clone())); + + // Update the record to have challenge. + let challenge_record = DomainRecord::new("test-token", + "local.test.example.org", + "test.example.org", + Some("dns-challenge"), + None, + None, + "Test Server", + None, + 0); + assert_eq!(db.update_record(challenge_record.clone()).recv().unwrap(), + Ok(())); + + // Check that we can find it and that it matches our record. + assert_eq!(db.get_record_by_name("test.example.org").recv().unwrap(), + Ok(challenge_record.clone())); + + assert_eq!(db.get_record_by_token("test-token").recv().unwrap(), + Ok(challenge_record.clone())); + + // Remove by token. + assert_eq!(db.delete_record_by_token(&challenge_record.token) + .recv() + .unwrap(), + Ok(1)); + + assert_eq!(db.get_record_by_name(&challenge_record.local_name) + .recv() + .unwrap(), + Err(DatabaseError::NoRecord)); + + // Add again a token and evict it. + let max_age = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let no_challenge_record = DomainRecord::new("test-token", + "local.test.example.org", + "test.example.org", + None, + None, + None, + "Test Server", + None, + max_age - 1); + assert_eq!(db.add_record(no_challenge_record.clone()).recv().unwrap(), + Ok(())); + assert_eq!(db.evict_records(SqlParam::Integer(max_age as i64)) + .recv() + .unwrap(), + Ok(1)); +} + +#[test] +fn test_email() { + let db = Database::new("domain_db_test_email.sqlite"); + + // Start with an empty db. + db.flush().recv().unwrap().expect("Flushing the db"); + + let email = "test@example.com".to_owned(); + let link = "secret-link".to_owned(); + let token = "domain-token".to_owned(); + + assert_eq!(db.get_email_by_link(&link).recv().unwrap(), + Err(DatabaseError::NoRecord)); + assert_eq!(db.add_email(&email, &token, &link).recv().unwrap(), Ok(())); + assert_eq!(db.get_email_by_link(&link).recv().unwrap(), + Ok((email.clone(), token.clone()))); + assert_eq!(db.get_email_by_token(&token).recv().unwrap(), + Ok((email.clone(), link.clone()))); + assert_eq!(db.delete_email(&email).recv().unwrap(), Ok(1)); + assert_eq!(db.get_email_by_link(&link).recv().unwrap(), + Err(DatabaseError::NoRecord)); +} + + +#[test] +fn test_discovery() { + let db = Database::new("domain_db_test_discovery.sqlite"); + + // Start with an empty db. + db.flush().recv().unwrap().expect("Flushing the db"); + + assert_eq!(db.get_token_for_discovery("disco-token").recv().unwrap(), + Err(DatabaseError::NoRecord)); + assert_eq!(db.add_discovery("secret-token", "disco-token") + .recv() + .unwrap(), + Ok(())); + assert_eq!(db.get_token_for_discovery("disco-token").recv().unwrap(), + Ok("secret-token".to_owned())); + assert_eq!(db.delete_discovery("disco-token").recv().unwrap(), Ok((1))); + assert_eq!(db.get_token_for_discovery("disco-token").recv().unwrap(), + Err(DatabaseError::NoRecord)); +} diff --git a/server/src/discovery.rs b/server/src/discovery.rs new file mode 100644 index 0000000..839b42a --- /dev/null +++ b/server/src/discovery.rs @@ -0,0 +1,163 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Discovery related routes. + +use types::Discovered; +use config::Config; +use database::DatabaseError; +use errors::*; +use iron::headers::ContentType; +use iron::prelude::*; +use iron::status::{self, Status}; +use params::{FromValue, Params}; +use serde_json; + +macro_rules! remove_last { + ($obj:ident.$prop:ident) => ( + $obj.$prop[..$obj.$prop.len() - 1].to_owned() + ) +} + +// Public ping endpoint, returning names of servers on the same +// local network than the client. +pub fn ping(req: &mut Request, config: &Config) -> IronResult { + info!("GET /ping"); + + let remote_ip = format!("{}", req.remote_addr.ip()); + + match config + .db + .get_records_by_public_ip(&remote_ip) + .recv() + .unwrap() { + Ok(records) => { + let results: Vec = records + .into_iter() + .map(|item| { + Discovered { + href: format!("https://{}", remove_last!(item.local_name)), + desc: item.description, + } + }) + .collect(); + + json_response!(&results) + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +pub fn adddiscovery(req: &mut Request, config: &Config) -> IronResult { + info!("GET /adddiscovery"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + let disco = map.find(&["disco"]); + + if token.is_none() || disco.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let token = String::from_value(token.unwrap()).unwrap(); + let disco = String::from_value(disco.unwrap()).unwrap(); + + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(_) => { + match config.db.add_discovery(&token, &disco).recv().unwrap() { + Ok(()) => ok_response!(), + Err(_) => EndpointError::with(status::BadRequest, 400), + } + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +pub fn revokediscovery(req: &mut Request, config: &Config) -> IronResult { + info!("GET /revokediscovery"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + let disco = map.find(&["disco"]); + + if token.is_none() || disco.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let token = String::from_value(token.unwrap()).unwrap(); + let disco = String::from_value(disco.unwrap()).unwrap(); + + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(_) => { + match config.db.delete_discovery(&disco).recv().unwrap() { + Ok(_) => ok_response!(), + Err(_) => EndpointError::with(status::BadRequest, 400), + } + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +pub fn discovery(req: &mut Request, config: &Config) -> IronResult { + info!("GET /discovery"); + + let remote_ip = format!("{}", req.remote_addr.ip()); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let disco = map.find(&["disco"]); + + if disco.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let disco = String::from_value(disco.unwrap()).unwrap(); + + match config.db.get_token_for_discovery(&disco).recv().unwrap() { + Ok(token) => { + match config + .db + .get_records_by_public_ip(&remote_ip) + .recv() + .unwrap() { + Ok(records) => { + // Filter out and only return the records that matches the token. + let results: Vec = records + .into_iter() + .filter(|item| item.token == token) + .map(|item| { + Discovered { + href: format!("https://{}", remove_last!(item.local_name)), + desc: item.description, + } + }) + .collect(); + + if results.is_empty() { + // If the result vector is empty, return the remote name for this token. + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(record) => { + let result = vec![Discovered { + href: + format!("https://{}", + remove_last!(record.remote_name)), + desc: record.description, + }]; + json_response!(&result) + } + Err(_) => EndpointError::with(status::BadRequest, 400), + } + } else { + json_response!(&results) + } + } + Err(_) => EndpointError::with(status::BadRequest, 400), + } + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} diff --git a/server/src/email_routes.rs b/server/src/email_routes.rs new file mode 100644 index 0000000..2ec935e --- /dev/null +++ b/server/src/email_routes.rs @@ -0,0 +1,240 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Email related routes. + +use config::Config; +use database::DatabaseError; +use email::Mailbox; +use errors::*; +use iron::headers::ContentType; +use lettre::email::EmailBuilder; +use lettre::transport::smtp::{SUBMISSION_PORT, SecurityLevel, SmtpTransport, SmtpTransportBuilder}; +use lettre::transport::smtp::authentication::Mechanism; +use lettre::transport::EmailTransport; +#[cfg(test)] +use lettre::transport::stub::StubEmailTransport; +use iron::prelude::*; +use iron::status::{self, Status}; +use params::{FromValue, Params}; +use std::str::FromStr; +use uuid::Uuid; + +pub struct EmailSender { + connection: SmtpTransport, + from: String, +} + +impl EmailSender { + pub fn new(config: &Config) -> Result { + + let options = &config.options; + + if options.email.server.is_none() || options.email.user.is_none() || + options.email.password.is_none() || options.email.sender.is_none() { + error!("All email fields need to be set."); + return Err(()); + } + + let builder = + match SmtpTransportBuilder::new((options.clone().email.server.unwrap().as_str(), + SUBMISSION_PORT)) { + Ok(builder) => builder, + Err(error) => { + error!("{:?}", error); + return Err(()); + } + }; + + let user = options.clone().email.user.unwrap().clone(); + let password = options.clone().email.password.unwrap().clone(); + let connection = builder + .hello_name("localhost") + .credentials(&user, &password) + .security_level(SecurityLevel::AlwaysEncrypt) + .smtp_utf8(true) + .authentication_mechanism(Mechanism::Plain) + .connection_reuse(true) + .build(); + + Ok(EmailSender { + connection: connection, + from: options.clone().email.sender.unwrap().clone(), + }) + } + + pub fn send(&mut self, to: &str, body: &str, subject: &str) -> Result<(), ()> { + let email = match EmailBuilder::new() + .to(to) + .from(&*self.from) + .body(body) + .subject(subject) + .build() { + Ok(email) => email, + Err(error) => { + error!("{:?}", error); + return Err(()); + } + }; + + #[cfg(not(test))] + match self.connection.send(email.clone()) { + Ok(_) => Ok(()), + Err(error) => { + error!("{:?}", error); + Err(()) + } + } + + #[cfg(test)] + { + let mut transport = StubEmailTransport; + match transport.send(email.clone()) { + Ok(_) => Ok(()), + Err(error) => { + error!("{:?}", error); + Err(()) + } + } + } + } +} + +pub fn setemail(req: &mut Request, config: &Config) -> IronResult { + info!("GET /setemail"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + let email = map.find(&["email"]); + + if token.is_none() || email.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let token = String::from_value(token.unwrap()).unwrap(); + let email = String::from_value(email.unwrap()).unwrap(); + // Check that this is a valid email address. + if Mailbox::from_str(&email).is_err() { + return EndpointError::with(status::BadRequest, 400); + } + + let link = format!("{}", Uuid::new_v4()); + + // Check that this is a valid token. + if config + .db + .get_record_by_token(&token) + .recv() + .unwrap() + .is_err() { + return EndpointError::with(status::BadRequest, 400); + } + + match config.db.add_email(&email, &token, &link).recv().unwrap() { + Ok(_) => { + match EmailSender::new(config) { + Ok(mut sender) => { + let scheme = match config.options.general.cert_directory { + Some(_) => "https", + None => "http", + }; + let full_link = format!("{}://{}:{}/confirmemail?s={}", + scheme, + config.options.general.domain, + config.options.general.port, + link); + let body = config + .options + .email + .clone() + .confirmation_body + .unwrap() + .replace("{link}", &full_link); + match sender.send(&email, + &body, + &config.options.email.clone().confirmation_title.unwrap()) { + Ok(_) => ok_response!(), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +// Process the email confirmation links, that have the link as the "s" parameter. +pub fn verifyemail(req: &mut Request, config: &Config) -> IronResult { + info!("GET /verifyemail"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let link = map.find(&["s"]); + + if link.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + let link = String::from_value(link.unwrap()).unwrap(); + + match config.db.get_email_by_link(&link).recv().unwrap() { + Ok((email, token)) => { + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(mut record) => { + // Update the record to set the email address. + record.email = Some(email); + match config.db.update_record(record).recv().unwrap() { + Ok(_) => html_response!(config.options.email.clone().success_page.unwrap()), + Err(DatabaseError::NoRecord) => { + html_response!(config.options.email.clone().error_page.unwrap()) + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(DatabaseError::NoRecord) => { + html_response!(config.options.email.clone().error_page.unwrap()) + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(DatabaseError::NoRecord) => { + html_response!(config.options.email.clone().error_page.unwrap()) + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +pub fn revokeemail(req: &mut Request, config: &Config) -> IronResult { + info!("GET /revokeemail"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + let email = map.find(&["email"]); + + if token.is_none() || email.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let token = String::from_value(token.unwrap()).unwrap(); + let email = String::from_value(email.unwrap()).unwrap(); + // Check that this is a valid email address. + if Mailbox::from_str(&email).is_err() { + return EndpointError::with(status::BadRequest, 400); + } + + // Check that this is a valid token. + if config + .db + .get_record_by_token(&token) + .recv() + .unwrap() + .is_err() { + return EndpointError::with(status::BadRequest, 400); + } + + if config.db.delete_email(&email).recv().unwrap().is_err() { + return EndpointError::with(status::BadRequest, 400); + } + + ok_response!() +} diff --git a/server/src/errors.rs b/server/src/errors.rs new file mode 100644 index 0000000..51d099a --- /dev/null +++ b/server/src/errors.rs @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use iron::status; +use iron::prelude::*; +use serde_json; +use std::error::Error; +use std::fmt::{self, Debug}; + +#[derive(Debug)] +struct StringError(pub String); + +impl fmt::Display for StringError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Debug::fmt(self, f) + } +} + +impl Error for StringError { + fn description(&self) -> &str { + &*self.0 + } +} + +#[derive(Debug, Serialize)] +pub struct ErrorBody { + pub code: u16, + pub errno: u16, + pub error: String, +} + +pub struct EndpointError; + +impl EndpointError { + pub fn with(status: status::Status, errno: u16) -> IronResult { + let error = status.canonical_reason().unwrap().to_owned(); + let body = ErrorBody { + code: status.to_u16(), + errno: errno, + error: error.clone(), + }; + + Err(IronError::new(StringError(error), + (status, serde_json::to_string(&body).unwrap()))) + } +} + +#[test] +fn test_error() { + let s_error = StringError(status::BadRequest.canonical_reason().unwrap().to_owned()); + let error = format!("{} {}", s_error, s_error.description()); + assert_eq!(error, r#"StringError("Bad Request") Bad Request"#); +} diff --git a/server/src/eviction.rs b/server/src/eviction.rs new file mode 100644 index 0000000..fe241f6 --- /dev/null +++ b/server/src/eviction.rs @@ -0,0 +1,95 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use config::Config; +use database::SqlParam; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +// We evict the local_ip info from records older than EVICTION_DELAY. +// Clients should renew their registration at a shorter interval. + +pub fn evict_old_entries(config: &Config) { + let delay = config.options.general.eviction_delay; + let db = config.db.clone(); + thread::Builder::new() + .name("eviction".into()) + .spawn(move || { + info!("Starting eviction thread, delay is {}s", delay); + loop { + thread::sleep(Duration::new(delay as u64, 0)); + let max_age = (SystemTime::now().duration_since(UNIX_EPOCH).unwrap() - + Duration::new(delay as u64, 0)) + .as_secs(); + info!("Checking for records older than {}", max_age); + // Eviction a record means resetting the IP fields to be empty. + match db.evict_records(SqlParam::Integer(max_age as i64)) + .recv() + .unwrap() { + Err(err) => error!("Error evicting old records: {:?}", err), + Ok(count) => info!("Evicted {} records.", count), + } + } + }) + .expect("Failed to start eviction thread!"); +} + +#[cfg(test)] +mod tests { + use super::*; + use args::ArgsParser; + use config::Config; + use database::{Database, DomainRecord}; + use std::time::Duration; + + #[test] + fn eviction_thread() { + let args = ArgsParser::from_vec(vec!["registration_server", + "--config-file=./config.toml.test"]); + + let db = Database::new("domain_db_test_eviction.sqlite"); + db.flush().recv().unwrap().expect("Flushing the db"); + + let mut arg_config = Config::from_args(args); + let config = arg_config.with_db(db.clone()); + + // Add a entry to the database, with a current timestamp. + let max_age = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let eviction_record = DomainRecord::new("test-token-eviction", + "local.eviction.example.org", + "eviction.example.org", + None, + Some("local_ip"), + Some("public_ip"), + "Test Server", + Some("eviction@example.com"), + max_age); + assert_eq!(db.add_record(eviction_record.clone()).recv().unwrap(), + Ok(())); + + // Check that the record has not been evicted yet. + assert_eq!(db.get_record_by_token("test-token-eviction") + .recv() + .unwrap(), + Ok(eviction_record)); + + + // Start the eviction thread and wait forthe eviction to happen. + evict_old_entries(&config); + + thread::sleep(Duration::new(6, 0)); + + let evicted = db.get_record_by_token("test-token-eviction") + .recv() + .unwrap() + .unwrap(); + assert_eq!(evicted.email, Some("eviction@example.com".to_owned())); + assert_eq!(evicted.local_ip, None); + assert_eq!(evicted.public_ip, None); + assert!(evicted.timestamp > max_age); + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 0000000..beb55b1 --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// Simple server that manages foxbox registrations. +/// Two end points are available: +/// POST /register => to register a match between public IP and mesage. +/// GET /ping => to get the list of public IP matches. +/// +/// Boxes are supposed to register themselves at regular intervals so we +/// discard data which is too old periodically. +#[macro_use] +extern crate clap; +extern crate crypto; +extern crate email; +#[macro_use] +extern crate iron; +extern crate iron_cors; +#[cfg(test)] +extern crate iron_test; +extern crate lettre; +#[macro_use] +extern crate log; +extern crate mount; +extern crate params; +extern crate registration_types as types; +extern crate r2d2; +extern crate r2d2_sqlite; +extern crate router; +extern crate rusqlite; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +extern crate toml; +extern crate uuid; + +macro_rules! json_response { + ($json:expr) => ( + { + let mut response = Response::with(serde_json::to_string($json).unwrap()); + response.headers.set(ContentType::json()); + response.status = Some(Status::Ok); + Ok(response) + } + ) +} + +macro_rules! html_response { + ($html:expr) => ( + { + let mut response = Response::with($html); + response.headers.set(ContentType::html()); + response.status = Some(Status::Ok); + Ok(response) + } + ) +} + +macro_rules! ok_response { + () => ( + { + let mut response = Response::new(); + response.status = Some(Status::Ok); + Ok(response) + } + ) +} + +pub mod args; +pub mod config; +pub mod database; +pub mod discovery; +pub mod email_routes; +pub mod errors; +pub mod eviction; +pub mod pdns; +pub mod routes; diff --git a/server/src/pdns.rs b/server/src/pdns.rs new file mode 100644 index 0000000..6dac64b --- /dev/null +++ b/server/src/pdns.rs @@ -0,0 +1,554 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Communication with the PowerDNS server happens through the http +// server. +// See https://doc.powerdns.com/md/authoritative/backend-remote/ for +// details about the various requests and responses. + +use config::Config; +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use errors::*; +use iron::headers::ContentType; +use iron::prelude::*; +use iron::status::{self, Status}; +use serde_json; +use std::fs; +use std::io::{Read, Write}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; +use std::thread; + +#[derive(Debug, Deserialize, Serialize)] +struct PdnsRequestParameters { + // intialize method + path: Option, + timeout: Option, + + // lookup method + qtype: Option, + qname: Option, + #[serde(rename="zone-id")] + zone_id: Option, + remote: Option, + local: Option, + real_remote: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +struct PdnsRequest { + method: String, + parameters: PdnsRequestParameters, +} + +#[derive(Serialize)] +struct PdnsLookupResponse { + qtype: String, + qname: String, + content: String, + ttl: u32, + #[serde(skip_serializing_if = "Option::is_none")] + domain_id: Option, + #[serde(rename="scopeMask")] + #[serde(skip_serializing_if = "Option::is_none")] + scope_mask: Option, + #[serde(skip_serializing_if = "Option::is_none")] + auth: Option, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum PdnsResponseParams { + Lookup(PdnsLookupResponse), +} + +#[derive(Serialize)] +struct PdnsResponse { + result: Vec, +} + +fn pdns_failure_as_iron(reason: &str) -> IronResult { + debug!("pdns_failure: {}", reason); + let mut response = Response::with("{\"result\":false}"); + response.status = Some(Status::Ok); + response.headers.set(ContentType::json()); + Ok(response) +} + +fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult { + match serde_json::to_string(response) { + Ok(serialized) => { + debug!("Response is: {}", serialized); + let mut response = Response::with(serialized); + response.status = Some(Status::Ok); + response.headers.set(ContentType::json()); + + Ok(response) + } + Err(err) => { + error!("{}", err); + EndpointError::with(status::InternalServerError, 501) + } + } +} + +// Returns a SOA record for a given qname. +fn soa_response(qname: &str, config: &Config) -> PdnsLookupResponse { + PdnsLookupResponse { + qtype: "SOA".to_owned(), + qname: qname.to_owned(), + content: config.options.pdns.soa_content.to_owned(), + ttl: config.options.pdns.dns_ttl, + domain_id: None, + scope_mask: None, + auth: None, + } +} + +fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> Result { + // Pagekite sends dns requests to qnames like: + // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c. + // 625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org + // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication + debug!("PageKite query for {} {}", qtype, qname); + + let mut pdns_response = PdnsResponse { result: Vec::new() }; + + if qtype == "SOA" { + pdns_response + .result + .push(PdnsResponseParams::Lookup(soa_response(qname, config))); + return Ok(pdns_response); + } + + if qtype != "A" && qtype != "ANY" { + return Err(format!("Unsupported PageKite request type: {}", qtype)); + } + + // Split up the qname. + let parts: Vec<&str> = qname.split('.').collect(); + let subdomain = format!("{}.box.{}.", parts[4], config.options.general.domain); + let ip = match config.db.get_record_by_name(&subdomain).recv().unwrap() { + Ok(record) => { + let srand = parts[0]; + let token = parts[1]; + let sign = parts[2]; + let proto = parts[3]; + let kite_domain = format!("{}.box.{}", parts[4], config.options.general.domain); + let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token); + let salt = sign[..8].to_owned(); + + debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain); + + let mut hasher = Sha1::new(); + hasher.input_str(&format!("{}{}{}", record.token, payload, salt)); + let calc = hasher.result_str(); + + let calc_sub = calc[..28].to_owned(); + let sign_sub = sign[8..36].to_owned(); + + debug!("Signatures: {} {}", calc_sub, sign_sub); + + if calc_sub == sign_sub { + "255.255.254.255" + } else { + "255.255.255.1" + } + } + Err(_) => { + // Return 255.255.255.0 to PageKite to indicate failure. + "255.255.255.0" + } + }; + + let ns_record = PdnsLookupResponse { + qtype: "A".to_owned(), + qname: qname.to_owned(), + content: ip.to_owned(), + ttl: config.options.pdns.dns_ttl, + domain_id: None, + scope_mask: None, + auth: None, + }; + pdns_response + .result + .push(PdnsResponseParams::Lookup(ns_record)); + + Ok(pdns_response) +} + +fn process_request(req: PdnsRequest, config: &Config) -> Result { + debug!("pdns request is {:?}", req); + + if req.method == "lookup" { + let original_qname = req.parameters.qname.unwrap().to_lowercase(); + let mut qname = original_qname.clone(); + let qtype = req.parameters.qtype.unwrap(); + debug!("lookup for qtype={} qname={}", qtype, original_qname); + + // Example payload: + // + // {"method": "lookup", + // "parameters": {"local": "0.0.0.0", + // "qname": "fabrice.box.knilxof.org.", + // "qtype": "SOA", + // "real-remote": "63.245.221.198/32", + // "remote": "63.245.221.198", + // "zone-id": -1}} + + // If the qname ends up with .box.$domain.box.$domain. we consider that it's a + // PageKite request and process it separately. + let domain = &config.options.general.domain; + if qname.ends_with(&format!(".box.{}.box.{}.", domain, domain)) { + return pakegite_query(&qname, &qtype, config); + } + + // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification, + // so remove that part of the domain to retrieve our record. + // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4 + if qname.starts_with("_acme-challenge.") { + qname = qname[16..].to_owned(); + } + + debug!("final qname={}", qname); + + // Look for a record with for the qname. + match config.db.get_record_by_name(&qname).recv().unwrap() { + Ok(record) => { + if record.local_ip.is_none() && qtype == "A" { + // No info on this domain, bail out. + return Err("No local_ip".to_owned()); + } + + // Choose either the local or public ip based on wether the qname matches + // the local_name or remote_name. + let a_record = if record.local_ip.is_some() && qname == record.local_name { + // We are inside of the home network, return the local ip for the A record. + record.local_ip.unwrap() + } else { + // We are outside of the home network, return the ip of the tunnel + // for the A record. + config.options.general.tunnel_ip.to_owned() + }; + + let mut pdns_response = PdnsResponse { result: Vec::new() }; + + if qtype == "SOA" { + pdns_response + .result + .push(PdnsResponseParams::Lookup(soa_response(&original_qname, config))); + } + + if qtype == "ANY" || qtype == "A" { + // Add an "A" record. + let ns_record = PdnsLookupResponse { + qtype: "A".to_owned(), + qname: original_qname.to_owned(), + content: a_record, + ttl: config.options.pdns.dns_ttl, + domain_id: None, + scope_mask: None, + auth: None, + }; + pdns_response + .result + .push(PdnsResponseParams::Lookup(ns_record)); + } + + if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() { + // Add a "TXT" record with the dns challenge content. + let ns_record = PdnsLookupResponse { + qtype: "TXT".to_owned(), + qname: original_qname.to_owned(), + content: record.dns_challenge.unwrap(), + ttl: config.options.pdns.dns_ttl, + domain_id: None, + scope_mask: None, + auth: None, + }; + pdns_response + .result + .push(PdnsResponseParams::Lookup(ns_record)); + } + + return Ok(pdns_response); + } + Err(_) => { + // No such domain, return a `false` result to PowerDNS. + return Err("No record for this name.".to_owned()); + } + } + } + + Err(format!("Unsupported method: {}", req.method)) +} + +// Answers to an HTTP request when using the HTTP remote backend. +pub fn pdns(req: &mut Request, config: &Config) -> IronResult { + use std::net::SocketAddr::V4; + use std::net::Ipv4Addr; + + info!("GET /pdns"); + // Only allow clients from localhost. + match req.remote_addr { + V4(addr) => { + if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) { + return EndpointError::with(status::BadRequest, 400); + } + } + _ => return EndpointError::with(status::BadRequest, 400), + } + + // Read the request from the json body. + let mut s = String::new(); + itry!(req.body.read_to_string(&mut s)); + + debug!("Body is: {}", s); + + let input: PdnsRequest = match serde_json::from_str(&s) { + Ok(value) => value, + Err(err) => { + error!("Bad request: {}", err); + return EndpointError::with(status::BadRequest, 400); + } + }; + + match process_request(input, config) { + Ok(ref response) => pdns_response_as_iron(response), + Err(err) => pdns_failure_as_iron(&err), + } + +} + +// Custom method to read just enough characters from the stream to +// build a JSON object. +// Directly using read_to_string or serde_json::from_reader cause +// the stream to reach EOF and subsequent write fail with a +// "Broken Pipe" error. +fn read_json_from_stream(mut stream: &UnixStream) -> String { + let mut buffer = [0; 1]; + let mut balance_count = 0; + let mut result = String::new(); + loop { + match stream.read(&mut buffer) { + Ok(_) => { + if buffer[0] == b'{' { + balance_count += 1; + } else if buffer[0] == b'}' { + balance_count -= 1; + } + result.push(buffer[0] as char); + } + Err(err) => { + error!("Stream reading error: {}", err); + } + } + + if balance_count == 0 { + break; + } + } + + // Read the trailing \n + #[allow(unused_must_use)] + { + stream.read(&mut buffer); + } + + result +} + +fn handle_socket_request(mut stream: UnixStream, config: &Config) { + let error_response = b"{\"result\":false}"; + + macro_rules! send { + ($content:expr) => ( + stream.write_all($content).expect("Failed to write answer to the pdns socket"); + ) + } + + loop { + let s = read_json_from_stream(&stream); + debug!("JSON String is {}", s); + let input: PdnsRequest = match serde_json::from_str(&s) { + Ok(value) => value, + Err(err) => { + error!("JSON error: {}", err); + break; + } + }; + + // Special case for the `initialize` method which is a no-op that + // just returns success. + if input.method == "initialize" { + debug!("Answering to initialization request"); + send!(b"{\"result\":true}"); + continue; + } + + match process_request(input, config) { + Ok(ref response) => { + match serde_json::to_string(response) { + Ok(serialized) => { + debug!("Response is: {}", serialized); + send!(serialized.as_bytes()); + } + Err(err) => { + error!("Error serializing JSON: {}", err); + send!(error_response); + } + } + } + Err(err) => { + error!("Error processing request: {}", err); + send!(error_response); + } + } + } +} + +pub fn start_socket_endpoint(config: &Config) { + if config.options.pdns.socket_path.is_none() { + error!("No socket path configured!"); + return; + } + + let path = &config.options.pdns.socket_path.clone().unwrap(); + + debug!("Starting the pdns socket endpoint at {}", path); + + if Path::exists(Path::new(&path)) { + #[allow(unused_must_use)] + { + fs::remove_file(path.clone()); + } + } + + let config = config.clone(); + let path = path.clone(); + thread::Builder::new() + .name("tunnel pdns socket".to_owned()) + .spawn(move || { + let socket = match UnixListener::bind(path) { + Ok(sock) => sock, + Err(e) => { + error!("Couldn't bind: {:?}", e); + return; + } + }; + for stream in socket.incoming() { + match stream { + Ok(stream) => { + let config = config.clone(); + thread::spawn(move || handle_socket_request(stream, &config)); + } + Err(_) => { + break; + } + } + } + }) + .expect("Failed to start pdns socket thread."); +} + +#[cfg(test)] +mod tests { + use super::*; + use args::ArgsParser; + use config::Config; + use database::Database; + use std::time::Duration; + + fn build_request(method: &str, qtype: Option<&str>, qname: Option<&str>) -> PdnsRequest { + let qtype = match qtype { + Some(val) => Some(val.to_owned()), + None => None, + }; + let qname = match qname { + Some(val) => Some(val.to_owned()), + None => None, + }; + PdnsRequest { + method: method.to_owned(), + parameters: PdnsRequestParameters { + path: None, + timeout: None, + + // lookup method + qtype: qtype, + qname: qname, + zone_id: None, + remote: None, + local: None, + real_remote: None, + }, + } + } + + #[test] + fn test_socket() { + let args = ArgsParser::from_vec(vec!["registration_server", + "--config-file=./config.toml.test"]); + + let db = Database::new("domain_db_test_pdns.sqlite"); + db.flush().recv().unwrap().expect("Flushing the db"); + + let mut arg_config = Config::from_args(args); + let config = arg_config.with_db(db.clone()); + + start_socket_endpoint(&config); + + // Let enough time for the socket thread to start up and bind the socket. + thread::sleep(Duration::new(1, 0)); + // Connect to the socket. + let mut stream = UnixStream::connect(&config.clone().options.pdns.socket_path.unwrap()) + .unwrap(); + // Build a initialization request and send it to the stream. + let request = build_request("initialize", None, None); + let body = serde_json::to_string(&request).unwrap(); + stream.write_all(body.as_bytes()).unwrap(); + stream.write_all(b"\n").unwrap(); + + let empty_success = b"{\"result\":true}"; + let empty_error = b"{\"result\":false}"; + + let mut answer: [u8; 256] = [0; 256]; + assert_eq!(stream.read(&mut answer).unwrap(), 15); + assert_eq!(&answer[..15], empty_success); + + // Build a lookup request and send it to the stream. + let request = build_request("lookup", Some("A"), Some("example.org")); + let body = serde_json::to_string(&request).unwrap(); + stream.write_all(body.as_bytes()).unwrap(); + stream.write_all(b"\n").unwrap(); + + assert_eq!(stream.read(&mut answer).unwrap(), 16); + assert_eq!(&answer[..16], empty_error); + + // Build a SOA lookup request and send it to the stream. + let request = build_request("lookup", Some("SOA"), Some("example.org")); + let body = serde_json::to_string(&request).unwrap(); + stream.write_all(body.as_bytes()).unwrap(); + stream.write_all(b"\n").unwrap(); + + assert_eq!(stream.read(&mut answer).unwrap(), 16); + assert_eq!(&answer[..16], empty_error); + + // SOA pagekite query, to create a successfull response without + // having to setup records in the db. + let request = build_request("lookup", + Some("A"), + Some("1d48.https-4443.test.box.knilxof.org.box.knilxof.org.")); + let body = serde_json::to_string(&request).unwrap(); + stream.write_all(body.as_bytes()).unwrap(); + stream.write_all(b"\n").unwrap(); + + assert_eq!(stream.read(&mut answer).unwrap(), 125); + let result = String::from_utf8(answer[..125].to_vec()).unwrap(); + let soa_success = +r#"{"result":[{"qtype":"A","qname":"1d48.https-4443.test.box.knilxof.org.box.knilxof.org.","content":"255.255.255.0","ttl":89}]}"#; + assert_eq!(&result, soa_success); + } +} diff --git a/server/src/routes.rs b/server/src/routes.rs new file mode 100644 index 0000000..cf829e8 --- /dev/null +++ b/server/src/routes.rs @@ -0,0 +1,743 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use types::NameAndToken; +use config::Config; +use database::{DatabaseError, DomainRecord}; +use discovery::{adddiscovery, discovery, ping, revokediscovery}; +use email_routes::{revokeemail, setemail, verifyemail}; +use errors::*; +use iron::headers::ContentType; +use iron::method::Method; +use iron::prelude::*; +use iron::status::{self, Status}; +use iron_cors::CORS; +use mount::Mount; +use params::{FromValue, Params, Value}; +use pdns::pdns; +use router::Router; +use serde_json; +use std::time::{SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +fn domain_for_name(name: &str, config: &Config) -> String { + format!("{}.box.{}.", name, config.options.general.domain).to_lowercase() +} + +fn register(req: &mut Request, config: &Config) -> IronResult { + // Extract the local_ip and token parameter, + // and the public IP from the socket. + let public_ip = format!("{}", req.remote_addr.ip()); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + let local_ip = map.find(&["local_ip"]); + + // Both parameters are mandatory. + if token.is_none() || local_ip.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let token = String::from_value(token.unwrap()).unwrap(); + let local_ip = String::from_value(local_ip.unwrap()).unwrap(); + + info!("GET /register token={} local_ip={} public_ip={}", + token, + local_ip, + public_ip); + + // Save this registration in the database if we know about this token. + // Check if we have a record with this token, bail out if not. + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(record) => { + // Update the record with the challenge. + let dns_challenge = match record.dns_challenge { + Some(ref challenge) => Some(challenge.as_str()), + None => None, + }; + // Update the timestamp to be current. + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let email = match record.email { + Some(ref email) => Some(email.as_str()), + None => None, + }; + let new_record = DomainRecord::new(&record.token, + &record.local_name, + &record.remote_name, + dns_challenge, + Some(&local_ip), + Some(&public_ip), + &record.description, + email, + timestamp); + match config.db.update_record(new_record).recv().unwrap() { + Ok(()) => { + // Everything went fine, return an empty 200 OK for now. + ok_response!() + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +fn info(req: &mut Request, config: &Config) -> IronResult { + info!("GET /info"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + if token.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + let token = String::from_value(token.unwrap()).unwrap(); + + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(record) => json_response!(&record), + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +fn unsubscribe(req: &mut Request, config: &Config) -> IronResult { + info!("GET /unsubscribe"); + + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let token = map.find(&["token"]); + if token.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + let token = String::from_value(token.unwrap()).unwrap(); + + match config.db.delete_record_by_token(&token).recv().unwrap() { + Ok(0) => EndpointError::with(status::BadRequest, 400), // No record found for this token. + Ok(_) => ok_response!(), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +fn subscribe(req: &mut Request, config: &Config) -> IronResult { + info!("GET /subscribe"); + + // Extract the name parameter. + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + match map.find(&["name"]) { + Some(&Value::String(ref name)) => { + let full_name = domain_for_name(name, config); + info!("trying to subscribe {}", full_name); + + let record = config.db.get_record_by_name(&full_name).recv().unwrap(); + match record { + Ok(_) => { + // We already have a record for this name, return an error. + let mut response = Response::with(r#"{"error": "UnavailableName"}"#); + response.status = Some(Status::BadRequest); + response.headers.set(ContentType::json()); + Ok(response) + } + Err(DatabaseError::NoRecord) => { + // Create a token, create and store a record and finally return the token. + let token = format!("{}", Uuid::new_v4()); + let local_name = format!("local.{}", full_name); + + + let description = match map.find(&["desc"]) { + Some(&Value::String(ref desc)) => desc.to_owned(), + _ => format!("{}'s server", name), + }; + let record = DomainRecord::new(&token, + &local_name, + &full_name, + None, + None, + None, + &description, + None, + 0); + match config.db.add_record(record).recv().unwrap() { + Ok(()) => { + // We don't want the full domain name or the dns challenge in the + // response so we create a local struct. + let n_and_t = NameAndToken { + name: name.to_owned(), + token: token, + }; + json_response!(&n_and_t) + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + // Other error, like a db issue. + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + // Missing `name` parameter. + _ => EndpointError::with(status::BadRequest, 400), + } +} + +fn dnsconfig(req: &mut Request, config: &Config) -> IronResult { + info!("GET /dnsconfig"); + + // Extract the challenge and token parameter. + let map = req.get_ref::().unwrap(); // TODO: don't unwrap. + let challenge = map.find(&["challenge"]); + let token = map.find(&["token"]); + + // Both parameters are mandatory. + if challenge.is_none() || token.is_none() { + return EndpointError::with(status::BadRequest, 400); + } + + let challenge = String::from_value(challenge.unwrap()).unwrap(); + let token = String::from_value(token.unwrap()).unwrap(); + + // Check if we have a record with this token, bail out if not. + match config.db.get_record_by_token(&token).recv().unwrap() { + Ok(record) => { + // Update the record with the challenge. + let local_ip = match record.local_ip { + Some(ref ip) => Some(ip.as_str()), + None => None, + }; + let public_ip = match record.public_ip { + Some(ref ip) => Some(ip.as_str()), + None => None, + }; + let email = match record.email { + Some(ref email) => Some(email.as_str()), + None => None, + }; + let new_record = DomainRecord::new(&record.token, + &record.local_name, + &record.remote_name, + Some(&challenge), + local_ip, + public_ip, + &record.description, + email, + record.timestamp); + match config.db.update_record(new_record).recv().unwrap() { + Ok(()) => { + // Everything went fine, return an empty 200 OK for now. + ok_response!() + } + Err(_) => EndpointError::with(status::InternalServerError, 501), + } + } + Err(DatabaseError::NoRecord) => EndpointError::with(status::BadRequest, 400), + Err(_) => EndpointError::with(status::InternalServerError, 501), + } +} + +pub fn create_router(config: &Config) -> Router { + let mut router = Router::new(); + + macro_rules! handler { + ($name:ident) => ( + let config_ = config.clone(); + router.get(stringify!($name), + move |req: &mut Request| -> IronResult { + $name(req, &config_) + }, stringify!($name)); + ) + } + + handler!(register); + handler!(info); + handler!(subscribe); + handler!(unsubscribe); + handler!(dnsconfig); + + handler!(ping); + handler!(adddiscovery); + handler!(revokediscovery); + handler!(discovery); + + handler!(verifyemail); + handler!(setemail); + handler!(revokeemail); + + if config.options.pdns.socket_path.is_none() { + handler!(pdns); + } + + // Tests need the pdns handler in all cases. + #[cfg(test)] + { + if config.options.pdns.socket_path.is_some() { + handler!(pdns); + } + } + + router +} + +pub fn create_chain(root_path: &str, config: &Config) -> Chain { + let mut mount = Mount::new(); + mount.mount(root_path, create_router(&config)); + + let mut chain = Chain::new(mount); + let cors = CORS::new(vec![(vec![Method::Get], "info".to_owned()), + (vec![Method::Get], "subscribe".to_owned()), + (vec![Method::Get], "unsubscribe".to_owned()), + (vec![Method::Get], "register".to_owned()), + (vec![Method::Get], "dnsconfig".to_owned()), + (vec![Method::Get], "ping".to_owned()), + (vec![Method::Get], "adddiscovery".to_owned()), + (vec![Method::Get], "revokediscovery".to_owned()), + (vec![Method::Get], "discovery".to_owned()), + (vec![Method::Get], "setemail".to_owned()), + (vec![Method::Get], "revokeemail".to_owned())]); + chain.link_after(cors); + chain +} + +#[cfg(test)] +mod tests { + extern crate hyper; + + use super::*; + use types::{NameAndToken, ServerInfo}; + use args::ArgsParser; + use config::Config; + use database::{Database, SqlParam}; + use iron::{Handler, Url}; + use iron::status::Status; + use iron::method; + use iron; + use iron_test::response; + use iron_test::mock_stream::MockStream; + use std::io::Cursor; + use self::hyper::buffer::BufReader; + use self::hyper::net::NetworkStream; + + fn get(path: &str, router: &Router) -> (String, Status) { + let resp = match request(method::Method::Get, path, "", router) { + Ok(response) => response, + Err(err) => err.response, + }; + let status = resp.status.unwrap(); + (response::extract_body_to_string(resp), status) + } + + fn put(path: &str, body: &str, router: &Router) -> (String, Status) { + let resp = match request(method::Method::Get, path, body, router) { + Ok(response) => response, + Err(err) => err.response, + }; + let status = resp.status.unwrap(); + (response::extract_body_to_string(resp), status) + } + + // Triggers a request for a url on the router. + fn request(method: method::Method, + path: &str, + body: &str, + router: &Router) + -> IronResult { + let url = Url::parse(&format!("http://localhost/{}", path)).unwrap(); + // From iron 0.5.x, iron::Request contains private field. So, it is not good to + // create iron::Request directly. Make http request and parse it with hyper, + // and make iron::Request from hyper::client::Request. + let mut buffer = String::new(); + buffer.push_str(&format!("{} {} HTTP/1.1\r\n", &method, url)); + buffer.push_str(&format!("Content-Length: {}\r\n", body.len() as u64)); + buffer.push_str("\r\n"); + buffer.push_str(body); + + let addr = "127.0.0.1:3000".parse().unwrap(); + let protocol = match url.scheme() { + "http" => iron::Protocol::http(), + "https" => iron::Protocol::https(), + _ => panic!("unknown protocol {}", url.scheme()), + }; + + let mut stream = MockStream::new(Cursor::new(buffer.as_bytes().to_vec())); + let mut buf_reader = BufReader::new(&mut stream as &mut NetworkStream); + let http_request = hyper::server::Request::new(&mut buf_reader, addr).unwrap(); + let mut req = Request::from_http(http_request, addr, &protocol).unwrap(); + + router.handle(&mut req) + } + + #[test] + fn test_router() { + let db = Database::new("domain_db_test_routes.sqlite"); + db.flush().recv().unwrap().expect("Flushing the db"); + + let args = ArgsParser::from_vec(vec!["registration_server", + "--config-file=./config.toml.test"]); + let mut arg_config = Config::from_args(args); + let router = create_router(&arg_config.with_db(db.clone())); + + let bad_request_error = (r#"{"code":400,"errno":400,"error":"Bad Request"}"#.to_owned(), + Status::BadRequest); + let empty_ok = ("".to_owned(), Status::Ok); + + // Nothing is registered yet. + assert_eq!(get("ping", &router), ("[]".to_owned(), Status::Ok)); + + // Subscribe a test user. + assert_eq!(get("subscribe", &router), bad_request_error); + + let resp = get("subscribe?name=test", &router); + let registration: NameAndToken = serde_json::from_str(&resp.0).unwrap(); + let token = registration.token; + + assert_eq!(registration.name, "test".to_owned()); + + // Unsubscribe + assert_eq!(get("unsubscribe", &router), bad_request_error); + assert_eq!(get("unsubscribe?token=wrong_token", &router), + bad_request_error); + assert_eq!(get(&format!("unsubscribe?token={}", token), &router), + empty_ok); + + // Subscribe again + let resp = get("subscribe?name=test", &router); + let registration: NameAndToken = serde_json::from_str(&resp.0).unwrap(); + let token = registration.token; + + assert_eq!(registration.name, "test".to_owned()); + + // Fail to register twice the same user. + let res = get("subscribe?name=test", &router); + assert_eq!(res, + (r#"{"error": "UnavailableName"}"#.to_owned(), Status::BadRequest)); + + // Register without the expected parameters. + assert_eq!(get("register", &router), bad_request_error); + assert_eq!(get("register?name=test", &router), bad_request_error); + assert_eq!(get(&format!("register?token={}", token), &router), + bad_request_error); + assert_eq!(get("register?local_ip=10.0.0.1&token=wrong_token", &router), + bad_request_error); + + // Register properly. + assert_eq!(get(&format!("register?local_ip=10.0.0.1&token={}", token), + &router), + empty_ok); + + // Now retrieve our registered client. + assert_eq!(get("ping", &router), + (r#"[{"href":"https://local.test.box.knilxof.org","desc":"test's server"}]"# + .to_owned(), + Status::Ok)); + + // Get the full info + assert_eq!(get("info", &router), bad_request_error); + assert_eq!(get("info?token=wrong_token", &router), bad_request_error); + + let response = get(&format!("info?token={}", token), &router); + assert_eq!(response.1, Status::Ok); + let record: ServerInfo = serde_json::from_str(&response.0).unwrap(); + assert_eq!(record.token, token); + assert_eq!(record.local_name, "local.test.box.knilxof.org.".to_owned()); + assert_eq!(record.remote_name, "test.box.knilxof.org."); + assert_eq!(record.local_ip, Some("10.0.0.1".to_owned())); + assert_eq!(record.public_ip, Some("127.0.0.1".to_owned())); + assert_eq!(record.description, r#"test's server"#); + + // Test the LE challenge endpoints. + assert_eq!(get("dnsconfig", &router), bad_request_error); + assert_eq!(get("dnsconfig?token=wrong_token", &router), + bad_request_error); + assert_eq!(get(&format!("dnsconfig?token={}", token), &router), + bad_request_error); + assert_eq!(get("dnsconfig?token=wrong_token&challenge=test_challenge", + &router), + bad_request_error); + assert_eq!(get(&format!("dnsconfig?token={}&challenge=test_challenge", token), + &router), + empty_ok); + + // Tests for the pdns endpoint. + + // Bogus payload. + assert_eq!(put("pdns", r#"{"foo": true}"#, &router), bad_request_error); + + // Unsupported method. + assert_eq!(put("pdns", + r#"{"method":"dummy", "parameters":{"qtype":"a","qname":"b"}}"#, + &router), + (r#"{"result":false}"#.to_owned(), Status::Ok)); + + // Simplified local redeclaration of the pdns data structures since + // we don't need them to be public. + #[derive(Debug, Serialize)] + struct PdnsRequestParameters { + // intialize method + // path: Option, + // timeout: Option, + + // lookup method + qtype: Option, + qname: Option, + // #[serde(rename="zone-id")] + // zone_id: Option, + // remote: Option, + // local: Option, + // real_remote: Option, + } + + #[derive(Debug, Serialize)] + struct PdnsRequest { + method: String, + parameters: PdnsRequestParameters, + } + + // Failure for an unknown domain. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("A".to_owned()), + qname: Some("www.example.org.".to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + assert_eq!(put("pdns", &body, &router), + (r#"{"result":false}"#.to_owned(), Status::Ok)); + + // Test the "remote" dns name. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("A".to_owned()), + qname: Some("test.box.knilxof.org.".to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + + let success = +r#"{"result":[{"qtype":"A","qname":"test.box.knilxof.org.","content":"1.2.3.4","ttl":89}]}"#; + assert_eq!(put("pdns", &body, &router), + (success.to_owned(), Status::Ok)); + + // Test the "local" dns name. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("A".to_owned()), + qname: Some("local.test.box.knilxof.org.".to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + + let success = +r#"{"result":[{"qtype":"A","qname":"local.test.box.knilxof.org.","content":"10.0.0.1","ttl":89}]}"#; + assert_eq!(put("pdns", &body, &router), + (success.to_owned(), Status::Ok)); + + // Test LE challenge queries. + // Test the "local" dns name. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("TXT".to_owned()), + qname: Some("_acme-challenge.local.test.box.knilxof.org.".to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + + let success = +r#"{"result":[{"qtype":"TXT","qname":"_acme-challenge.local.test.box.knilxof.org.","content":"test_challenge","ttl":89}]}"#; + assert_eq!(put("pdns", &body, &router), + (success.to_owned(), Status::Ok)); + + // Test SOA queries. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("SOA".to_owned()), + qname: Some("test.box.knilxof.org.".to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + + let success = +r#"{"result":[{"qtype":"SOA","qname":"test.box.knilxof.org.","content":"a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800","ttl":89}]}"#; + assert_eq!(put("pdns", &body, &router), + (success.to_owned(), Status::Ok)); + + // PageKite queries + #[derive(Deserialize)] + struct PdnsLookupResponse { + #[allow(dead_code)] + qtype: String, + #[allow(dead_code)] + qname: String, + content: String, + #[allow(dead_code)] + ttl: u32, + #[allow(dead_code)] + domain_id: Option, + #[allow(dead_code)] + #[serde(rename="scopeMask")] + scope_mask: Option, + #[allow(dead_code)] + auth: Option, + } + #[derive(Deserialize)] + struct PdnsResponse { + result: Vec, + } + + // A request with a bogus domain. + let qname = "dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org."; + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("A".to_owned()), + qname: Some(qname.to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + let result = put("pdns", &body, &router); + assert_eq!(result.1, Status::Ok); + let response: PdnsResponse = serde_json::from_str(&result.0).unwrap(); + // 255.255.255.0 Means "no such name found for pagekite" + assert_eq!(response.result[0].content, "255.255.255.0"); + + // A request with a correct domain. + let qname = "dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.test.box.knilxof.org.box.knilxof.org."; + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("A".to_owned()), + qname: Some(qname.to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + let result = put("pdns", &body, &router); + assert_eq!(result.1, Status::Ok); + let response: PdnsResponse = serde_json::from_str(&result.0).unwrap(); + // 255.255.255.1 Means "failed to verify signature for pagekite" + assert_eq!(response.result[0].content, "255.255.255.1"); + + // SOA request. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("SOA".to_owned()), + qname: Some(qname.to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + let result = put("pdns", &body, &router); + assert_eq!(result.1, Status::Ok); + let response: PdnsResponse = serde_json::from_str(&result.0).unwrap(); + assert_eq!(response.result[0].content, + "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"); + + // TXT request. + let pdns_request = PdnsRequest { + method: "lookup".to_owned(), + parameters: PdnsRequestParameters { + qtype: Some("TXT".to_owned()), + qname: Some(qname.to_owned()), + }, + }; + let body = serde_json::to_string(&pdns_request).unwrap(); + let result = put("pdns", &body, &router); + assert_eq!(result.1, Status::Ok); + assert_eq!(result.0, r#"{"result":false}"#); + + // Discovery tests. + + // Add a discovery token + assert_eq!(get("adddiscovery", &router), bad_request_error); + assert_eq!(get("adddiscovery?token=wrong_token", &router), + bad_request_error); + assert_eq!(get("adddiscovery?token=wrong_token&disco=disco_token", &router), + bad_request_error); + assert_eq!(get(&format!("adddiscovery?token={}&disco=disco_token", token), + &router), + empty_ok); + + // Get records for a given token. + assert_eq!(get("discovery", &router), bad_request_error); + assert_eq!(get("discovery?disco=wrong_disco", &router), + bad_request_error); + assert_eq!(get("discovery?disco=disco_token", &router), + (r#"[{"href":"https://local.test.box.knilxof.org","desc":"test's server"}]"#.to_owned(), + Status::Ok)); + + // Get the record with to evict it. + let db = Database::new("domain_db_test_routes.sqlite"); + let timestamp = db.get_record_by_token(&token) + .recv() + .unwrap() + .unwrap() + .timestamp; + assert_eq!(db.evict_records(SqlParam::Integer(timestamp + 1)) + .recv() + .unwrap(), + Ok(1)); + + // Check that we discover it now as a remote server. + assert_eq!(get("discovery?disco=disco_token", &router), + (r#"[{"href":"https://test.box.knilxof.org","desc":"test's server"}]"#.to_owned(), + Status::Ok)); + + // Revoke the token. + assert_eq!(get("revokediscovery", &router), bad_request_error); + assert_eq!(get("revokediscovery?token=wrong_token", &router), + bad_request_error); + assert_eq!(get(&format!("revokediscovery?token={}", token), &router), + bad_request_error); + assert_eq!(get(&format!("revokediscovery?token={}&disco=disco_token", token), + &router), + empty_ok); + assert_eq!(get("discovery?disco=disco_token", &router), + bad_request_error); + + // Email routes tests + // 1. set an email address + let email = "test@example.com".to_owned(); + assert_eq!(get("setemail", &router), bad_request_error); + assert_eq!(get("setemail?token=wrong_token", &router), + bad_request_error); + assert_eq!(get("setemail?token=wrong_token&email=me@example.com", &router), + bad_request_error); + assert_eq!(get(&format!("setemail?token={}&email=not_an_email", token), + &router), + bad_request_error); + assert_eq!(get(&format!("setemail?token={}&email={}", token, email), + &router), + empty_ok); + let email_record = db.get_email_by_token(&token).recv().unwrap().unwrap(); + assert_eq!(email_record.0, email); + let link = email_record.1; + // 2. verify the email + assert_eq!(get("verifyemail", &router), bad_request_error); + assert_eq!(get("verifyemail?s=wrong_link", &router), + (arg_config.options.email.error_page.unwrap(), Status::Ok)); + assert_eq!(get(&format!("verifyemail?s={}", link), &router), + (arg_config.options.email.success_page.unwrap(), Status::Ok)); + // 3. check that the email has been set on the domain record. + let domain_record = db.get_record_by_token(&token).recv().unwrap().unwrap(); + assert_eq!(domain_record.email, Some(email.clone())); + // 4. email revokation + assert_eq!(get("revokeemail", &router), bad_request_error); + assert_eq!(get("revokeemail?token=wrong_token", &router), + bad_request_error); + assert_eq!(get("revokeemail?token=wrong_token&email=me@example.com", + &router), + bad_request_error); + assert_eq!(get(&format!("revokeemail?token={}&email=not_an_email", token), + &router), + bad_request_error); + assert_eq!(get(&format!("revokeemail?token={}&email={}", token, email), + &router), + empty_ok); + // 5. Verify we don't have this email record anymore. + let email_record = db.get_email_by_token(&token).recv().unwrap(); + assert_eq!(email_record, Err(DatabaseError::NoRecord)); + } +} diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index e2af1b8..0000000 --- a/src/db.rs +++ /dev/null @@ -1,250 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use redis::{ Client, cmd, Connection, ConnectionAddr, ConnectionInfo, - pipe, RedisResult }; -use std::time::Duration; -use std::thread::sleep; - -static RECORD_TTL: i32 = 2 * 60; // 2 minutes - -#[derive(RustcEncodable, Debug)] -pub struct Record { - pub public_ip: String, - pub client: String, - pub message: String, -} - -pub struct Db { - connection: Connection -} - -impl Db { - pub fn new(db_host: String, - db_port: u16, - db_password: Option) -> Db { - let client = Client::open(ConnectionInfo { - addr: Box::new(ConnectionAddr::Tcp(db_host.clone(), db_port)), - db: 0, - passwd: db_password.clone() - }).unwrap(); - - loop { - match client.get_connection() { - Err(err) => { - if err.is_connection_refusal() { - warn!("Could not connect: {} (Will retry)", err); - sleep(Duration::from_millis(1)); - } else { - panic!("Could not connect: {}", err); - } - }, - Ok(connection) => { - return Db { - connection: connection - } - }, - } - } - } - - /// - /// Add or update a DB record. - /// We keep a set with the record's public IP as key containing the list - /// of client IDs registered for this public IP and we store a message - /// per each "publicIP:clientID" tuple. - /// For example: - /// - /// "88.22.170.96": [ - /// "e7ce02eaa73da35bddea00c82124c7fbbe49b731", - /// "2b3e83cca3ee12c8b41d86bfeca6034ea8cb9056" - /// ] - /// - /// "88.22.170.96:e7ce02eaa73da35bddea00c82124c7fbbe49b731": "message1" - /// "88.22.170.96:2b3e83cca3ee12c8b41d86bfeca6034ea8cb9056": "message2" - /// - /// Each "publicIP:clientID" tuple has a ttl of 2 minutes. - /// - pub fn set(&self, record: Record) -> RedisResult<()> { - let key = format!("{}:{}", record.public_ip, record.client); - - // We need to start watching the keys we care about (public_ip and - // public_ip:client) so that our exec fails if the key changes. - let _: () = try!( - cmd("WATCH").arg(key.clone()) - .arg(record.public_ip.clone()) - .query(&self.connection) - ); - - // We check if there's already an entry for this public IP. - let is_member: isize = try!( - cmd("SISMEMBER").arg(record.public_ip.clone()) - .arg(record.client.clone()) - .query(&self.connection) - ); - - if is_member == 0 { - // If there is no previous entry for this public IP, we add one - // and add the message corresponding to this key (IP:user tuple) - info!("{} is not a member of {} yet", - record.client.clone(), record.public_ip.clone()); - let _: () = try!( - pipe().atomic() - .cmd("SADD").arg(record.public_ip.clone()) - .arg(record.client.clone()) - .ignore() - .cmd("SET").arg(key.clone()) - .arg(record.message.clone()) - .query(&self.connection) - ); - } else { - // Otherwise, we just update the message from the existing - // entry. - info!("{} is already a member of {}", - record.client.clone(), record.public_ip.clone()); - let _: () = try!( - cmd("SET").arg(key.clone()) - .arg(record.message.clone()) - .query(&self.connection) - ); - } - - // And set the TTL of the message. - // The entry in the list of clients for this public IP will be - // cleaned up during the .get call. It does no harm to keep it - // around until that point. - let _: () = try!( - cmd("EXPIRE").arg(key.clone()) - .arg(RECORD_TTL) // 2 min. - .query(&self.connection) - ); - - Ok(()) - } - - /// - /// Get the registration entries for a given public IP. - /// - pub fn get(&self, public_ip: String) -> RedisResult> { - let _: () = try!( - cmd("WATCH").arg(public_ip.clone()) - .query(&self.connection) - ); - - // Get the clients for the given public IP. - let members: Vec = try!( - cmd("SMEMBERS").arg(public_ip.clone()) - .query(&self.connection) - ); - - info!("Members of {}: {:?}", public_ip.clone(), members.clone()); - - let mut result = Vec::new(); - - // For each client we get the associated message. - for member in members { - let key = format!("{}:{}", public_ip.clone(), member); - info!("Key {}", key.clone()); - match cmd("GET").arg(key.clone()) - .query(&self.connection) { - Ok(message) => { - info!("Message for {}: {}", key.clone(), message); - - result.push(Record { - public_ip: public_ip.clone(), - client: member.clone(), - message: message - }); - }, - Err(_) => { - // Remove the client id from the list of clients of this public - // IP that has no associated message. - info!("Removing {} from {}", member.clone(), public_ip.clone()); - let _: () = try!( - cmd("SREM").arg(public_ip.clone()) - .arg(member.clone()) - .query(&self.connection) - ); - } - }; - } - - Ok(result) - } - - #[cfg(test)] - pub fn flush(&self) -> RedisResult<()> { - let _: () = try!( - cmd("FLUSHDB").query(&self.connection) - ); - - Ok(()) - } -} - -#[test] -fn test_db() { - use super::db_test_context::TestContext; - - let ctx = TestContext::new(); - let db = ctx.db; - - // Look for a record, but the db is empty. - match db.get("127.0.0.1".to_owned()) { - Ok(vec) => { assert!(vec.is_empty()); }, - Err(err) => { println!("Unexpected error: {}", err); assert!(false); } - }; - - let mut r = Record { - public_ip: "127.0.0.1".to_owned(), - message: "".to_owned(), - client: "".to_owned() - }; - - // Add this new record. - match db.set(r) { - Ok(_) => { assert!(true); }, - Err(err) => { println!("Unexpected error: {}", err); assert!(false); } - } - - // Check that we find it. - match db.get("127.0.0.1".to_owned()) { - Ok(records) => { - assert_eq!(records.len(), 1); - assert_eq!(records[0].message, ""); - }, - Err(err) => { println!("Unexpected error: {}", err); assert!(false); } - } - - // Add another record with the same public IP, but a different local one. - r = Record { - public_ip: "127.0.0.1".to_owned(), - message: "".to_owned(), - client: "".to_owned() - }; - - match db.set(r) { - Ok(_) => { assert!(true); }, - Err(err) => { println!("Unexpected error: {}", err); assert!(false); } - } - - // Now search for all the records with this public IP. Will find 2. - match db.get("127.0.0.1".to_owned()) { - Ok(records) => { - assert_eq!(records.len(), 2); - assert!(records[0].message == "" || - records[0].message == ""); - assert!(records[1].message == "" || - records[1].message == ""); - assert!(records[0].client == "" || - records[0].client == ""); - assert!(records[1].client == "" || - records[1].client == ""); - }, - Err(err) => { println!("Unexpected error: {}", err); assert!(false); } - } - - // Fake travelling in the future, and evict both records. - db.flush().unwrap(); -} diff --git a/src/db_test_context.rs b/src/db_test_context.rs deleted file mode 100644 index 249bbfa..0000000 --- a/src/db_test_context.rs +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use std::process; -use super::db::Db; - -static SERVER_PORT: u16 = 38991; -static SERVER_HOST: &'static str = "127.0.0.1"; - -pub struct RedisServer { - pub process: process::Child, -} - -impl RedisServer { - - pub fn new() -> RedisServer { - let mut cmd = process::Command::new("redis-server"); - cmd - .stdout(process::Stdio::null()) - .stderr(process::Stdio::null()) - .arg("--port").arg(SERVER_PORT.to_string()) - .arg("--bind").arg(SERVER_HOST.to_string()); - - let process = cmd.spawn().unwrap(); - RedisServer { process: process } - } -} - -impl Drop for RedisServer { - fn drop(&mut self) { - let _ = self.process.kill(); - let _ = self.process.wait(); - } -} - -pub struct TestContext { - pub server: RedisServer, - pub db: Db -} - -impl TestContext { - pub fn new() -> TestContext { - let server = RedisServer::new(); - - let db = Db::new(SERVER_HOST.to_string(), - SERVER_PORT, - None /* password */); - - db.flush().unwrap(); - - TestContext { - server: server, - db: db, - } - } -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 94ce926..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use iron::status; -use iron::prelude::*; -use rustc_serialize::json; -use std::error::Error; -use std::fmt::{ self, Debug }; - -#[derive(Debug)] -struct StringError(pub String); - -impl fmt::Display for StringError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Debug::fmt(self, f) - } -} - -impl Error for StringError { - fn description(&self) -> &str { - &*self.0 - } -} - -#[derive(Debug, RustcDecodable, RustcEncodable)] -pub struct ErrorBody { - pub code: u16, - pub errno: u16, - pub error: String -} - -pub struct EndpointError; - -impl EndpointError { - pub fn with(status: status::Status, errno: u16) - -> IronResult { - let error = status.canonical_reason().unwrap().to_owned(); - let body = ErrorBody { - code: status.to_u16(), - errno: errno, - error: error.clone() - }; - - Err( - IronError::new(StringError(error), - (status, json::encode(&body).unwrap())) - ) - } -} - -pub fn from_decoder_error(error: json::DecoderError) -> IronResult { - match error { - json::DecoderError::MissingFieldError(field) => { - let errno = match field.as_ref() { - "local_ip" => 100, - "tunnel_url" => 101, - _ => 400 - }; - EndpointError::with(status::BadRequest, errno) - }, - _ => EndpointError::with(status::BadRequest, 400) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7fe3a5b..0000000 --- a/src/main.rs +++ /dev/null @@ -1,132 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/// Simple server that manages foxbox registrations. -/// Two end points are available: -/// POST /register => to register a match between public IP and mesage. -/// GET /ping => to get the list of public IP matches. -/// -/// Boxes are supposed to register themselves at regular intervals so we -/// discard data which is too old periodically. - -extern crate docopt; -extern crate env_logger; -extern crate iron; -extern crate iron_cors; -#[macro_use] -extern crate log; -extern crate mount; -extern crate params; -extern crate redis; -extern crate router; -extern crate rusqlite; -extern crate rustc_serialize; - -use docopt::Docopt; -use iron::{ Chain, Iron, Protocol }; -use iron::method::Method; -use iron_cors::CORS; -use mount::Mount; -use std::path::PathBuf; - -mod errors; -mod db; -mod routes; - -#[cfg(test)] -mod db_test_context; - -const USAGE: &'static str = " -Usage: registration_server [-d ] [--db-port ] [--db-pass ] [-h ] [-p ] [--cert-directory ] - -Options: - -d, --db-host Set Redis database hostname. - --db-port Set Redis database port. - --db-pass Set Redis database password. - -h, --host Set local hostname. - -p, --port Set port to listen on for http connections. - --cert-directory Certificate directory. -"; - - -#[derive(RustcDecodable)] -struct Args { - flag_db_host: Option, - flag_db_port: Option, - flag_db_pass: Option, - flag_host: Option, - flag_port: Option, - flag_cert_directory: Option, -} - - -fn main() { - env_logger::init().unwrap(); - - let args: Args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); - - let port = args.flag_port.unwrap_or(4242); - let host = args.flag_host.unwrap_or("0.0.0.0".to_string()); - let using_tls = args.flag_cert_directory.is_some(); - let db_host = args.flag_db_host.unwrap_or("localhost".to_string()); - let db_port = args.flag_db_port.unwrap_or(6379); - let db_pass = args.flag_db_pass; - - info!("Redis server on {}:{}", db_host, db_port); - - let mut mount = Mount::new(); - mount.mount("/", routes::create(db_host.clone(), db_port, db_pass.clone())); - - let mut chain = Chain::new(mount); - let cors = CORS::new(vec![ - (vec![Method::Get], "ping".to_owned()), - (vec![Method::Post], "register".to_owned()), - ]); - chain.link_after(cors); - - let iron = Iron::new(chain); - info!("Starting server on {}:{}", host, port); - let addr = format!("{}:{}", host, port); - - if !using_tls { - iron.http(addr.as_ref() as &str) - .unwrap(); - } else { - info!("Starting TLS server"); - let certificate_directory = args.flag_cert_directory.unwrap(); - let certificate_directory = PathBuf::from(certificate_directory); - - let mut private_key = certificate_directory.clone(); - private_key.push("privkey.pem"); - - let mut cert = certificate_directory.clone(); - cert.push("fullchain.pem"); - - info!("Using cert: '{:?}' pk: '{:?}'", cert, private_key); - - let protocol = Protocol::Https { - certificate: cert, - key: private_key, - }; - iron.listen_with(addr.as_ref() as &str, 8, protocol, None).unwrap(); - } -} - -// TODO: add iron tests. - -#[test] -fn options_are_good() { - // short form options - { - let argv = || vec!["registration_server", "-p", "1234", "-h", "foobar"]; - - let args: Args = Docopt::new(USAGE) - .and_then(|d| d.argv(argv().into_iter()).decode()) - .unwrap(); - - assert_eq!(args.flag_host, Some("foobar".to_string())); - assert_eq!(args.flag_port, Some(1234)); - } -} diff --git a/src/routes.rs b/src/routes.rs deleted file mode 100644 index cb70cdf..0000000 --- a/src/routes.rs +++ /dev/null @@ -1,141 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use db::{ Db, Record}; -use errors::*; -use iron::headers::ContentType; -use iron::prelude::*; -use iron::status::{ self, Status }; -use router::Router; -use rustc_serialize::json; -use std::error::Error; -use std::fmt::{ self, Debug }; -use std::io::Read; - -#[derive(Debug)] -struct StringError(String); - -impl fmt::Display for StringError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Debug::fmt(self, f) - } -} - -impl Error for StringError { - fn description(&self) -> &str { &*self.0 } -} - -fn register(req: &mut Request, - db_host: String, - db_port: u16, - db_password: Option) -> IronResult { - // Get the local IP and optional tunnel url from the body, - #[derive(RustcDecodable, Debug)] - struct RegisterBody { - client: String, - message: String, - } - - let mut payload = String::new(); - req.body.read_to_string(&mut payload).unwrap(); - let body: RegisterBody = match json::decode(&payload) { - Ok(body) => body, - Err(error) => { - error!("{:?}", error); - return from_decoder_error(error); - } - }; - - let message = body.message; - let client_id = body.client; - - // And the public IP from the socket. - let public_ip = format!("{}", req.remote_addr.ip()); - - info!("POST /register public_ip={} client={} message={}", - public_ip, client_id, message); - - // Save this registration in the database. - // If we already have the same (local, tunnel, public) match, update it, - // if not create a new match. - let db = Db::new(db_host, db_port, db_password); - - let record = Record { - public_ip: public_ip.clone(), - client: client_id.clone(), - message: message.clone() - }; - - if let Err(e) = db.set(record) { - error!("{}", e); - return EndpointError::with(status::InternalServerError, 501) - } - - let mut response = Response::with("{\"status\" : \"registered\"}"); - response.status = Some(Status::Ok); - response.headers.set(ContentType::json()); - - Ok(response) -} - -fn ping(req: &mut Request, - db_host: String, - db_port: u16, - db_password: Option) -> IronResult { - info!("GET /ping"); - let public_ip = format!("{}", req.remote_addr.ip()); - - let mut serialized = String::from("["); - - let db = Db::new(db_host, db_port, db_password); - match db.get(public_ip.clone()) { - Ok(rvect) => { - info!("Registrations {:?}", rvect); - // Serialize the vector. - let max = rvect.len(); - let mut index = 0; - for record in rvect { - match json::encode(&record) { - Ok(ref record) => serialized.push_str(record), - Err(_) => { - return EndpointError::with(status::InternalServerError, 501) - } - } - - index += 1; - if index < max { - serialized.push_str(","); - } - } - }, - Err(_) => {} - }; - - serialized.push_str("]"); - let mut response = Response::with(serialized); - response.status = Some(Status::Ok); - response.headers.set(ContentType::json()); - - Ok(response) -} - -pub fn create(db_host: String, - db_port: u16, - db_password: Option) -> Router { - let mut router = Router::new(); - - let host = db_host.clone(); - let pass = db_password.clone(); - router.post("register", move |req: &mut Request| -> IronResult { - register(req, host.clone(), db_port, pass.clone()) - }, "post_message"); - - let host = db_host.clone(); - let pass = db_password.clone(); - router.get("ping", move |req: &mut Request| -> IronResult { - ping(req, host.clone(), db_port, pass.clone()) - }, "ping"); - - router -}