├── tests ├── env │ ├── test.1 │ └── .gitignore ├── cors │ ├── test.yaml │ ├── README.md │ └── index.html ├── go.mod ├── go.sum └── structs.go ├── .dockerignore ├── TODO.md ├── profiler ├── .gitignore ├── environment │ ├── test_ws4sqlite.yaml │ └── test.yaml ├── stress_sqliterg.sh ├── stress_ws4sqlite.sh └── Profile.java ├── Dockerfile.binaries ├── .gitignore ├── Dockerfile ├── CHANGELOG.md ├── RELEASE_CHECKLIST.md ├── CONTRIBUTING.md ├── Cargo.toml ├── src ├── commandline.rs ├── auth.rs ├── req_res.rs ├── db_config.rs ├── main.rs ├── commons.rs ├── backup.rs ├── main_config.rs ├── macros.rs └── logic.rs ├── Makefile ├── db_conf.template.yaml ├── CODE_OF_CONDUCT.md ├── README.md ├── LICENSE └── Cargo.lock /tests/env/test.1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Comments 2 | -------------------------------------------------------------------------------- /tests/env/.gitignore: -------------------------------------------------------------------------------- 1 | *.db* 2 | *.yaml 3 | backup/ -------------------------------------------------------------------------------- /profiler/.gitignore: -------------------------------------------------------------------------------- 1 | environment/*db* 2 | *.class 3 | ws4sqlite* -------------------------------------------------------------------------------- /tests/cors/test.yaml: -------------------------------------------------------------------------------- 1 | corsOrigin: "*" 2 | auth: 3 | mode: HTTP_BASIC 4 | byCredentials: 5 | - user: user 6 | password: ciao 7 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module sqliterg_tests 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.4 7 | gopkg.in/yaml.v3 v3.0.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /profiler/environment/test_ws4sqlite.yaml: -------------------------------------------------------------------------------- 1 | auth: 2 | mode: INLINE 3 | byQuery: SELECT 1 FROM AUTH WHERE USER = :user AND PASS = :password 4 | corsOrigin: "*" 5 | initStatements: 6 | - CREATE TABLE IF NOT EXISTS TBL (ID INT, VAL TEXT) 7 | - CREATE TABLE IF NOT EXISTS AUTH (USER TEXT, PASS TEXT) 8 | - DELETE FROM AUTH 9 | - INSERT INTO AUTH VALUES ('myUser', 'ciao') -------------------------------------------------------------------------------- /tests/cors/README.md: -------------------------------------------------------------------------------- 1 | To test CORS, setup two sqliterg like this (suppose to be in project's base dir): 2 | 3 | ```bash 4 | sqliterg --mem-db test::tests/cors/test.yaml & 5 | sqliterg --serve-dir tests/cors/ --port 12322 --index-file index.html & 6 | ``` 7 | 8 | Then visit `http://localhost:12322` with a browser; in the network debugger you should find that the OPTIONS call (preflight) and the POST call are successful. -------------------------------------------------------------------------------- /Dockerfile.binaries: -------------------------------------------------------------------------------- 1 | # Used by make docker-zbuild-linux. See BUILDING.md 2 | 3 | FROM rust:latest as build 4 | 5 | RUN apt-get update 6 | RUN apt-get full-upgrade -y 7 | RUN apt-get install -y zip 8 | # RUN cargo install -f cross 9 | 10 | WORKDIR /abc 11 | COPY . . 12 | 13 | RUN make build-static-nostatic 14 | 15 | # Now copy it into our base image. 16 | FROM scratch AS export 17 | COPY --from=build /abc/bin/* . -------------------------------------------------------------------------------- /profiler/environment/test.yaml: -------------------------------------------------------------------------------- 1 | auth: 2 | mode: INLINE 3 | byQuery: SELECT 1 FROM AUTH WHERE USER = :user AND PASS = :password 4 | corsOrigin: "*" 5 | macros: 6 | - id: M1 7 | statements: 8 | - CREATE TABLE IF NOT EXISTS TBL (ID INT, VAL TEXT) 9 | - CREATE TABLE IF NOT EXISTS AUTH (USER TEXT, PASS TEXT) 10 | - DELETE FROM AUTH 11 | - INSERT INTO AUTH VALUES ('myUser', 'ciao') 12 | execution: 13 | onCreate: true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mine 2 | 3 | clippy_results.txt 4 | bin/ 5 | 6 | # Generated by Cargo 7 | # will have compiled files and executables 8 | debug/ 9 | target/ 10 | 11 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 12 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 13 | # Cargo.lock 14 | 15 | # These are backup files generated by rustfmt 16 | **/*.rs.bk 17 | 18 | # MSVC Windows builds of rustc generate these, which store debugging information 19 | *.pdb 20 | -------------------------------------------------------------------------------- /profiler/stress_sqliterg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | 5 | URL="http://localhost:12321/test" 6 | REQUESTS=20000 7 | 8 | mkdir environment/backup 9 | rm -f environment/*.db* 10 | 11 | pkill -x ws4sqlite 12 | pkill -x sqliterg 13 | 14 | cd .. 15 | cargo build --release 16 | cd profiler 17 | ../target/release/sqliterg --db environment/test.db & 18 | 19 | javac Profile.java 20 | 21 | sleep 1 22 | 23 | echo -n "Elapsed seconds in sqliterg: " 24 | java -cp ./ Profile $REQUESTS $URL $REQ 25 | 26 | rm Profile.class 27 | 28 | pkill -x sqliterg 29 | rm -f environment/*.db* 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as build 2 | 3 | RUN apk add --no-cache rust cargo sqlite-libs sqlite-dev sed 4 | 5 | COPY . /build 6 | WORKDIR /build 7 | 8 | RUN cp Cargo.toml Cargo.toml.orig 9 | RUN sed 's/^rusqlite.*$/rusqlite = { version = "~0", features = ["serde_json", "load_extension"] }/' Cargo.toml.orig > Cargo.toml 10 | 11 | RUN ["cargo", "build", "--release"] 12 | 13 | # Now copy it into our base image. 14 | FROM alpine:latest 15 | 16 | COPY --from=build /build/target/release/sqliterg / 17 | 18 | # TODO why libgcc? 19 | RUN apk add --no-cache sqlite-libs libgcc 20 | 21 | EXPOSE 12321 22 | VOLUME /data 23 | 24 | CMD ["/sqliterg"] 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.18.0 - 4 December 2023 2 | 3 | - [Issue #1] Implement positional parameters for SQL; 4 | - [Issue #2] Consider loading of sqlite extension on startup. (or load_extension statements in macro); 5 | - Library updates. 6 | 7 | # v0.17.1 - 3 October 2023 8 | 9 | - Aligned to [the documentation](https://docs.sqliterg.dev) (for the first time); 10 | - For backup and macros web services, the `Content-Type` header is not needed; 11 | - At authentication failures, wait 1 second; 12 | - Check for non-empty statement list in macros; 13 | - Check that the backup directory is not the same as the database file; 14 | - Library updates. 15 | 16 | # v0.17.0 - 1 October 2023 17 | 18 | First version 19 | -------------------------------------------------------------------------------- /RELEASE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | - `make update` 2 | - `make clean` 3 | - `make test` [~5min] 4 | - Git: commit ("Pre-release commit") + push 5 | - Git(flow): Release start + push the branch 6 | - Update the version number 7 | - Update the changelog 8 | - Git: commit ("Version labeling & changelog") + push 9 | - `make docker` [33min] 10 | - `make docker-zbuild-linux` [1h] 11 | - Compile on macos 12 | - checkout the release branch 13 | - `make build-macos` [17min] 14 | - Compile on windows 15 | - checkout the release branch 16 | - `cargo build --release` [7min] 17 | - Zip to a filename like `sqliterg-v0.x.y-win-x86_64-bundled.zip` 18 | - Git(flow): Release finish 19 | - Assemble the release on Github 20 | - `make clean` (again) 21 | -------------------------------------------------------------------------------- /profiler/stress_ws4sqlite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL="http://localhost:12321/test_ws4sqlite" 4 | REQUESTS=20000 5 | 6 | cd "$(dirname "$0")" 7 | 8 | rm -f environment/*.db* 9 | rm -f ws4sqlite* 10 | 11 | pkill -x ws4sqlite 12 | pkill -x sqliterg 13 | 14 | wget -q https://github.com/proofrock/ws4sqlite/releases/download/v0.15.1/ws4sqlite-v0.15.1-linux-amd64.tar.gz 15 | tar xzf ws4sqlite-v0.15.0-linux-amd64.tar.gz &> /dev/null 16 | 17 | ./ws4sqlite --db environment/test_ws4sqlite.db & 18 | 19 | javac Profile.java 20 | 21 | sleep 1 22 | 23 | echo -n "Elapsed seconds in ws4sqlite: " 24 | java -cp ./ Profile $REQUESTS $URL $REQ 25 | 26 | rm Profile.class 27 | 28 | pkill -x ws4sqlite 29 | rm -f ws4sqlite* 30 | rm -f environment/*.db* 31 | -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /tests/cors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Germano Rizzo "] 3 | description = "A SQLite remote gateway - query SQLite via HTTP" 4 | name = "sqliterg" 5 | version = "0.18.0" 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | 9 | [dependencies] 10 | actix-cors = "~0" 11 | actix-files = "~0" 12 | actix-web = "~4" 13 | actix-web-httpauth = "~0" 14 | chrono = "~0" 15 | clap = { version = "~4", features = [ "derive" ] } 16 | eyre = "~0" 17 | hex = "~0" 18 | ring = "~0" 19 | # rusqlite = { git = "https://github.com/rusqlite/rusqlite", features = ["serde_json", "load_extension"] } 20 | rusqlite = { version = "~0", features = ["bundled", "serde_json", "load_extension" ] } 21 | # rusqlite = { version = "~0", features = ["serde_json", "load_extension"] } 22 | serde = { version = "~1", features = ["derive"] } 23 | serde_derive = "~1" 24 | serde_json = "~1" 25 | serde_yaml = "~0" 26 | shellexpand = "~3" 27 | 28 | [profile.dev] 29 | opt-level = 0 30 | overflow-checks = true 31 | strip = false 32 | lto = false 33 | 34 | [profile.release] 35 | opt-level = 3 36 | overflow-checks = false 37 | strip = true 38 | lto = true 39 | 40 | [[bin]] 41 | name = "sqliterg" 42 | path = "src/main.rs" -------------------------------------------------------------------------------- /src/commandline.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use clap::Parser; 16 | 17 | use crate::commons::{assert, is_dir, resolve_tilde}; 18 | 19 | #[derive(Debug, Parser)] 20 | #[command(author, version, about, long_about = None)] 21 | #[command( 22 | help_template = "{name} {version}\n {about-section}\n {usage-heading} {usage}\n {all-args} {tab}" 23 | )] 24 | pub struct AppConfig { 25 | #[arg( 26 | long, 27 | value_name = "HOST", 28 | default_value = "0.0.0.0", 29 | help = "The host to bind" 30 | )] 31 | pub bind_host: String, 32 | #[arg(long, value_name = "DB_PATH", help = "Repeatable; paths of file-based databases [format: \"dbFilePath[::configFilePath]\"]", num_args = 0..)] 33 | pub db: Vec, 34 | #[arg(long, value_name = "MEM_DB", help = "Repeatable; config for memory-based databases [format: \"ID[::configFilePath]\"]", num_args = 0..)] 35 | pub mem_db: Vec, 36 | #[arg( 37 | short, 38 | long, 39 | value_name = "PORT", 40 | default_value = "12321", 41 | help = "Port for the web service" 42 | )] 43 | pub port: u16, 44 | #[arg( 45 | long, 46 | value_name = "DIR", 47 | help = "A directory to serve with builtin HTTP server" 48 | )] 49 | pub serve_dir: Option, 50 | #[arg( 51 | long, 52 | value_name = "FILE", 53 | help = "If --serve-dir is configured, the file to treat as index.", 54 | default_value = "index.html" 55 | )] 56 | pub index_file: String, 57 | } 58 | 59 | pub fn parse_cli() -> AppConfig { 60 | let mut ret = AppConfig::parse(); 61 | 62 | assert( 63 | ret.db.len() + ret.mem_db.len() > 0 || ret.serve_dir.is_some(), 64 | "no database and no dir to serve specified".to_string(), 65 | ); 66 | 67 | if let Some(sd) = ret.serve_dir { 68 | let sd = resolve_tilde(&sd); 69 | assert( 70 | is_dir(&sd), 71 | format!("directory to serve does not exist: {}", sd), 72 | ); 73 | ret.serve_dir = Some(sd.to_owned()); 74 | } 75 | 76 | ret 77 | } 78 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::DerefMut; 16 | 17 | use actix_web_httpauth::headers::authorization::{Authorization, Basic}; 18 | use rusqlite::named_params; 19 | 20 | use crate::{ 21 | commons::{equal_case_insensitive, sha256}, 22 | db_config::Auth, 23 | db_config::{AuthMode, Credentials}, 24 | req_res::ReqCredentials, 25 | MUTEXES, 26 | }; 27 | 28 | /// Given the provided password and the expected ones (unhashed and hashed), returns if 29 | /// the passwords match. All three passwords may be Options. 30 | pub fn process_creds( 31 | given_password: &Option, 32 | password: &Option, 33 | hashed_password: &Option, 34 | ) -> bool { 35 | match (given_password, password, hashed_password) { 36 | (_, None, None) => true, 37 | (Some(gp), Some(p), _) => gp == p, 38 | (Some(gp), None, Some(hp)) => equal_case_insensitive(hp, &sha256(gp)), 39 | _ => false, 40 | } 41 | } 42 | 43 | fn auth_by_credentials(user: String, password: String, creds: &Vec) -> bool { 44 | for c in creds { 45 | // TODO hash table lookup? I don't expect the credentials list to grow very much, so it may be overkill (and use memory) 46 | if equal_case_insensitive(&user, &c.user) { 47 | return process_creds(&Some(password), &c.password, &c.hashed_password); 48 | } 49 | } 50 | false 51 | } 52 | 53 | fn auth_by_query(user: String, password: String, query: &str, db_name: &str) -> bool { 54 | let db_lock = MUTEXES.get().unwrap().get(db_name).unwrap(); 55 | let mut db_lock_guard = db_lock.lock().unwrap(); 56 | let conn = db_lock_guard.deref_mut(); 57 | 58 | let res = conn.query_row( 59 | query, 60 | named_params! {":user": user, ":password":password}, 61 | |_| Ok(()), 62 | ); 63 | res.is_ok() 64 | } 65 | 66 | pub fn process_auth( 67 | auth_config: &Auth, 68 | auth_inline: &Option, 69 | auth_header: &Option>, 70 | db_name: &str, 71 | ) -> bool { 72 | let (user, password) = match auth_config.mode { 73 | AuthMode::HttpBasic => match auth_header { 74 | Some(auth_header) => ( 75 | auth_header.as_ref().user_id().to_string(), 76 | auth_header.as_ref().password().unwrap().to_string(), 77 | ), 78 | None => return false, 79 | }, 80 | AuthMode::Inline => match auth_inline { 81 | Some(auth_inline) => (auth_inline.user.to_owned(), auth_inline.password.to_owned()), 82 | None => return false, 83 | }, 84 | }; 85 | 86 | match &auth_config.by_credentials { 87 | Some(creds) => auth_by_credentials(user, password, creds), 88 | None => match &auth_config.by_query { 89 | Some(query) => auth_by_query(user, password, query, db_name), 90 | None => false, 91 | }, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /profiler/Profile.java: -------------------------------------------------------------------------------- 1 | import java.io.InputStream; 2 | import java.io.UnsupportedEncodingException; 3 | import java.net.HttpURLConnection; 4 | import java.net.URL; 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | 9 | public class Profile { 10 | private static final int NUM_THREADS = 8; 11 | private static final String JSON = "{\"credentials\":{\"user\":\"myUser\",\"password\":\"ciao\"},\"transaction\":[{\"statement\":\"DELETE FROM TBL\"},{\"query\":\"SELECT * FROM TBL\"},{\"statement\":\"INSERT INTO TBL (ID, VAL) VALUES (:id, :val)\",\"values\":{\"id\":0,\"val\":\"zero\"}},{\"statement\":\"INSERT INTO TBL (ID, VAL) VALUES (:id, :val)\",\"valuesBatch\":[{\"id\":1,\"val\":\"uno\"},{\"id\":2,\"val\":\"due\"}]},{\"noFail\":true,\"statement\":\"INSERT INTO TBL (ID, VAL) VALUES (:id, :val, 1)\",\"valuesBatch\":[{\"id\":1,\"val\":\"uno\"},{\"id\":2,\"val\":\"due\"}]},{\"statement\":\"INSERT INTO TBL (ID, VAL) VALUES (:id, :val)\",\"valuesBatch\":[{\"id\":3,\"val\":\"tre\"}]},{\"query\":\"SELECT * FROM TBL WHERE ID=:id\",\"values\":{\"id\":1}},{\"statement\":\"DELETE FROM TBL\"}]}"; 12 | private static byte[] JSON_BYTES; 13 | private static int JSON_LEN; 14 | 15 | static { 16 | try { 17 | JSON_BYTES = JSON.getBytes("utf-8"); 18 | JSON_LEN = JSON_BYTES.length; 19 | } catch (UnsupportedEncodingException e) { 20 | e.printStackTrace(); 21 | System.exit(1); 22 | } 23 | } 24 | 25 | private static int numRequests; 26 | private static String urlToCall; 27 | 28 | private static ExecutorService threadPool = Executors.newFixedThreadPool(NUM_THREADS); 29 | 30 | public static void main(String[] args) throws Exception { 31 | numRequests = Integer.parseInt(args[0]); 32 | urlToCall = args[1]; 33 | 34 | var cdl = new CountDownLatch(numRequests); 35 | 36 | var start = System.currentTimeMillis(); 37 | 38 | for (int i = 0; i < numRequests; i++) { 39 | threadPool.execute(() -> { 40 | performHttpRequest(); 41 | cdl.countDown(); 42 | }); 43 | } 44 | cdl.await(); 45 | 46 | System.out.println((System.currentTimeMillis() - start) / 1000.0); 47 | 48 | threadPool.shutdown(); 49 | } 50 | 51 | private static void performHttpRequest() { 52 | try { 53 | var url = new URL(urlToCall); 54 | var connection = (HttpURLConnection) url.openConnection(); 55 | connection.setRequestMethod("POST"); 56 | connection.setRequestProperty("Content-Type", "application/json"); // Set Content-Type header 57 | connection.setDoOutput(true); 58 | 59 | try (var os = connection.getOutputStream()) { 60 | os.write(JSON_BYTES, 0, JSON_LEN); 61 | } 62 | 63 | var ret = connection.getResponseCode(); 64 | if (ret != 200) { 65 | try (InputStream errorStream = connection.getErrorStream()) { 66 | byte[] buffer = new byte[1024]; 67 | int bytesRead; 68 | while ((bytesRead = errorStream.read(buffer)) != -1) { 69 | System.err.write(buffer, 0, bytesRead); 70 | } 71 | } 72 | try (InputStream errorStream = connection.getInputStream()) { 73 | byte[] buffer = new byte[1024]; 74 | int bytesRead; 75 | while ((bytesRead = errorStream.read(buffer)) != -1) { 76 | System.err.write(buffer, 0, bytesRead); 77 | } 78 | } 79 | } 80 | connection.disconnect(); 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | clean: 4 | cargo clean 5 | rm -rf bin 6 | - docker image prune -af 7 | - docker builder prune -af 8 | rm -f profiler/*.class 9 | 10 | update: 11 | cargo update 12 | cd tests && go get -u 13 | cd tests && go mod tidy 14 | 15 | lint: 16 | cargo clippy 2> clippy_results.txt 17 | 18 | profile: 19 | bash profiler/stress_sqliterg.sh 20 | bash profiler/stress_ws4sqlite.sh 21 | 22 | test: 23 | - pkill sqliterg 24 | make build-debug 25 | cd tests; go test -v -timeout 5m 26 | 27 | test-short: 28 | - pkill sqliterg 29 | make build-debug 30 | cd tests; go test -v -timeout 1m -short 31 | 32 | build-debug: 33 | cargo build 34 | 35 | build: 36 | cargo build --release 37 | 38 | build-static-nostatic: 39 | rm -rf bin 40 | - mkdir bin 41 | bash -c "RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target `uname -m`-unknown-linux-gnu" 42 | bash -c "tar czf bin/sqliterg-v0.18.0-linux-`uname -m`-static-bundled.tar.gz -C target/`uname -m`-unknown-linux-gnu/release/ sqliterg" 43 | cp Cargo.toml Cargo.toml.orig 44 | sed 's/^rusqlite.*$$/rusqlite = { version = "~0", features = [\"serde_json\", \"load_extension\"] }/' Cargo.toml.orig > Cargo.toml 45 | bash -c "cargo build --release --target `uname -m`-unknown-linux-gnu" 46 | bash -c "tar czf bin/sqliterg-v0.18.0-linux-`uname -m`-dynamic.tar.gz -C target/`uname -m`-unknown-linux-gnu/release/ sqliterg" 47 | mv Cargo.toml.orig Cargo.toml 48 | 49 | build-macos: 50 | rm -rf bin 51 | - mkdir bin 52 | cargo build --release 53 | tar czf bin/sqliterg-v0.18.0-macos-x86_64-bundled.tar.gz -C target/release/ sqliterg 54 | cargo build --release --target aarch64-apple-darwin 55 | tar czf bin/sqliterg-v0.18.0-macos-aarch64-bundled.tar.gz -C target/aarch64-apple-darwin/release/ sqliterg 56 | cp Cargo.toml Cargo.toml.orig 57 | sed 's/^rusqlite.*$$/rusqlite = { version = "~0", features = [\"serde_json\", "load_extension"] }/' Cargo.toml.orig > Cargo.toml 58 | cargo build --release 59 | tar czf bin/sqliterg-v0.18.0-macos-x86_64-dynamic.tar.gz -C target/release/ sqliterg 60 | cargo build --release --target aarch64-apple-darwin 61 | tar czf bin/sqliterg-v0.18.0-macos-aarch64-dynamic.tar.gz -C target/aarch64-apple-darwin/release/ sqliterg 62 | mv Cargo.toml.orig Cargo.toml 63 | 64 | docker: 65 | docker run --privileged --rm tonistiigi/binfmt --install arm64 66 | docker buildx build --no-cache --platform linux/amd64 -t germanorizzo/sqliterg:v0.18.0-x86_64 --push . 67 | docker buildx build --no-cache --platform linux/arm64 -t germanorizzo/sqliterg:v0.18.0-aarch64 --push . 68 | - docker manifest rm germanorizzo/sqliterg:v0.18.0 69 | docker manifest create germanorizzo/sqliterg:v0.18.0 germanorizzo/sqliterg:v0.18.0-x86_64 germanorizzo/sqliterg:v0.18.0-aarch64 70 | docker manifest push germanorizzo/sqliterg:v0.18.0 71 | - docker manifest rm germanorizzo/sqliterg:latest 72 | docker manifest create germanorizzo/sqliterg:latest germanorizzo/sqliterg:v0.18.0-x86_64 germanorizzo/sqliterg:v0.18.0-aarch64 73 | docker manifest push germanorizzo/sqliterg:latest 74 | 75 | docker-edge: 76 | # in Cargo.toml, set 'version = "0.x.999"' where x is the current minor 77 | docker run --privileged --rm tonistiigi/binfmt --install arm64,arm 78 | docker buildx build --no-cache --platform linux/amd64 -t germanorizzo/sqliterg:edge --push . 79 | 80 | docker-zbuild-linux: 81 | - mkdir bin 82 | docker run --privileged --rm tonistiigi/binfmt --install arm64,arm 83 | docker buildx build --no-cache --platform linux/amd64 -f Dockerfile.binaries --target export -t tmp_binaries_build . --output bin 84 | docker buildx build --no-cache --platform linux/arm64 -f Dockerfile.binaries --target export -t tmp_binaries_build . --output bin 85 | # Doesn't work. armv7-unknown-linux-gnueabihf must be used. Anyway, for now ARMv7 is out of scope. 86 | # docker buildx build --no-cache --platform linux/arm/v7 -f Dockerfile.binaries --target export -t tmp_binaries_build . --output bin 87 | 88 | -------------------------------------------------------------------------------- /src/req_res.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::commons::default_as_false; 16 | use actix_web::{ 17 | body::BoxBody, 18 | http::{header::ContentType, StatusCode}, 19 | HttpRequest, HttpResponse, Responder, 20 | }; 21 | use serde::{Deserialize, Serialize}; 22 | 23 | use serde_json::Value as JsonValue; 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub struct ReqCredentials { 27 | pub user: String, 28 | pub password: String, 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | pub struct ReqTransactionItem { 33 | #[serde(rename = "noFail")] 34 | #[serde(default = "default_as_false")] 35 | pub no_fail: bool, 36 | pub query: Option, 37 | pub statement: Option, 38 | pub values: Option, 39 | #[serde(rename = "valuesBatch")] 40 | pub values_batch: Option>, 41 | } 42 | 43 | #[derive(Debug, Deserialize)] 44 | pub struct Request { 45 | pub credentials: Option, 46 | pub transaction: Vec, 47 | } 48 | 49 | #[derive(Debug, Serialize)] 50 | pub struct ResponseItem { 51 | pub success: bool, 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub error: Option, 54 | #[serde(rename = "resultSet")] 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub result_set: Option>, 57 | #[serde(rename = "rowsUpdated")] 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub rows_updated: Option, 60 | #[serde(rename = "rowsUpdatedBatch")] 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | pub rows_updated_batch: Option>, 63 | } 64 | 65 | #[derive(Debug, Serialize)] 66 | pub struct Response { 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub results: Option>, 69 | #[serde(rename = "reqIdx")] 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub req_idx: Option, 72 | #[serde(skip_serializing_if = "Option::is_none")] 73 | pub message: Option, 74 | #[serde(skip_serializing)] 75 | pub status_code: u16, 76 | #[serde(skip_serializing)] 77 | pub success: bool, 78 | } 79 | 80 | impl Responder for Response { 81 | type Body = BoxBody; 82 | 83 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 84 | let body = serde_json::to_string(&self).unwrap(); 85 | 86 | HttpResponse::Ok() 87 | .status(StatusCode::from_u16(self.status_code).unwrap()) 88 | .content_type(ContentType::json()) 89 | .body(body) 90 | } 91 | } 92 | 93 | impl Response { 94 | pub fn new_ok(results: Vec) -> Response { 95 | Response { 96 | results: Some(results), 97 | req_idx: None, 98 | message: None, 99 | status_code: 200, 100 | success: true, 101 | } 102 | } 103 | 104 | pub fn new_err(status_code: u16, req_idx: isize, msg: String) -> Response { 105 | Response { 106 | results: None, 107 | req_idx: Some(req_idx), 108 | message: Some(msg), 109 | status_code, 110 | success: false, 111 | } 112 | } 113 | } 114 | 115 | #[derive(Deserialize)] 116 | pub struct Token { 117 | pub token: Option, 118 | } 119 | -------------------------------------------------------------------------------- /src/db_config.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use eyre::Result; 16 | use std::fs::File; 17 | use std::io::Read; 18 | 19 | use crate::commons::{default_as_false, default_as_zero}; 20 | 21 | #[derive(Debug, Deserialize, Clone)] 22 | pub enum AuthMode { 23 | #[serde(rename = "HTTP_BASIC")] 24 | HttpBasic, 25 | #[serde(rename = "INLINE")] 26 | Inline, 27 | } 28 | 29 | fn default_401() -> u16 { 30 | 401 31 | } 32 | 33 | #[derive(Debug, Deserialize, Clone)] 34 | pub struct Auth { 35 | #[serde(rename = "authErrorCode")] 36 | #[serde(default = "default_401")] 37 | pub auth_error_code: u16, 38 | pub mode: AuthMode, 39 | #[serde(rename = "byQuery")] 40 | pub by_query: Option, 41 | #[serde(rename = "byCredentials")] 42 | pub by_credentials: Option>, 43 | } 44 | 45 | #[derive(Debug, Deserialize, Clone)] 46 | pub struct Credentials { 47 | pub user: String, 48 | pub password: Option, 49 | #[serde(rename = "hashedPassword")] 50 | pub hashed_password: Option, 51 | } 52 | 53 | #[derive(Debug, Deserialize, Clone)] 54 | pub struct StoredStatement { 55 | pub id: String, 56 | pub sql: String, 57 | } 58 | 59 | #[derive(Debug, Deserialize, Clone)] 60 | pub struct ExecutionWebService { 61 | #[serde(rename = "authErrorCode")] 62 | #[serde(default = "default_401")] 63 | pub auth_error_code: u16, 64 | #[serde(rename = "authToken")] 65 | pub auth_token: Option, 66 | #[serde(rename = "hashedAuthToken")] 67 | pub hashed_auth_token: Option, 68 | } 69 | 70 | #[derive(Debug, Deserialize, Clone)] 71 | pub struct ExecutionMode { 72 | #[serde(rename = "onCreate")] 73 | #[serde(default = "default_as_false")] 74 | pub on_create: bool, 75 | #[serde(rename = "onStartup")] 76 | #[serde(default = "default_as_false")] 77 | pub on_startup: bool, 78 | #[serde(default = "default_as_zero")] 79 | pub period: i32, 80 | #[serde(rename = "webService")] 81 | pub web_service: Option, 82 | } 83 | 84 | #[derive(Debug, Deserialize, Clone)] 85 | pub struct Macro { 86 | pub id: String, 87 | #[serde(rename = "disableTransaction")] 88 | #[serde(default = "default_as_false")] 89 | pub disable_transaction: bool, 90 | pub statements: Vec, 91 | pub execution: ExecutionMode, 92 | } 93 | 94 | #[derive(Debug, Deserialize, Clone)] 95 | pub struct Backup { 96 | #[serde(rename = "backupDir")] 97 | pub backup_dir: String, 98 | #[serde(rename = "numFiles")] 99 | pub num_files: usize, 100 | pub execution: ExecutionMode, 101 | } 102 | 103 | #[derive(Debug, Default, Deserialize, Clone)] 104 | pub struct DbConfig { 105 | pub auth: Option, 106 | #[serde(rename = "journalMode")] 107 | pub journal_mode: Option, 108 | #[serde(rename = "readOnly")] 109 | #[serde(default = "default_as_false")] 110 | pub read_only: bool, 111 | #[serde(rename = "corsOrigin")] 112 | pub cors_origin: Option, 113 | #[serde(rename = "useOnlyStoredStatements")] 114 | #[serde(default = "default_as_false")] 115 | pub use_only_stored_statements: bool, 116 | #[serde(rename = "storedStatements")] 117 | pub stored_statements: Option>, 118 | pub macros: Option>, 119 | pub backup: Option, 120 | } 121 | 122 | pub fn parse_dbconf(filename: &String) -> Result { 123 | let mut file = File::open(filename)?; 124 | let mut content = String::new(); 125 | file.read_to_string(&mut content)?; 126 | 127 | let ret = serde_yaml::from_str(&content)?; 128 | 129 | Ok(ret) 130 | } 131 | -------------------------------------------------------------------------------- /tests/structs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022-, Germano Rizzo 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | // These are for parsing the config file (from YAML) 20 | 21 | type credentialsCfg struct { 22 | User string `yaml:"user,omitempty"` 23 | Password string `yaml:"password,omitempty"` 24 | HashedPassword string `yaml:"hashedPassword,omitempty"` 25 | } 26 | 27 | type authr struct { 28 | AuthErrorCode *int `yaml:"authErrorCode,omitempty"` 29 | Mode string `yaml:"mode,omitempty"` // 'INLINE' or 'HTTP_BASIC' 30 | CustomErrorCode *int `yaml:"customErrorCode,omitempty"` 31 | ByQuery string `yaml:"byQuery,omitempty"` 32 | ByCredentials []credentialsCfg `yaml:"byCredentials,omitempty"` 33 | } 34 | 35 | type storedStatement struct { 36 | Id string `yaml:"id,omitempty"` 37 | Sql string `yaml:"sql,omitempty"` 38 | } 39 | 40 | type webService struct { 41 | AuthErrorCode *int `yaml:"authErrorCode,omitempty"` 42 | AuthToken *string `yaml:"authToken,omitempty"` 43 | HashedAuthToken *string `yaml:"hashedAuthToken,omitempty"` 44 | } 45 | 46 | type execution struct { 47 | OnCreate *bool `yaml:"onCreate,omitempty"` 48 | OnStartup *bool `yaml:"onStartup,omitempty"` 49 | Period *uint `yaml:"period,omitempty"` 50 | WebService *webService `yaml:"webService,omitempty"` 51 | } 52 | 53 | type macro struct { 54 | Id string `yaml:"id,omitempty"` 55 | DisableTransaction *bool `yaml:"disableTransaction,omitempty"` 56 | Statements []string `yaml:"statements,omitempty"` 57 | Execution execution `yaml:"execution,omitempty"` 58 | } 59 | 60 | type backup struct { 61 | BackupDir string `yaml:"backupDir,omitempty"` 62 | NumFiles uint `yaml:"numFiles,omitempty"` 63 | Execution execution `yaml:"execution,omitempty"` 64 | } 65 | 66 | type db struct { 67 | Auth *authr `yaml:"auth,omitempty"` 68 | ReadOnly bool `yaml:"readOnly,omitempty"` 69 | CORSOrigin string `yaml:"corsOrigin,omitempty"` 70 | UseOnlyStoredStatements bool `yaml:"useOnlyStoredStatements,omitempty"` 71 | JournalMode string `yaml:"journalMode,omitempty"` 72 | StoredStatement []storedStatement `yaml:"storedStatements,omitempty"` 73 | Macros []macro `yaml:"macros,omitempty"` 74 | Backup backup `yaml:"backup,omitempty"` 75 | } 76 | 77 | // These are for parsing the request (from JSON) 78 | 79 | type credentials struct { 80 | User string `json:"user,omitempty"` 81 | Password string `json:"password,omitempty"` 82 | } 83 | 84 | type requestItem struct { 85 | Query string `json:"query,omitempty"` 86 | Statement string `json:"statement,omitempty"` 87 | NoFail bool `json:"noFail,omitempty"` 88 | Values interface{} `json:"values,omitempty"` 89 | ValuesBatch interface{} `json:"valuesBatch,omitempty"` 90 | } 91 | 92 | type request struct { 93 | Credentials *credentials `json:"credentials,omitempty"` 94 | Transaction []requestItem `json:"transaction,omitempty"` 95 | } 96 | 97 | // These are for generating the response 98 | 99 | type responseItem struct { 100 | Success bool `json:"success"` 101 | RowsUpdated *int `json:"rowsUpdated,omitempty"` 102 | RowsUpdatedBatch []int `json:"rowsUpdatedBatch,omitempty"` 103 | ResultSet []map[string]interface{} `json:"resultSet"` 104 | Error string `json:"error,omitempty"` 105 | } 106 | 107 | type response struct { 108 | Results []responseItem `json:"results"` 109 | } 110 | -------------------------------------------------------------------------------- /db_conf.template.yaml: -------------------------------------------------------------------------------- 1 | # Main endpoint for requests is http://:/ 2 | 3 | # If present, "auth" defines the authentication for this database 4 | auth: 5 | # Optional, by default 401. The error HTTP code to be returned if auth fails. 6 | authErrorCode: 499 7 | # Mandatory. Defines how the credentials are passed to the server. 8 | # "INLINE" means that credentials are passed in the request 9 | # "HTTP_BASIC" uses Basic Authentication (via the "Authorization: Basic" header) 10 | mode: INLINE 11 | # Only one among "byQuery" and "byCredentials" must be specified. 12 | # This query validates credentials against a query in the database, it must have 13 | # two parameters named ":user" and ":password" and if the credentials are valid must 14 | # return (at least) one row. 15 | byQuery: SELECT 1 FROM AUTH WHERE USER = :user AND PASS = :password 16 | # This is a list of valid credentials, "statically" specified. "user" is case-insensitive 17 | # while either a plaintext "password" or a SHA-256 hashed "hashedPassword" must be supplied. 18 | byCredentials: 19 | - user: myUser1 20 | password: ciao 21 | - user: myUser2 22 | hashedPassword: b133a0c0e9bee3be20163d2ad31d6248db292aa6dcb1ee087a2aa50e0fc75ae2 # "ciao" 23 | # Journal mode for the database. Optional, default is "WAL". Not validated to accommodate for 24 | # future versions of SQLite. From SQLite docs: DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF 25 | journalMode: WAL 26 | # Database is read-only. This is set after startup macros or backup are performed, so they can 27 | # still modify the database. It's implemented using the query_only PRAGMA. 28 | readOnly: false 29 | # Instruct the web server to povide the CORS header and preflight system as needed. 30 | corsOrigin: "*" 31 | # A "map" of statements (or queries) that can be called from request or macros using '^'. If I 32 | # want to use Q1, for example, I should specify '"query": "^Q1"' in the request. 33 | storedStatements: 34 | - id: Q1 35 | sql: SELECT * FROM TBL 36 | - id: Q2 37 | sql: CREATE TABLE IF NOT EXISTS AUTH (USER TEXT, PASS TEXT) 38 | # If set, only a Stored Statement can be used in the requests. Useful to avoid SQL injection. 39 | useOnlyStoredStatements: false 40 | # A "map" of macros, that are named groups of statements (not queries) that can be run at db 41 | # creation, at startup, every /n/ minutes, or via a web request. 42 | macros: 43 | # ID of the macro 44 | - id: M1 45 | # Allow to execute out of a transaction, e.g. for VACUUM 46 | disableTransaction: false 47 | # Which statements it must execute (in a transaction). Stored Statements references can be used. 48 | statements: 49 | - CREATE TABLE IF NOT EXISTS TBL (ID INT, VAL TEXT) 50 | - ^Q2 51 | # Control of execution. Mandatory. All the contents have defaults meaning "disabled". 52 | execution: 53 | # Executes if the database is created (file wasn't present or in-memory) 54 | onCreate: false 55 | # Executes at each startup. Implies onCreate; if both are specified the macro will be executed once. 56 | onStartup: false 57 | # Executes every /n/ minutes. First execution is /n/ minutes after startup. 58 | period: 1 # in minutes, <= 0: never 59 | # Exposes an endpoint to execute the macro. A token-based for of authentication is mandatory. 60 | # Endpoint is http://://macro/ 61 | webService: 62 | # Optional, by default 401. The error HTTP code to be returned if auth fails. 63 | authErrorCode: 499 64 | # Either a plaintext "authToken" or a SHA-256 hashed "hashedAuthToken" must be supplied. 65 | authToken: ciao 66 | hashedAuthToken: b133a0c0e9bee3be20163d2ad31d6248db292aa6dcb1ee087a2aa50e0fc75ae2 67 | # Optional. Configuration of the backups. Backups can be run at startup, every /n/ minutes, 68 | # or via a web request. 69 | backup: 70 | # Directory for the backups. Must exist; mandatory config. 71 | backupDir: backups/ 72 | # Keeps only the last /n/ backup files. Mandatory. 73 | numFiles: 3 74 | # Control of execution. Mandatory. All the contents have defaults meaning "disabled". 75 | execution: 76 | # Executes if the database is created (file wasn't present or in-memory) 77 | onCreate: false 78 | # Executes at each startup. Implies onCreate; if both are specified the macro will be executed once. 79 | onStartup: false 80 | # Executes every /n/ minutes. First execution is /n/ minutes after startup. 81 | period: 1 # in minutes, <= 0: never 82 | # Exposes an endpoint to execute the backup. A token-based for of authentication is mandatory. 83 | # Endpoint is http://://backup 84 | webService: 85 | # Optional, by default 401. The error HTTP code to be returned if auth fails. 86 | authErrorCode: 499 87 | # Either a plaintext "authToken" or a SHA-256 hashed "hashedAuthToken" must be supplied. 88 | authToken: ciao 89 | hashedAuthToken: b133a0c0e9bee3be20163d2ad31d6248db292aa6dcb1ee087a2aa50e0fc75ae2 90 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[macro_use] 16 | extern crate serde_derive; 17 | #[macro_use] 18 | extern crate eyre; 19 | 20 | use std::{ 21 | collections::HashMap, 22 | ops::Deref, 23 | sync::{Mutex, OnceLock}, 24 | }; 25 | 26 | use actix_cors::Cors; 27 | use actix_files::Files; 28 | use actix_web::{ 29 | guard, 30 | web::{route, scope, Data}, 31 | App, HttpServer, Scope, 32 | }; 33 | use rusqlite::Connection; 34 | 35 | pub mod auth; 36 | mod backup; 37 | pub mod commandline; 38 | pub mod commons; 39 | pub mod db_config; 40 | mod logic; 41 | mod macros; 42 | pub mod main_config; 43 | pub mod req_res; 44 | 45 | use crate::{commandline::parse_cli, db_config::AuthMode, main_config::compose_db_map}; 46 | 47 | pub const CURRENT_PROTO_VERSION: u8 = 1; 48 | 49 | pub static MUTEXES: OnceLock>> = OnceLock::new(); 50 | 51 | fn get_sqlite_version() -> String { 52 | let conn: Connection = Connection::open_in_memory().unwrap(); 53 | conn.query_row("SELECT sqlite_version()", [], |row| row.get(0)) 54 | .unwrap() 55 | } 56 | 57 | #[actix_web::main] 58 | async fn main() -> std::io::Result<()> { 59 | println!( 60 | "{} v{}. based on SQLite v{}\n", 61 | env!("CARGO_PKG_NAME"), 62 | env!("CARGO_PKG_VERSION"), 63 | get_sqlite_version() 64 | ); 65 | 66 | let cli = parse_cli(); 67 | 68 | // side effect of compose_db_map: populate MUTEXES 69 | // aborts on error 70 | let db_map = compose_db_map(&cli); 71 | 72 | if let Some(sd) = &cli.serve_dir { 73 | println!("- serving directory: {}", sd); 74 | println!(" - with index file: {}", &cli.index_file); 75 | }; 76 | 77 | let app_lambda = move || { 78 | let dir = cli.serve_dir.to_owned(); 79 | let index_file = cli.index_file.to_owned(); 80 | let mut a = App::new(); 81 | for (db_name, db_conf) in db_map.iter() { 82 | let scop: Scope = scope(format!("/{}", db_name.to_owned()).deref()) 83 | .app_data(Data::new(db_name.to_owned())) 84 | .app_data(Data::new(db_conf.to_owned())) 85 | .route( 86 | "", 87 | route() 88 | .guard(guard::Post()) 89 | .guard(guard::Header("content-type", "application/json")) 90 | .to(logic::handler), 91 | ) 92 | .route( 93 | "/macro/{macro_name}", 94 | route() 95 | .guard(guard::Any(guard::Get()).or(guard::Post())) 96 | .to(macros::handler), 97 | ) 98 | .route( 99 | "/backup", 100 | route() 101 | .guard(guard::Any(guard::Get()).or(guard::Post())) 102 | .to(backup::handler), 103 | ); 104 | 105 | match &db_conf.conf.cors_origin { 106 | Some(orig) => { 107 | let mut cors = Cors::default() 108 | .allowed_methods(vec!["POST"]) 109 | .allowed_header("content-type"); 110 | if db_conf.conf.auth.is_some() 111 | && matches!( 112 | db_conf.conf.auth.as_ref().unwrap().mode, 113 | AuthMode::HttpBasic 114 | ) 115 | { 116 | cors = cors.allowed_header("authorization"); 117 | } 118 | if orig == "*" { 119 | cors = cors.allow_any_origin(); 120 | } else { 121 | cors = cors.allowed_origin(orig); 122 | } 123 | a = a.service(scop.wrap(cors)) 124 | } 125 | None => a = a.service(scop), 126 | } 127 | } 128 | 129 | if let Some(dir) = dir { 130 | a = a.service(Files::new("/", dir).index_file(index_file)); 131 | }; 132 | 133 | a 134 | }; 135 | 136 | let bind_addr = format!("{}:{}", cli.bind_host, cli.port); 137 | println!("- Listening on {}", &bind_addr); 138 | HttpServer::new(app_lambda).bind(bind_addr)?.run().await 139 | } 140 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | https://github.com/proofrock/sqliterg/issues. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Hey! This project is archived. I decided to switch back the development on [ws4sqlite](https://github.com/proofrock/ws4sqlite), for reasons expressed in [this discussion](https://github.com/proofrock/ws4sqlite/discussions/44). If you'll want to hack on it, though, I'll be more than glad to assist!** 2 | 3 | # 🌿 Introduction 4 | 5 | > *This is a rewrite in Rust of [ws4sqlite](https://github.com/proofrock/ws4sqlite), 30-50% faster, 10x less memory used, more flexible in respect to sqlite support. It is not a direct rewrite, more like a "sane" (I hope) redesign. You can read more about what's changed and how to migrate [here](https://docs.sqliterg.dev/features/migrating-from-ws4sqlite).* 6 | 7 | **sqliterg** is a server-side application that, applied to one or more SQLite files, allows to perform SQL queries and statements on them via REST (or better, JSON over HTTP). 8 | 9 | Full docs are available [here](https://docs.sqliterg.dev/) and a [tutorial](https://docs.sqliterg.dev/tutorial) too. 10 | 11 | Possible use cases are the ones where remote access to a sqlite db is useful/needed, for example a data layer for a remote application, possibly serverless or even called from a web page ([*after security considerations*](https://docs.sqliterg.dev/security) of course). 12 | 13 | As a quick example, after launching: 14 | 15 | ```bash 16 | sqliterg --db mydatabase.db 17 | ``` 18 | 19 | It's possible to make a POST call to `http://localhost:12321/mydatabase`, e.g. with the following body: 20 | 21 | ```json 22 | { 23 | "transaction": [ 24 | { 25 | "statement": "INSERT INTO TEST_TABLE (ID, VAL, VAL2) VALUES (:id, :val, :val2)", 26 | "values": { "id": 1, "val": "hello", "val2": null } 27 | }, 28 | { 29 | "query": "SELECT * FROM TEST_TABLE" 30 | } 31 | ] 32 | } 33 | ``` 34 | 35 | Obtaining an answer of: 36 | 37 | ```json 38 | { 39 | "results": [ 40 | { 41 | "success": true, 42 | "rowsUpdated": 1 43 | }, 44 | { 45 | "success": true, 46 | "resultSet": [ 47 | { "ID": 1, "VAL": "hello", "VAL2": null } 48 | ] 49 | } 50 | ] 51 | } 52 | ``` 53 | 54 | # 🎞️ Features 55 | 56 | - A [**single executable file**](https://docs.sqliterg.dev/documentation/installation) (written in Rust); 57 | - Can be [built](https://docs.sqliterg.dev/building-and-testing#supported-platforms) either against the system's SQLite or embedding one; 58 | - HTTP/JSON access; 59 | - Directly call `sqliterg` on a database (as above), many options available using a YAML companion file; 60 | - [**In-memory DBs**](https://docs.sqliterg.dev/documentation/running#file-based-and-in-memory) are supported; 61 | - Serving of [**multiple databases**](https://docs.sqliterg.dev/documentation/configuration-file) in the same server instance; 62 | - Named or positional parameters in SQL are supported; 63 | - [**Batching**](https://docs.sqliterg.dev/documentation/requests#batch-parameter-values-for-a-statement) of multiple value sets for a single statement; 64 | - All queries of a call are executed in a [**transaction**](https://docs.sqliterg.dev/documentation/requests); 65 | - For each query/statement, specify if a failure should rollback the whole transaction, or the failure is [**limited**](https://docs.sqliterg.dev/documentation/errors#managed-errors) to that query; 66 | - "[**Stored Statements**](https://docs.sqliterg.dev/documentation/stored-statements)": define SQL in the server, and call it from the client; 67 | - "[**Macros**](https://docs.sqliterg.dev/documentation/macros)": lists of statements that can be executed at db creation, at startup, periodically or calling a web service; 68 | - [**Backups**](https://docs.sqliterg.dev/documentation/backup), rotated and also runnable at db creation, at startup, periodically or calling a web service; 69 | - [**CORS**](https://docs.sqliterg.dev/documentation/configuration-file#corsorigin) mode, configurable per-db; 70 | - [**Journal Mode**](https://docs.sqliterg.dev/documentation/configuration-file#journalmode) (e.g. WAL) can be configured; 71 | - [**Embedded web server**](https://docs.sqliterg.dev/documentation/web-server) to directly serve web pages that can access `sqliterg` without CORS; 72 | - [**Quite fast**](https://docs.sqliterg.dev/features/performances)! 73 | - Comprehensive [**test suite**](https://docs.sqliterg.dev/building-and-testing#testing); 74 | - [**Docker images**](https://docs.sqliterg.dev/documentation/installation/docker), for x86_64 and arm64; 75 | - Binaries are provided with a bundled SQLite "inside" them, or linked against the system's installed SQLite. 76 | 77 | ### Security Features 78 | 79 | * [**Authentication**](https://docs.sqliterg.dev/security#authentication) can be configured 80 | * on the client, either using HTTP Basic Authentication or specifying the credentials in the request; 81 | * on the server, either by specifying credentials (also with hashed passwords) or providing a query to look them up in the db itself; 82 | * customizable `Not Authorized` error code (if `401` is not optimal); 83 | * A database can be opened in [**read-only mode**](https://docs.sqliterg.dev/security#read-only-databases) (only queries will be allowed); 84 | * It's possible to enforce using [**only stored statements**](https://docs.sqliterg.dev/security#stored-statements-to-prevent-sql-injection), to avoid some forms of SQL injection and receiving SQL from the client altogether; 85 | * [**CORS/Allowed Origin**](https://docs.sqliterg.dev/security#cors-allowed-origin) can be configured and enforced; 86 | * It's possible to [**bind**](https://docs.sqliterg.dev/security#binding-to-a-network-interface) to a network interface, to limit access. 87 | 88 | Some design choices: 89 | 90 | * Very thin layer over SQLite. Errors and type translation, for example, are those provided by the SQLite driver; 91 | * Doesn't include HTTPS, as this can be done easily (and much more securely) with a [reverse proxy](https://docs.sqliterg.dev/security#use-a-reverse-proxy-if-going-on-the-internet). 92 | 93 | # 🥇 Credits 94 | 95 | Kindly supported by [JetBrains for Open Source development](https://www.jetbrains.com/community/opensource/?utm_campaign=opensource&utm_content=approved&utm_medium=email&utm_source=newsletter&utm_term=jblogo#support). 96 | -------------------------------------------------------------------------------- /src/commons.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use chrono::{Datelike, Local, Timelike}; 16 | use eyre::Result; 17 | use ring::digest::{Context, SHA256}; 18 | use std::{ 19 | borrow::Borrow, 20 | collections::HashMap, 21 | fs::{read_dir, remove_file}, 22 | ops::Deref, 23 | path::Path, 24 | process::exit, 25 | }; 26 | 27 | // General utils 28 | 29 | pub fn abort(str: String) -> ! { 30 | eprintln!("FATAL: {}", str); 31 | exit(1); 32 | } 33 | 34 | // https://github.com/serde-rs/serde/issues/1030#issuecomment-522278006 35 | pub fn default_as_false() -> bool { 36 | false 37 | } 38 | 39 | pub fn default_as_true() -> bool { 40 | true 41 | } 42 | 43 | pub fn default_as_zero() -> i32 { 44 | 0 45 | } 46 | 47 | pub fn file_exists(path: &str) -> bool { 48 | let path = Path::new(path); 49 | Path::new(path).exists() 50 | } 51 | 52 | pub fn is_dir(path: &String) -> bool { 53 | let path = Path::new(path); 54 | Path::new(path).is_dir() 55 | } 56 | 57 | pub fn is_file_in_directory(file_path: &str, dir_path: &str) -> bool { 58 | let file_path = Path::new(file_path); 59 | let dir_path = Path::new(dir_path); 60 | 61 | file_path.starts_with(dir_path) 62 | } 63 | 64 | pub fn sha256(input: &String) -> String { 65 | let digest = { 66 | let mut context = Context::new(&SHA256); 67 | context.update(input.as_bytes()); // UTF-8 68 | context.finish() 69 | }; 70 | 71 | hex::encode(digest.as_ref()) 72 | } 73 | 74 | pub fn equal_case_insensitive(s1: &str, s2: &str) -> bool { 75 | s1.to_lowercase() == s2.to_lowercase() 76 | } 77 | 78 | pub fn now() -> String { 79 | let current_datetime = Local::now(); 80 | 81 | let year = current_datetime.year(); 82 | let month = current_datetime.month(); 83 | let day = current_datetime.day(); 84 | let hour = current_datetime.hour(); 85 | let minute = current_datetime.minute(); 86 | 87 | format!("{:04}{:02}{:02}-{:02}{:02}", year, month, day, hour, minute) 88 | } 89 | 90 | pub fn delete_old_files(dir: &str, files_to_keep: usize) -> Result<()> { 91 | let path = Path::new(dir); 92 | let dir = path.parent().map(|parent| parent.to_path_buf()).unwrap(); 93 | 94 | let mut entries: Vec<_> = read_dir(dir)?.filter_map(|entry| entry.ok()).collect(); 95 | 96 | entries.sort_by(|a, b| { 97 | let a_meta = a.metadata().unwrap(); 98 | let b_meta = b.metadata().unwrap(); 99 | a_meta.modified().unwrap().cmp(&b_meta.modified().unwrap()) 100 | }); 101 | 102 | let num_entries = entries.len(); 103 | 104 | if num_entries > files_to_keep { 105 | for entry in entries.iter().take(num_entries - files_to_keep) { 106 | if entry.path().is_file() { 107 | remove_file(entry.path())?; 108 | } 109 | } 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | pub fn check_stored_stmt<'a>( 116 | sql: &'a String, 117 | stored_statements: &'a HashMap, 118 | use_only_stored_statements: bool, 119 | ) -> Result<&'a String> { 120 | match sql.strip_prefix('^') { 121 | Some(s) => match stored_statements.get(&s.to_string()) { 122 | Some(s) => Ok(s), 123 | None => Err(eyre!("Stored statement '{}' not found", sql)), 124 | }, 125 | None => { 126 | if use_only_stored_statements { 127 | Err(eyre!( 128 | "UseOnlyStoredStatement set but a stored statement wasn't used" 129 | )) 130 | } else { 131 | Ok(sql) 132 | } 133 | } 134 | } 135 | } 136 | 137 | pub fn resolve_tilde(p: &String) -> String { 138 | shellexpand::tilde(p).into_owned() 139 | } 140 | 141 | pub fn split_on_first_double_colon(input: &str) -> (String, String) { 142 | let mut parts = input.splitn(2, "::"); 143 | let first_part = parts.next().unwrap_or_default().to_string(); 144 | let second_part = parts.next().unwrap_or_default().to_string(); 145 | 146 | (first_part, second_part) 147 | } 148 | 149 | pub fn if_abort_eyre(result: eyre::Result) -> T { 150 | result.unwrap_or_else(|e| abort(e.to_string())) 151 | } 152 | 153 | pub fn if_abort_rusqlite(result: rusqlite::Result) -> T { 154 | result.unwrap_or_else(|e| abort(e.to_string())) 155 | } 156 | 157 | pub fn assert(condition: bool, msg: String) { 158 | if !condition { 159 | abort(msg); 160 | } 161 | } 162 | 163 | // Utils to convert serde structs to slices accepted by rusqlite as named params 164 | // adapted from serde-rusqlite, https://github.com/twistedfall/serde_rusqlite/blob/master/LICENSE 165 | 166 | pub struct NamedParamsContainer(Vec<(String, Box)>); 167 | 168 | impl NamedParamsContainer { 169 | pub fn slice(&self) -> Vec<(&str, &dyn rusqlite::types::ToSql)> { 170 | self.0 171 | .iter() 172 | .map(|el| (el.0.deref(), el.1.borrow())) 173 | .collect() 174 | } 175 | } 176 | 177 | impl From)>> for NamedParamsContainer { 178 | fn from(src: Vec<(String, Box)>) -> Self { 179 | Self(src) 180 | } 181 | } 182 | 183 | pub struct PositionalParamsContainer(Vec>); 184 | 185 | impl PositionalParamsContainer { 186 | pub fn slice(&self) -> Vec<&dyn rusqlite::types::ToSql> { 187 | self.0.iter().map(|el| (el.borrow())).collect() 188 | } 189 | } 190 | 191 | impl From>> for PositionalParamsContainer { 192 | fn from(src: Vec>) -> Self { 193 | Self(src) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/backup.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ops::DerefMut, path::Path as SysPath, time::Duration}; 16 | 17 | use actix_web::{ 18 | rt::{ 19 | spawn, 20 | time::{interval_at, sleep, Instant}, 21 | }, 22 | web, Responder, 23 | }; 24 | use rusqlite::Connection; 25 | 26 | use crate::{ 27 | auth::process_creds, 28 | commons::{abort, delete_old_files, file_exists}, 29 | db_config::Backup, 30 | main_config::Db, 31 | req_res::{Response, Token}, 32 | MUTEXES, 33 | }; 34 | 35 | fn gen_bkp_file(directory: &str, filepath: &str) -> String { 36 | let path = SysPath::new(directory); 37 | let fpath = SysPath::new(filepath); 38 | let base_name = fpath.file_stem().unwrap().to_str().unwrap(); 39 | let extension = fpath.extension(); 40 | let intermission = crate::commons::now(); 41 | 42 | let new_file_name = match extension { 43 | Some(e) => format!("{}_{}.{}", base_name, intermission, e.to_str().unwrap()), 44 | None => format!("{}_{}", base_name, intermission), 45 | }; 46 | let new_file_path = path.join(new_file_name); 47 | 48 | new_file_path.into_os_string().into_string().unwrap() 49 | } 50 | 51 | fn do_backup(bkp_dir: &str, num_files: usize, db_path: &str, conn: &Connection) -> Response { 52 | if !file_exists(bkp_dir) { 53 | return Response::new_err(404, -1, format!("Backup dir '{}' not found", bkp_dir)); 54 | } 55 | 56 | let file = gen_bkp_file(bkp_dir, db_path); 57 | if file_exists(&file) { 58 | Response::new_err(409, -1, format!("File '{}' already exists", file)) 59 | } else { 60 | match conn.execute("VACUUM INTO ?1", [&file]) { 61 | Ok(_) => match delete_old_files(&file, num_files) { 62 | Ok(_) => Response::new_ok(vec![]), 63 | Err(e) => Response::new_err( 64 | 500, 65 | -1, 66 | format!("Database backed up but error in deleting old files: {}", e), 67 | ), 68 | }, 69 | Err(e) => Response::new_err(500, -1, e.to_string()), 70 | } 71 | } 72 | } 73 | 74 | pub async fn handler( 75 | db_conf: web::Data, 76 | db_name: web::Data, 77 | token: web::Query, 78 | ) -> impl Responder { 79 | let db_name = db_name.to_string(); 80 | match &db_conf.conf.backup { 81 | Some(bkp) => match &bkp.execution.web_service { 82 | Some(bkp_ws) => { 83 | if !process_creds(&token.token, &bkp_ws.auth_token, &bkp_ws.hashed_auth_token) { 84 | sleep(Duration::from_millis(1000)).await; 85 | 86 | return Response::new_err( 87 | bkp_ws.auth_error_code, 88 | -1, 89 | format!("In database '{}', backup: token mismatch", db_name), 90 | ); 91 | } 92 | 93 | let db_lock = MUTEXES.get().unwrap().get(&db_name).unwrap(); 94 | let mut db_lock_guard = db_lock.lock().unwrap(); 95 | let conn = db_lock_guard.deref_mut(); 96 | 97 | do_backup(&bkp.backup_dir, bkp.num_files, &db_conf.path, conn) 98 | } 99 | None => Response::new_err( 100 | 404, 101 | -1, 102 | format!( 103 | "Database '{}' doesn't have a backup.execution.webService node", 104 | db_name 105 | ), 106 | ), 107 | }, 108 | None => Response::new_err( 109 | 404, 110 | -1, 111 | format!("Database '{}' doesn't have a backup node", db_name), 112 | ), 113 | } 114 | } 115 | 116 | pub fn bootstrap_backup( 117 | is_new_db: bool, 118 | bkp: &Backup, 119 | db_name: &String, 120 | db_path: &str, 121 | conn: &Connection, 122 | ) { 123 | let bkp = bkp.to_owned(); 124 | let bex = &bkp.execution; 125 | if bex.on_startup || (is_new_db && bex.on_create) { 126 | let res = do_backup(&bkp.backup_dir, bkp.num_files, db_path, conn); 127 | if !res.success { 128 | abort(format!( 129 | "Backup of database '{}': {}", 130 | db_name, 131 | res.message.unwrap() 132 | )); 133 | } 134 | } 135 | println!(" - backup configured"); 136 | if bex.on_create { 137 | println!(" - performed on database creation"); 138 | } 139 | if bex.on_startup { 140 | println!(" - performed on server startup"); 141 | } 142 | if bex.period > 0 { 143 | println!(" - performed periodically"); 144 | } 145 | if bex.web_service.is_some() { 146 | println!(" - callable via web service"); 147 | } 148 | } 149 | 150 | pub fn periodic_backup(bkp: &Backup, db_name: String, db_path: String) { 151 | let bkp = bkp.to_owned(); 152 | let period = bkp.execution.period; 153 | let bkp_dir = bkp.backup_dir; 154 | let num_files = bkp.num_files; 155 | if period > 0 { 156 | let period = period as u64 * 60; 157 | spawn(async move { 158 | let p = Duration::from_secs(period); 159 | let first_start = Instant::now().checked_add(p).unwrap(); 160 | let mut interval = interval_at(first_start, p); 161 | 162 | loop { 163 | interval.tick().await; 164 | 165 | if !file_exists(&bkp_dir) { 166 | eprintln!("Backup dir '{}' not found", bkp_dir); 167 | return; 168 | } 169 | 170 | let file = gen_bkp_file(&bkp_dir, &db_path); 171 | if file_exists(&file) { 172 | eprintln!("File '{}' already exists", file); 173 | } else { 174 | let db_lock = MUTEXES.get().unwrap().get(&db_name).unwrap(); 175 | let mut db_lock_guard = db_lock.lock().unwrap(); 176 | let conn = db_lock_guard.deref_mut(); 177 | 178 | let res = do_backup(&bkp_dir, num_files, &db_path, conn); 179 | if !res.success { 180 | eprintln!("Backing up '{}': {}", db_name, res.message.unwrap()) 181 | } 182 | } 183 | } 184 | }); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main_config.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fs::remove_file; 16 | use std::sync::Mutex; 17 | use std::{collections::HashMap, path::Path}; 18 | 19 | use rusqlite::Connection; 20 | 21 | use crate::backup::{bootstrap_backup, periodic_backup}; 22 | use crate::commandline::AppConfig; 23 | use crate::commons::{ 24 | abort, assert, file_exists, if_abort_rusqlite, is_dir, is_file_in_directory, resolve_tilde, 25 | split_on_first_double_colon, 26 | }; 27 | use crate::db_config::{parse_dbconf, DbConfig, Macro}; 28 | use crate::macros::{bootstrap_db_macros, count_macros, periodic_macro, resolve_macros}; 29 | use crate::MUTEXES; 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct Db { 33 | pub is_mem: bool, 34 | pub path: String, 35 | pub conf: DbConfig, 36 | 37 | // calculated 38 | pub stored_statements: HashMap, 39 | pub macros: HashMap, 40 | } 41 | 42 | fn split_path(path: &str) -> (String, String, String) { 43 | // returns (db_path, yaml, db_name) 44 | let (mut db_path, mut yaml) = split_on_first_double_colon(path); 45 | db_path = resolve_tilde(&db_path); 46 | let path = Path::new(&db_path); 47 | if yaml.is_empty() { 48 | let file_stem = path.file_stem().unwrap().to_str().unwrap(); 49 | let yaml_file_name = format!("{file_stem}.yaml"); 50 | let yaml_path = path.with_file_name(yaml_file_name); 51 | yaml = yaml_path.to_str().unwrap().to_string(); 52 | } 53 | let yaml = resolve_tilde(&yaml); 54 | 55 | let db_name = path.file_stem().unwrap().to_str().unwrap().to_string(); 56 | 57 | (db_path, yaml, db_name) 58 | } 59 | 60 | fn compose_single_db( 61 | yaml: &String, 62 | conn_string: &String, 63 | db_name: &String, 64 | db_path: &String, // simple name if in-mem 65 | is_new_db: bool, 66 | is_mem: bool, 67 | ) -> (Db, Connection) { 68 | println!("- Database '{}'", db_name); 69 | 70 | if is_mem { 71 | println!(" - in-memory database"); 72 | } else { 73 | println!(" - from file '{}'", db_path); 74 | if is_new_db { 75 | println!(" - file not present, it will be created"); 76 | } 77 | } 78 | 79 | let mut dbconf = if yaml.is_empty() || !file_exists(yaml) { 80 | println!(" - companion file not found: assuming defaults"); 81 | DbConfig::default() 82 | } else { 83 | println!(" - parsing companion file '{}'", yaml); 84 | parse_dbconf(yaml).unwrap_or_else(|e| abort(format!("parsing YAML {}: {}", yaml, e))) 85 | }; 86 | 87 | if let Some(orig) = &dbconf.cors_origin { 88 | println!(" - allowed CORS origin: {}", orig); 89 | } 90 | 91 | if let Some(b) = &mut dbconf.backup { 92 | assert( 93 | b.num_files > 0, 94 | "backup: num_files must be 1 or more".to_string(), 95 | ); 96 | let bd = resolve_tilde(&b.backup_dir); 97 | assert( 98 | is_dir(&bd), 99 | format!("backup directory does not exist: {}", bd), 100 | ); 101 | b.backup_dir = bd; 102 | } 103 | 104 | if let Some(a) = &dbconf.auth { 105 | assert( 106 | a.by_credentials.is_none() != a.by_query.is_none(), 107 | "auth: exactly one among by_credentials and by_query must be specified".to_string(), 108 | ); 109 | if let Some(vc) = &a.by_credentials { 110 | for c in vc { 111 | assert( 112 | c.password.is_some() || c.hashed_password.is_some(), 113 | format!( 114 | "auth: user '{}': password or hashedPassword must be specified", 115 | &c.user 116 | ), 117 | ); 118 | } 119 | } 120 | println!(" - authentication set up"); 121 | } 122 | 123 | let stored_statements: HashMap = dbconf 124 | .to_owned() 125 | .stored_statements 126 | .map(|ss| { 127 | ss.iter() 128 | .map(|el| (el.id.to_owned(), el.sql.to_owned())) 129 | .collect() 130 | }) 131 | .unwrap_or_default(); 132 | 133 | if !stored_statements.is_empty() { 134 | println!( 135 | " - {} stored statements configured", 136 | stored_statements.len() 137 | ); 138 | if dbconf.use_only_stored_statements { 139 | println!(" - allowing only stored statements for requests") 140 | } 141 | } 142 | 143 | let macros: HashMap = resolve_macros(&mut dbconf, &stored_statements); 144 | 145 | for macr in macros.values() { 146 | assert( 147 | !macr.statements.is_empty(), 148 | format!("Macro '{}' does not have any statement", macr.id), 149 | ); 150 | } 151 | 152 | if !macros.is_empty() { 153 | println!(" - {} macro(s) configured", macros.len()); 154 | let count = count_macros(macros.to_owned()); 155 | if count[0] > 0 { 156 | println!(" - {} applied on database creation", count[0]); 157 | } 158 | if count[1] > 0 { 159 | println!(" - {} applied on server startup", count[1]); 160 | } 161 | if count[2] > 0 { 162 | println!(" - {} applied periodically", count[2]); 163 | } 164 | if count[3] > 0 { 165 | println!(" - {} callable via web service", count[3]); 166 | } 167 | } 168 | 169 | let mut conn = if_abort_rusqlite(Connection::open(conn_string)); 170 | 171 | let res = bootstrap_db_macros(is_new_db, &dbconf, db_name, &mut conn); 172 | if res.is_err() { 173 | let _ = conn.close(); 174 | if !is_mem && is_new_db { 175 | let _ = remove_file(Path::new(db_path)); 176 | } 177 | abort(res.err().unwrap().to_string()); 178 | } 179 | 180 | for macr in macros.values() { 181 | periodic_macro(macr.to_owned(), db_name.to_owned()); 182 | } 183 | 184 | if let Some(backup) = dbconf.to_owned().backup { 185 | if !is_mem { 186 | assert( 187 | !is_file_in_directory(db_path, &backup.backup_dir), 188 | format!( 189 | "Backup config for '{}': backup dir cannot be the same as db file dir", 190 | db_name 191 | ), 192 | ); 193 | } 194 | 195 | bootstrap_backup(is_new_db, &backup, db_name, db_path, &conn); 196 | 197 | periodic_backup(&backup, db_name.to_owned(), conn_string.to_owned()); 198 | } 199 | 200 | if dbconf.read_only { 201 | if_abort_rusqlite(conn.execute("PRAGMA query_only = true", [])); 202 | println!(" - read-only"); 203 | } 204 | 205 | let jm = dbconf.journal_mode.to_owned().unwrap_or("WAL".to_string()); 206 | if_abort_rusqlite(conn.query_row(&format!("PRAGMA journal_mode = {}", jm), [], |_| Ok(()))); 207 | println!(" - journal mode: {}", jm); 208 | 209 | let db_conf = Db { 210 | is_mem, 211 | path: conn_string.to_owned(), 212 | conf: dbconf, 213 | stored_statements, 214 | macros, 215 | }; 216 | (db_conf, conn) 217 | } 218 | 219 | fn check_db_name(db_name: &String, db_map: &HashMap) { 220 | assert( 221 | !db_map.contains_key(db_name), 222 | format!("database '{}' already defined", db_name), 223 | ); 224 | } 225 | 226 | pub fn compose_db_map(cl: &AppConfig) -> HashMap { 227 | let mut db_map = HashMap::new(); 228 | let mut mutexes = HashMap::new(); 229 | for db_path in &cl.db { 230 | let (db_path, yaml, db_name) = split_path(db_path); 231 | check_db_name(&db_name, &db_map); 232 | 233 | let is_new_db = !file_exists(&db_path); 234 | 235 | let (db_cfg, conn) = 236 | compose_single_db(&yaml, &db_path, &db_name, &db_path, is_new_db, false); 237 | 238 | db_map.insert(db_name.to_owned(), db_cfg); 239 | mutexes.insert(db_name.to_owned(), Mutex::new(conn)); 240 | } 241 | for db in &cl.mem_db { 242 | let (db_name, yaml) = split_on_first_double_colon(db); 243 | check_db_name(&db_name, &db_map); 244 | 245 | let yaml = resolve_tilde(&yaml); 246 | let conn_string = format!("file:{}?mode=memory", db_name); 247 | 248 | let (db_cfg, conn) = compose_single_db(&yaml, &conn_string, &db_name, &db_name, true, true); 249 | 250 | db_map.insert(db_name.to_owned(), db_cfg); 251 | mutexes.insert(db_name.to_owned(), Mutex::new(conn)); 252 | } 253 | let _ = MUTEXES.set(mutexes); 254 | db_map 255 | } 256 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{collections::HashMap, ops::DerefMut, time::Duration}; 16 | 17 | use actix_web::{ 18 | rt::{ 19 | spawn, 20 | time::{interval_at, sleep, Instant}, 21 | }, 22 | web::{self, Path}, 23 | Responder, 24 | }; 25 | use eyre::Result; 26 | use rusqlite::Connection; 27 | 28 | use crate::{ 29 | auth::process_creds, 30 | commons::{check_stored_stmt, if_abort_eyre}, 31 | db_config::{DbConfig, Macro}, 32 | main_config::Db, 33 | req_res::{Response, ResponseItem, Token}, 34 | MUTEXES, 35 | }; 36 | 37 | /// Parses the macro list and substitutes the references to stored statements with the target sql 38 | pub fn resolve_macros( 39 | dbconf: &mut DbConfig, 40 | stored_statements: &HashMap, 41 | ) -> HashMap { 42 | let mut ret: HashMap = HashMap::new(); 43 | if let Some(ms) = &mut dbconf.macros { 44 | for macr in ms { 45 | let mut statements: Vec = vec![]; 46 | #[allow(clippy::unnecessary_to_owned)] 47 | for statement in macr.statements.to_owned() { 48 | let statement = 49 | if_abort_eyre(check_stored_stmt(&statement, stored_statements, false)); 50 | statements.push(statement.to_owned()); 51 | } 52 | macr.statements = statements; 53 | ret.insert(macr.id.to_owned(), macr.to_owned()); 54 | } 55 | } 56 | ret 57 | } 58 | 59 | fn exec_macro_single_notrx(macr: &Macro, conn: &mut Connection) -> Response { 60 | let mut ret = vec![]; 61 | for (i, statement) in macr.statements.iter().enumerate() { 62 | let changed_rows = conn.execute(statement, []); 63 | match changed_rows { 64 | Ok(cr) => { 65 | ret.push(ResponseItem { 66 | success: true, 67 | error: None, 68 | result_set: None, 69 | rows_updated: Some(cr), 70 | rows_updated_batch: None, 71 | }); 72 | } 73 | Err(e) => { 74 | return Response::new_err(500, i as isize, e.to_string()); 75 | } 76 | } 77 | } 78 | 79 | Response::new_ok(ret) 80 | } 81 | 82 | fn exec_macro_single(macr: &Macro, conn: &mut Connection) -> Response { 83 | if macr.disable_transaction { 84 | return exec_macro_single_notrx(macr, conn); 85 | } 86 | 87 | let tx = match conn.transaction() { 88 | Ok(tx) => tx, 89 | Err(_) => { 90 | return Response::new_err( 91 | 500, 92 | -1, 93 | format!("Transaction open failed for macro '{}'", macr.id), 94 | ) 95 | } 96 | }; 97 | 98 | let mut ret = vec![]; 99 | for (i, statement) in macr.statements.iter().enumerate() { 100 | let changed_rows = tx.execute(statement, []); 101 | match changed_rows { 102 | Ok(cr) => { 103 | ret.push(ResponseItem { 104 | success: true, 105 | error: None, 106 | result_set: None, 107 | rows_updated: Some(cr), 108 | rows_updated_batch: None, 109 | }); 110 | } 111 | Err(e) => { 112 | let _ = tx.rollback(); 113 | return Response::new_err(500, i as isize, e.to_string()); 114 | } 115 | } 116 | } 117 | 118 | match tx.commit() { 119 | Ok(_) => Response::new_ok(ret), 120 | Err(_) => Response::new_err(500, -1, format!("Commit failed for macro '{}'", macr.id)), 121 | } 122 | } 123 | 124 | pub fn bootstrap_db_macros( 125 | is_new_db: bool, 126 | db_conf: &DbConfig, 127 | db_name: &String, 128 | conn: &mut Connection, 129 | ) -> Result<()> { 130 | match &db_conf.macros { 131 | Some(macros) => { 132 | for macr in macros { 133 | if macr.execution.on_startup || (is_new_db && macr.execution.on_create) { 134 | let res = exec_macro_single(macr, conn); 135 | if !res.success { 136 | return Result::Err(eyre!( 137 | "In macro '{}' of db '{}', index {}: {}", 138 | macr.id, 139 | db_name, 140 | res.req_idx.unwrap_or(-1), 141 | res.message.unwrap_or("unknown error".to_string()) 142 | )); 143 | }; 144 | } 145 | } 146 | } 147 | None => (), 148 | } 149 | 150 | Result::Ok(()) 151 | } 152 | 153 | pub async fn handler( 154 | db_conf: web::Data, 155 | db_name: web::Data, 156 | macro_name: Path, 157 | token: web::Query, 158 | ) -> impl Responder { 159 | let db_name = db_name.to_string(); 160 | let macro_name = macro_name.to_string(); 161 | 162 | match db_conf.macros.get(¯o_name) { 163 | Some(macr) => match ¯.execution.web_service { 164 | Some(mex_ws) => { 165 | if !process_creds(&token.token, &mex_ws.auth_token, &mex_ws.hashed_auth_token) { 166 | sleep(Duration::from_millis(1000)).await; 167 | 168 | return Response::new_err( 169 | mex_ws.auth_error_code, 170 | -1, 171 | format!( 172 | "In database '{}', macro '{}': token mismatch", 173 | db_name, macro_name 174 | ), 175 | ); 176 | } 177 | 178 | let db_lock = MUTEXES.get().unwrap().get(&db_name).unwrap(); 179 | let mut db_lock_guard = db_lock.lock().unwrap(); 180 | let conn = db_lock_guard.deref_mut(); 181 | 182 | exec_macro_single(macr, conn) 183 | } 184 | None => Response::new_err( 185 | 404, 186 | -1, 187 | format!( 188 | "In database '{}', macro '{}' doesn't have an execution node", 189 | db_name, macro_name 190 | ), 191 | ), 192 | }, 193 | None => Response::new_err( 194 | 404, 195 | -1, 196 | format!( 197 | "Database '{}' doesn't have a macro named '{}'", 198 | db_name, macro_name 199 | ), 200 | ), 201 | } 202 | } 203 | 204 | pub fn periodic_macro(macr: Macro, db_name: String) { 205 | if macr.execution.period > 0 { 206 | spawn(async move { 207 | let p = Duration::from_secs(macr.execution.period as u64 * 60); 208 | let first_start = Instant::now().checked_add(p).unwrap(); 209 | let mut interval = interval_at(first_start, p); 210 | 211 | loop { 212 | interval.tick().await; // skip first execution 213 | 214 | let db_lock = MUTEXES.get().unwrap().get(&db_name).unwrap(); 215 | let mut db_lock_guard = db_lock.lock().unwrap(); 216 | let conn = db_lock_guard.deref_mut(); 217 | 218 | let tx = match conn.transaction() { 219 | Ok(tx) => tx, 220 | Err(_) => { 221 | eprintln!( 222 | "Transaction open failed for db '{}', macro '{}'", 223 | db_name, macr.id, 224 | ); 225 | return; 226 | } 227 | }; 228 | 229 | for (i, statement) in macr.statements.iter().enumerate() { 230 | match tx.execute(statement, []) { 231 | Ok(_) => (), 232 | Err(e) => { 233 | let _ = tx.rollback(); 234 | eprintln!( 235 | "In macro '{}' of db '{}', index {}: {}", 236 | macr.id, db_name, i, e 237 | ); 238 | return; 239 | } 240 | } 241 | } 242 | 243 | match tx.commit() { 244 | Ok(_) => println!("Macro '{}' executed for db '{}'", macr.id, db_name), 245 | Err(e) => { 246 | eprintln!( 247 | "Commit failed for startup macros in db '{}': {}", 248 | db_name, e 249 | ); 250 | } 251 | } 252 | } 253 | }); 254 | } 255 | } 256 | 257 | pub fn count_macros(macros: HashMap) -> [usize; 4] { 258 | // return [num_on_create, num_on_startup, num_periodic, num_exposed_via_webservice] 259 | let mut ret = [0, 0, 0, 0]; 260 | for macr in macros.values() { 261 | let e = ¯.execution; 262 | if e.on_create { 263 | ret[0] += 1; 264 | } 265 | if e.on_startup { 266 | ret[1] += 1; 267 | } 268 | if e.period > 0 { 269 | ret[2] += 1; 270 | } 271 | if e.web_service.is_some() { 272 | ret[3] += 1; 273 | } 274 | } 275 | ret 276 | } 277 | -------------------------------------------------------------------------------- /src/logic.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-, Germano Rizzo 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{collections::HashMap, ops::DerefMut, time::Duration}; 16 | 17 | use actix_web::{http::header::Header, rt::time::sleep, web, HttpRequest, Responder}; 18 | use actix_web_httpauth::headers::authorization::{Authorization, Basic}; 19 | use eyre::Result; 20 | use rusqlite::{types::Value, ToSql, Transaction}; 21 | use serde_json::{json, Map as JsonMap, Value as JsonValue}; 22 | 23 | use crate::{ 24 | auth::process_auth, 25 | commons::{check_stored_stmt, NamedParamsContainer, PositionalParamsContainer}, 26 | db_config::{AuthMode, DbConfig}, 27 | main_config::Db, 28 | req_res::{self, Response, ResponseItem}, 29 | MUTEXES, 30 | }; 31 | 32 | fn val_db2val_json(val: Value) -> JsonValue { 33 | match val { 34 | Value::Null => JsonValue::Null, 35 | Value::Integer(v) => json!(v), 36 | Value::Real(v) => json!(v), 37 | Value::Text(v) => json!(v), 38 | Value::Blob(v) => json!(v), 39 | } 40 | } 41 | 42 | fn calc_named_params(params: &JsonMap) -> NamedParamsContainer { 43 | let mut named_params: Vec<(String, Box)> = Vec::new(); 44 | 45 | for (k, v) in params { 46 | let mut key: String = String::from(":"); 47 | key.push_str(k); 48 | let val: Box = if v.is_string() { 49 | Box::new(v.as_str().unwrap().to_owned()) 50 | } else { 51 | Box::new(v.to_owned()) 52 | }; 53 | named_params.push((key, val)); 54 | } 55 | 56 | NamedParamsContainer::from(named_params) 57 | } 58 | 59 | fn calc_positional_params(params: &Vec) -> PositionalParamsContainer { 60 | let mut ret_params: Vec> = Vec::new(); 61 | 62 | for v in params { 63 | let val: Box = if v.is_string() { 64 | Box::new(v.as_str().unwrap().to_owned()) 65 | } else { 66 | Box::new(v.to_owned()) 67 | }; 68 | ret_params.push(val); 69 | } 70 | 71 | PositionalParamsContainer::from(ret_params) 72 | } 73 | 74 | #[allow(clippy::type_complexity)] 75 | fn do_query( 76 | tx: &Transaction, 77 | sql: &str, 78 | values: &Option, 79 | ) -> Result<(Option>, Option, Option>)> { 80 | let mut stmt = tx.prepare(sql)?; 81 | let column_names: Vec = stmt 82 | .column_names() 83 | .iter() 84 | .map(|cn| cn.to_string()) 85 | .collect(); 86 | let mut rows = match values { 87 | Some(p) => { 88 | // FIXME this code is repeated three times; I wish I could make a common 89 | // function but, no matter how I try, it seems not to be possible 90 | if p.is_object() { 91 | let map = p.as_object().unwrap(); 92 | stmt.query(calc_named_params(map).slice().as_slice())? 93 | } else if p.is_array() { 94 | let array = p.as_array().unwrap(); 95 | stmt.query(calc_positional_params(array).slice().as_slice())? 96 | } else { 97 | return Err(eyre!("Values are neither positional nor named".to_string())); 98 | } 99 | } 100 | None => stmt.query([])?, 101 | }; 102 | let mut response = vec![]; 103 | loop { 104 | let row = rows.next(); 105 | match row { 106 | Ok(row) => match row { 107 | Some(row) => { 108 | let mut map: JsonMap = JsonMap::new(); 109 | for (i, col_name) in column_names.iter().enumerate() { 110 | let value: Value = row.get_unwrap(i); 111 | map.insert(col_name.to_string(), val_db2val_json(value)); 112 | } 113 | response.push(JsonValue::Object(map)); 114 | } 115 | None => break, 116 | }, 117 | Err(e) => return Err(eyre!(e.to_string())), 118 | } 119 | } 120 | Ok((Some(response), None, None)) 121 | } 122 | 123 | #[allow(clippy::type_complexity)] 124 | fn do_statement( 125 | tx: &Transaction, 126 | sql: &str, 127 | values: &Option, 128 | values_batch: &Option>, 129 | ) -> Result<(Option>, Option, Option>)> { 130 | Ok(if values.is_none() && values_batch.is_none() { 131 | let changed_rows = tx.execute(sql, [])?; 132 | (None, Some(changed_rows), None) 133 | } else if values.is_some() { 134 | let p = values.as_ref().unwrap(); 135 | let changed_rows = if p.is_object() { 136 | let map = p.as_object().unwrap(); 137 | tx.execute(sql, calc_named_params(map).slice().as_slice())? 138 | } else if p.is_array() { 139 | let array = p.as_array().unwrap(); 140 | tx.execute(sql, calc_positional_params(array).slice().as_slice())? 141 | } else { 142 | return Err(eyre!("Values are neither positional nor named".to_string())); 143 | }; 144 | 145 | (None, Some(changed_rows), None) 146 | } else { 147 | // values_batch.is_some() 148 | let mut stmt = tx.prepare(sql)?; 149 | let mut ret = vec![]; 150 | for p in values_batch.as_ref().unwrap() { 151 | let changed_rows = if p.is_object() { 152 | let map = p.as_object().unwrap(); 153 | stmt.execute(calc_named_params(map).slice().as_slice())? 154 | } else if p.is_array() { 155 | let array = p.as_array().unwrap(); 156 | stmt.execute(calc_positional_params(array).slice().as_slice())? 157 | } else { 158 | return Err(eyre!("Values are neither positional nor named".to_string())); 159 | }; 160 | 161 | ret.push(changed_rows); 162 | } 163 | (None, None, Some(ret)) 164 | }) 165 | } 166 | 167 | fn process( 168 | db_name: &str, 169 | http_req: web::Json, 170 | stored_statements: &HashMap, 171 | dbconf: &DbConfig, 172 | ) -> Result { 173 | let db_lock = MUTEXES.get().unwrap().get(db_name).unwrap(); 174 | let mut db_lock_guard = db_lock.lock().unwrap(); 175 | let conn = db_lock_guard.deref_mut(); 176 | let tx = conn.transaction()?; 177 | 178 | let mut results = vec![]; 179 | let mut failed: Option<(u16, usize, String)> = None; // http code, index, error 180 | 181 | for (idx, trx_item) in http_req.transaction.iter().enumerate() { 182 | #[allow(clippy::type_complexity)] 183 | let ret: Result< 184 | (Option>, Option, Option>), 185 | (u16, String), // Error: (http code, message) 186 | > = if trx_item.query.is_some() == trx_item.statement.is_some() { 187 | Err(( 188 | 400, 189 | "exactly one of 'query' and 'statement' must be provided".to_string(), 190 | )) 191 | } else if let Some(query) = &trx_item.query { 192 | match check_stored_stmt(query, stored_statements, dbconf.use_only_stored_statements) { 193 | Ok(sql) => match do_query(&tx, sql, &trx_item.values) { 194 | Ok(ok_payload) => Ok(ok_payload), 195 | Err(err) => Err((500, err.to_string())), 196 | }, 197 | Err(e) => Err((409, e.to_string())), 198 | } 199 | } else if trx_item.values.is_some() && trx_item.values_batch.is_some() { 200 | Err(( 201 | 400, 202 | "at most one of values and values_batch must be provided".to_string(), 203 | )) 204 | } else { 205 | let statement = trx_item.statement.as_ref().unwrap(); // always present, for the previous if's 206 | match check_stored_stmt( 207 | statement, 208 | stored_statements, 209 | dbconf.use_only_stored_statements, 210 | ) { 211 | Ok(sql) => match do_statement(&tx, sql, &trx_item.values, &trx_item.values_batch) { 212 | Ok(ok_payload) => Ok(ok_payload), 213 | Err(err) => Err((500, err.to_string())), 214 | }, 215 | Err(e) => Err((409, e.to_string())), 216 | } 217 | }; 218 | 219 | if !trx_item.no_fail { 220 | if let Err(err) = ret { 221 | failed = Some((err.0, idx, err.1)); 222 | break; 223 | } 224 | } 225 | 226 | results.push(match ret { 227 | Ok(val) => ResponseItem { 228 | success: true, 229 | error: None, 230 | result_set: val.0, 231 | rows_updated: val.1, 232 | rows_updated_batch: val.2, 233 | }, 234 | Err(err) => ResponseItem { 235 | success: false, 236 | error: Some(err.1), 237 | result_set: None, 238 | rows_updated: None, 239 | rows_updated_batch: None, 240 | }, 241 | }); 242 | } 243 | 244 | Ok(match failed { 245 | Some(f) => { 246 | tx.rollback()?; 247 | Response::new_err(f.0, f.1 as isize, f.2) 248 | } 249 | None => { 250 | tx.commit()?; 251 | Response::new_ok(results) 252 | } 253 | }) 254 | } 255 | 256 | pub async fn handler( 257 | req: HttpRequest, 258 | body: web::Json, 259 | db_conf: web::Data, 260 | db_name: web::Data, 261 | ) -> impl Responder { 262 | if let Some(ac) = &db_conf.conf.auth { 263 | let ac_headers = if matches!( 264 | db_conf.conf.auth.as_ref().unwrap().mode, 265 | AuthMode::HttpBasic 266 | ) { 267 | match Authorization::::parse(&req) { 268 | Ok(a) => Some(a), 269 | Err(_) => None, 270 | } 271 | } else { 272 | None 273 | }; 274 | 275 | if !process_auth(ac, &body.credentials, &ac_headers, &db_name) { 276 | sleep(Duration::from_millis(1000)).await; 277 | 278 | return Response::new_err(ac.auth_error_code, -1, "Authorization failed".to_string()); 279 | } 280 | } 281 | 282 | process(&db_name, body, &db_conf.stored_statements, &db_conf.conf).unwrap() 283 | } 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2023-, Germano Rizzo 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /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 = "actix-codec" 7 | version = "0.5.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" 10 | dependencies = [ 11 | "bitflags 1.3.2", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "memchr", 16 | "pin-project-lite", 17 | "tokio", 18 | "tokio-util", 19 | "tracing", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-cors" 24 | version = "0.6.4" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" 27 | dependencies = [ 28 | "actix-utils", 29 | "actix-web", 30 | "derive_more", 31 | "futures-util", 32 | "log", 33 | "once_cell", 34 | "smallvec", 35 | ] 36 | 37 | [[package]] 38 | name = "actix-files" 39 | version = "0.6.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" 42 | dependencies = [ 43 | "actix-http", 44 | "actix-service", 45 | "actix-utils", 46 | "actix-web", 47 | "askama_escape", 48 | "bitflags 1.3.2", 49 | "bytes", 50 | "derive_more", 51 | "futures-core", 52 | "http-range", 53 | "log", 54 | "mime", 55 | "mime_guess", 56 | "percent-encoding", 57 | "pin-project-lite", 58 | ] 59 | 60 | [[package]] 61 | name = "actix-http" 62 | version = "3.4.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" 65 | dependencies = [ 66 | "actix-codec", 67 | "actix-rt", 68 | "actix-service", 69 | "actix-utils", 70 | "ahash", 71 | "base64", 72 | "bitflags 2.4.1", 73 | "brotli", 74 | "bytes", 75 | "bytestring", 76 | "derive_more", 77 | "encoding_rs", 78 | "flate2", 79 | "futures-core", 80 | "h2", 81 | "http", 82 | "httparse", 83 | "httpdate", 84 | "itoa", 85 | "language-tags", 86 | "local-channel", 87 | "mime", 88 | "percent-encoding", 89 | "pin-project-lite", 90 | "rand", 91 | "sha1", 92 | "smallvec", 93 | "tokio", 94 | "tokio-util", 95 | "tracing", 96 | "zstd", 97 | ] 98 | 99 | [[package]] 100 | name = "actix-macros" 101 | version = "0.2.4" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 104 | dependencies = [ 105 | "quote", 106 | "syn 2.0.39", 107 | ] 108 | 109 | [[package]] 110 | name = "actix-router" 111 | version = "0.5.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" 114 | dependencies = [ 115 | "bytestring", 116 | "http", 117 | "regex", 118 | "serde", 119 | "tracing", 120 | ] 121 | 122 | [[package]] 123 | name = "actix-rt" 124 | version = "2.9.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" 127 | dependencies = [ 128 | "futures-core", 129 | "tokio", 130 | ] 131 | 132 | [[package]] 133 | name = "actix-server" 134 | version = "2.3.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" 137 | dependencies = [ 138 | "actix-rt", 139 | "actix-service", 140 | "actix-utils", 141 | "futures-core", 142 | "futures-util", 143 | "mio", 144 | "socket2", 145 | "tokio", 146 | "tracing", 147 | ] 148 | 149 | [[package]] 150 | name = "actix-service" 151 | version = "2.0.2" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 154 | dependencies = [ 155 | "futures-core", 156 | "paste", 157 | "pin-project-lite", 158 | ] 159 | 160 | [[package]] 161 | name = "actix-utils" 162 | version = "3.0.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 165 | dependencies = [ 166 | "local-waker", 167 | "pin-project-lite", 168 | ] 169 | 170 | [[package]] 171 | name = "actix-web" 172 | version = "4.4.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" 175 | dependencies = [ 176 | "actix-codec", 177 | "actix-http", 178 | "actix-macros", 179 | "actix-router", 180 | "actix-rt", 181 | "actix-server", 182 | "actix-service", 183 | "actix-utils", 184 | "actix-web-codegen", 185 | "ahash", 186 | "bytes", 187 | "bytestring", 188 | "cfg-if", 189 | "cookie", 190 | "derive_more", 191 | "encoding_rs", 192 | "futures-core", 193 | "futures-util", 194 | "itoa", 195 | "language-tags", 196 | "log", 197 | "mime", 198 | "once_cell", 199 | "pin-project-lite", 200 | "regex", 201 | "serde", 202 | "serde_json", 203 | "serde_urlencoded", 204 | "smallvec", 205 | "socket2", 206 | "time", 207 | "url", 208 | ] 209 | 210 | [[package]] 211 | name = "actix-web-codegen" 212 | version = "4.2.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" 215 | dependencies = [ 216 | "actix-router", 217 | "proc-macro2", 218 | "quote", 219 | "syn 2.0.39", 220 | ] 221 | 222 | [[package]] 223 | name = "actix-web-httpauth" 224 | version = "0.8.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" 227 | dependencies = [ 228 | "actix-utils", 229 | "actix-web", 230 | "base64", 231 | "futures-core", 232 | "futures-util", 233 | "log", 234 | "pin-project-lite", 235 | ] 236 | 237 | [[package]] 238 | name = "addr2line" 239 | version = "0.21.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 242 | dependencies = [ 243 | "gimli", 244 | ] 245 | 246 | [[package]] 247 | name = "adler" 248 | version = "1.0.2" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 251 | 252 | [[package]] 253 | name = "ahash" 254 | version = "0.8.6" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" 257 | dependencies = [ 258 | "cfg-if", 259 | "getrandom", 260 | "once_cell", 261 | "version_check", 262 | "zerocopy", 263 | ] 264 | 265 | [[package]] 266 | name = "aho-corasick" 267 | version = "1.1.2" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 270 | dependencies = [ 271 | "memchr", 272 | ] 273 | 274 | [[package]] 275 | name = "alloc-no-stdlib" 276 | version = "2.0.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 279 | 280 | [[package]] 281 | name = "alloc-stdlib" 282 | version = "0.2.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 285 | dependencies = [ 286 | "alloc-no-stdlib", 287 | ] 288 | 289 | [[package]] 290 | name = "allocator-api2" 291 | version = "0.2.16" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 294 | 295 | [[package]] 296 | name = "android-tzdata" 297 | version = "0.1.1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 300 | 301 | [[package]] 302 | name = "android_system_properties" 303 | version = "0.1.5" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 306 | dependencies = [ 307 | "libc", 308 | ] 309 | 310 | [[package]] 311 | name = "anstream" 312 | version = "0.6.4" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 315 | dependencies = [ 316 | "anstyle", 317 | "anstyle-parse", 318 | "anstyle-query", 319 | "anstyle-wincon", 320 | "colorchoice", 321 | "utf8parse", 322 | ] 323 | 324 | [[package]] 325 | name = "anstyle" 326 | version = "1.0.4" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 329 | 330 | [[package]] 331 | name = "anstyle-parse" 332 | version = "0.2.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 335 | dependencies = [ 336 | "utf8parse", 337 | ] 338 | 339 | [[package]] 340 | name = "anstyle-query" 341 | version = "1.0.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" 344 | dependencies = [ 345 | "windows-sys 0.52.0", 346 | ] 347 | 348 | [[package]] 349 | name = "anstyle-wincon" 350 | version = "3.0.2" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 353 | dependencies = [ 354 | "anstyle", 355 | "windows-sys 0.52.0", 356 | ] 357 | 358 | [[package]] 359 | name = "askama_escape" 360 | version = "0.10.3" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" 363 | 364 | [[package]] 365 | name = "autocfg" 366 | version = "1.1.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 369 | 370 | [[package]] 371 | name = "backtrace" 372 | version = "0.3.69" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 375 | dependencies = [ 376 | "addr2line", 377 | "cc", 378 | "cfg-if", 379 | "libc", 380 | "miniz_oxide", 381 | "object", 382 | "rustc-demangle", 383 | ] 384 | 385 | [[package]] 386 | name = "base64" 387 | version = "0.21.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" 390 | 391 | [[package]] 392 | name = "bitflags" 393 | version = "1.3.2" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 396 | 397 | [[package]] 398 | name = "bitflags" 399 | version = "2.4.1" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 402 | 403 | [[package]] 404 | name = "block-buffer" 405 | version = "0.10.4" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 408 | dependencies = [ 409 | "generic-array", 410 | ] 411 | 412 | [[package]] 413 | name = "brotli" 414 | version = "3.4.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" 417 | dependencies = [ 418 | "alloc-no-stdlib", 419 | "alloc-stdlib", 420 | "brotli-decompressor", 421 | ] 422 | 423 | [[package]] 424 | name = "brotli-decompressor" 425 | version = "2.5.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" 428 | dependencies = [ 429 | "alloc-no-stdlib", 430 | "alloc-stdlib", 431 | ] 432 | 433 | [[package]] 434 | name = "bumpalo" 435 | version = "3.14.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 438 | 439 | [[package]] 440 | name = "bytes" 441 | version = "1.5.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 444 | 445 | [[package]] 446 | name = "bytestring" 447 | version = "1.3.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" 450 | dependencies = [ 451 | "bytes", 452 | ] 453 | 454 | [[package]] 455 | name = "cc" 456 | version = "1.0.83" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 459 | dependencies = [ 460 | "jobserver", 461 | "libc", 462 | ] 463 | 464 | [[package]] 465 | name = "cfg-if" 466 | version = "1.0.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 469 | 470 | [[package]] 471 | name = "chrono" 472 | version = "0.4.31" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 475 | dependencies = [ 476 | "android-tzdata", 477 | "iana-time-zone", 478 | "js-sys", 479 | "num-traits", 480 | "wasm-bindgen", 481 | "windows-targets 0.48.5", 482 | ] 483 | 484 | [[package]] 485 | name = "clap" 486 | version = "4.4.11" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" 489 | dependencies = [ 490 | "clap_builder", 491 | "clap_derive", 492 | ] 493 | 494 | [[package]] 495 | name = "clap_builder" 496 | version = "4.4.11" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" 499 | dependencies = [ 500 | "anstream", 501 | "anstyle", 502 | "clap_lex", 503 | "strsim", 504 | ] 505 | 506 | [[package]] 507 | name = "clap_derive" 508 | version = "4.4.7" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 511 | dependencies = [ 512 | "heck", 513 | "proc-macro2", 514 | "quote", 515 | "syn 2.0.39", 516 | ] 517 | 518 | [[package]] 519 | name = "clap_lex" 520 | version = "0.6.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 523 | 524 | [[package]] 525 | name = "colorchoice" 526 | version = "1.0.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 529 | 530 | [[package]] 531 | name = "convert_case" 532 | version = "0.4.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 535 | 536 | [[package]] 537 | name = "cookie" 538 | version = "0.16.2" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 541 | dependencies = [ 542 | "percent-encoding", 543 | "time", 544 | "version_check", 545 | ] 546 | 547 | [[package]] 548 | name = "core-foundation-sys" 549 | version = "0.8.6" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 552 | 553 | [[package]] 554 | name = "cpufeatures" 555 | version = "0.2.11" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 558 | dependencies = [ 559 | "libc", 560 | ] 561 | 562 | [[package]] 563 | name = "crc32fast" 564 | version = "1.3.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 567 | dependencies = [ 568 | "cfg-if", 569 | ] 570 | 571 | [[package]] 572 | name = "crypto-common" 573 | version = "0.1.6" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 576 | dependencies = [ 577 | "generic-array", 578 | "typenum", 579 | ] 580 | 581 | [[package]] 582 | name = "deranged" 583 | version = "0.3.10" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" 586 | dependencies = [ 587 | "powerfmt", 588 | ] 589 | 590 | [[package]] 591 | name = "derive_more" 592 | version = "0.99.17" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 595 | dependencies = [ 596 | "convert_case", 597 | "proc-macro2", 598 | "quote", 599 | "rustc_version", 600 | "syn 1.0.109", 601 | ] 602 | 603 | [[package]] 604 | name = "digest" 605 | version = "0.10.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 608 | dependencies = [ 609 | "block-buffer", 610 | "crypto-common", 611 | ] 612 | 613 | [[package]] 614 | name = "dirs" 615 | version = "5.0.1" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 618 | dependencies = [ 619 | "dirs-sys", 620 | ] 621 | 622 | [[package]] 623 | name = "dirs-sys" 624 | version = "0.4.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 627 | dependencies = [ 628 | "libc", 629 | "option-ext", 630 | "redox_users", 631 | "windows-sys 0.48.0", 632 | ] 633 | 634 | [[package]] 635 | name = "encoding_rs" 636 | version = "0.8.33" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 639 | dependencies = [ 640 | "cfg-if", 641 | ] 642 | 643 | [[package]] 644 | name = "equivalent" 645 | version = "1.0.1" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 648 | 649 | [[package]] 650 | name = "eyre" 651 | version = "0.6.9" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" 654 | dependencies = [ 655 | "indenter", 656 | "once_cell", 657 | ] 658 | 659 | [[package]] 660 | name = "fallible-iterator" 661 | version = "0.3.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 664 | 665 | [[package]] 666 | name = "fallible-streaming-iterator" 667 | version = "0.1.9" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 670 | 671 | [[package]] 672 | name = "flate2" 673 | version = "1.0.28" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 676 | dependencies = [ 677 | "crc32fast", 678 | "miniz_oxide", 679 | ] 680 | 681 | [[package]] 682 | name = "fnv" 683 | version = "1.0.7" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 686 | 687 | [[package]] 688 | name = "form_urlencoded" 689 | version = "1.2.1" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 692 | dependencies = [ 693 | "percent-encoding", 694 | ] 695 | 696 | [[package]] 697 | name = "futures-core" 698 | version = "0.3.29" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 701 | 702 | [[package]] 703 | name = "futures-sink" 704 | version = "0.3.29" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 707 | 708 | [[package]] 709 | name = "futures-task" 710 | version = "0.3.29" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 713 | 714 | [[package]] 715 | name = "futures-util" 716 | version = "0.3.29" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 719 | dependencies = [ 720 | "futures-core", 721 | "futures-task", 722 | "pin-project-lite", 723 | "pin-utils", 724 | "slab", 725 | ] 726 | 727 | [[package]] 728 | name = "generic-array" 729 | version = "0.14.7" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 732 | dependencies = [ 733 | "typenum", 734 | "version_check", 735 | ] 736 | 737 | [[package]] 738 | name = "getrandom" 739 | version = "0.2.11" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 742 | dependencies = [ 743 | "cfg-if", 744 | "libc", 745 | "wasi", 746 | ] 747 | 748 | [[package]] 749 | name = "gimli" 750 | version = "0.28.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 753 | 754 | [[package]] 755 | name = "h2" 756 | version = "0.3.22" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" 759 | dependencies = [ 760 | "bytes", 761 | "fnv", 762 | "futures-core", 763 | "futures-sink", 764 | "futures-util", 765 | "http", 766 | "indexmap", 767 | "slab", 768 | "tokio", 769 | "tokio-util", 770 | "tracing", 771 | ] 772 | 773 | [[package]] 774 | name = "hashbrown" 775 | version = "0.14.3" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 778 | dependencies = [ 779 | "ahash", 780 | "allocator-api2", 781 | ] 782 | 783 | [[package]] 784 | name = "hashlink" 785 | version = "0.8.4" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 788 | dependencies = [ 789 | "hashbrown", 790 | ] 791 | 792 | [[package]] 793 | name = "heck" 794 | version = "0.4.1" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 797 | 798 | [[package]] 799 | name = "hex" 800 | version = "0.4.3" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 803 | 804 | [[package]] 805 | name = "http" 806 | version = "0.2.11" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" 809 | dependencies = [ 810 | "bytes", 811 | "fnv", 812 | "itoa", 813 | ] 814 | 815 | [[package]] 816 | name = "http-range" 817 | version = "0.1.5" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" 820 | 821 | [[package]] 822 | name = "httparse" 823 | version = "1.8.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 826 | 827 | [[package]] 828 | name = "httpdate" 829 | version = "1.0.3" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 832 | 833 | [[package]] 834 | name = "iana-time-zone" 835 | version = "0.1.58" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 838 | dependencies = [ 839 | "android_system_properties", 840 | "core-foundation-sys", 841 | "iana-time-zone-haiku", 842 | "js-sys", 843 | "wasm-bindgen", 844 | "windows-core", 845 | ] 846 | 847 | [[package]] 848 | name = "iana-time-zone-haiku" 849 | version = "0.1.2" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 852 | dependencies = [ 853 | "cc", 854 | ] 855 | 856 | [[package]] 857 | name = "idna" 858 | version = "0.5.0" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 861 | dependencies = [ 862 | "unicode-bidi", 863 | "unicode-normalization", 864 | ] 865 | 866 | [[package]] 867 | name = "indenter" 868 | version = "0.3.3" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 871 | 872 | [[package]] 873 | name = "indexmap" 874 | version = "2.1.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 877 | dependencies = [ 878 | "equivalent", 879 | "hashbrown", 880 | ] 881 | 882 | [[package]] 883 | name = "itoa" 884 | version = "1.0.9" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 887 | 888 | [[package]] 889 | name = "jobserver" 890 | version = "0.1.27" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" 893 | dependencies = [ 894 | "libc", 895 | ] 896 | 897 | [[package]] 898 | name = "js-sys" 899 | version = "0.3.66" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" 902 | dependencies = [ 903 | "wasm-bindgen", 904 | ] 905 | 906 | [[package]] 907 | name = "language-tags" 908 | version = "0.3.2" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 911 | 912 | [[package]] 913 | name = "libc" 914 | version = "0.2.150" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 917 | 918 | [[package]] 919 | name = "libredox" 920 | version = "0.0.1" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 923 | dependencies = [ 924 | "bitflags 2.4.1", 925 | "libc", 926 | "redox_syscall", 927 | ] 928 | 929 | [[package]] 930 | name = "libsqlite3-sys" 931 | version = "0.27.0" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" 934 | dependencies = [ 935 | "cc", 936 | "pkg-config", 937 | "vcpkg", 938 | ] 939 | 940 | [[package]] 941 | name = "local-channel" 942 | version = "0.1.5" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" 945 | dependencies = [ 946 | "futures-core", 947 | "futures-sink", 948 | "local-waker", 949 | ] 950 | 951 | [[package]] 952 | name = "local-waker" 953 | version = "0.1.4" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" 956 | 957 | [[package]] 958 | name = "lock_api" 959 | version = "0.4.11" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 962 | dependencies = [ 963 | "autocfg", 964 | "scopeguard", 965 | ] 966 | 967 | [[package]] 968 | name = "log" 969 | version = "0.4.20" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 972 | 973 | [[package]] 974 | name = "memchr" 975 | version = "2.6.4" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 978 | 979 | [[package]] 980 | name = "mime" 981 | version = "0.3.17" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 984 | 985 | [[package]] 986 | name = "mime_guess" 987 | version = "2.0.4" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 990 | dependencies = [ 991 | "mime", 992 | "unicase", 993 | ] 994 | 995 | [[package]] 996 | name = "miniz_oxide" 997 | version = "0.7.1" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 1000 | dependencies = [ 1001 | "adler", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "mio" 1006 | version = "0.8.9" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" 1009 | dependencies = [ 1010 | "libc", 1011 | "log", 1012 | "wasi", 1013 | "windows-sys 0.48.0", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "num-traits" 1018 | version = "0.2.17" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 1021 | dependencies = [ 1022 | "autocfg", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "object" 1027 | version = "0.32.1" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 1030 | dependencies = [ 1031 | "memchr", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "once_cell" 1036 | version = "1.18.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 1039 | 1040 | [[package]] 1041 | name = "option-ext" 1042 | version = "0.2.0" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1045 | 1046 | [[package]] 1047 | name = "parking_lot" 1048 | version = "0.12.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1051 | dependencies = [ 1052 | "lock_api", 1053 | "parking_lot_core", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "parking_lot_core" 1058 | version = "0.9.9" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 1061 | dependencies = [ 1062 | "cfg-if", 1063 | "libc", 1064 | "redox_syscall", 1065 | "smallvec", 1066 | "windows-targets 0.48.5", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "paste" 1071 | version = "1.0.14" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 1074 | 1075 | [[package]] 1076 | name = "percent-encoding" 1077 | version = "2.3.1" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1080 | 1081 | [[package]] 1082 | name = "pin-project-lite" 1083 | version = "0.2.13" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 1086 | 1087 | [[package]] 1088 | name = "pin-utils" 1089 | version = "0.1.0" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1092 | 1093 | [[package]] 1094 | name = "pkg-config" 1095 | version = "0.3.27" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 1098 | 1099 | [[package]] 1100 | name = "powerfmt" 1101 | version = "0.2.0" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1104 | 1105 | [[package]] 1106 | name = "ppv-lite86" 1107 | version = "0.2.17" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1110 | 1111 | [[package]] 1112 | name = "proc-macro2" 1113 | version = "1.0.70" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 1116 | dependencies = [ 1117 | "unicode-ident", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "quote" 1122 | version = "1.0.33" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 1125 | dependencies = [ 1126 | "proc-macro2", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "rand" 1131 | version = "0.8.5" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1134 | dependencies = [ 1135 | "libc", 1136 | "rand_chacha", 1137 | "rand_core", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "rand_chacha" 1142 | version = "0.3.1" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1145 | dependencies = [ 1146 | "ppv-lite86", 1147 | "rand_core", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "rand_core" 1152 | version = "0.6.4" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1155 | dependencies = [ 1156 | "getrandom", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "redox_syscall" 1161 | version = "0.4.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1164 | dependencies = [ 1165 | "bitflags 1.3.2", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "redox_users" 1170 | version = "0.4.4" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 1173 | dependencies = [ 1174 | "getrandom", 1175 | "libredox", 1176 | "thiserror", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "regex" 1181 | version = "1.10.2" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 1184 | dependencies = [ 1185 | "aho-corasick", 1186 | "memchr", 1187 | "regex-automata", 1188 | "regex-syntax", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "regex-automata" 1193 | version = "0.4.3" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 1196 | dependencies = [ 1197 | "aho-corasick", 1198 | "memchr", 1199 | "regex-syntax", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "regex-syntax" 1204 | version = "0.8.2" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1207 | 1208 | [[package]] 1209 | name = "ring" 1210 | version = "0.17.6" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" 1213 | dependencies = [ 1214 | "cc", 1215 | "getrandom", 1216 | "libc", 1217 | "spin", 1218 | "untrusted", 1219 | "windows-sys 0.48.0", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "rusqlite" 1224 | version = "0.30.0" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" 1227 | dependencies = [ 1228 | "bitflags 2.4.1", 1229 | "fallible-iterator", 1230 | "fallible-streaming-iterator", 1231 | "hashlink", 1232 | "libsqlite3-sys", 1233 | "serde_json", 1234 | "smallvec", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "rustc-demangle" 1239 | version = "0.1.23" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1242 | 1243 | [[package]] 1244 | name = "rustc_version" 1245 | version = "0.4.0" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1248 | dependencies = [ 1249 | "semver", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "ryu" 1254 | version = "1.0.15" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 1257 | 1258 | [[package]] 1259 | name = "scopeguard" 1260 | version = "1.2.0" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1263 | 1264 | [[package]] 1265 | name = "semver" 1266 | version = "1.0.20" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 1269 | 1270 | [[package]] 1271 | name = "serde" 1272 | version = "1.0.193" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 1275 | dependencies = [ 1276 | "serde_derive", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "serde_derive" 1281 | version = "1.0.193" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 1284 | dependencies = [ 1285 | "proc-macro2", 1286 | "quote", 1287 | "syn 2.0.39", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "serde_json" 1292 | version = "1.0.108" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 1295 | dependencies = [ 1296 | "itoa", 1297 | "ryu", 1298 | "serde", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "serde_urlencoded" 1303 | version = "0.7.1" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1306 | dependencies = [ 1307 | "form_urlencoded", 1308 | "itoa", 1309 | "ryu", 1310 | "serde", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "serde_yaml" 1315 | version = "0.9.27" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" 1318 | dependencies = [ 1319 | "indexmap", 1320 | "itoa", 1321 | "ryu", 1322 | "serde", 1323 | "unsafe-libyaml", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "sha1" 1328 | version = "0.10.6" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1331 | dependencies = [ 1332 | "cfg-if", 1333 | "cpufeatures", 1334 | "digest", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "shellexpand" 1339 | version = "3.1.0" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" 1342 | dependencies = [ 1343 | "dirs", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "signal-hook-registry" 1348 | version = "1.4.1" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1351 | dependencies = [ 1352 | "libc", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "slab" 1357 | version = "0.4.9" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1360 | dependencies = [ 1361 | "autocfg", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "smallvec" 1366 | version = "1.11.2" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 1369 | 1370 | [[package]] 1371 | name = "socket2" 1372 | version = "0.5.5" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 1375 | dependencies = [ 1376 | "libc", 1377 | "windows-sys 0.48.0", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "spin" 1382 | version = "0.9.8" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1385 | 1386 | [[package]] 1387 | name = "sqliterg" 1388 | version = "0.18.0" 1389 | dependencies = [ 1390 | "actix-cors", 1391 | "actix-files", 1392 | "actix-web", 1393 | "actix-web-httpauth", 1394 | "chrono", 1395 | "clap", 1396 | "eyre", 1397 | "hex", 1398 | "ring", 1399 | "rusqlite", 1400 | "serde", 1401 | "serde_derive", 1402 | "serde_json", 1403 | "serde_yaml", 1404 | "shellexpand", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "strsim" 1409 | version = "0.10.0" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1412 | 1413 | [[package]] 1414 | name = "syn" 1415 | version = "1.0.109" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1418 | dependencies = [ 1419 | "proc-macro2", 1420 | "quote", 1421 | "unicode-ident", 1422 | ] 1423 | 1424 | [[package]] 1425 | name = "syn" 1426 | version = "2.0.39" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 1429 | dependencies = [ 1430 | "proc-macro2", 1431 | "quote", 1432 | "unicode-ident", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "thiserror" 1437 | version = "1.0.50" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1440 | dependencies = [ 1441 | "thiserror-impl", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "thiserror-impl" 1446 | version = "1.0.50" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1449 | dependencies = [ 1450 | "proc-macro2", 1451 | "quote", 1452 | "syn 2.0.39", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "time" 1457 | version = "0.3.30" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" 1460 | dependencies = [ 1461 | "deranged", 1462 | "itoa", 1463 | "powerfmt", 1464 | "serde", 1465 | "time-core", 1466 | "time-macros", 1467 | ] 1468 | 1469 | [[package]] 1470 | name = "time-core" 1471 | version = "0.1.2" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1474 | 1475 | [[package]] 1476 | name = "time-macros" 1477 | version = "0.2.15" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" 1480 | dependencies = [ 1481 | "time-core", 1482 | ] 1483 | 1484 | [[package]] 1485 | name = "tinyvec" 1486 | version = "1.6.0" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1489 | dependencies = [ 1490 | "tinyvec_macros", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "tinyvec_macros" 1495 | version = "0.1.1" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1498 | 1499 | [[package]] 1500 | name = "tokio" 1501 | version = "1.34.0" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" 1504 | dependencies = [ 1505 | "backtrace", 1506 | "bytes", 1507 | "libc", 1508 | "mio", 1509 | "parking_lot", 1510 | "pin-project-lite", 1511 | "signal-hook-registry", 1512 | "socket2", 1513 | "windows-sys 0.48.0", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "tokio-util" 1518 | version = "0.7.10" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1521 | dependencies = [ 1522 | "bytes", 1523 | "futures-core", 1524 | "futures-sink", 1525 | "pin-project-lite", 1526 | "tokio", 1527 | "tracing", 1528 | ] 1529 | 1530 | [[package]] 1531 | name = "tracing" 1532 | version = "0.1.40" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1535 | dependencies = [ 1536 | "log", 1537 | "pin-project-lite", 1538 | "tracing-core", 1539 | ] 1540 | 1541 | [[package]] 1542 | name = "tracing-core" 1543 | version = "0.1.32" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1546 | dependencies = [ 1547 | "once_cell", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "typenum" 1552 | version = "1.17.0" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1555 | 1556 | [[package]] 1557 | name = "unicase" 1558 | version = "2.7.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1561 | dependencies = [ 1562 | "version_check", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "unicode-bidi" 1567 | version = "0.3.13" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1570 | 1571 | [[package]] 1572 | name = "unicode-ident" 1573 | version = "1.0.12" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1576 | 1577 | [[package]] 1578 | name = "unicode-normalization" 1579 | version = "0.1.22" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1582 | dependencies = [ 1583 | "tinyvec", 1584 | ] 1585 | 1586 | [[package]] 1587 | name = "unsafe-libyaml" 1588 | version = "0.2.9" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" 1591 | 1592 | [[package]] 1593 | name = "untrusted" 1594 | version = "0.9.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1597 | 1598 | [[package]] 1599 | name = "url" 1600 | version = "2.5.0" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1603 | dependencies = [ 1604 | "form_urlencoded", 1605 | "idna", 1606 | "percent-encoding", 1607 | ] 1608 | 1609 | [[package]] 1610 | name = "utf8parse" 1611 | version = "0.2.1" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1614 | 1615 | [[package]] 1616 | name = "vcpkg" 1617 | version = "0.2.15" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1620 | 1621 | [[package]] 1622 | name = "version_check" 1623 | version = "0.9.4" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1626 | 1627 | [[package]] 1628 | name = "wasi" 1629 | version = "0.11.0+wasi-snapshot-preview1" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1632 | 1633 | [[package]] 1634 | name = "wasm-bindgen" 1635 | version = "0.2.89" 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" 1637 | checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" 1638 | dependencies = [ 1639 | "cfg-if", 1640 | "wasm-bindgen-macro", 1641 | ] 1642 | 1643 | [[package]] 1644 | name = "wasm-bindgen-backend" 1645 | version = "0.2.89" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" 1648 | dependencies = [ 1649 | "bumpalo", 1650 | "log", 1651 | "once_cell", 1652 | "proc-macro2", 1653 | "quote", 1654 | "syn 2.0.39", 1655 | "wasm-bindgen-shared", 1656 | ] 1657 | 1658 | [[package]] 1659 | name = "wasm-bindgen-macro" 1660 | version = "0.2.89" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" 1663 | dependencies = [ 1664 | "quote", 1665 | "wasm-bindgen-macro-support", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "wasm-bindgen-macro-support" 1670 | version = "0.2.89" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" 1673 | dependencies = [ 1674 | "proc-macro2", 1675 | "quote", 1676 | "syn 2.0.39", 1677 | "wasm-bindgen-backend", 1678 | "wasm-bindgen-shared", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "wasm-bindgen-shared" 1683 | version = "0.2.89" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" 1686 | 1687 | [[package]] 1688 | name = "windows-core" 1689 | version = "0.51.1" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 1692 | dependencies = [ 1693 | "windows-targets 0.48.5", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "windows-sys" 1698 | version = "0.48.0" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1701 | dependencies = [ 1702 | "windows-targets 0.48.5", 1703 | ] 1704 | 1705 | [[package]] 1706 | name = "windows-sys" 1707 | version = "0.52.0" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1710 | dependencies = [ 1711 | "windows-targets 0.52.0", 1712 | ] 1713 | 1714 | [[package]] 1715 | name = "windows-targets" 1716 | version = "0.48.5" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1719 | dependencies = [ 1720 | "windows_aarch64_gnullvm 0.48.5", 1721 | "windows_aarch64_msvc 0.48.5", 1722 | "windows_i686_gnu 0.48.5", 1723 | "windows_i686_msvc 0.48.5", 1724 | "windows_x86_64_gnu 0.48.5", 1725 | "windows_x86_64_gnullvm 0.48.5", 1726 | "windows_x86_64_msvc 0.48.5", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "windows-targets" 1731 | version = "0.52.0" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1734 | dependencies = [ 1735 | "windows_aarch64_gnullvm 0.52.0", 1736 | "windows_aarch64_msvc 0.52.0", 1737 | "windows_i686_gnu 0.52.0", 1738 | "windows_i686_msvc 0.52.0", 1739 | "windows_x86_64_gnu 0.52.0", 1740 | "windows_x86_64_gnullvm 0.52.0", 1741 | "windows_x86_64_msvc 0.52.0", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "windows_aarch64_gnullvm" 1746 | version = "0.48.5" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1749 | 1750 | [[package]] 1751 | name = "windows_aarch64_gnullvm" 1752 | version = "0.52.0" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1755 | 1756 | [[package]] 1757 | name = "windows_aarch64_msvc" 1758 | version = "0.48.5" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1761 | 1762 | [[package]] 1763 | name = "windows_aarch64_msvc" 1764 | version = "0.52.0" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1767 | 1768 | [[package]] 1769 | name = "windows_i686_gnu" 1770 | version = "0.48.5" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1773 | 1774 | [[package]] 1775 | name = "windows_i686_gnu" 1776 | version = "0.52.0" 1777 | source = "registry+https://github.com/rust-lang/crates.io-index" 1778 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1779 | 1780 | [[package]] 1781 | name = "windows_i686_msvc" 1782 | version = "0.48.5" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1785 | 1786 | [[package]] 1787 | name = "windows_i686_msvc" 1788 | version = "0.52.0" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1791 | 1792 | [[package]] 1793 | name = "windows_x86_64_gnu" 1794 | version = "0.48.5" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1797 | 1798 | [[package]] 1799 | name = "windows_x86_64_gnu" 1800 | version = "0.52.0" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1803 | 1804 | [[package]] 1805 | name = "windows_x86_64_gnullvm" 1806 | version = "0.48.5" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1809 | 1810 | [[package]] 1811 | name = "windows_x86_64_gnullvm" 1812 | version = "0.52.0" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1815 | 1816 | [[package]] 1817 | name = "windows_x86_64_msvc" 1818 | version = "0.48.5" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1821 | 1822 | [[package]] 1823 | name = "windows_x86_64_msvc" 1824 | version = "0.52.0" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1827 | 1828 | [[package]] 1829 | name = "zerocopy" 1830 | version = "0.7.28" 1831 | source = "registry+https://github.com/rust-lang/crates.io-index" 1832 | checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" 1833 | dependencies = [ 1834 | "zerocopy-derive", 1835 | ] 1836 | 1837 | [[package]] 1838 | name = "zerocopy-derive" 1839 | version = "0.7.28" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" 1842 | dependencies = [ 1843 | "proc-macro2", 1844 | "quote", 1845 | "syn 2.0.39", 1846 | ] 1847 | 1848 | [[package]] 1849 | name = "zstd" 1850 | version = "0.12.4" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" 1853 | dependencies = [ 1854 | "zstd-safe", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "zstd-safe" 1859 | version = "6.0.6" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" 1862 | dependencies = [ 1863 | "libc", 1864 | "zstd-sys", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "zstd-sys" 1869 | version = "2.0.9+zstd.1.5.5" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" 1872 | dependencies = [ 1873 | "cc", 1874 | "pkg-config", 1875 | ] 1876 | --------------------------------------------------------------------------------