From a81847f10af4fed2951882a82a6f41b5c641cd3f Mon Sep 17 00:00:00 2001 From: Skye Date: Tue, 15 Aug 2023 14:17:44 +0900 Subject: [PATCH] owo --- .cargo/config | 2 + .gitignore | 5 + Cargo.lock | 511 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 18 ++ LICENSE-APACHE | 201 ++++++++++++++++ LICENSE-MIT | 23 ++ fastly.dev-example.toml | 23 ++ fastly.toml | 9 + rust-toolchain.toml | 3 + src/index.html | 15 ++ src/main.rs | 443 ++++++++++++++++++++++++++++++++++ 11 files changed, 1253 insertions(+) create mode 100644 .cargo/config create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 fastly.dev-example.toml create mode 100644 fastly.toml create mode 100644 rust-toolchain.toml create mode 100644 src/index.html create mode 100644 src/main.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..6b77899 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a92854c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +**/*.rs.bk +/bin +/pkg +fastly.dev.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d495792 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,511 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake3" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "fastly" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b59249dde10f901c578a8eb50577e3c6ee062b0e75f70f9d852da05b75c664d" +dependencies = [ + "anyhow", + "bytes", + "cfg-if", + "fastly-macros", + "fastly-shared", + "fastly-sys", + "http", + "lazy_static", + "mime", + "serde", + "serde_json", + "serde_urlencoded", + "sha2", + "thiserror", + "time", + "url", +] + +[[package]] +name = "fastly-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68ee83e8e1c9613d0448f50687a363b289501952f3c046f6cbdcdd82f29e7c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fastly-shared" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8c7df017449664ec1ee065dac781b5d4ab405e44533c2483cfb31b3f1c6124" +dependencies = [ + "bitflags", + "http", + "thiserror", +] + +[[package]] +name = "fastly-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a5c0dc150d4e1eb2bfdbe169df6bed8d7fafda38ac5a792d800544b35d6b58" +dependencies = [ + "bitflags", + "fastly-shared", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synkronisera" +version = "0.1.0" +dependencies = [ + "base64", + "blake3", + "fastly", + "hex", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fc8cb8f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "synkronisera" +license = "MIT or Apache-2.0" +version = "0.1.0" +authors = ["me@skye.vg"] +edition = "2021" + +[profile.release] +debug = 1 + +[dependencies] +base64 = "0.21.2" +blake3 = "1.4.1" +fastly = "0.9.4" +hex = "0.4.3" +serde = { version = "1.0.183", features = ["derive"] } +serde_json = "1.0.104" +time = "0.3.25" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/fastly.dev-example.toml b/fastly.dev-example.toml new file mode 100644 index 0000000..21f18f5 --- /dev/null +++ b/fastly.dev-example.toml @@ -0,0 +1,23 @@ +[local_server] + + [local_server.backends] + + [local_server.backends.discord] + url = "https://discord.com/" + + [local_server.backends.upstash_redis] + url = "https://totally-real-upstash-instance.upstash.io/" + + [local_server.config_stores] + + [local_server.config_stores.synkronisera] + format = "inline-toml" + + [local_server.config_stores.synkronisera.contents] + client_id = "1234567890123456789" + client_secret = "TOTALLY_LEGIT_DISCORD_CLIENT_SECRET" + mac_secret = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + max_config_size = "524288" + redirect_url = "http://127.0.0.1:7676/v1/oauth/callback" + upstash_redis_key = "TOTALLY_LEGIT_REDIS_KEY" + upstash_redis_url = "https://totally-real-upstash-instance.upstash.io" diff --git a/fastly.toml b/fastly.toml new file mode 100644 index 0000000..fd014ad --- /dev/null +++ b/fastly.toml @@ -0,0 +1,9 @@ +# This file describes a Fastly Compute@Edge package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["me@skye.vg"] +description = "A Vencord Cloud server implementation using Fastly Compute@Edge" +language = "rust" +manifest_version = 3 +name = "synkronisera" +service_id = "uaUI7PGPaJEYQJIrMh0056" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5914a56 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +targets = [ "wasm32-wasi" ] diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..9af8635 --- /dev/null +++ b/src/index.html @@ -0,0 +1,15 @@ + + + + + + + synkronisera + + + Use this with Vencord not your browser idiot + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bafae78 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,443 @@ +use base64::Engine; +use fastly::error::anyhow; +use fastly::http::{header, HeaderValue, Method, StatusCode}; +use fastly::{mime, ConfigStore, Error, Request, Response}; +use hex::FromHex; +use serde::Deserialize; +use serde_json::json; + +type Result = core::result::Result; + +struct UpstashRedisBackend { + url: String, + key: String, +} + +#[derive(Deserialize)] +enum UpstashResponse { + #[serde(rename = "result")] + Ok(serde_json::Value), + #[serde(rename = "error")] + Err(String), +} + +impl From for Result { + fn from(value: UpstashResponse) -> Self { + match value { + UpstashResponse::Ok(value) => Ok(value), + UpstashResponse::Err(err) => Err(anyhow!("Upstash Redis error: {}", err)), + } + } +} + +impl StorageBackend for UpstashRedisBackend { + fn from_configstore(store: &ConfigStore) -> Self { + Self { + url: store.get("upstash_redis_url").unwrap(), + key: store.get("upstash_redis_key").unwrap(), + } + } + + fn get_modified(&mut self, id: u64) -> Result> { + let id_hex = hex::encode(id.to_le_bytes()); + let response: Result = Request::new( + Method::GET, + format!("{}/HGET/{}/last_modified", self.url, id_hex), + ) + .with_header(header::AUTHORIZATION, format!("Bearer {}", self.key)) + .send("upstash_redis")? + .take_body_json::()? + .into(); + let response: Option = serde_json::from_value(response?)?; + Ok(if let Some(response) = response { + let timestamp = u64::from_le_bytes(<[u8; 8]>::from_hex(response)?); + Some(timestamp) + } else { + None + }) + } + + fn get(&mut self, id: u64) -> Result, u64)>> { + let id_hex = hex::encode(id.to_le_bytes()); + let response: Result = Request::new( + Method::GET, + format!("{}/HMGET/{}/last_modified/settings", self.url, id_hex), + ) + .with_header(header::AUTHORIZATION, format!("Bearer {}", self.key)) + .send("upstash_redis")? + .take_body_json::()? + .into(); + let response: (Option, Option) = serde_json::from_value(response?)?; + Ok(if let (Some(timestamp), Some(data)) = response { + let timestamp = u64::from_le_bytes(<[u8; 8]>::from_hex(timestamp)?); + let data = BASE64_ENGINE.decode(data)?; + Some((data, timestamp)) + } else { + None + }) + } + + fn set(&mut self, id: u64, data: Vec) -> Result { + let now = time::OffsetDateTime::now_utc(); + let now = (now.unix_timestamp_nanos() / 1000000) as u64; + + let id_hex = hex::encode(id.to_le_bytes()); + let now_hex = hex::encode(now.to_le_bytes()); + + let data_base64 = BASE64_ENGINE.encode(data); + + let response: Result = Request::new( + Method::GET, + format!( + "{}/HSET/{}/last_modified/{}/settings", + self.url, id_hex, now_hex + ), + ) + .with_header(header::AUTHORIZATION, format!("Bearer {}", self.key)) + .with_body_octet_stream(data_base64.as_bytes()) + .send("upstash_redis")? + .take_body_json::()? + .into(); + response.map(|_| now) + } + + fn del(&mut self, id: u64) -> Result<()> { + let id_hex = hex::encode(id.to_le_bytes()); + let response: Result = Request::new( + Method::GET, + format!("{}/HDEL/{}/last_modified/settings", self.url, id_hex), + ) + .with_header(header::AUTHORIZATION, format!("Bearer {}", self.key)) + .send("upstash_redis")? + .take_body_json::()? + .into(); + response.map(|_| ()) + } +} + +type DefaultStorageBackend = UpstashRedisBackend; + +trait StorageBackend { + fn from_configstore(store: &ConfigStore) -> Self; + fn get_modified(&mut self, id: u64) -> Result>; + fn get(&mut self, id: u64) -> Result, u64)>>; + fn set(&mut self, id: u64, data: Vec) -> Result; + fn del(&mut self, id: u64) -> Result<()>; +} + +const ALLOWED_DISCORD_ORIGINS: [&[u8]; 3] = [ + b"https://discord.com", + b"https://ptb.discord.com", + b"https://canary.discord.com", +]; + +const DISCORD_OAUTH_TOKEN_URL: &str = "https://discord.com/api/oauth2/token"; +const DISCORD_SELF_INFO_URL: &str = "https://discord.com/api/users/@me"; + +const BASE64_ENGINE: base64::engine::GeneralPurpose = + base64::engine::general_purpose::STANDARD_NO_PAD; + +fn handle_with_cors( + request: Request, + methods: &'static str, + allowed_headers: &'static str, + mut handler: impl FnMut(Request) -> Result, +) -> Result { + Ok(if let &Method::OPTIONS = request.get_method() { + match request.get_header(header::ORIGIN) { + Some(origin) => { + if ALLOWED_DISCORD_ORIGINS.contains(&origin.as_bytes()) { + Response::from_status(StatusCode::OK) + .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin) + .with_header(header::ACCESS_CONTROL_ALLOW_HEADERS, allowed_headers) + .with_header(header::ACCESS_CONTROL_ALLOW_METHODS, methods) + .with_header(header::VARY, "Origin") + } else { + Response::from_status(StatusCode::FORBIDDEN) + } + } + None => Response::from_status(StatusCode::OK).with_header(header::ALLOW, methods), + } + } else { + let origin = request.get_header(header::ORIGIN).cloned(); + let mut response = handler(request)?; + if response.get_status() == StatusCode::METHOD_NOT_ALLOWED { + response.set_header(header::ALLOW, methods); + } else if let Some(origin) = origin { + response.set_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); + response.set_header(header::ACCESS_CONTROL_ALLOW_HEADERS, allowed_headers); + response.set_header(header::ACCESS_CONTROL_ALLOW_METHODS, methods); + response.set_header(header::VARY, "Origin"); + } + response + }) +} + +#[derive(Deserialize)] +struct TokenResponse { + access_token: String, +} + +#[derive(Deserialize)] +struct UserInfo { + id: String, +} + +fn check_auth(token: &HeaderValue, secret: &[u8; 32]) -> Option { + let decoded = base64::engine::general_purpose::STANDARD.decode(token).ok()?; + let decoded = String::from_utf8(decoded).ok()?; + let token = decoded.split_terminator(':').next()?; + let decoded = BASE64_ENGINE.decode(token).ok()?; + if decoded.len() != 40 { + return None; + } + let (id, mac) = decoded.split_at(8); + let id = u64::from_le_bytes(id.try_into().unwrap()); + let mac: [u8; 32] = mac.try_into().unwrap(); + let mac: blake3::Hash = mac.into(); + let correct_mac = blake3::keyed_hash(secret, &id.to_le_bytes()); + if mac == correct_mac { + Some(id) + } else { + println!("mac is wrong; got {:?}, expected {:?}", mac, correct_mac); + None + } +} + +#[fastly::main] +fn main(req: Request) -> Result { + let config = ConfigStore::open("synkronisera"); + + let client_id = config.get("client_id").unwrap(); + let client_secret = config.get("client_secret").unwrap(); + let redirect_url = config.get("redirect_url").unwrap(); + + let mac_secret = <[u8; 32]>::from_hex(config.get("mac_secret").unwrap()).unwrap(); + let max_config_size: usize = config.get("max_config_size").unwrap().parse().unwrap(); + + let mut store = DefaultStorageBackend::from_configstore(&config); + + match req.get_path() { + "/" => Ok(Response::from_status(StatusCode::OK) + .with_content_type(mime::TEXT_HTML_UTF_8) + .with_body(include_str!("index.html"))), + + "/v1" => handle_with_cors(req, "OPTIONS, GET, HEAD", "Authorization", |req| { + Ok(match *req.get_method() { + Method::GET => Response::from_status(StatusCode::OK).with_body_json(&json!({ + "ping": "pong" + }))?, + Method::HEAD => Response::from_status(StatusCode::OK), + _ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED), + }) + }), + + "/v1/" => handle_with_cors(req, "OPTIONS, DELETE", "Authorization", |req| { + Ok(match *req.get_method() { + Method::DELETE => { + let id = if let Some(id) = req + .get_header(header::AUTHORIZATION) + .and_then(|token| check_auth(token, &mac_secret)) + { + id + } else { + return Ok(Response::from_status(StatusCode::UNAUTHORIZED) + .with_body_json(&json!({ + "error": "Unauthorized" + }))?); + }; + store.del(id)?; + Response::from_status(StatusCode::NO_CONTENT) + } + _ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED), + }) + }), + + "/v1/oauth/settings" => handle_with_cors(req, "OPTIONS, GET, HEAD", "Authorization", |req| { + Ok(match *req.get_method() { + Method::GET => Response::from_status(StatusCode::OK).with_body_json(&json!({ + "clientId": client_id, + "redirectUri": redirect_url + }))?, + Method::HEAD => Response::from_status(StatusCode::OK), + _ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED), + }) + }), + + "/v1/oauth/callback" => handle_with_cors(req, "OPTIONS, GET", "Authorization", |req| { + Ok(match *req.get_method() { + Method::GET => { + if let Some(code) = req.get_query_parameter("code") { + let mut token_response = + Request::new(Method::POST, DISCORD_OAUTH_TOKEN_URL) + .with_body_form(&json!({ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_url, + "scope": "identify" + }))? + .send("discord")?; + if token_response.get_status() != StatusCode::OK { + Response::from_status(StatusCode::UNAUTHORIZED).with_body_json( + &json!({ + "error": "Failed to authorize with Discord" + }), + )? + } else { + let token = token_response + .take_body_json::()? + .access_token; + let user_id: u64 = Request::new(Method::GET, DISCORD_SELF_INFO_URL) + .with_header(header::AUTHORIZATION, format!("Bearer {}", token)) + .send("discord")? + .take_body_json::()? + .id + .parse()?; + let user_id_bytes = user_id.to_le_bytes(); + let mac: [u8; 32] = + blake3::keyed_hash(&mac_secret, &user_id_bytes).into(); + let combined_token = [ + user_id_bytes[0], + user_id_bytes[1], + user_id_bytes[2], + user_id_bytes[3], + user_id_bytes[4], + user_id_bytes[5], + user_id_bytes[6], + user_id_bytes[7], + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5], + mac[6], + mac[7], + mac[8], + mac[9], + mac[10], + mac[11], + mac[12], + mac[13], + mac[14], + mac[15], + mac[16], + mac[17], + mac[18], + mac[19], + mac[20], + mac[21], + mac[22], + mac[23], + mac[24], + mac[25], + mac[26], + mac[27], + mac[28], + mac[29], + mac[30], + mac[31], + ]; + Response::from_status(StatusCode::OK).with_body_json(&json!({ + "secret": BASE64_ENGINE.encode(combined_token) + }))? + } + } else { + Response::from_status(StatusCode::BAD_REQUEST).with_body_json(&json!({ + "error": "Missing code" + }))? + } + } + _ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED), + }) + }), + + "/v1/settings" => handle_with_cors(req, "OPTIONS, GET, HEAD, PUT, DELETE", "Authorization, Content-Type, If-None-Match", |req| { + let id = if let Some(id) = req + .get_header(header::AUTHORIZATION) + .and_then(|token| check_auth(token, &mac_secret)) + { + id + } else { + return Ok( + Response::from_status(StatusCode::UNAUTHORIZED).with_body_json(&json!({ + "error": "Unauthorized" + }))?, + ); + }; + Ok(match *req.get_method() { + Method::GET => { + if let Some((data, timestamp)) = store.get(id)? { + fn get_cached_timestamp(header: Option<&HeaderValue>) -> Option { + header?.to_str().ok()?.parse().ok() + } + + if let Some(cached_timestamp) = + get_cached_timestamp(req.get_header(header::IF_NONE_MATCH)) + { + if cached_timestamp == timestamp { + return Ok(Response::from_status(StatusCode::NOT_MODIFIED) + .with_header(header::ETAG, format!("{}", timestamp))); + } + } + + Response::from_status(StatusCode::OK) + .with_header(header::ETAG, format!("{}", timestamp)) + .with_body_octet_stream(&data) + } else { + Response::from_status(StatusCode::NOT_FOUND) + } + } + Method::HEAD => { + if let Some(timestamp) = store.get_modified(id)? { + fn get_cached_timestamp(header: Option<&HeaderValue>) -> Option { + header?.to_str().ok()?.parse().ok() + } + + if let Some(cached_timestamp) = + get_cached_timestamp(req.get_header(header::IF_NONE_MATCH)) + { + if cached_timestamp == timestamp { + return Ok(Response::from_status(StatusCode::NOT_MODIFIED) + .with_header(header::ETAG, format!("{}", timestamp))); + } + } + + Response::from_status(StatusCode::OK) + .with_header(header::ETAG, format!("{}", timestamp)) + .with_header(header::CONTENT_TYPE, "application/octet-stream") + } else { + Response::from_status(StatusCode::NOT_FOUND) + } + } + Method::PUT => { + if Some(b"application/octet-stream" as &[u8]) + != req + .get_header(header::CONTENT_TYPE) + .map(|header| header.as_bytes()) + { + return Ok(Response::from_status(StatusCode::UNSUPPORTED_MEDIA_TYPE)); + } + let body = req.into_body_bytes(); + if body.len() > max_config_size { + return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); + } + let etag = store.set(id, body)?; + Response::from_status(StatusCode::OK).with_body_json(&json!({ + "written": etag + }))? + } + Method::DELETE => { + store.del(id)?; + Response::from_status(StatusCode::NO_CONTENT) + } + _ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED), + }) + }), + + _ => Ok(Response::from_status(StatusCode::NOT_FOUND) + .with_body_text_plain("The page you requested could not be found\n")), + } +}