├── .envrc ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── default.nix ├── device_blocker.procd ├── flake.lock ├── flake.nix ├── known_devices.json └── src ├── app_server.rs ├── bundle.js ├── config.rs ├── files.rs ├── graphql.rs ├── index.html ├── main.rs ├── schedule.rs ├── script.rs ├── script_handler.rs ├── server.rs ├── table.rs └── types.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | /target/ 3 | /src/bundle.js 4 | /src/index.html 5 | /.direnv/ 6 | /result 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "ansi_term" 37 | version = "0.12.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 40 | dependencies = [ 41 | "winapi", 42 | ] 43 | 44 | [[package]] 45 | name = "atty" 46 | version = "0.2.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 49 | dependencies = [ 50 | "hermit-abi 0.1.19", 51 | "libc", 52 | "winapi", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "0.1.8" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" 60 | dependencies = [ 61 | "autocfg 1.4.0", 62 | ] 63 | 64 | [[package]] 65 | name = "autocfg" 66 | version = "1.4.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 69 | 70 | [[package]] 71 | name = "backtrace" 72 | version = "0.3.74" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 75 | dependencies = [ 76 | "addr2line", 77 | "cfg-if", 78 | "libc", 79 | "miniz_oxide", 80 | "object", 81 | "rustc-demangle", 82 | "windows-targets", 83 | ] 84 | 85 | [[package]] 86 | name = "base64" 87 | version = "0.9.3" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" 90 | dependencies = [ 91 | "byteorder", 92 | "safemem 0.3.3", 93 | ] 94 | 95 | [[package]] 96 | name = "bitflags" 97 | version = "1.3.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 100 | 101 | [[package]] 102 | name = "bodyparser" 103 | version = "0.8.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" 106 | dependencies = [ 107 | "iron", 108 | "persistent", 109 | "plugin", 110 | "serde", 111 | "serde_json", 112 | ] 113 | 114 | [[package]] 115 | name = "buf_redux" 116 | version = "0.6.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" 119 | dependencies = [ 120 | "memchr 1.0.2", 121 | "safemem 0.2.0", 122 | ] 123 | 124 | [[package]] 125 | name = "bumpalo" 126 | version = "3.16.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 129 | 130 | [[package]] 131 | name = "byteorder" 132 | version = "1.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 135 | 136 | [[package]] 137 | name = "cc" 138 | version = "1.1.34" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" 141 | dependencies = [ 142 | "shlex", 143 | ] 144 | 145 | [[package]] 146 | name = "cfg-if" 147 | version = "1.0.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 150 | 151 | [[package]] 152 | name = "checksum" 153 | version = "0.2.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d5c24f6a463e9973db3df3c2cc276f689f5baf289c87a693dc859e004d3eb45f" 156 | 157 | [[package]] 158 | name = "chrono" 159 | version = "0.4.38" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 162 | dependencies = [ 163 | "android-tzdata", 164 | "iana-time-zone", 165 | "js-sys", 166 | "num-traits", 167 | "serde", 168 | "wasm-bindgen", 169 | "windows-targets", 170 | ] 171 | 172 | [[package]] 173 | name = "clap" 174 | version = "2.34.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 177 | dependencies = [ 178 | "ansi_term", 179 | "atty", 180 | "bitflags", 181 | "strsim", 182 | "textwrap", 183 | "unicode-width", 184 | "vec_map", 185 | ] 186 | 187 | [[package]] 188 | name = "cloudabi" 189 | version = "0.0.3" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 192 | dependencies = [ 193 | "bitflags", 194 | ] 195 | 196 | [[package]] 197 | name = "core-foundation-sys" 198 | version = "0.8.7" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 201 | 202 | [[package]] 203 | name = "device-blocker" 204 | version = "0.1.0" 205 | dependencies = [ 206 | "checksum", 207 | "chrono", 208 | "clap", 209 | "error-chain", 210 | "iron", 211 | "juniper", 212 | "juniper_codegen", 213 | "juniper_iron", 214 | "params", 215 | "router", 216 | "serde", 217 | "serde_derive", 218 | "serde_json", 219 | "time", 220 | "urlencoded", 221 | ] 222 | 223 | [[package]] 224 | name = "error-chain" 225 | version = "0.7.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "318cb3c71ee4cdea69fdc9e15c173b245ed6063e1709029e8fd32525a881120f" 228 | dependencies = [ 229 | "backtrace", 230 | ] 231 | 232 | [[package]] 233 | name = "fnv" 234 | version = "1.0.7" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 237 | 238 | [[package]] 239 | name = "fuchsia-cprng" 240 | version = "0.1.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 243 | 244 | [[package]] 245 | name = "gimli" 246 | version = "0.31.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 249 | 250 | [[package]] 251 | name = "hermit-abi" 252 | version = "0.1.19" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 255 | dependencies = [ 256 | "libc", 257 | ] 258 | 259 | [[package]] 260 | name = "hermit-abi" 261 | version = "0.3.9" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 264 | 265 | [[package]] 266 | name = "httparse" 267 | version = "1.9.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 270 | 271 | [[package]] 272 | name = "hyper" 273 | version = "0.10.16" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" 276 | dependencies = [ 277 | "base64", 278 | "httparse", 279 | "language-tags", 280 | "log 0.3.9", 281 | "mime", 282 | "num_cpus", 283 | "time", 284 | "traitobject", 285 | "typeable", 286 | "unicase", 287 | "url", 288 | ] 289 | 290 | [[package]] 291 | name = "iana-time-zone" 292 | version = "0.1.61" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 295 | dependencies = [ 296 | "android_system_properties", 297 | "core-foundation-sys", 298 | "iana-time-zone-haiku", 299 | "js-sys", 300 | "wasm-bindgen", 301 | "windows-core", 302 | ] 303 | 304 | [[package]] 305 | name = "iana-time-zone-haiku" 306 | version = "0.1.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 309 | dependencies = [ 310 | "cc", 311 | ] 312 | 313 | [[package]] 314 | name = "idna" 315 | version = "0.1.5" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" 318 | dependencies = [ 319 | "matches", 320 | "unicode-bidi", 321 | "unicode-normalization", 322 | ] 323 | 324 | [[package]] 325 | name = "iron" 326 | version = "0.6.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "c6d308ca2d884650a8bf9ed2ff4cb13fbb2207b71f64cda11dc9b892067295e8" 329 | dependencies = [ 330 | "hyper", 331 | "log 0.3.9", 332 | "mime_guess", 333 | "modifier", 334 | "num_cpus", 335 | "plugin", 336 | "typemap", 337 | "url", 338 | ] 339 | 340 | [[package]] 341 | name = "itoa" 342 | version = "1.0.11" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 345 | 346 | [[package]] 347 | name = "js-sys" 348 | version = "0.3.72" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 351 | dependencies = [ 352 | "wasm-bindgen", 353 | ] 354 | 355 | [[package]] 356 | name = "juniper" 357 | version = "0.9.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "bc520ae5efce621611ad03aa0ad6ebec0aabc60efa1e47df7d835609c079dd31" 360 | dependencies = [ 361 | "chrono", 362 | "fnv", 363 | "juniper_codegen", 364 | "ordermap", 365 | "serde", 366 | "serde_derive", 367 | "url", 368 | "uuid", 369 | ] 370 | 371 | [[package]] 372 | name = "juniper_codegen" 373 | version = "0.9.2" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "d2605e2fd568ff0ad62e2e6ca985950bbe53708c0e75b08d4fc640f05a564c9e" 376 | dependencies = [ 377 | "quote 0.3.15", 378 | "syn 0.11.11", 379 | ] 380 | 381 | [[package]] 382 | name = "juniper_iron" 383 | version = "0.1.2" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "5da68bbf6ee85b0988345da820e9ecd687cbafa53ed6b9543f5d97b0ebfc0525" 386 | dependencies = [ 387 | "iron", 388 | "juniper", 389 | "serde", 390 | "serde_json", 391 | "urlencoded", 392 | ] 393 | 394 | [[package]] 395 | name = "language-tags" 396 | version = "0.2.2" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" 399 | 400 | [[package]] 401 | name = "libc" 402 | version = "0.2.161" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 405 | 406 | [[package]] 407 | name = "log" 408 | version = "0.3.9" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 411 | dependencies = [ 412 | "log 0.4.22", 413 | ] 414 | 415 | [[package]] 416 | name = "log" 417 | version = "0.4.22" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 420 | 421 | [[package]] 422 | name = "matches" 423 | version = "0.1.10" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 426 | 427 | [[package]] 428 | name = "memchr" 429 | version = "1.0.2" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" 432 | dependencies = [ 433 | "libc", 434 | ] 435 | 436 | [[package]] 437 | name = "memchr" 438 | version = "2.7.4" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 441 | 442 | [[package]] 443 | name = "mime" 444 | version = "0.2.6" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" 447 | dependencies = [ 448 | "log 0.3.9", 449 | ] 450 | 451 | [[package]] 452 | name = "mime_guess" 453 | version = "1.8.8" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" 456 | dependencies = [ 457 | "mime", 458 | "phf", 459 | "phf_codegen", 460 | "unicase", 461 | ] 462 | 463 | [[package]] 464 | name = "miniz_oxide" 465 | version = "0.8.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 468 | dependencies = [ 469 | "adler2", 470 | ] 471 | 472 | [[package]] 473 | name = "modifier" 474 | version = "0.1.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" 477 | 478 | [[package]] 479 | name = "multipart" 480 | version = "0.13.6" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "92f54eb45230c3aa20864ccf0c277eeaeadcf5e437e91731db498dbf7fbe0ec6" 483 | dependencies = [ 484 | "buf_redux", 485 | "httparse", 486 | "log 0.3.9", 487 | "mime", 488 | "mime_guess", 489 | "rand 0.3.23", 490 | "safemem 0.2.0", 491 | "tempdir", 492 | "twoway", 493 | ] 494 | 495 | [[package]] 496 | name = "num" 497 | version = "0.1.42" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 500 | dependencies = [ 501 | "num-bigint", 502 | "num-complex", 503 | "num-integer", 504 | "num-iter", 505 | "num-rational", 506 | "num-traits", 507 | ] 508 | 509 | [[package]] 510 | name = "num-bigint" 511 | version = "0.1.44" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" 514 | dependencies = [ 515 | "num-integer", 516 | "num-traits", 517 | "rand 0.4.6", 518 | "rustc-serialize", 519 | ] 520 | 521 | [[package]] 522 | name = "num-complex" 523 | version = "0.1.43" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" 526 | dependencies = [ 527 | "num-traits", 528 | "rustc-serialize", 529 | ] 530 | 531 | [[package]] 532 | name = "num-integer" 533 | version = "0.1.46" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 536 | dependencies = [ 537 | "num-traits", 538 | ] 539 | 540 | [[package]] 541 | name = "num-iter" 542 | version = "0.1.45" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 545 | dependencies = [ 546 | "autocfg 1.4.0", 547 | "num-integer", 548 | "num-traits", 549 | ] 550 | 551 | [[package]] 552 | name = "num-rational" 553 | version = "0.1.42" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" 556 | dependencies = [ 557 | "num-bigint", 558 | "num-integer", 559 | "num-traits", 560 | "rustc-serialize", 561 | ] 562 | 563 | [[package]] 564 | name = "num-traits" 565 | version = "0.2.19" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 568 | dependencies = [ 569 | "autocfg 1.4.0", 570 | ] 571 | 572 | [[package]] 573 | name = "num_cpus" 574 | version = "1.16.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 577 | dependencies = [ 578 | "hermit-abi 0.3.9", 579 | "libc", 580 | ] 581 | 582 | [[package]] 583 | name = "object" 584 | version = "0.36.5" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 587 | dependencies = [ 588 | "memchr 2.7.4", 589 | ] 590 | 591 | [[package]] 592 | name = "once_cell" 593 | version = "1.20.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 596 | 597 | [[package]] 598 | name = "ordermap" 599 | version = "0.2.13" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "b81cf3b8cb96aa0e73bbedfcdc9708d09fec2854ba8d474be4e6f666d7379e8b" 602 | dependencies = [ 603 | "serde", 604 | ] 605 | 606 | [[package]] 607 | name = "params" 608 | version = "0.8.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "c789fdad2cfdaa551ea0e3a9eadb74c5d634968a9fb3a8c767d89be470d21589" 611 | dependencies = [ 612 | "bodyparser", 613 | "iron", 614 | "multipart", 615 | "num", 616 | "plugin", 617 | "serde_json", 618 | "tempdir", 619 | "urlencoded", 620 | ] 621 | 622 | [[package]] 623 | name = "percent-encoding" 624 | version = "1.0.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" 627 | 628 | [[package]] 629 | name = "persistent" 630 | version = "0.4.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" 633 | dependencies = [ 634 | "iron", 635 | "plugin", 636 | ] 637 | 638 | [[package]] 639 | name = "phf" 640 | version = "0.7.24" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" 643 | dependencies = [ 644 | "phf_shared", 645 | ] 646 | 647 | [[package]] 648 | name = "phf_codegen" 649 | version = "0.7.24" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" 652 | dependencies = [ 653 | "phf_generator", 654 | "phf_shared", 655 | ] 656 | 657 | [[package]] 658 | name = "phf_generator" 659 | version = "0.7.24" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" 662 | dependencies = [ 663 | "phf_shared", 664 | "rand 0.6.5", 665 | ] 666 | 667 | [[package]] 668 | name = "phf_shared" 669 | version = "0.7.24" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" 672 | dependencies = [ 673 | "siphasher", 674 | "unicase", 675 | ] 676 | 677 | [[package]] 678 | name = "plugin" 679 | version = "0.2.6" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" 682 | dependencies = [ 683 | "typemap", 684 | ] 685 | 686 | [[package]] 687 | name = "proc-macro2" 688 | version = "1.0.89" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 691 | dependencies = [ 692 | "unicode-ident", 693 | ] 694 | 695 | [[package]] 696 | name = "quote" 697 | version = "0.3.15" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 700 | 701 | [[package]] 702 | name = "quote" 703 | version = "1.0.37" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 706 | dependencies = [ 707 | "proc-macro2", 708 | ] 709 | 710 | [[package]] 711 | name = "rand" 712 | version = "0.3.23" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 715 | dependencies = [ 716 | "libc", 717 | "rand 0.4.6", 718 | ] 719 | 720 | [[package]] 721 | name = "rand" 722 | version = "0.4.6" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 725 | dependencies = [ 726 | "fuchsia-cprng", 727 | "libc", 728 | "rand_core 0.3.1", 729 | "rdrand", 730 | "winapi", 731 | ] 732 | 733 | [[package]] 734 | name = "rand" 735 | version = "0.6.5" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 738 | dependencies = [ 739 | "autocfg 0.1.8", 740 | "libc", 741 | "rand_chacha", 742 | "rand_core 0.4.2", 743 | "rand_hc", 744 | "rand_isaac", 745 | "rand_jitter", 746 | "rand_os", 747 | "rand_pcg", 748 | "rand_xorshift", 749 | "winapi", 750 | ] 751 | 752 | [[package]] 753 | name = "rand_chacha" 754 | version = "0.1.1" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 757 | dependencies = [ 758 | "autocfg 0.1.8", 759 | "rand_core 0.3.1", 760 | ] 761 | 762 | [[package]] 763 | name = "rand_core" 764 | version = "0.3.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 767 | dependencies = [ 768 | "rand_core 0.4.2", 769 | ] 770 | 771 | [[package]] 772 | name = "rand_core" 773 | version = "0.4.2" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 776 | 777 | [[package]] 778 | name = "rand_hc" 779 | version = "0.1.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 782 | dependencies = [ 783 | "rand_core 0.3.1", 784 | ] 785 | 786 | [[package]] 787 | name = "rand_isaac" 788 | version = "0.1.1" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 791 | dependencies = [ 792 | "rand_core 0.3.1", 793 | ] 794 | 795 | [[package]] 796 | name = "rand_jitter" 797 | version = "0.1.4" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 800 | dependencies = [ 801 | "libc", 802 | "rand_core 0.4.2", 803 | "winapi", 804 | ] 805 | 806 | [[package]] 807 | name = "rand_os" 808 | version = "0.1.3" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 811 | dependencies = [ 812 | "cloudabi", 813 | "fuchsia-cprng", 814 | "libc", 815 | "rand_core 0.4.2", 816 | "rdrand", 817 | "winapi", 818 | ] 819 | 820 | [[package]] 821 | name = "rand_pcg" 822 | version = "0.1.2" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 825 | dependencies = [ 826 | "autocfg 0.1.8", 827 | "rand_core 0.4.2", 828 | ] 829 | 830 | [[package]] 831 | name = "rand_xorshift" 832 | version = "0.1.1" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 835 | dependencies = [ 836 | "rand_core 0.3.1", 837 | ] 838 | 839 | [[package]] 840 | name = "rdrand" 841 | version = "0.4.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 844 | dependencies = [ 845 | "rand_core 0.3.1", 846 | ] 847 | 848 | [[package]] 849 | name = "remove_dir_all" 850 | version = "0.5.3" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 853 | dependencies = [ 854 | "winapi", 855 | ] 856 | 857 | [[package]] 858 | name = "route-recognizer" 859 | version = "0.1.13" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "ea509065eb0b3c446acdd0102f0d46567dc30902dc0be91d6552035d92b0f4f8" 862 | 863 | [[package]] 864 | name = "router" 865 | version = "0.6.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "dc63b6f3b8895b0d04e816b2b1aa58fdba2d5acca3cbb8f0ab8e017347d57397" 868 | dependencies = [ 869 | "iron", 870 | "route-recognizer", 871 | "url", 872 | ] 873 | 874 | [[package]] 875 | name = "rustc-demangle" 876 | version = "0.1.24" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 879 | 880 | [[package]] 881 | name = "rustc-serialize" 882 | version = "0.3.25" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" 885 | 886 | [[package]] 887 | name = "ryu" 888 | version = "1.0.18" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 891 | 892 | [[package]] 893 | name = "safemem" 894 | version = "0.2.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" 897 | 898 | [[package]] 899 | name = "safemem" 900 | version = "0.3.3" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 903 | 904 | [[package]] 905 | name = "serde" 906 | version = "1.0.214" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 909 | dependencies = [ 910 | "serde_derive", 911 | ] 912 | 913 | [[package]] 914 | name = "serde_derive" 915 | version = "1.0.214" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 918 | dependencies = [ 919 | "proc-macro2", 920 | "quote 1.0.37", 921 | "syn 2.0.87", 922 | ] 923 | 924 | [[package]] 925 | name = "serde_json" 926 | version = "1.0.132" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 929 | dependencies = [ 930 | "itoa", 931 | "memchr 2.7.4", 932 | "ryu", 933 | "serde", 934 | ] 935 | 936 | [[package]] 937 | name = "shlex" 938 | version = "1.3.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 941 | 942 | [[package]] 943 | name = "siphasher" 944 | version = "0.2.3" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" 947 | 948 | [[package]] 949 | name = "strsim" 950 | version = "0.8.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 953 | 954 | [[package]] 955 | name = "syn" 956 | version = "0.11.11" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 959 | dependencies = [ 960 | "quote 0.3.15", 961 | "synom", 962 | "unicode-xid", 963 | ] 964 | 965 | [[package]] 966 | name = "syn" 967 | version = "2.0.87" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 970 | dependencies = [ 971 | "proc-macro2", 972 | "quote 1.0.37", 973 | "unicode-ident", 974 | ] 975 | 976 | [[package]] 977 | name = "synom" 978 | version = "0.11.3" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 981 | dependencies = [ 982 | "unicode-xid", 983 | ] 984 | 985 | [[package]] 986 | name = "tempdir" 987 | version = "0.3.7" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 990 | dependencies = [ 991 | "rand 0.4.6", 992 | "remove_dir_all", 993 | ] 994 | 995 | [[package]] 996 | name = "textwrap" 997 | version = "0.11.0" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1000 | dependencies = [ 1001 | "unicode-width", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "time" 1006 | version = "0.1.45" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1009 | dependencies = [ 1010 | "libc", 1011 | "wasi", 1012 | "winapi", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "tinyvec" 1017 | version = "1.8.0" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1020 | dependencies = [ 1021 | "tinyvec_macros", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "tinyvec_macros" 1026 | version = "0.1.1" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1029 | 1030 | [[package]] 1031 | name = "traitobject" 1032 | version = "0.1.0" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" 1035 | 1036 | [[package]] 1037 | name = "twoway" 1038 | version = "0.1.8" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 1041 | dependencies = [ 1042 | "memchr 2.7.4", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "typeable" 1047 | version = "0.1.2" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" 1050 | 1051 | [[package]] 1052 | name = "typemap" 1053 | version = "0.3.3" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" 1056 | dependencies = [ 1057 | "unsafe-any", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "unicase" 1062 | version = "1.4.2" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" 1065 | dependencies = [ 1066 | "version_check", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "unicode-bidi" 1071 | version = "0.3.17" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 1074 | 1075 | [[package]] 1076 | name = "unicode-ident" 1077 | version = "1.0.13" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1080 | 1081 | [[package]] 1082 | name = "unicode-normalization" 1083 | version = "0.1.24" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1086 | dependencies = [ 1087 | "tinyvec", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "unicode-width" 1092 | version = "0.1.14" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1095 | 1096 | [[package]] 1097 | name = "unicode-xid" 1098 | version = "0.0.4" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 1101 | 1102 | [[package]] 1103 | name = "unsafe-any" 1104 | version = "0.4.2" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" 1107 | dependencies = [ 1108 | "traitobject", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "url" 1113 | version = "1.7.2" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" 1116 | dependencies = [ 1117 | "idna", 1118 | "matches", 1119 | "percent-encoding", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "urlencoded" 1124 | version = "0.6.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" 1127 | dependencies = [ 1128 | "bodyparser", 1129 | "iron", 1130 | "plugin", 1131 | "url", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "uuid" 1136 | version = "0.5.1" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" 1139 | 1140 | [[package]] 1141 | name = "vec_map" 1142 | version = "0.8.2" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1145 | 1146 | [[package]] 1147 | name = "version_check" 1148 | version = "0.1.5" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1151 | 1152 | [[package]] 1153 | name = "wasi" 1154 | version = "0.10.0+wasi-snapshot-preview1" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1157 | 1158 | [[package]] 1159 | name = "wasm-bindgen" 1160 | version = "0.2.95" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 1163 | dependencies = [ 1164 | "cfg-if", 1165 | "once_cell", 1166 | "wasm-bindgen-macro", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "wasm-bindgen-backend" 1171 | version = "0.2.95" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 1174 | dependencies = [ 1175 | "bumpalo", 1176 | "log 0.4.22", 1177 | "once_cell", 1178 | "proc-macro2", 1179 | "quote 1.0.37", 1180 | "syn 2.0.87", 1181 | "wasm-bindgen-shared", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "wasm-bindgen-macro" 1186 | version = "0.2.95" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 1189 | dependencies = [ 1190 | "quote 1.0.37", 1191 | "wasm-bindgen-macro-support", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "wasm-bindgen-macro-support" 1196 | version = "0.2.95" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 1199 | dependencies = [ 1200 | "proc-macro2", 1201 | "quote 1.0.37", 1202 | "syn 2.0.87", 1203 | "wasm-bindgen-backend", 1204 | "wasm-bindgen-shared", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "wasm-bindgen-shared" 1209 | version = "0.2.95" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1212 | 1213 | [[package]] 1214 | name = "winapi" 1215 | version = "0.3.9" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1218 | dependencies = [ 1219 | "winapi-i686-pc-windows-gnu", 1220 | "winapi-x86_64-pc-windows-gnu", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "winapi-i686-pc-windows-gnu" 1225 | version = "0.4.0" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1228 | 1229 | [[package]] 1230 | name = "winapi-x86_64-pc-windows-gnu" 1231 | version = "0.4.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1234 | 1235 | [[package]] 1236 | name = "windows-core" 1237 | version = "0.52.0" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1240 | dependencies = [ 1241 | "windows-targets", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "windows-targets" 1246 | version = "0.52.6" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1249 | dependencies = [ 1250 | "windows_aarch64_gnullvm", 1251 | "windows_aarch64_msvc", 1252 | "windows_i686_gnu", 1253 | "windows_i686_gnullvm", 1254 | "windows_i686_msvc", 1255 | "windows_x86_64_gnu", 1256 | "windows_x86_64_gnullvm", 1257 | "windows_x86_64_msvc", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "windows_aarch64_gnullvm" 1262 | version = "0.52.6" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1265 | 1266 | [[package]] 1267 | name = "windows_aarch64_msvc" 1268 | version = "0.52.6" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1271 | 1272 | [[package]] 1273 | name = "windows_i686_gnu" 1274 | version = "0.52.6" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1277 | 1278 | [[package]] 1279 | name = "windows_i686_gnullvm" 1280 | version = "0.52.6" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1283 | 1284 | [[package]] 1285 | name = "windows_i686_msvc" 1286 | version = "0.52.6" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1289 | 1290 | [[package]] 1291 | name = "windows_x86_64_gnu" 1292 | version = "0.52.6" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1295 | 1296 | [[package]] 1297 | name = "windows_x86_64_gnullvm" 1298 | version = "0.52.6" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1301 | 1302 | [[package]] 1303 | name = "windows_x86_64_msvc" 1304 | version = "0.52.6" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1307 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "device-blocker" 3 | version = "0.1.0" 4 | authors = ["darrinth@gmail.com"] 5 | 6 | [dependencies] 7 | error-chain = {version = "0.7"} 8 | serde = {version = "1.0"} 9 | serde_derive = {version = "1.0"} 10 | serde_json = {version = "1.0"} 11 | iron = {version = "0.6"} 12 | router = {version = "0.6"} 13 | params = {version = "0.8"} 14 | chrono = {version = "0.4", features = ["serde"]} 15 | time = {version = "0.1"} 16 | clap = {version = "2.19"} 17 | checksum = {version = "0.2"} 18 | juniper = {version = "0.9"} 19 | juniper_codegen = {version = "0.9"} 20 | juniper_iron = {version = "0.1"} 21 | urlencoded = {version = "0.6"} 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Device Blocker 2 | ============== 3 | 4 | Limit screen time to children's various mobile devices by blocking internet 5 | access on the family Wifi router. 6 | 7 | This is the server which runs on the WiFi router. It has an API which lists 8 | devices, opens and closes internet access for devices, and discovers new 9 | devices on the router. 10 | 11 | I run this on Lede 17.01. 12 | 13 | It has a companion UI project: 14 | [device-blocker-ui](https://github.com/darrint/device-blocker-ui) 15 | 16 | Build/Installation 17 | ================== 18 | 19 | Install Lede on a router and also download the correct Lede SDK for your 20 | router. 21 | 22 | Add the SDK to your path. For example: 23 | 24 | `PATH=~/Downloads/lede-sdk-17.01.2-x86-64_gcc-5.4.0_musl-1.1.16.Linux-x86_64/staging_dir/toolchain-x86_64_gcc-5.4.0_musl-1.1.16/bin:$PATH` 25 | 26 | Build the companion UI project: 27 | [device-blocker-ui](https://github.com/darrint/device-blocker-ui) 28 | 29 | ``` 30 | # ... in the device-blocker-ui directory 31 | npm install -g yarn 32 | yarn 33 | webpack --optimize-minimize --define process.env.NODE_ENV="'production'" 34 | ``` 35 | 36 | Next copy the resulting bundle.js and index.html files to this project's `src` 37 | directory. 38 | 39 | Use rustup to install a stable compiler for the architecture appropriate for 40 | your router. 41 | 42 | `cargo build --target x86_64-unknown-linux-musl --release` 43 | 44 | Edit and copy some files from this project to your router: 45 | 46 | * Edit and copy `known_devices.json` to `/etc/` 47 | * Edit and copy `device_blocker.procd` to `/etc/init.d/device_blocker` 48 | * Copy `target/x86_64-unknown-linux-musl/release/device-blocker` to `/root/` 49 | * Make sure both `/root/device-blocker` and `/etc/init.d/device_blocker` are executable. 50 | 51 | Use the LuCi interface to enable and start the service: 52 | 53 | * Go to System -> Startup. 54 | * Find device-blocker. 55 | * Enable the service. 56 | * Start the service. 57 | 58 | ToDo 59 | ==== 60 | * Write files atomically. 61 | * Figure out how to build this project with Xargo and deploy to mips and other router architecture. 62 | * Security? Require a login similar to OpenWRT's LuCi 63 | 64 | License 65 | ======= 66 | 67 | MIT License 68 | 69 | Copyright (c) 2017 Darrin Thompson 70 | 71 | Permission is hereby granted, free of charge, to any person obtaining a copy 72 | of this software and associated documentation files (the "Software"), to deal 73 | in the Software without restriction, including without limitation the rights 74 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 75 | copies of the Software, and to permit persons to whom the Software is 76 | furnished to do so, subject to the following conditions: 77 | 78 | The above copyright notice and this permission notice shall be included in all 79 | copies or substantial portions of the Software. 80 | 81 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 82 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 83 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 84 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 85 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 86 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 87 | SOFTWARE. 88 | 89 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } 5 | ) { 6 | src = ./.; 7 | }).defaultNix 8 | -------------------------------------------------------------------------------- /device_blocker.procd: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | 5 | USE_PROCD=1 6 | 7 | BIN=/root/device-blocker 8 | CONFIG=/etc/known_devices.json 9 | 10 | start_service() { 11 | procd_open_instance 12 | procd_set_param command $BIN -c $CONFIG 13 | procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} 14 | procd_close_instance 15 | } 16 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1730702146, 12 | "narHash": "sha256-a657FU8MS5m0Y4pQvcmQPfvXYOPpxih7u2hU57Bn2i4=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "fa3610f841725c8e20fc0fab070ee60609fdd5ee", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-utils": { 25 | "inputs": { 26 | "systems": "systems" 27 | }, 28 | "locked": { 29 | "lastModified": 1726560853, 30 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "numtide", 38 | "repo": "flake-utils", 39 | "type": "github" 40 | } 41 | }, 42 | "naersk": { 43 | "inputs": { 44 | "nixpkgs": [ 45 | "nixpkgs" 46 | ] 47 | }, 48 | "locked": { 49 | "lastModified": 1721727458, 50 | "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", 51 | "owner": "nix-community", 52 | "repo": "naersk", 53 | "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "nix-community", 58 | "repo": "naersk", 59 | "type": "github" 60 | } 61 | }, 62 | "nixpkgs": { 63 | "locked": { 64 | "lastModified": 1730602179, 65 | "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", 66 | "owner": "NixOS", 67 | "repo": "nixpkgs", 68 | "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "id": "nixpkgs", 73 | "ref": "nixos-24.05", 74 | "type": "indirect" 75 | } 76 | }, 77 | "root": { 78 | "inputs": { 79 | "fenix": "fenix", 80 | "flake-utils": "flake-utils", 81 | "naersk": "naersk", 82 | "nixpkgs": "nixpkgs" 83 | } 84 | }, 85 | "rust-analyzer-src": { 86 | "flake": false, 87 | "locked": { 88 | "lastModified": 1730645367, 89 | "narHash": "sha256-RnmBO+9zmZ3NpU6+NfYUDRg31dsPZ17xUqXVw/ZOKZ8=", 90 | "owner": "rust-lang", 91 | "repo": "rust-analyzer", 92 | "rev": "e44691a60443f1246a077df659607ca89f2ddc58", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "rust-lang", 97 | "ref": "nightly", 98 | "repo": "rust-analyzer", 99 | "type": "github" 100 | } 101 | }, 102 | "systems": { 103 | "locked": { 104 | "lastModified": 1681028828, 105 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 106 | "owner": "nix-systems", 107 | "repo": "default", 108 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 109 | "type": "github" 110 | }, 111 | "original": { 112 | "owner": "nix-systems", 113 | "repo": "default", 114 | "type": "github" 115 | } 116 | } 117 | }, 118 | "root": "root", 119 | "version": 7 120 | } 121 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | fenix = { 4 | url = "github:nix-community/fenix"; 5 | inputs.nixpkgs.follows = "nixpkgs"; 6 | }; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | naersk = { 9 | url = "github:nix-community/naersk"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | nixpkgs.url = "nixpkgs/nixos-24.05"; 13 | }; 14 | 15 | outputs = { self, fenix, flake-utils, naersk, nixpkgs }: 16 | flake-utils.lib.eachDefaultSystem (system: { 17 | packages.default = 18 | let 19 | pkgs = nixpkgs.legacyPackages.${system}; 20 | target = "x86_64-unknown-linux-musl"; 21 | toolchain = with fenix.packages.${system}; combine [ 22 | minimal.cargo 23 | minimal.rustc 24 | targets.${target}.latest.rust-std 25 | ]; 26 | in 27 | 28 | (naersk.lib.${system}.override { 29 | cargo = toolchain; 30 | rustc = toolchain; 31 | }).buildPackage { 32 | src = ./.; 33 | CARGO_BUILD_TARGET = target; 34 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 35 | let 36 | inherit (pkgs.pkgsCross.aarch64-multiplatform.stdenv) cc; 37 | in 38 | "${cc}/bin/${cc.targetPrefix}cc"; 39 | }; 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /known_devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "exit_interfaces": [ 3 | "eth0" 4 | ], 5 | "state_file": "/tmp/device-world.json", 6 | "dhcp_lease_file": "/tmp/dhcp.leases", 7 | "known_devices": [] 8 | } 9 | -------------------------------------------------------------------------------- /src/app_server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::sync::{Arc, Mutex, Condvar}; 3 | use std::fs::File; 4 | use std::io::{BufReader, BufRead}; 5 | use std::ops::DerefMut; 6 | use files::{write_json_file, read_json_file}; 7 | use schedule::{World, Device, ScheduleEntry, GuestPath, DeviceOverride}; 8 | use script_handler::ScriptHandler; 9 | use config::{Config, reconcile_config}; 10 | use chrono::{DateTime, Utc}; 11 | use time::Duration; 12 | use ::script_handler::HandleScript; 13 | use ::script::write_script; 14 | use errors::{Result, ResultExt, ErrorKind}; 15 | 16 | pub type AppServerWrapped = Arc>; 17 | 18 | pub fn new_wrapped_scheduler(wrapped_server: AppServerWrapped) -> AppServerScheduler { 19 | AppServerScheduler { 20 | wrapped_server, 21 | condvar: Condvar::new(), 22 | } 23 | } 24 | 25 | pub struct AppServerScheduler { 26 | pub wrapped_server: AppServerWrapped, 27 | condvar: Condvar, 28 | } 29 | 30 | pub type AppServerSchedulerWrapped = Arc; 31 | 32 | pub trait Scheduler { 33 | fn kick_scheduler(self); 34 | } 35 | 36 | impl Scheduler for AppServerSchedulerWrapped { 37 | fn kick_scheduler(self) { 38 | let scheduler = &self; 39 | scheduler.condvar.notify_one(); 40 | } 41 | } 42 | 43 | pub fn run_expiration(wrapped_scheduler: &mut AppServerSchedulerWrapped) { 44 | let condvar = &wrapped_scheduler.condvar; 45 | let mut guard = wrapped_scheduler.wrapped_server.lock().unwrap(); 46 | loop { 47 | let option_max_date: Option> = { 48 | let world = &guard.world; 49 | world.get_soonest_event_time() 50 | }; 51 | let now : DateTime = Utc::now(); 52 | let dur = option_max_date.map(|max_date| 53 | max_date.signed_duration_since(now)).unwrap_or_else(|| chrono::Duration::days(30)); 54 | let std_dur = dur.to_std().unwrap_or_else(|_| ::std::time::Duration::new(0, 0)); 55 | let (g2, _) = condvar.wait_timeout(guard, std_dur).unwrap(); 56 | guard = g2; 57 | { 58 | { 59 | let world = &mut guard.deref_mut().world; 60 | let now = Utc::now(); 61 | world.expire_bounded(now); 62 | } 63 | guard.refresh_world().unwrap_or_else(|err| println!("{:?}", err)); 64 | }; 65 | } 66 | } 67 | 68 | pub trait RequestErrExt<'a> { 69 | fn require_param(&self, String) -> Result<&'a str>; 70 | } 71 | 72 | impl<'a> RequestErrExt<'a> for Option<&'a str> { 73 | fn require_param(&self, msg: String) -> Result<&'a str> { 74 | self.ok_or(ErrorKind::RequestError(msg).into()) 75 | } 76 | } 77 | 78 | pub struct AppServer { 79 | pub world: World, 80 | pub handler: ScriptHandler, 81 | pub config: Config, 82 | pub config_file: String, 83 | } 84 | 85 | impl AppServer { 86 | pub fn open_device(&mut self, 87 | mac_param: Option<&str>, 88 | time_bound: Option>) 89 | -> Result<()> { 90 | let mac = mac_param.require_param("Missing mac parameter".to_owned())?; 91 | self.world.open_device(mac, time_bound)?; 92 | self.refresh_world() 93 | } 94 | 95 | pub fn close_device(&mut self, mac_param: Option<&str>) -> Result<()> { 96 | let mac = mac_param.require_param("Missing mac parameter".to_owned())?; 97 | self.world.close_device(mac)?; 98 | self.refresh_world() 99 | } 100 | 101 | pub fn set_guest_path(&mut self, 102 | allow_param: Option<&str>, 103 | time_bound: Option>) 104 | -> Result<()> { 105 | let allow_str = allow_param.require_param("Missing allow parameter".to_owned())?; 106 | let allow = if allow_str.to_lowercase() == "true" { 107 | GuestPath::Open 108 | } else { 109 | GuestPath::Closed 110 | }; 111 | self.world.schedule.guest_entry = ScheduleEntry { 112 | item: allow, 113 | time_bound, 114 | }; 115 | self.refresh_world() 116 | } 117 | 118 | pub fn set_device_override(&mut self, 119 | override_param: Option<&str>, 120 | time_bound: Option>) 121 | -> Result<()> { 122 | let override_str = override_param.require_param("Missing override paramter".to_owned())?; 123 | let override_arg = if override_str.to_lowercase() == "null" { 124 | None 125 | } else if override_str.to_lowercase() == "true" { 126 | Some(DeviceOverride::Open) 127 | } else { 128 | Some(DeviceOverride::Closed) 129 | }; 130 | self.world.schedule.override_entry = override_arg.map(|i| { 131 | ScheduleEntry { 132 | item: i, 133 | time_bound, 134 | } 135 | }); 136 | self.refresh_world() 137 | } 138 | 139 | pub fn add_device(&mut self, mac_param: Option<&str>, name_param: Option<&str>) -> Result<()> { 140 | let mac = mac_param.require_param("Missing mac parameter".to_owned())?; 141 | let name = name_param.require_param("Missing name parameter".to_owned())?; 142 | let dev = Device { 143 | mac: mac.to_owned(), 144 | name: name.to_owned(), 145 | }; 146 | self.config.known_devices.insert(dev); 147 | self.write_config()?; 148 | let mut devs = BTreeSet::new(); 149 | read_dhcp_devices(&self.config.dhcp_lease_file, &mut devs)?; 150 | let reconcile_result = { 151 | reconcile_config(&self.config, &devs, &mut self.world) 152 | }; 153 | if reconcile_result.updated_world { 154 | self.refresh_world()?; 155 | } 156 | Ok(()) 157 | } 158 | 159 | pub fn refresh_devices(&mut self) -> Result<()> { 160 | let mut devs = BTreeSet::new(); 161 | read_dhcp_devices(&self.config.dhcp_lease_file, &mut devs)?; 162 | let reconcile_result = { 163 | reconcile_config(&self.config, &devs, &mut self.world) 164 | }; 165 | if reconcile_result.updated_world { 166 | self.refresh_world()?; 167 | } 168 | Ok(()) 169 | } 170 | 171 | fn handle_script(&self) -> Result<()> { 172 | let mut script = String::new(); 173 | write_script(&self.world, 174 | "old_blocked_devices", 175 | "blocked_devices", 176 | &self.config.exit_interfaces, 177 | &mut script); 178 | self.handler.handle(&script) 179 | } 180 | 181 | pub fn refresh_world(&self) -> Result<()> { 182 | self.write_world()?; 183 | self.handle_script() 184 | } 185 | 186 | pub fn write_world(&self) -> Result<()> { 187 | write_json_file(&self.config.state_file, &self.world) 188 | .chain_err(|| "Failed to write new state_file") 189 | } 190 | 191 | fn write_config(&self) -> Result<()> { 192 | write_json_file(&self.config_file, &self.config) 193 | .chain_err(|| "Failed to write new config file") 194 | } 195 | 196 | pub fn read_or_create_world(&mut self) -> Result<()> { 197 | self.world = read_json_file(&self.config.state_file).unwrap_or_default(); 198 | self.write_world() 199 | } 200 | } 201 | 202 | pub fn read_dhcp_devices(dhcp_leases_file: &str, devs: &mut BTreeSet) -> Result<()> { 203 | let reader = 204 | File::open(dhcp_leases_file).chain_err(|| "Failed to open dhcp lease file.")?; 205 | let buf_reader = BufReader::new(reader); 206 | for line_result in buf_reader.lines() { 207 | let line = line_result.chain_err(|| "Failed to read line")?; 208 | let parts: Vec = line.splitn(4, |c: char| c.is_whitespace()) 209 | .into_iter() 210 | .map(|s| s.to_string()) 211 | .collect(); 212 | let mac = &parts[1]; 213 | let name = &parts[3]; 214 | let dev = Device { 215 | mac: mac.clone(), 216 | name: name.clone(), 217 | }; 218 | devs.insert(dev); 219 | } 220 | Ok(()) 221 | } 222 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | pub use ::types::Config; 3 | use schedule::{World, Device}; 4 | 5 | #[derive(Debug, Clone, Eq, PartialEq)] 6 | pub struct ReconcileResult { 7 | pub updated_world: bool, 8 | } 9 | 10 | pub fn reconcile_config(config: &Config, 11 | dhcp_devs: &BTreeSet, 12 | world: &mut World) 13 | -> ReconcileResult { 14 | let mut config_set: BTreeSet = BTreeSet::new(); 15 | for device in &config.known_devices { 16 | config_set.insert(device.clone()); 17 | } 18 | 19 | let mut world_set: BTreeSet = BTreeSet::new(); 20 | for device in &world.closed_devices { 21 | world_set.insert(device.clone()); 22 | } 23 | for device in world.schedule.open_device_entries.iter().map(|e| &e.item) { 24 | world_set.insert(device.clone()); 25 | } 26 | 27 | let config_set = config_set; 28 | let world_set = world_set; 29 | 30 | world.closed_devices = world.closed_devices 31 | .clone() 32 | .into_iter() 33 | .filter(|d| config_set.contains(d)) 34 | .collect(); 35 | 36 | world.schedule.open_device_entries = world.schedule 37 | .open_device_entries 38 | .clone() 39 | .into_iter() 40 | .filter(|e| config_set.contains(&e.item)) 41 | .collect(); 42 | 43 | let new_device_iter = config_set.difference(&world_set); 44 | for new_device in new_device_iter { 45 | world.closed_devices.insert(new_device.clone()); 46 | } 47 | 48 | let known_set: BTreeSet = world.closed_devices 49 | .iter() 50 | .chain(world.schedule.open_device_entries.iter().map(|e| &e.item)) 51 | .map(|d| d.mac.to_uppercase()) 52 | .collect(); 53 | let unknown_devs = dhcp_devs.iter() 54 | .filter(|d| !known_set.contains(&d.mac.to_uppercase())) 55 | .cloned() 56 | .collect(); 57 | world.unknown_devices = unknown_devs; 58 | 59 | ReconcileResult { updated_world: !(config_set == world_set) } 60 | } 61 | 62 | #[cfg(test)] 63 | mod test { 64 | use std::collections::BTreeSet; 65 | use schedule::test::world_fixture; 66 | use config::{reconcile_config, Config, ReconcileResult}; 67 | use schedule::{Device, ScheduleEntry}; 68 | 69 | fn unknown_devs_fixture() -> BTreeSet { 70 | let mut devs = BTreeSet::new(); 71 | devs.insert(Device { 72 | name: "TV2".to_owned(), 73 | mac: "1234".to_owned(), 74 | }); 75 | // Has different case than in world. 76 | devs.insert(Device { 77 | name: "TV3".to_owned(), 78 | mac: "bBbB".to_owned(), 79 | }); 80 | devs.insert(Device { 81 | name: "TV20".to_owned(), 82 | mac: "2020".to_owned(), 83 | }); 84 | devs.insert(Device { 85 | name: "TV21".to_owned(), 86 | mac: "2121".to_owned(), 87 | }); 88 | devs 89 | } 90 | 91 | fn config_fixture() -> Config { 92 | Config { 93 | exit_interfaces: BTreeSet::new(), 94 | dhcp_lease_file: "".to_owned(), 95 | state_file: "".to_owned(), 96 | known_devices: [Device { 97 | name: "TV1".to_owned(), 98 | mac: "5678".to_owned(), 99 | }, 100 | Device { 101 | name: "TV2".to_owned(), 102 | mac: "1234".to_owned(), 103 | }, 104 | Device { 105 | name: "TV3".to_owned(), 106 | mac: "bbbb".to_owned(), 107 | }, 108 | Device { 109 | name: "TV4".to_owned(), 110 | mac: "abcd".to_owned(), 111 | }] 112 | .iter() 113 | .cloned() 114 | .collect(), 115 | } 116 | } 117 | 118 | #[test] 119 | fn no_changes() { 120 | let config = config_fixture(); 121 | let mut world = world_fixture(); 122 | let unknown_devs = BTreeSet::new(); 123 | 124 | let expected_world = world.clone(); 125 | 126 | let result = reconcile_config(&config, &unknown_devs, &mut world); 127 | assert_eq!(ReconcileResult { updated_world: false }, result); 128 | assert_eq!(expected_world, world); 129 | } 130 | 131 | #[test] 132 | fn some_changes() { 133 | let mut config = config_fixture(); 134 | config.known_devices.insert(Device { 135 | name: "TV5".to_owned(), 136 | mac: "xyz".to_owned(), 137 | }); 138 | 139 | let mut world = world_fixture(); 140 | world.schedule.open_device_entries.insert(ScheduleEntry { 141 | item: Device { 142 | name: "TV6".to_owned(), 143 | mac: "qrs".to_owned(), 144 | }, 145 | time_bound: None, 146 | }); 147 | let tv2_item = world.schedule 148 | .open_device_entries 149 | .iter() 150 | .find(|e| e.item.name == "TV2") 151 | .unwrap() 152 | .clone(); 153 | world.schedule.open_device_entries.remove(&tv2_item); 154 | world.closed_devices.insert(Device { 155 | name: "TV7".to_owned(), 156 | mac: "tuv".to_owned(), 157 | }); 158 | 159 | let unknown_devs = unknown_devs_fixture(); 160 | 161 | let mut expected_world = world_fixture(); 162 | let tv2_item = expected_world.schedule 163 | .open_device_entries 164 | .iter() 165 | .find(|e| e.item.name == "TV2") 166 | .unwrap() 167 | .clone(); 168 | expected_world.schedule.open_device_entries.remove(&tv2_item); 169 | expected_world.closed_devices.insert(Device { 170 | name: "TV5".to_owned(), 171 | mac: "xyz".to_owned(), 172 | }); 173 | expected_world.closed_devices.insert(Device { 174 | name: "TV2".to_owned(), 175 | mac: "1234".to_owned(), 176 | }); 177 | expected_world.unknown_devices.insert(Device { 178 | name: "TV20".to_owned(), 179 | mac: "2020".to_owned(), 180 | }); 181 | expected_world.unknown_devices.insert(Device { 182 | name: "TV21".to_owned(), 183 | mac: "2121".to_owned(), 184 | }); 185 | 186 | let result = reconcile_config(&config, &unknown_devs, &mut world); 187 | assert_eq!(ReconcileResult { updated_world: true }, result); 188 | println!("{:#?}\n{:#?}", expected_world, world); 189 | assert_eq!(expected_world, world); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::ser::Serialize; 3 | use serde_json; 4 | use std::fs::File; 5 | 6 | use errors::{Result, ResultExt}; 7 | 8 | pub fn write_json_file(file_name: &str, obj: &T) -> Result<()> { 9 | let mut writer = 10 | File::create(file_name).chain_err(|| format!("Failed to open {} for writing", file_name))?; 11 | serde_json::to_writer_pretty(&mut writer, obj) 12 | .chain_err(|| format!("Failed to write json file {}", file_name)) 13 | } 14 | 15 | pub fn read_json_file(file_name: &str) -> Result { 16 | serde_json::from_reader( 17 | File::open(file_name) 18 | .chain_err(|| format!("Failed to open {}", file_name))?) 19 | .chain_err(|| format!("Failed to read json file {}", file_name)) 20 | } 21 | -------------------------------------------------------------------------------- /src/graphql.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::ops::DerefMut; 3 | use iron::{Request, Response, IronResult, IronError, Handler}; 4 | use iron::Plugin; 5 | use iron::status; 6 | use iron::method; 7 | use iron::mime::Mime; 8 | use iron::Error; 9 | use juniper::{RootNode, InputValue}; 10 | use juniper::http; 11 | use serde_json; 12 | use urlencoded::{UrlEncodedQuery}; 13 | 14 | use app_server::AppServerSchedulerWrapped; 15 | use types::World; 16 | use errors::ErrorKind; 17 | 18 | impl ::iron::Error for ErrorKind { 19 | fn description(&self) -> &str { 20 | self.description() 21 | } 22 | } 23 | 24 | pub struct QueryRoot; 25 | 26 | graphql_object!(QueryRoot: AppServerSchedulerWrapped |&self| { 27 | field world(&executor) -> World { 28 | let app_server_scheduler_wrapped = executor.context(); 29 | let mut guard = app_server_scheduler_wrapped 30 | .wrapped_server.lock().unwrap(); 31 | let app_server = guard.deref_mut(); 32 | 33 | app_server.world.clone() 34 | } 35 | }); 36 | 37 | pub struct MutationRoot; 38 | 39 | graphql_object!(MutationRoot: AppServerSchedulerWrapped |&self| { 40 | field foo() -> String { 41 | "Bar".to_owned() 42 | } 43 | }); 44 | 45 | fn iron_err(msg: &str) -> IronError { 46 | IronError::new(ErrorKind::RequestError(msg.to_owned()), status::BadRequest) 47 | } 48 | 49 | // Lifted from juniper iron. 50 | fn get_single_value(mut values: Vec) -> IronResult { 51 | if values.len() == 1 { 52 | Ok(values.remove(0)) 53 | } else { 54 | Err(iron_err("duplicate or missing url param")) 55 | } 56 | } 57 | 58 | // Lifted from juniper iron. 59 | fn parse_url_param(params: Option>) -> IronResult> { 60 | if let Some(values) = params { 61 | get_single_value(values).map(Some) 62 | } else { 63 | Ok(None) 64 | } 65 | } 66 | 67 | // Lifted from juniper iron. 68 | fn parse_variable_param(params: Option>) -> IronResult> { 69 | if let Some(values) = params { 70 | Ok( 71 | serde_json::from_str::(get_single_value(values)?.as_ref()) 72 | .map(Some).map_err(|e| iron_err(e.description()))? 73 | ) 74 | } else { 75 | Ok(None) 76 | } 77 | } 78 | 79 | pub struct GraphQLHandler<'a> { 80 | app_server_scheduler_wrapped: AppServerSchedulerWrapped, 81 | root_node: RootNode<'a, QueryRoot, MutationRoot>, 82 | } 83 | 84 | // Lifted from juniper_iron 85 | impl <'a> GraphQLHandler<'a> 86 | { 87 | /// Build a new GraphQL handler 88 | /// 89 | /// The context factory will receive the Iron request object and is 90 | /// expected to construct a context object for the given schema. This can 91 | /// be used to construct e.g. database connections or similar data that 92 | /// the schema needs to execute the query. 93 | pub fn new(app_server_scheduler_wrapped: AppServerSchedulerWrapped, query: QueryRoot, mutation: MutationRoot) -> Self { 94 | GraphQLHandler { 95 | app_server_scheduler_wrapped, 96 | root_node: RootNode::new(query, mutation), 97 | } 98 | } 99 | 100 | fn handle_get(&self, req: &mut Request) -> IronResult { 101 | let url_query_string = req.get_mut::().map_err(|e| iron_err(e.description()))?; 102 | 103 | let input_query = parse_url_param(url_query_string.remove("query"))? 104 | .ok_or_else(|| iron_err("No query provided"))?; 105 | let operation_name = parse_url_param(url_query_string.remove("operationName"))?; 106 | let variables = parse_variable_param(url_query_string.remove("variables"))?; 107 | 108 | Ok(http::GraphQLRequest::new( 109 | input_query, 110 | operation_name, 111 | variables, 112 | )) 113 | } 114 | 115 | fn handle_post(&self, req: &mut Request) -> IronResult { 116 | let mut request_payload = String::new(); 117 | itry!(req.body.read_to_string(&mut request_payload)); 118 | 119 | Ok( 120 | serde_json::from_str::(request_payload.as_str()) 121 | .map_err(|err| IronError::new(err, status::BadRequest))?, 122 | ) 123 | } 124 | 125 | fn execute(&self, request: &http::GraphQLRequest) -> IronResult { 126 | let response = request.execute(&self.root_node, &self.app_server_scheduler_wrapped.clone()); 127 | let content_type = "application/json".parse::().unwrap(); 128 | let json = serde_json::to_string_pretty(&response).unwrap(); 129 | let status = if response.is_ok() { 130 | status::Ok 131 | } else { 132 | status::BadRequest 133 | }; 134 | Ok(Response::with((content_type, status, json))) 135 | } 136 | } 137 | 138 | impl <'a> Handler for GraphQLHandler<'a> where 'a: 'static { 139 | fn handle(&self, mut req: &mut Request) -> IronResult { 140 | let graphql_request = match req.method { 141 | method::Get => self.handle_get(&mut req)?, 142 | method::Post => self.handle_post(&mut req)?, 143 | _ => return Ok(Response::with(status::MethodNotAllowed)), 144 | }; 145 | 146 | self.execute(&graphql_request) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | #[macro_use] 7 | extern crate error_chain; 8 | 9 | #[macro_use] 10 | extern crate iron; 11 | extern crate router; 12 | extern crate params; 13 | extern crate serde_json; 14 | extern crate serde; 15 | extern crate chrono; 16 | extern crate clap; 17 | extern crate time; 18 | extern crate checksum; 19 | 20 | #[macro_use] 21 | extern crate juniper; 22 | 23 | #[macro_use] 24 | extern crate juniper_codegen; 25 | extern crate juniper_iron; 26 | extern crate urlencoded; 27 | 28 | mod script; 29 | mod types; 30 | mod server; 31 | mod schedule; 32 | mod script_handler; 33 | mod files; 34 | mod config; 35 | mod app_server; 36 | mod graphql; 37 | mod errors { 38 | error_chain!{ 39 | errors { 40 | RequestError(msg: String) { 41 | description("problem with request") 42 | display("problem in request: {}", msg) 43 | } 44 | } 45 | } 46 | } 47 | 48 | use schedule::World; 49 | use std::sync::{Arc, Mutex}; 50 | use std::thread; 51 | use server::run_server; 52 | use script_handler::ScriptHandler; 53 | use clap::{Arg, App}; 54 | use files::read_json_file; 55 | use config::{Config, reconcile_config}; 56 | use app_server::{AppServer, new_wrapped_scheduler, run_expiration}; 57 | 58 | use errors::{Result, ResultExt}; 59 | 60 | quick_main!(run); 61 | 62 | fn run() -> Result<()> { 63 | let matches = App::new("Device Route Manater") 64 | .version("0.1") 65 | .arg(Arg::with_name("config_file") 66 | .long("config-file") 67 | .short("c") 68 | .help("Config file") 69 | .value_name("FILE") 70 | .takes_value(true) 71 | .required(true)) 72 | .arg(Arg::with_name("bind") 73 | .long("bind") 74 | .short("b") 75 | .default_value("0.0.0.0:8000") 76 | .help("Port to listen on.")) 77 | .arg(Arg::with_name("debug") 78 | .long("debug") 79 | .short("d") 80 | .help("Prints scripts instead of running them")) 81 | .get_matches(); 82 | 83 | let script_handler = if matches.is_present("debug") { 84 | ScriptHandler::PrintScript 85 | } else { 86 | ScriptHandler::RunScript 87 | }; 88 | 89 | let config_file: &str = matches.value_of("config_file") 90 | .ok_or("Config file argument required")?; 91 | let config: Config = read_json_file(config_file).chain_err(|| "Failed to read config file")?; 92 | 93 | let mut internal = AppServer { 94 | config_file: config_file.to_owned(), 95 | config: config.clone(), 96 | world: World::default(), 97 | handler: script_handler, 98 | }; 99 | let mut devs = std::collections::BTreeSet::new(); 100 | app_server::read_dhcp_devices(&config.dhcp_lease_file, &mut devs) 101 | .chain_err(|| "Failed to read dhcp leases file")?; 102 | internal.read_or_create_world()?; 103 | let reconcile_result = reconcile_config(&internal.config, &devs, &mut internal.world); 104 | if reconcile_result.updated_world { 105 | internal.write_world()?; 106 | } 107 | 108 | let sync_app_server = Arc::new(Mutex::new(internal)); 109 | 110 | let app_server_scheduler = Arc::new(new_wrapped_scheduler(sync_app_server.clone())); 111 | let mut app_server_scheduler2 = app_server_scheduler.clone(); 112 | thread::spawn(move || { 113 | run_expiration(&mut app_server_scheduler2); 114 | }); 115 | 116 | let bind: String = matches.value_of("bind").unwrap().to_string(); 117 | run_server(&app_server_scheduler, bind); 118 | 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /src/schedule.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use chrono::{DateTime, Utc}; 3 | use errors::{Result, ErrorKind}; 4 | 5 | pub use ::types::{World, Schedule, ScheduleEntry, Device, DeviceOverride, GuestPath}; 6 | 7 | impl Default for World { 8 | fn default() -> World { 9 | World { 10 | schedule: Schedule { 11 | guest_entry: ScheduleEntry { 12 | item: GuestPath::Closed, 13 | time_bound: None, 14 | }, 15 | override_entry: None, 16 | open_device_entries: BTreeSet::new(), 17 | }, 18 | closed_devices: BTreeSet::new(), 19 | unknown_devices: BTreeSet::new(), 20 | } 21 | } 22 | } 23 | 24 | impl World { 25 | pub fn open_device(&mut self, mac: &str, time_bound: Option>) -> Result<()> { 26 | let result = self.closed_devices 27 | .iter() 28 | .find(|d| d.mac == mac) 29 | .cloned(); 30 | if let Some(dev) = result { 31 | self.closed_devices.remove(&dev); 32 | let entry = ScheduleEntry { 33 | item: dev.clone(), 34 | time_bound, 35 | }; 36 | self.schedule.open_device_entries.insert(entry); 37 | return Ok(()); 38 | } else { 39 | return Err(ErrorKind::RequestError("mac not found".to_owned()).into()); 40 | } 41 | } 42 | 43 | pub fn close_device(&mut self, mac: &str) -> Result<()> { 44 | let result = self.schedule 45 | .open_device_entries 46 | .iter() 47 | .find(|d| d.item.mac == mac) 48 | .cloned(); 49 | if let Some(entry) = result { 50 | self.schedule.open_device_entries.remove(&entry); 51 | let dev = entry.item; 52 | self.closed_devices.insert(dev); 53 | return Ok(()); 54 | } else { 55 | return Err(ErrorKind::RequestError("mac not found".to_owned()).into()); 56 | } 57 | } 58 | 59 | pub fn expire_bounded(&mut self, time_bound: DateTime) { 60 | let expired_open: BTreeSet> = self.schedule 61 | .open_device_entries 62 | .iter() 63 | .filter(|d| d.time_bound.map(|t| t <= time_bound).unwrap_or(false)) 64 | .cloned() 65 | .collect(); 66 | for expired_entry in expired_open { 67 | self.schedule.open_device_entries.remove(&expired_entry); 68 | self.closed_devices.insert(expired_entry.item); 69 | } 70 | if self.schedule.guest_entry.time_bound.map(|t| t <= time_bound).unwrap_or(false) { 71 | self.schedule.guest_entry.item = GuestPath::Closed; 72 | self.schedule.guest_entry.time_bound = None; 73 | } 74 | let mut clear_override = false; 75 | if let Some(ref inner_override_entry) = self.schedule.override_entry { 76 | if inner_override_entry.time_bound.map(|t| t <= time_bound).unwrap_or(false) { 77 | clear_override = true; 78 | } 79 | } 80 | if clear_override { 81 | self.schedule.override_entry = None; 82 | } 83 | } 84 | 85 | pub fn get_soonest_event_time(&self) -> Option> { 86 | let mut all_dates : Vec> = vec!(); 87 | all_dates.extend(self.schedule.guest_entry.time_bound); 88 | if let Some(ref se) = self.schedule.override_entry { 89 | all_dates.extend(se.time_bound); 90 | } 91 | all_dates.extend(self.schedule.open_device_entries.clone().into_iter().flat_map(|se| se.time_bound)); 92 | 93 | all_dates.into_iter().min() 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | pub mod test { 99 | use std::collections::BTreeSet; 100 | use schedule::{Schedule, World, ScheduleEntry, GuestPath, Device, DeviceOverride}; 101 | use chrono::{Utc, TimeZone}; 102 | 103 | pub fn world_fixture() -> World { 104 | return World { 105 | schedule: Schedule { 106 | guest_entry: ScheduleEntry { 107 | item: GuestPath::Closed, 108 | time_bound: None, 109 | }, 110 | override_entry: Some(ScheduleEntry { 111 | item: DeviceOverride::Closed, 112 | time_bound: None, 113 | }), 114 | open_device_entries: [ScheduleEntry { 115 | item: Device { 116 | name: "TV2".to_owned(), 117 | mac: "1234".to_owned(), 118 | }, 119 | time_bound: None, 120 | }, 121 | ScheduleEntry { 122 | item: Device { 123 | name: "TV1".to_owned(), 124 | mac: "5678".to_owned(), 125 | }, 126 | time_bound: None, 127 | }] 128 | .iter() 129 | .cloned() 130 | .collect(), 131 | }, 132 | closed_devices: [Device { 133 | name: "TV4".to_owned(), 134 | mac: "abcd".to_owned(), 135 | }, 136 | Device { 137 | name: "TV3".to_owned(), 138 | mac: "bbbb".to_owned(), 139 | }] 140 | .iter() 141 | .cloned() 142 | .collect(), 143 | unknown_devices: BTreeSet::new(), 144 | }; 145 | } 146 | 147 | #[test] 148 | fn open_device() { 149 | let mut world = world_fixture(); 150 | world.open_device("bbbb", None).unwrap(); 151 | let expected = World { 152 | schedule: Schedule { 153 | guest_entry: ScheduleEntry { 154 | item: GuestPath::Closed, 155 | time_bound: None, 156 | }, 157 | override_entry: Some(ScheduleEntry { 158 | item: DeviceOverride::Closed, 159 | time_bound: None, 160 | }), 161 | open_device_entries: [ScheduleEntry { 162 | item: Device { 163 | name: "TV2".to_owned(), 164 | mac: "1234".to_owned(), 165 | }, 166 | time_bound: None, 167 | }, 168 | ScheduleEntry { 169 | item: Device { 170 | name: "TV1".to_owned(), 171 | mac: "5678".to_owned(), 172 | }, 173 | time_bound: None, 174 | }, 175 | ScheduleEntry { 176 | item: Device { 177 | name: "TV3".to_owned(), 178 | mac: "bbbb".to_owned(), 179 | }, 180 | time_bound: None, 181 | }] 182 | .iter() 183 | .cloned() 184 | .collect(), 185 | }, 186 | closed_devices: [Device { 187 | name: "TV4".to_owned(), 188 | mac: "abcd".to_owned(), 189 | }] 190 | .iter() 191 | .cloned() 192 | .collect(), 193 | unknown_devices: BTreeSet::new(), 194 | }; 195 | assert_eq!(expected, world); 196 | } 197 | 198 | #[test] 199 | fn close_device() { 200 | let mut world = world_fixture(); 201 | world.close_device("5678").unwrap(); 202 | let expected = World { 203 | schedule: Schedule { 204 | guest_entry: ScheduleEntry { 205 | item: GuestPath::Closed, 206 | time_bound: None, 207 | }, 208 | override_entry: Some(ScheduleEntry { 209 | item: DeviceOverride::Closed, 210 | time_bound: None, 211 | }), 212 | open_device_entries: [ScheduleEntry { 213 | item: Device { 214 | name: "TV2".to_owned(), 215 | mac: "1234".to_owned(), 216 | }, 217 | time_bound: None, 218 | }] 219 | .iter() 220 | .cloned() 221 | .collect(), 222 | }, 223 | closed_devices: [Device { 224 | name: "TV4".to_owned(), 225 | mac: "abcd".to_owned(), 226 | }, 227 | Device { 228 | name: "TV3".to_owned(), 229 | mac: "bbbb".to_owned(), 230 | }, 231 | Device { 232 | name: "TV1".to_owned(), 233 | mac: "5678".to_owned(), 234 | }] 235 | .iter() 236 | .cloned() 237 | .collect(), 238 | unknown_devices: BTreeSet::new(), 239 | }; 240 | assert_eq!(expected, world); 241 | } 242 | 243 | #[test] 244 | fn do_timed_events() { 245 | let mut world = World { 246 | schedule: Schedule { 247 | guest_entry: ScheduleEntry { 248 | item: GuestPath::Open, 249 | time_bound: Some(Utc.ymd(2017, 2, 1).and_hms(11, 0, 0)), 250 | }, 251 | override_entry: Some(ScheduleEntry { 252 | item: DeviceOverride::Open, 253 | time_bound: Some(Utc.ymd(2017, 2, 1).and_hms(11, 0, 0)), 254 | }), 255 | open_device_entries: [ScheduleEntry { 256 | item: Device { 257 | name: "TV2".to_owned(), 258 | mac: "1234".to_owned(), 259 | }, 260 | time_bound: Some(Utc.ymd(2017, 2, 1) 261 | .and_hms(10, 0, 0)), 262 | }, 263 | ScheduleEntry { 264 | item: Device { 265 | name: "TV1".to_owned(), 266 | mac: "5678".to_owned(), 267 | }, 268 | time_bound: Some(Utc.ymd(2017, 2, 1) 269 | .and_hms(11, 0, 0)), 270 | }] 271 | .iter() 272 | .cloned() 273 | .collect(), 274 | }, 275 | closed_devices: [Device { 276 | name: "TV4".to_owned(), 277 | mac: "abcd".to_owned(), 278 | }, 279 | Device { 280 | name: "TV3".to_owned(), 281 | mac: "bbbb".to_owned(), 282 | }] 283 | .iter() 284 | .cloned() 285 | .collect(), 286 | unknown_devices: BTreeSet::new(), 287 | }; 288 | world.expire_bounded(Utc.ymd(2017, 2, 1).and_hms(10, 30, 0)); 289 | let expected_1 = World { 290 | schedule: Schedule { 291 | guest_entry: ScheduleEntry { 292 | item: GuestPath::Open, 293 | time_bound: Some(Utc.ymd(2017, 2, 1).and_hms(11, 0, 0)), 294 | }, 295 | override_entry: Some(ScheduleEntry { 296 | item: DeviceOverride::Open, 297 | time_bound: Some(Utc.ymd(2017, 2, 1).and_hms(11, 0, 0)), 298 | }), 299 | open_device_entries: [ScheduleEntry { 300 | item: Device { 301 | name: "TV1".to_owned(), 302 | mac: "5678".to_owned(), 303 | }, 304 | time_bound: Some(Utc.ymd(2017, 2, 1) 305 | .and_hms(11, 0, 0)), 306 | }] 307 | .iter() 308 | .cloned() 309 | .collect(), 310 | }, 311 | closed_devices: [Device { 312 | name: "TV4".to_owned(), 313 | mac: "abcd".to_owned(), 314 | }, 315 | Device { 316 | name: "TV2".to_owned(), 317 | mac: "1234".to_owned(), 318 | }, 319 | Device { 320 | name: "TV3".to_owned(), 321 | mac: "bbbb".to_owned(), 322 | }] 323 | .iter() 324 | .cloned() 325 | .collect(), 326 | unknown_devices: BTreeSet::new(), 327 | }; 328 | assert_eq!(expected_1, world); 329 | 330 | world.expire_bounded(Utc.ymd(2017, 2, 1).and_hms(11, 30, 0)); 331 | let expected_2 = World { 332 | schedule: Schedule { 333 | guest_entry: ScheduleEntry { 334 | item: GuestPath::Closed, 335 | time_bound: None, 336 | }, 337 | override_entry: None, 338 | open_device_entries: BTreeSet::new(), 339 | }, 340 | closed_devices: [Device { 341 | name: "TV4".to_owned(), 342 | mac: "abcd".to_owned(), 343 | }, 344 | Device { 345 | name: "TV2".to_owned(), 346 | mac: "1234".to_owned(), 347 | }, 348 | Device { 349 | name: "TV3".to_owned(), 350 | mac: "bbbb".to_owned(), 351 | }, 352 | Device { 353 | name: "TV1".to_owned(), 354 | mac: "5678".to_owned(), 355 | }] 356 | .iter() 357 | .cloned() 358 | .collect(), 359 | unknown_devices: BTreeSet::new(), 360 | }; 361 | assert_eq!(expected_2, world); 362 | } 363 | 364 | #[test] 365 | fn test_get_soonest_event_time() { 366 | let mut world = world_fixture(); 367 | assert_eq!(None, world.get_soonest_event_time()); 368 | let date_3 = Utc.ymd(2017, 2, 3).and_hms(0, 0, 0); 369 | world.schedule.guest_entry.time_bound = Some(date_3); 370 | assert_eq!(Some(date_3), world.get_soonest_event_time()); 371 | let date_2 = Utc.ymd(2017, 2, 2).and_hms(0, 0, 0); 372 | world.schedule.override_entry.as_mut().unwrap().time_bound = Some(date_2); 373 | assert_eq!(Some(date_2), world.get_soonest_event_time()); 374 | let date_1 = Utc.ymd(2017, 2, 1).and_hms(0, 0, 0); 375 | let entry = ScheduleEntry { 376 | item: Device{mac: "".to_owned(), name: "".to_owned()}, 377 | time_bound: Some(date_1), 378 | }; 379 | world.schedule.open_device_entries.insert(entry); 380 | assert_eq!(Some(date_1), world.get_soonest_event_time()); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/script.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use schedule::{World, GuestPath, DeviceOverride, ScheduleEntry}; 3 | 4 | enum Action { 5 | Accept, 6 | Drop, 7 | } 8 | 9 | fn action_with_override(override_entry: &Option>, 10 | device_action: Action) 11 | -> Action { 12 | let device_override = match *override_entry { 13 | None => None, 14 | Some(ref entry) => Some(&entry.item), 15 | }; 16 | match device_override { 17 | None => device_action, 18 | Some(&DeviceOverride::Open) => Action::Accept, 19 | Some(&DeviceOverride::Closed) => Action::Drop, 20 | } 21 | } 22 | 23 | impl Action { 24 | fn script(self) -> &'static str { 25 | match self { 26 | Action::Accept => "ACCEPT", 27 | Action::Drop => "DROP", 28 | } 29 | } 30 | } 31 | 32 | pub fn write_script(world: &World, old_chain: &str, new_chain: &str, exit_interfaces: &BTreeSet, dest: &mut String) { 33 | dest.push_str(&format!(" 34 | set -e 35 | set -x 36 | if iptables -L {old} >/dev/null 2>&1; then 37 | iptables -D FORWARD -j {old} || true 38 | iptables -F {old} 39 | iptables -X {old} 40 | fi 41 | if iptables -L {new} >/dev/null 2>&1; then 42 | iptables -E {new} {old} 43 | fi 44 | iptables -N {new} 45 | 46 | if ip6tables -L {old} >/dev/null 2>&1; then 47 | ip6tables -D FORWARD -j {old} || true 48 | ip6tables -F {old} 49 | ip6tables -X {old} 50 | fi 51 | if ip6tables -L {new} >/dev/null 2>&1; then 52 | ip6tables -E {new} {old} 53 | fi 54 | ip6tables -N {new} 55 | ", 56 | new = new_chain, 57 | old = old_chain)); 58 | 59 | for interface in exit_interfaces { 60 | let action = &format!( 61 | "iptables -A {new} -i {eth} -j ACCEPT\n", 62 | new = new_chain, eth = interface); 63 | dest.push_str(action); 64 | let action = &format!( 65 | "ip6tables -A {new} -i {eth} -j ACCEPT\n", 66 | new = new_chain, eth = interface); 67 | dest.push_str(action); 68 | } 69 | 70 | let sch = &world.schedule; 71 | let device_override = &sch.override_entry; 72 | for entry in &sch.open_device_entries { 73 | let action = action_with_override(device_override, Action::Accept).script(); 74 | dest.push_str(&format!("iptables -A {} -m mac --mac-source {} -j {}\n", 75 | new_chain, 76 | entry.item.mac, 77 | action)); 78 | dest.push_str(&format!("ip6tables -A {} -m mac --mac-source {} -j {}\n", 79 | new_chain, 80 | entry.item.mac, 81 | action)); 82 | } 83 | 84 | for dev in &world.closed_devices { 85 | let action = action_with_override(device_override, Action::Drop).script(); 86 | dest.push_str(&format!("iptables -A {} -m mac --mac-source {} -j {}\n", 87 | new_chain, 88 | dev.mac, 89 | action)); 90 | dest.push_str(&format!("ip6tables -A {} -m mac --mac-source {} -j {}\n", 91 | new_chain, 92 | dev.mac, 93 | action)); 94 | } 95 | 96 | if world.schedule.guest_entry.item == GuestPath::Closed { 97 | dest.push_str(&format!("iptables -A {} -j DROP\n", new_chain)); 98 | dest.push_str(&format!("ip6tables -A {} -j DROP\n", new_chain)); 99 | } 100 | 101 | dest.push_str(&format!(" 102 | iptables -I FORWARD 1 -j {new} 103 | if iptables -L {old} >/dev/null 2>&1; then 104 | iptables -D FORWARD -j {old} 105 | iptables -F {old} 106 | iptables -X {old} 107 | fi 108 | 109 | ip6tables -I FORWARD 1 -j {new} 110 | if ip6tables -L {old} >/dev/null 2>&1; then 111 | ip6tables -D FORWARD -j {old} 112 | ip6tables -F {old} 113 | ip6tables -X {old} 114 | fi 115 | ", 116 | new = new_chain, 117 | old = old_chain)); 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/script_handler.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Output}; 2 | 3 | use errors::{Result, ResultExt}; 4 | 5 | pub trait HandleScript { 6 | fn handle(&self, script: &str) -> Result<()>; 7 | } 8 | 9 | #[derive(Debug, PartialEq, Eq)] 10 | pub enum ScriptHandler { 11 | PrintScript, 12 | RunScript, 13 | } 14 | 15 | 16 | impl HandleScript for ScriptHandler { 17 | fn handle(&self, script: &str) -> Result<()> { 18 | match *self { 19 | ScriptHandler::PrintScript => { 20 | println!("{}", script); 21 | Ok(()) 22 | } 23 | ScriptHandler::RunScript => run_script(script), 24 | } 25 | } 26 | } 27 | 28 | fn fail_script(script: &str, output: &Option) -> String { 29 | format!("Failed to run script, {}:\noutput:\n{:#?}", script, output) 30 | } 31 | 32 | fn run_script(script: &str) -> Result<()> { 33 | Command::new("sh") 34 | .arg("-c") 35 | .arg(script) 36 | .output() 37 | .chain_err(|| fail_script(script, &None)) 38 | .and_then(|output| { 39 | if output.status.success() { 40 | Ok(()) 41 | } else { 42 | Err(fail_script(script, &Some(output)).into()) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use iron::{Iron, Handler, Request, Response, IronResult, Plugin}; 2 | use iron::headers::{ETag, EntityTag}; 3 | use iron::modifiers::Header; 4 | use iron::mime::Mime; 5 | use iron::status; 6 | use router::Router; 7 | use params::{Params, Value}; 8 | use std::ops::DerefMut; 9 | use checksum::crc64::Crc64; 10 | use juniper_iron::{GraphiQLHandler}; 11 | 12 | use serde_json; 13 | 14 | use chrono::{Utc, Duration}; 15 | 16 | use app_server::{AppServerSchedulerWrapped, AppServer, Scheduler}; 17 | use graphql::{QueryRoot, MutationRoot, GraphQLHandler}; 18 | 19 | use ::errors::{ResultExt}; 20 | 21 | macro_rules! define_handler { 22 | ($t:ident, $f:ident) => { 23 | struct $t { 24 | app_server_scheduler_wrapped: AppServerSchedulerWrapped, 25 | } 26 | 27 | impl $t { 28 | fn new(app_server_scheduler_wrapped: AppServerSchedulerWrapped) 29 | -> $t { 30 | $t { 31 | app_server_scheduler_wrapped: app_server_scheduler_wrapped, 32 | } 33 | } 34 | } 35 | 36 | impl Handler for $t { 37 | fn handle(&self, req: &mut Request) -> IronResult { 38 | let mut guard = self.app_server_scheduler_wrapped 39 | .wrapped_server.lock().unwrap(); 40 | let app_server = guard.deref_mut(); 41 | let scheduler = self.app_server_scheduler_wrapped.clone(); 42 | $f(scheduler, app_server, req) 43 | } 44 | } 45 | } 46 | } 47 | 48 | const INDEX_HTML: &[u8] = include_bytes!("index.html"); 49 | const BUNDLE_JS: &[u8] = include_bytes!("bundle.js"); 50 | 51 | define_handler!(GetWorldHandler, get_world); 52 | fn get_world( 53 | scheduler: AppServerSchedulerWrapped, app_server: &AppServer, 54 | _req: &mut Request) -> IronResult { 55 | scheduler.kick_scheduler(); 56 | let world = &app_server.world; 57 | let serialized = itry!(serde_json::to_string_pretty(world)); 58 | Ok(Response::with((status::Ok, serialized))) 59 | } 60 | 61 | define_handler!(OpenDeviceHandler, open_device); 62 | fn open_device( 63 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 64 | req: &mut Request) -> IronResult { 65 | scheduler.kick_scheduler(); 66 | let params = itry!(req.get_ref::()); 67 | 68 | let mac_value = params.find(&["mac"]); 69 | let mac_param = match mac_value { 70 | Some(&Value::String(ref m)) => Some(m.as_ref()), 71 | _ => None, 72 | }; 73 | let optional_time_secs_string = params.find(&["time_secs"]); 74 | let time_bound = itry!( 75 | match optional_time_secs_string { 76 | Some(&Value::String(ref tss)) => tss.parse::() 77 | .map(|secs| Some(Utc::now() + Duration::seconds(secs))), 78 | _ => Ok(None), 79 | }.chain_err(|| "Failed to parse time secs."), status::BadRequest); 80 | itry!(app_server.open_device(mac_param, time_bound)); 81 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 82 | Ok(Response::with((status::Ok, serialized))) 83 | } 84 | 85 | define_handler!(CloseDeviceHandler, close_device); 86 | fn close_device( 87 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 88 | req: &mut Request) -> IronResult { 89 | scheduler.kick_scheduler(); 90 | let params = itry!(req.get_ref::()); 91 | let mac_value = params.find(&["mac"]); 92 | let mac_param = match mac_value { 93 | Some(&Value::String(ref m)) => Some(m.as_ref()), 94 | _ => None, 95 | }; 96 | itry!(app_server.close_device(mac_param)); 97 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 98 | Ok(Response::with((status::Ok, serialized))) 99 | } 100 | 101 | define_handler!(SetGuestHandler, set_guest); 102 | fn set_guest( 103 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 104 | req: &mut Request) -> IronResult { 105 | scheduler.kick_scheduler(); 106 | let params = itry!(req.get_ref::()); 107 | let allow_value = params.find(&["allow"]); 108 | let allow_param = match allow_value { 109 | Some(&Value::String(ref m)) => Some(m.as_ref()), 110 | _ => None, 111 | }; 112 | itry!(app_server.set_guest_path(allow_param, None)); 113 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 114 | Ok(Response::with((status::Ok, serialized))) 115 | } 116 | 117 | define_handler!(SetOverrideAllHandler, set_override_all); 118 | fn set_override_all( 119 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 120 | req: &mut Request) -> IronResult { 121 | scheduler.kick_scheduler(); 122 | let params = itry!(req.get_ref::()); 123 | let override_value = params.find(&["override"]); 124 | let override_param = match override_value { 125 | Some(&Value::String(ref m)) => Some(m.as_ref()), 126 | _ => None, 127 | }; 128 | itry!(app_server.set_device_override(override_param, None)); 129 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 130 | Ok(Response::with((status::Ok, serialized))) 131 | } 132 | 133 | define_handler!(AddDeviceHandler, add_device); 134 | fn add_device( 135 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 136 | req: &mut Request) -> IronResult { 137 | scheduler.kick_scheduler(); 138 | let params = itry!(req.get_ref::()); 139 | let name_value = params.find(&["name"]); 140 | let name_param = match name_value { 141 | Some(&Value::String(ref m)) => Some(m.as_ref()), 142 | _ => None, 143 | }; 144 | let mac_value = params.find(&["mac"]); 145 | let mac_param = match mac_value { 146 | Some(&Value::String(ref m)) => Some(m.as_ref()), 147 | _ => None, 148 | }; 149 | itry!(app_server.add_device(mac_param, name_param)); 150 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 151 | Ok(Response::with((status::Ok, serialized))) 152 | } 153 | 154 | define_handler!(RefreshDevicesHandler, refresh_devices); 155 | fn refresh_devices( 156 | scheduler: AppServerSchedulerWrapped, app_server: &mut AppServer, 157 | _req: &mut Request) -> IronResult { 158 | scheduler.kick_scheduler(); 159 | itry!(app_server.refresh_devices()); 160 | let serialized = itry!(serde_json::to_string_pretty(&app_server.world)); 161 | Ok(Response::with((status::Ok, serialized))) 162 | } 163 | 164 | 165 | struct StaticHandler { 166 | buf: &'static [u8], 167 | etag: Header, 168 | mime: Mime, 169 | } 170 | 171 | impl StaticHandler { 172 | fn new(buf: &'static [u8], etag: &str, mime: Mime) -> StaticHandler { 173 | let etag_header = Header(ETag(EntityTag::new(false, etag.to_owned()))); 174 | 175 | StaticHandler { 176 | buf, 177 | etag: etag_header, 178 | mime, 179 | } 180 | } 181 | } 182 | 183 | impl Handler for StaticHandler { 184 | fn handle(&self, _req: &mut Request) -> IronResult { 185 | Ok(Response::with(( 186 | self.mime.clone(), self.etag.clone(), status::Ok, self.buf))) 187 | } 188 | } 189 | 190 | pub fn run_server(app_server_wrapped: &AppServerSchedulerWrapped, bind: String) { 191 | let mut router = Router::new(); 192 | 193 | router.get( 194 | "/api", 195 | GetWorldHandler::new(app_server_wrapped.clone()), 196 | "get_world"); 197 | router.post( 198 | "/api/device/open", 199 | OpenDeviceHandler::new(app_server_wrapped.clone()), 200 | "open_device"); 201 | router.post( 202 | "/api/device/close", 203 | CloseDeviceHandler::new(app_server_wrapped.clone()), 204 | "close_device"); 205 | router.post("/api/guest", 206 | SetGuestHandler::new(app_server_wrapped.clone()), 207 | "set_guest"); 208 | router.post("/api/override_all", 209 | SetOverrideAllHandler::new(app_server_wrapped.clone()), 210 | "set_override_all"); 211 | router.post("/api/add_device", 212 | AddDeviceHandler::new(app_server_wrapped.clone()), 213 | "add_device"); 214 | router.post("/api/refresh_devices", 215 | RefreshDevicesHandler::new(app_server_wrapped.clone()), 216 | "refresh_devices"); 217 | 218 | let mut crc = Crc64::new(); 219 | crc.update(INDEX_HTML); 220 | crc.update(BUNDLE_JS); 221 | let sum = format!("{:x}", crc.getsum()); 222 | let mime_html: Mime = "text/html".parse().unwrap(); 223 | let mime_js: Mime = "application/javascript".parse().unwrap(); 224 | 225 | router.get("/", StaticHandler::new(INDEX_HTML, &sum, mime_html), "index"); 226 | router.get( 227 | "/bundle.js", StaticHandler::new(BUNDLE_JS, &sum, mime_js), 228 | "bundle_js"); 229 | 230 | 231 | let cf_wrapped = app_server_wrapped.clone(); 232 | 233 | let graphql_endpoint = GraphQLHandler::new( 234 | cf_wrapped, QueryRoot{}, MutationRoot{}); 235 | router.any("/graphql", graphql_endpoint, "graphql"); 236 | 237 | let graphiql_endpoint = GraphiQLHandler::new("/graphql"); 238 | router.get("/graphiql", graphiql_endpoint, "graphiql"); 239 | 240 | Iron::new(router).http(bind).unwrap(); 241 | } 242 | -------------------------------------------------------------------------------- /src/table.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::iter::FromIterator; 3 | use chrono::{DateTime, Utc}; 4 | use juniper::{GraphQLType}; 5 | 6 | #[derive(Serialize, Deserialize, Debug, GraphQLObject)] 7 | pub struct Entry { 8 | pub mac: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, GraphQLObject)] 12 | pub struct Table { 13 | pub entries: Vec, 14 | } 15 | 16 | #[derive( 17 | Debug, 18 | Clone, 19 | Eq, 20 | Ord, 21 | PartialOrd, 22 | PartialEq, 23 | Hash, 24 | Serialize, 25 | Deserialize, 26 | GraphQLObject, 27 | )] 28 | pub struct Device { 29 | pub name: String, 30 | pub mac: String, 31 | } 32 | 33 | #[derive( 34 | Debug, 35 | Clone, 36 | Eq, 37 | Ord, 38 | PartialOrd, 39 | PartialEq, 40 | Hash, 41 | Serialize, 42 | Deserialize, 43 | )] 44 | pub struct ScheduleEntry { 45 | pub item: T, 46 | pub time_bound: Option>, 47 | } 48 | 49 | graphql_object!(ScheduleEntry: () as "ScheduleEntryGuestPath" |&self| { 50 | field item() -> &GuestPath {&self.item}, 51 | field time_bound() -> Option> {self.time_bound}, 52 | }); 53 | 54 | graphql_object!(ScheduleEntry: () as "ScheduleEntryDeviceOverride" |&self| { 55 | field item() -> &DeviceOverride {&self.item}, 56 | field time_bound() -> Option> {self.time_bound}, 57 | }); 58 | 59 | graphql_object!(ScheduleEntry: () as "ScheduleEntryDevice" |&self| { 60 | field item() -> &Device {&self.item}, 61 | field time_bound() -> Option> {self.time_bound}, 62 | }); 63 | 64 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, GraphQLEnum)] 65 | pub enum DeviceOverride { 66 | Open, 67 | Closed, 68 | } 69 | 70 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, GraphQLEnum)] 71 | pub enum GuestPath { 72 | Open, 73 | Closed, 74 | } 75 | 76 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 77 | pub struct Schedule { 78 | pub guest_entry: ScheduleEntry, 79 | pub override_entry: Option>, 80 | pub open_device_entries: BTreeSet>, 81 | } 82 | 83 | fn set_to_vec(input: &BTreeSet) -> Vec { 84 | Vec::from_iter(input.clone().to_owned()) 85 | } 86 | 87 | graphql_object!(Schedule: () |&self| { 88 | field guest_entry() -> &ScheduleEntry {&self.guest_entry}, 89 | field override_entry() -> &Option> {&self.override_entry}, 90 | field open_device_entries() -> Vec> {set_to_vec(&self.open_device_entries)}, 91 | }); 92 | 93 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 94 | pub struct World { 95 | pub schedule: Schedule, 96 | pub closed_devices: BTreeSet, 97 | pub unknown_devices: BTreeSet, 98 | } 99 | 100 | graphql_object!(World: () |&self| { 101 | field schedule() -> &Schedule {&self.schedule}, 102 | field closed_devices() -> Vec {set_to_vec(&self.closed_devices)}, 103 | field unknown_devices() -> Vec {set_to_vec(&self.unknown_devices)}, 104 | }); 105 | 106 | 107 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 108 | pub struct Config { 109 | pub exit_interfaces: BTreeSet, 110 | pub state_file: String, 111 | pub dhcp_lease_file: String, 112 | pub known_devices: BTreeSet, 113 | } 114 | --------------------------------------------------------------------------------