├── server ├── src │ ├── storage │ │ ├── get_user.sql │ │ ├── in_memory.rs │ │ └── db.rs │ ├── storage.rs │ ├── error.rs │ ├── main.rs │ ├── routes.rs │ └── auth.rs ├── run_tests.sh ├── db │ ├── create_sqlite3_db.sh │ └── init_sqlite3_db.sql ├── Cargo.toml ├── tls │ ├── server.rsa.crt │ └── server.rsa.key ├── sqlx-data.json └── Cargo.lock ├── client ├── public │ ├── favicon.ico │ └── index.html ├── vue.config.js ├── babel.config.js ├── src │ ├── views │ │ ├── About.vue │ │ ├── Home.vue │ │ └── Login.vue │ ├── main.js │ ├── plugins │ │ └── vuetify.js │ ├── router │ │ └── index.js │ ├── App.vue │ └── api │ │ └── index.js ├── .gitignore ├── README.md └── package.json ├── .gitignore ├── LICENSE └── README.md /server/src/storage/get_user.sql: -------------------------------------------------------------------------------- 1 | SELECT email, hashed_pw, role FROM users WHERE email = ?1; 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/rust-spa-auth/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # cargo build output directory 2 | target/ 3 | 4 | # downloaded npm modules 5 | node_modules/ 6 | 7 | # output directory for client+server builds 8 | build-output/ 9 | 10 | # generated database files 11 | *.db* 12 | 13 | # ctags file 14 | tags 15 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import vuetify from './plugins/vuetify'; 4 | import router from './router' 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | vuetify, 10 | router, 11 | render: h => h(App) 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # rust-spa-auth 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /client/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | import colours from 'vuetify/lib/util/colors'; 5 | 6 | Vue.use(Vuetify); 7 | 8 | export default new Vuetify({ 9 | theme: { 10 | themes: { 11 | light: { 12 | primary: colours.pink.lighten1, 13 | secondary: colours.purple, 14 | accent: colours.pink.darken2, 15 | error: colours.red.darken3, 16 | } 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /server/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Running all the tests requires enabling different sets of features, so a 3 | # simple `cargo test` is not sufficient. 4 | # 5 | # This script exists to run all of the tests. 6 | 7 | set -eu 8 | 9 | GITROOT=$(git rev-parse --show-toplevel) 10 | 11 | echo 12 | echo "Running tests with in_memory store" 13 | cargo test --features in_memory -- --nocapture 14 | 15 | echo 16 | echo "Running tests with database store" 17 | 18 | DATABASE=/tmp/.rust_spa_auth_test.db 19 | 20 | function cleanup { 21 | rm -f $DATABASE 22 | } 23 | 24 | trap cleanup EXIT 25 | 26 | cd $GITROOT/server/db 27 | ./create_sqlite3_db.sh $DATABASE 28 | 29 | DATABASE_URL=sqlite://$DATABASE cargo test -- --nocapture 30 | -------------------------------------------------------------------------------- /server/db/create_sqlite3_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Creates a new sqlite3 database. 4 | # The script should be run from the same directory as the sql script 5 | # `init_sqlite3_db.sql` 6 | 7 | set -eu 8 | 9 | USAGE="$0 " 10 | 11 | if [ $# -ne 1 ]; then 12 | echo 13 | echo USAGE: 14 | echo $USAGE 15 | exit 1 16 | fi 17 | 18 | DB_FILE=$1 19 | 20 | if [ -f $DB_FILE ]; then 21 | echo 22 | echo "DB file $DB_FILE already exists - aborting" 23 | exit 1 24 | fi 25 | 26 | SQL_SCRIPT=init_sqlite3_db.sql 27 | 28 | if [ ! -f $SQL_SCRIPT ]; then 29 | echo 30 | echo "Could not find $SQL_SCRIPT" 31 | exit 1 32 | fi 33 | 34 | sqlite3 $DB_FILE < $SQL_SCRIPT 35 | 36 | echo 37 | echo "DB '$DB_FILE' created successfully" 38 | -------------------------------------------------------------------------------- /server/db/init_sqlite3_db.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | 3 | CREATE TABLE users ( 4 | email TEXT PRIMARY KEY NOT NULL, 5 | hashed_pw TEXT NOT NULL, 6 | role TEXT NOT NULL 7 | ); 8 | 9 | -- Add some default users. 10 | INSERT INTO users VALUES( 11 | 'user@localhost', 12 | '$argon2id$v=19$m=4096,t=3,p=1$tXoJG9eGHIhBQeNCfHFg7A$yfdnBeO5lRXV8rVFc768JPr8xe8MZfTQh1HxmMYk1ug', 13 | 'user' 14 | ); 15 | INSERT INTO users VALUES( 16 | 'admin@localhost', 17 | '$argon2id$v=19$m=4096,t=3,p=1$zW6Nsm7QNPDql8seEp1gEQ$lvt1LxV2rFqPsXLke9k9pDOlMxEyyXrPNL63ud0B3MQ', 18 | 'admin' 19 | ); 20 | 21 | CREATE TABLE refresh_tokens ( 22 | email TEXT NOT NULL, 23 | user_agent TEXT NOT NULL, 24 | expires INTEGER, 25 | PRIMARY KEY (email, user_agent, expires) 26 | ); 27 | 28 | COMMIT; 29 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | const routes = [ 7 | { 8 | path: '/home', 9 | name: 'Home', 10 | component: () => import('../views/Home.vue') 11 | }, 12 | { 13 | path: '/about', 14 | name: 'About', 15 | // route level code-splitting 16 | // this generates a separate chunk (about.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('../views/About.vue') 19 | }, 20 | { 21 | path: '*', 22 | name: 'Login', 23 | // route level code-splitting 24 | // this generates a separate chunk (about.[hash].js) for this route 25 | // which is lazy-loaded when the route is visited. 26 | component: () => import('../views/Login.vue') 27 | }, 28 | ] 29 | 30 | const router = new VueRouter({ 31 | mode: 'history', 32 | base: process.env.BASE_URL, 33 | routes 34 | }) 35 | 36 | export default router; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Jan Tache 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-spa-auth" 3 | version = "0.1.0" 4 | authors = ["Jan Tache "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | warp = { git = "https://github.com/jtroo/warp", features = ["tls"] } 11 | tokio = { version = "1.4", features = ["macros", "rt-multi-thread"] } 12 | serde = { version = "1", features = ["derive"] } 13 | 14 | anyhow = "1" 15 | thiserror = "1" 16 | once_cell = "1" 17 | parking_lot = "0.11" 18 | chrono = "0.4" 19 | async-trait = "0.1" 20 | env_logger = "0.8" 21 | log = "0.4" 22 | structopt = { version = "0.3", features = ["paw"] } 23 | paw = "1" 24 | sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "all", "any", "offline" ] } 25 | 26 | rand = { version = "0.8", features = ["std_rng"] } 27 | argon2 = { version = "0.1", features = ["password-hash"] } 28 | bincode = "1" 29 | chacha20poly1305 = "0.7" 30 | base64 = "0.13" 31 | jsonwebtoken = "7.2" 32 | 33 | [profile.release] 34 | panic = 'abort' 35 | lto = 'thin' 36 | 37 | [features] 38 | dev_cors = [] 39 | in_memory = [] 40 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 53 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-spa-auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "core-js": "^3.6.5", 13 | "jwt-decode": "^3.1.2", 14 | "vue": "^2.6.11", 15 | "vue-router": "^3.2.0", 16 | "vuetify": "^2.4.0" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "~4.5.0", 20 | "@vue/cli-plugin-eslint": "~4.5.0", 21 | "@vue/cli-plugin-router": "^4.5.12", 22 | "@vue/cli-service": "~4.5.0", 23 | "babel-eslint": "^10.1.0", 24 | "eslint": "^6.7.2", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "sass": "^1.32.0", 27 | "sass-loader": "^10.0.0", 28 | "vue-cli-plugin-vuetify": "~2.3.1", 29 | "vue-template-compiler": "^2.6.11", 30 | "vuetify-loader": "^1.7.0" 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "env": { 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/essential", 39 | "eslint:recommended" 40 | ], 41 | "parserOptions": { 42 | "parser": "babel-eslint" 43 | }, 44 | "rules": {} 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions", 49 | "not dead" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 43 | 44 | 65 | -------------------------------------------------------------------------------- /server/tls/server.rsa.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkzCCAnugAwIBAgIUcJJsrn99lgi7JyL6Rw1kkLMCMOowDQYJKoZIhvcNAQEL 3 | BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X 5 | DTIxMDMyODIxMzkzNloXDTMxMDMyNjIxMzkzNlowWTELMAkGA1UEBhMCQVUxEzAR 6 | BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 7 | IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 8 | MIIBCgKCAQEAp7DmQE9JnwtRMKbVLvkC4Kej1lM3jZbwLWepvDjM3QdadkeURpSV 9 | g3Wq+rI2Y7/qIqOQB1ekY9olssz9Z47y74hMtYbZ28Jk8wJ4E9TpqtgJKuE4m+S8 10 | FmA2n/k2Xf8jXTkl1vswJQf6dBU1nmh93CFwcQUIFycPVUCoIfbRT6g+rKdlnHwZ 11 | TIoinQvJDxhHQ1hHXVOGvoUQjR/lR+C/0KzSrrNUqGVtkXqgdNvdycqIjM/NwnqT 12 | HoZyrxgUzDCNZI4DySbUzHA7Gt00v1d6VPJ99Nj/q9wFVn34UQEwfafEHF0vkQxc 13 | pd/Hf15BbOi+1jYwcDIhuYoFHxPXe4neWQIDAQABo1MwUTAdBgNVHQ4EFgQUJyH1 14 | jy4duPYMgqTdwJoGL9j8iy0wHwYDVR0jBBgwFoAUJyH1jy4duPYMgqTdwJoGL9j8 15 | iy0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAmzmOrpLkVFo+ 16 | /lXdaqwJ5ShCEenvs/Xz37sdooq8cwarsa555YVRnQAtJQ7a5yVqPW60imtpJSmu 17 | u1yE6Szq1SdRE1TzEOwEolCjPPc9jGyupXpC4s8L4e7wqW62OSW+CrIm31povUqv 18 | 5gPw9zdGIgjz5wOc3vn+l4ouZCNcrcDsxbJ+9OttaaeAgYpByG4ZrbVXoKF2KbwB 19 | MNn3ajMFXkTsHcKKx86t3MgS/fqV5x5OJDERSNSqrG4nKudAA0gHQL11h6zAz1WY 20 | x6JW7z4giFFkoZtl7ZwcVngzXIpNkPa8US3PrpCPthqIyaOzbsAbh5+yTLbtZgv5 21 | NoYGy/d6mA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /server/src/storage.rs: -------------------------------------------------------------------------------- 1 | //! This module serves as an example for how a data store might be abstracted with multiple 2 | //! implementations. Currently the only data store implementation is an in-memory one using 3 | //! `parking_lot::RwLock` and `HashSet/HashMap`. 4 | //! 5 | //! A current TODO is to create a database-backed data store implementation. 6 | 7 | use crate::{auth, error::Error}; 8 | 9 | #[cfg(feature = "in_memory")] 10 | mod in_memory; 11 | #[cfg(feature = "in_memory")] 12 | pub use in_memory::*; 13 | 14 | #[cfg(not(feature = "in_memory"))] 15 | mod db; 16 | #[cfg(not(feature = "in_memory"))] 17 | pub use db::*; 18 | 19 | /// User storage 20 | #[derive(Clone, Debug)] 21 | pub struct User { 22 | pub email: String, 23 | pub hashed_pw: String, 24 | pub role: auth::Role, 25 | } 26 | 27 | /// Trait to unify the methods exposed by a data store implementation. The trait methods are async 28 | /// because database access should probably be async for better performance (see sqlx). 29 | #[async_trait::async_trait] 30 | pub trait Storage { 31 | async fn get_user(&self, email: &str) -> Result, Error>; 32 | 33 | async fn store_user(&self, user: User) -> Result<(), Error>; 34 | 35 | async fn refresh_token_exists(&self, token: &auth::RefreshToken) -> Result; 36 | 37 | async fn add_refresh_token(&self, token: auth::RefreshToken) -> Result<(), Error>; 38 | 39 | async fn remove_refresh_token(&self, token: &auth::RefreshToken) -> Result<(), Error>; 40 | } 41 | -------------------------------------------------------------------------------- /server/tls/server.rsa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnsOZAT0mfC1Ew 3 | ptUu+QLgp6PWUzeNlvAtZ6m8OMzdB1p2R5RGlJWDdar6sjZjv+oio5AHV6Rj2iWy 4 | zP1njvLviEy1htnbwmTzAngT1Omq2Akq4Tib5LwWYDaf+TZd/yNdOSXW+zAlB/p0 5 | FTWeaH3cIXBxBQgXJw9VQKgh9tFPqD6sp2WcfBlMiiKdC8kPGEdDWEddU4a+hRCN 6 | H+VH4L/QrNKus1SoZW2ReqB0293JyoiMz83CepMehnKvGBTMMI1kjgPJJtTMcDsa 7 | 3TS/V3pU8n302P+r3AVWffhRATB9p8QcXS+RDFyl38d/XkFs6L7WNjBwMiG5igUf 8 | E9d7id5ZAgMBAAECggEAc9vlOcmONDS9g7/AyxOAyXcJ26rjVxBSIQzeWQ1lfdD2 9 | z4HlEF7Yblt+Jne63HBcdFJN0MTc76BpK4A2FLbPYm/pkAH7w7orxTqPfCQ+zas1 10 | qkqxsgP3b/5Jv32P8qG07VPusPob6Xn5sPp2cVwAmXcBGRVA4b46+cwSXmUJeN7G 11 | 0bghv2edXA/QCulBpj6LsKY33elGr5zjzS6rUF2lBie4D+ga3AOG3OlgbFzvQXhb 12 | HVmJbh0YnwmEFqZgva4NVlXjCzaYpMuc01IBjr5aL+ZnddvYiir9Gn/Yz/s+WRsm 13 | jlxNUwC+wmVSJCeg3p5gVT9fTCFlB9COa4mumxKWAQKBgQDZAps7CRRof19ifRSG 14 | ucijzRrFPu/oOlA965/kU5zQL9JXM2NU7lpgORG7jzYys1rnG6AeF/RMSNHaTUKr 15 | plnRWQ94TOkbjsbK8VQGPsDLyK27MDtd/vHgYijKZ+wOYHcm/WA9R9arY2SeDTNR 16 | cCjJKEU0w4hP08lQYU/ebJhJzQKBgQDF0dsG+4+xJHnsqSPgAbm8hg9TaxebeyPj 17 | N+uDCklscNaDIKvYQ16ezTEBW/uhJWjGaGKqvPD7DTxEQQc2SWVqyZ8lzYJzbqqy 18 | Bc+fNd6jEciEu+m3iFleRk5gTBzU7XbP58qs6FazO2b2vDLelXMTPTsLKx8jnuQ4 19 | pzIBLD7qvQKBgQDMVNPtBmOc8THS/osxQAVXlmw6hURHmYCsjQwEZWYl8KFRUiWZ 20 | dsTKDXJQ9EvH+XECYtSlAVEZ8wcFUfIQxZWUtRMH5wMm24z60biYGMZYGyDaPFPp 21 | 8X+2EzbtPmTaNKJ8p97SVK4gunmnCC7l44HDxpNDV0kmQPgxIy+c8wuBkQKBgAHl 22 | ROgIORRFeTWfWUyoN5Nq5XKzMAK5Z1qiCouy6Yk0e5m+Emd8Hcf/x1xv+TVOGnbs 23 | QSXJsbV2JwGwbdVDVslzLSc7lOhuQwx5qceoyH1fikBbXJ35nIGewtmBYTVZIS5U 24 | 1khZ4ZrOEuqCWypu3C9vAsXUn/PDbgRs9bm1JtUxAoGANBb1td9tzn1fXmwHvYGq 25 | YEzLZmHdShN0xQvFvVJxKsDw6u05o2pmGCmYK3PH165irNFjJKOBKSWQBZ+anEWt 26 | a89pMJk03TZ/FsqM2BhoWkYMwrZynuKqWcEg77lx9Sgk/9kNOY+3LwLImdfcPs53 27 | 3uc+SY9S5G7tDgYGuDN7NSQ= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /server/src/storage/in_memory.rs: -------------------------------------------------------------------------------- 1 | use crate::{auth, error::Error}; 2 | use parking_lot::RwLock; 3 | use std::collections::{HashMap, HashSet}; 4 | 5 | use std::sync::Arc; 6 | 7 | use super::*; 8 | 9 | pub struct InMemoryStore { 10 | users: RwLock>, 11 | refresh_tokens: RwLock>, 12 | } 13 | 14 | // None of these functions are actually async. Could have use async locks, but if the critical 15 | // sections are short (which these should be), then it is suggested to use blocking locks. 16 | // 17 | // See: 18 | // https://docs.rs/tokio/1.4.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use 19 | #[async_trait::async_trait] 20 | impl Storage for Arc { 21 | async fn get_user(&self, email: &str) -> Result, Error> { 22 | Ok(self.users.read().get(email).cloned()) 23 | } 24 | 25 | async fn store_user(&self, user: User) -> Result<(), Error> { 26 | self.users.write().insert(user.email.clone(), user); 27 | Ok(()) 28 | } 29 | 30 | async fn refresh_token_exists(&self, token: &auth::RefreshToken) -> Result { 31 | Ok(self.refresh_tokens.read().get(token).is_some()) 32 | } 33 | 34 | async fn add_refresh_token(&self, token: auth::RefreshToken) -> Result<(), Error> { 35 | self.refresh_tokens.write().insert(token); 36 | Ok(()) 37 | } 38 | 39 | async fn remove_refresh_token(&self, token: &auth::RefreshToken) -> Result<(), Error> { 40 | self.refresh_tokens.write().remove(token); 41 | Ok(()) 42 | } 43 | } 44 | 45 | /// Returns an implementer of `Storage + Send + Sync + Clone` that stores data in memory, as 46 | /// opposed to in a file or database. 47 | pub fn new_in_memory_storage() -> Arc { 48 | Arc::new(InMemoryStore { 49 | users: RwLock::new(HashMap::new()), 50 | refresh_tokens: RwLock::new(HashSet::new()), 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 83 | -------------------------------------------------------------------------------- /server/sqlx-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "SQLite", 3 | "70dabd87113503cce92ae83b12d90c9f4d2c4d3a4dcbd9896d3bc2e6204e573e": { 4 | "query": "INSERT OR REPLACE INTO users VALUES(?1, ?2, ?3)", 5 | "describe": { 6 | "columns": [], 7 | "parameters": { 8 | "Right": 3 9 | }, 10 | "nullable": [] 11 | } 12 | }, 13 | "791f51673addeacb38195195deea53c1940c9c3c04a3bb670613c615f52e423c": { 14 | "query": "DELETE FROM refresh_tokens WHERE email = ? AND user_agent = ? AND expires = ?", 15 | "describe": { 16 | "columns": [], 17 | "parameters": { 18 | "Right": 3 19 | }, 20 | "nullable": [] 21 | } 22 | }, 23 | "84c8721a4c05b4ac51cc5dfd8e45c8bd0b4e552536064beefb75eab39bbf7834": { 24 | "query": "SELECT email, hashed_pw, role FROM users WHERE email = ?1;\n", 25 | "describe": { 26 | "columns": [ 27 | { 28 | "name": "email", 29 | "ordinal": 0, 30 | "type_info": "Text" 31 | }, 32 | { 33 | "name": "hashed_pw", 34 | "ordinal": 1, 35 | "type_info": "Text" 36 | }, 37 | { 38 | "name": "role", 39 | "ordinal": 2, 40 | "type_info": "Text" 41 | } 42 | ], 43 | "parameters": { 44 | "Right": 1 45 | }, 46 | "nullable": [ 47 | false, 48 | false, 49 | false 50 | ] 51 | } 52 | }, 53 | "b6691dc0338507d07f5fa3869189234c86fcd2e7ca9e8c8117d32d830df3e941": { 54 | "query": "INSERT OR REPLACE INTO refresh_tokens VALUES(?, ?, ?)", 55 | "describe": { 56 | "columns": [], 57 | "parameters": { 58 | "Right": 3 59 | }, 60 | "nullable": [] 61 | } 62 | }, 63 | "f61f8a90098f1c273198225f84b32199b66a53b893eb9b17c8059211d3c42262": { 64 | "query": "SELECT expires FROM refresh_tokens WHERE email = ? AND user_agent = ? AND expires = ?", 65 | "describe": { 66 | "columns": [ 67 | { 68 | "name": "expires", 69 | "ordinal": 0, 70 | "type_info": "Int64" 71 | } 72 | ], 73 | "parameters": { 74 | "Right": 3 75 | }, 76 | "nullable": [ 77 | true 78 | ] 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /server/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Custom errors that can be turned into Warp `Rejection`s. 2 | //! 3 | //! Also exposes `handle_rejection` which can be used to ensure all rejections become replies. 4 | 5 | use log::*; 6 | use serde::Serialize; 7 | use thiserror::Error; 8 | use warp::{http::StatusCode, Rejection, Reply}; 9 | 10 | #[derive(Clone, Copy, Error, Debug, PartialEq, Eq)] 11 | pub enum Error { 12 | #[error("wrong credentials")] 13 | WrongCredentialsError, 14 | #[error("refresh token not valid")] 15 | RefreshTokenError, 16 | #[error("jwt token not valid")] 17 | JwtTokenError, 18 | #[error("invalid auth header")] 19 | InvalidAuthHeaderError, 20 | #[error("no permission")] 21 | NoPermissionError, 22 | #[error("internal error")] 23 | InternalError, 24 | } 25 | 26 | impl Error { 27 | fn status_code(self) -> StatusCode { 28 | match self { 29 | // Note regarding FORBIDDEN vs. UNAUTHORIZED: 30 | // 31 | // According to RFC 7235 (https://tools.ietf.org/html/rfc7235#section-3.1), 32 | // UNAUTHORIZED should be used if using HTTP authentication. This can be inferred from 33 | // the RFC which states that the WWW-Authenticate header must be sent by the server 34 | // upon replying with UNAUTHORIZED. This server uses its own authentication method — 35 | // not HTTP authentication — so UNAUTHORIZED must not be used. 36 | // 37 | // The WrongCredentialsError variant will have a BAD_REQUEST response. 38 | Self::RefreshTokenError | Self::NoPermissionError | Self::JwtTokenError => { 39 | StatusCode::FORBIDDEN 40 | } 41 | Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, 42 | _ => StatusCode::BAD_REQUEST, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Serialize, Debug)] 48 | struct ErrorResponse { 49 | msg: String, 50 | status: String, 51 | } 52 | 53 | impl warp::reject::Reject for Error {} 54 | 55 | /// Turn a rejection into a handled reply. 56 | pub async fn handle_rejection( 57 | err: Rejection, 58 | ) -> std::result::Result { 59 | if let Some(e) = err.find::() { 60 | debug!("custom rejection: {}", e); 61 | let code = e.status_code(); 62 | let json = warp::reply::json(&ErrorResponse { 63 | status: code.to_string(), 64 | msg: e.to_string(), 65 | }); 66 | Ok(warp::reply::with_status(json, code).into_response()) 67 | } else { 68 | debug!("builtin rejection: {:?}", err); 69 | Ok(err.default_response()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod error; 3 | mod routes; 4 | mod storage; 5 | 6 | use std::net::SocketAddrV4; 7 | use std::process::exit; 8 | use warp::Filter; 9 | 10 | #[derive(structopt::StructOpt)] 11 | struct Args { 12 | #[structopt(short, long, help = "Address to bind to, e.g. 127.0.0.1:80")] 13 | address: Option, 14 | 15 | #[structopt(short, long, help = "Database URL, e.g. sqlite:///tmp/db.sql")] 16 | database: Option, 17 | } 18 | 19 | #[paw::main] 20 | #[tokio::main] 21 | async fn main(args: Args) { 22 | init_log(); 23 | 24 | log::info!("rust spa auth starting"); 25 | 26 | log::info!("initializing password pretend processing"); 27 | auth::pretend_password_processing().await; 28 | 29 | let store = init_store(args.database).await; 30 | 31 | let sockaddr: SocketAddrV4 = match &args.address { 32 | Some(v) => v.parse().unwrap_or_else(|_| { 33 | log::error!("Invalid socket address provided"); 34 | exit(1); 35 | }), 36 | None => SocketAddrV4::new(std::net::Ipv4Addr::new(127, 0, 0, 1), 8080), 37 | }; 38 | 39 | // It actually seems a bit cleaner to use a feature flag here rather than a conditional, 40 | // because the type of `api_routes` is changing here. So there would need to be a CORS branch 41 | // and a non-CORS branch with some repeated code to do this with a conditional instead of a 42 | // feature flag. 43 | #[cfg(feature = "dev_cors")] 44 | let sockaddr = { 45 | if args.address.is_some() { 46 | log::warn!( 47 | "for feature `dev_cors`, address 127.0.0.1:9090 is used instead of input {}", 48 | sockaddr 49 | ); 50 | } 51 | SocketAddrV4::new(std::net::Ipv4Addr::new(127, 0, 0, 1), 9090) 52 | }; 53 | 54 | let routes = routes::api(store).or(routes::spa("./public".into())); 55 | 56 | log::info!("running webserver"); 57 | warp::serve(routes) 58 | .tls() 59 | .key_path("tls/server.rsa.key") 60 | .cert_path("tls/server.rsa.crt") 61 | .run(sockaddr) 62 | .await; 63 | } 64 | 65 | fn init_log() { 66 | env_logger::Builder::from_env( 67 | env_logger::Env::default() 68 | // use info level by default 69 | .default_filter_or("info"), 70 | ) 71 | .init(); 72 | } 73 | 74 | #[cfg(not(feature = "in_memory"))] 75 | async fn init_store(db: Option) -> impl storage::Storage + Send + Sync + Clone { 76 | log::info!("connecting to database"); 77 | let db = db.unwrap_or_else(|| { 78 | std::env::var("DATABASE_URL") 79 | .expect("Need `--database ` flag or `DATABASE_URL` env variable") 80 | }); 81 | storage::new_db_storage(&db) 82 | .await 83 | .expect("could not connect to database") 84 | } 85 | 86 | #[cfg(feature = "in_memory")] 87 | async fn init_store(_b: Option) -> impl storage::Storage + Send + Sync + Clone { 88 | let store = storage::new_in_memory_storage(); 89 | // Need to store some default users for in_memory, otherwise nothing will exist. This 90 | // does not apply to a database, because a database can have pre-existing users. 91 | log::info!("preparing default users for in-memory store"); 92 | auth::store_user(&store, "user@localhost", "userpassword", auth::Role::User) 93 | .await 94 | .expect("could not store default user"); 95 | auth::store_user( 96 | &store, 97 | "admin@localhost", 98 | "adminpassword", 99 | auth::Role::Admin, 100 | ) 101 | .await 102 | .expect("could not store default admin"); 103 | store 104 | } 105 | -------------------------------------------------------------------------------- /server/src/storage/db.rs: -------------------------------------------------------------------------------- 1 | //! Provides a database backed implementation of `Storage`. This implementation puts everything in 2 | //! the database, but an alternative implementation could put only the users in the database and 3 | //! keep the refresh tokens in memory. 4 | 5 | #[cfg(not(feature = "in_memory"))] 6 | use sqlx::sqlite::SqlitePool; 7 | 8 | use super::*; 9 | 10 | fn map_sqlx_err(e: sqlx::Error) -> Error { 11 | log::error!("{}", e); 12 | Error::InternalError 13 | } 14 | 15 | #[cfg(not(feature = "in_memory"))] 16 | #[async_trait::async_trait] 17 | impl Storage for SqlitePool { 18 | async fn get_user(&self, email: &str) -> Result, Error> { 19 | sqlx::query_file!( 20 | "src/storage/get_user.sql", 21 | email, 22 | ) 23 | .fetch_optional(self) 24 | .await 25 | .map(|maybe_user| { 26 | maybe_user.map(|u| User { 27 | email: u.email, 28 | hashed_pw: u.hashed_pw, 29 | role: auth::Role::from_str(&u.role), 30 | }) 31 | }) 32 | .map_err(map_sqlx_err) 33 | } 34 | 35 | async fn store_user(&self, user: User) -> Result<(), Error> { 36 | let role = user.role.to_str().to_owned(); 37 | match sqlx::query!( 38 | "INSERT OR REPLACE INTO users VALUES(?1, ?2, ?3)", 39 | user.email, 40 | user.hashed_pw, 41 | role, 42 | ) 43 | .execute(self) 44 | .await 45 | .map_err(map_sqlx_err)? 46 | .rows_affected() 47 | { 48 | 1 => Ok(()), 49 | 0 => { 50 | log::error!("no rows affected when storing user"); 51 | Err(Error::InternalError) 52 | } 53 | _ => { 54 | log::error!("more than 1 row affected when storing user"); 55 | Err(Error::InternalError) 56 | } 57 | } 58 | } 59 | 60 | async fn refresh_token_exists(&self, token: &auth::RefreshToken) -> Result { 61 | sqlx::query!( 62 | "SELECT expires FROM refresh_tokens WHERE email = ? AND user_agent = ? AND expires = ?", 63 | token.email, 64 | token.user_agent, 65 | token.exp, 66 | ) 67 | .fetch_optional(self) 68 | .await 69 | .map(|t| t.is_some()) 70 | .map_err(map_sqlx_err) 71 | } 72 | 73 | async fn add_refresh_token(&self, token: auth::RefreshToken) -> Result<(), Error> { 74 | match sqlx::query!( 75 | "INSERT OR REPLACE INTO refresh_tokens VALUES(?, ?, ?)", 76 | token.email, 77 | token.user_agent, 78 | token.exp, 79 | ) 80 | .execute(self) 81 | .await 82 | .map_err(map_sqlx_err)? 83 | .rows_affected() 84 | { 85 | 1 => Ok(()), 86 | 0 => { 87 | log::error!("no rows affected when adding token"); 88 | Err(Error::InternalError) 89 | } 90 | _ => { 91 | log::error!("more than 1 row affected when adding token"); 92 | Err(Error::InternalError) 93 | } 94 | } 95 | } 96 | 97 | async fn remove_refresh_token(&self, token: &auth::RefreshToken) -> Result<(), Error> { 98 | match sqlx::query!( 99 | "DELETE FROM refresh_tokens WHERE email = ? AND user_agent = ? AND expires = ?", 100 | token.email, 101 | token.user_agent, 102 | token.exp, 103 | ) 104 | .execute(self) 105 | .await 106 | .map_err(map_sqlx_err)? 107 | .rows_affected() 108 | { 109 | 1 => Ok(()), 110 | 0 => { 111 | log::error!("no rows affected when deleting token"); 112 | Err(Error::RefreshTokenError) 113 | } 114 | _ => { 115 | log::error!("more than 1 row affected when deleting token"); 116 | Err(Error::InternalError) 117 | } 118 | } 119 | } 120 | } 121 | 122 | /// Returns an implementer of `Storage + Send + Clone` that is backed by the sqlite database 123 | /// provided as the URL input. 124 | #[cfg(not(feature = "in_memory"))] 125 | pub async fn new_db_storage(url: &str) -> Result { 126 | Ok(SqlitePool::connect(url).await.map_err(|e| { 127 | log::error!("{}", e); 128 | Error::InternalError 129 | })?) 130 | } 131 | -------------------------------------------------------------------------------- /client/src/api/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | API request library. 4 | 5 | */ 6 | import axios from 'axios'; 7 | import jwt_decode from 'jwt-decode'; 8 | import router from '@/router'; 9 | 10 | let { hostname, port } = window.location; 11 | 12 | // shared access token for all APIs 13 | let accessToken = null; 14 | let tokenClaims = {}; 15 | 16 | // change parameters for development since it uses CORS 17 | if (process.env.NODE_ENV !== 'production') { 18 | port = 9090; 19 | axios.defaults.withCredentials = true; 20 | } 21 | 22 | axios.defaults.baseURL = `https://${hostname}:${port}`; 23 | 24 | /** 25 | * Request an access token and stores it in the file variable `accessToken`. 26 | * 27 | * @param {bool} force - Set true if a brand new access token should be 28 | * requested rather than using the currently saved one. 29 | * @returns {Promise} 30 | */ 31 | function getAccessToken(force) { 32 | // If an access token already exists, do nothing. 33 | if (accessToken && !force) { 34 | return new Promise((resolve) => resolve(null)); 35 | } 36 | 37 | return axios({ 38 | method: 'get', 39 | url: `/api/auth/access`, 40 | }).then((result) => { 41 | accessToken = result.data; 42 | tokenClaims = jwt_decode(result.data) || {}; 43 | return null; 44 | }); 45 | } 46 | 47 | /** 48 | * Log out of the system 49 | * @returns {Promise} 50 | */ 51 | function doLogout() { 52 | return axios({ 53 | method: 'post', 54 | url: `/api/auth/logout`, 55 | }).then(() => { 56 | accessToken = ''; 57 | tokenClaims = {}; 58 | router.push('/login').catch((e) => { 59 | if (e.name != 'NavigationDuplicated') { 60 | throw e; 61 | } 62 | }); 63 | return null; 64 | }); 65 | } 66 | 67 | axios.interceptors.request.use( 68 | (config) => { 69 | if (!config.url.startsWith('/api/auth/') && config.url !== '/api/login') { 70 | // Add authorization header for non-auth related APIs 71 | config.headers.authorization = `Bearer ${accessToken}`; 72 | } 73 | return config; 74 | }, 75 | null, 76 | ); 77 | 78 | axios.interceptors.response.use( 79 | null, 80 | async (e) => { 81 | // Rethrow any non-authorization errors 82 | if (!e.response || e.response.status !== 403) { 83 | throw e; 84 | } 85 | 86 | const { config } = e; 87 | 88 | // If the original request was related to authorization already, logout and exit early. 89 | if (config.url.startsWith('/api/auth/')) { 90 | await doLogout(); 91 | throw e; 92 | } 93 | 94 | // Get the new access token 95 | await getAccessToken(true); 96 | 97 | // Resend the original request. Wrap in try/catch so that it only tries 98 | // once and does not keep repeating. 99 | return await axios.request(config); 100 | }, 101 | { synchronous: true }, 102 | ); 103 | 104 | export default { 105 | /** 106 | * Attempt login. 107 | * @returns {Promise} 108 | */ 109 | login(email, pw) { 110 | return axios({ 111 | method: 'post', 112 | url: `/api/login`, 113 | data: { email, pw }, 114 | }).then(this.access); 115 | }, 116 | 117 | /** 118 | * Request API access. 119 | * @returns {Promise} 120 | */ 121 | access() { 122 | return getAccessToken(); 123 | }, 124 | 125 | /** 126 | * Get the error message 127 | * @param {Object} e - Error response from an API. 128 | * @returns {string} 129 | */ 130 | getErrorMsg(e) { 131 | // e.response.data.msg is for custom rejections 132 | // e.response.data is for default Warp rejections 133 | // e.message is the default error message for the error code 134 | return e.response && (e.response.data.msg || e.response.data) || e.message; 135 | }, 136 | 137 | /** 138 | * Call the user API. 139 | * @returns {Promise} 140 | */ 141 | callUser() { 142 | return axios({ 143 | method: 'post', 144 | url: `/api/user`, 145 | }).then((resp) => resp.data); 146 | }, 147 | 148 | /** 149 | * Call the user API 150 | * @returns {Promise} 151 | */ 152 | callAdmin() { 153 | return axios({ 154 | method: 'post', 155 | url: `/api/admin`, 156 | }).then((resp) => resp.data); 157 | }, 158 | 159 | /** 160 | * Get the claims. 161 | * @returns {Promise} 162 | */ 163 | claims() { 164 | if (accessToken) { 165 | return new Promise((resolve) => resolve(tokenClaims)); 166 | } 167 | return this.access().then(() => tokenClaims); 168 | }, 169 | 170 | /** 171 | * Log out. 172 | * @returns {Promise} 173 | */ 174 | logout() { 175 | doLogout(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /server/src/routes.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::*; 2 | use crate::error; 3 | use crate::storage::Storage; 4 | use warp::{ 5 | filters::{self, BoxedFilter}, 6 | http, reply, Filter, 7 | }; 8 | 9 | pub fn api( 10 | store: S, 11 | ) -> BoxedFilter<(impl reply::Reply,)> { 12 | log::info!("creating routes"); 13 | let login_api = warp::path!("login") 14 | .and(with_storage(store.clone())) 15 | .and(warp::post()) 16 | .and(filters::header::header::("user-agent")) 17 | .and(warp::body::json()) 18 | .and_then(login_handler); 19 | 20 | // The `access` and `logout` routes have the `auth` prefix because these are the only two 21 | // routes that require the `refresh_token` cookie. These get a unique prefix so that the cookie 22 | // can use a more specific path and won't be sent for unnecessary routes. 23 | 24 | let access_api = warp::path!("auth" / "access") 25 | .and(warp::get()) 26 | .and(with_storage(store.clone())) 27 | .and(filters::header::header::("user-agent")) 28 | .and(filters::cookie::cookie("refresh_token")) 29 | .and_then(access_handler); 30 | 31 | let logout_api = warp::path!("auth" / "logout") 32 | .and(warp::post()) 33 | .and(with_storage(store)) 34 | .and(filters::header::header::("user-agent")) 35 | .and(filters::cookie::cookie("refresh_token")) 36 | .and_then(logout_handler); 37 | 38 | let user_api = warp::path!("user") 39 | .and(with_auth(Role::User)) 40 | .and_then(user_handler); 41 | 42 | let admin_api = warp::path!("admin") 43 | .and(with_auth(Role::Admin)) 44 | .and_then(admin_handler); 45 | 46 | // Note: warp::path is **not** the macro! The macro version would terminate path checking at 47 | // "api" as opposed to being a prefix for the other handlers. This puzzled me for longer than I 48 | // would have liked. ☹ 49 | // 50 | // This "api" prefix is used so that API handlers' rejections can all be turned into replies by 51 | // `error::handle_rejection`. This is needed so that they don't fall back to the SPA handlers. 52 | let apis = warp::path("api").and( 53 | access_api 54 | .or(login_api) 55 | .or(user_api) 56 | .or(admin_api) 57 | .or(logout_api) 58 | .recover(error::handle_rejection), 59 | ); 60 | 61 | #[cfg(feature = "dev_cors")] 62 | let apis = { 63 | const ORIGIN: &str = "http://localhost:8080"; 64 | log::info!("allowing CORS for development, origin: {}", ORIGIN); 65 | apis.with( 66 | warp::cors() 67 | .allow_origin(ORIGIN) 68 | .allow_methods(vec!["GET", "PUT", "POST", "DELETE"]) 69 | .allow_headers(vec!["content-type", "user-agent", "authorization"]) 70 | .allow_credentials(true), 71 | ) 72 | }; 73 | 74 | apis.boxed() 75 | } 76 | 77 | pub fn spa(client_files_dir: String) -> BoxedFilter<(impl reply::Reply,)> { 78 | warp::fs::dir(client_files_dir.clone()) 79 | .or(warp::fs::file(format!("{}/index.html", client_files_dir))) 80 | .boxed() 81 | } 82 | 83 | /// Creates a filter that passes storage to the receiving fn. 84 | fn with_storage( 85 | store: S, 86 | ) -> impl Filter + Clone { 87 | warp::any().map(move || store.clone()) 88 | } 89 | 90 | /// Authenticate with an email and a password to retrieve a refresh token cookie. 91 | async fn login_handler( 92 | store: S, 93 | user_agent: String, 94 | req: AuthenticateRequest, 95 | ) -> Result { 96 | Ok(authenticate(&store, &user_agent, "/api/auth", req) 97 | .await 98 | .map(|cookie| { 99 | warp::http::Response::builder() 100 | .header("set-cookie", &cookie) 101 | .body("success") 102 | })?) 103 | } 104 | 105 | /// Get a new access token using a refresh token. 106 | async fn access_handler( 107 | store: S, 108 | user_agent: String, 109 | refresh_token: String, 110 | ) -> Result { 111 | Ok(access(&store, &user_agent, &refresh_token) 112 | .await 113 | .map(|token| warp::http::Response::builder().body(token))?) 114 | } 115 | 116 | /// Explicitly log out by revoking the refresh token. 117 | async fn logout_handler( 118 | store: S, 119 | user_agent: String, 120 | refresh_token: String, 121 | ) -> Result { 122 | // Ignore result, always reply with 200. Don't want an attacker to know if they logged out with 123 | // actual credentials or not. 124 | let _ = logout(&store, &user_agent, &refresh_token).await; 125 | Ok(warp::reply()) 126 | } 127 | 128 | /// Returns a filter that checks if the request is authorized based on the `required_role` 129 | /// provided. 130 | /// 131 | /// For example, if this called with `auth::Role::Admin`, then the returned filter will reject any 132 | /// requests that do not have an access token that states they are an admin. 133 | fn with_auth( 134 | required_role: Role, 135 | ) -> impl Filter + Clone { 136 | use once_cell::sync::Lazy; 137 | static AUTH_HEADER: Lazy<&str> = Lazy::new(|| http::header::AUTHORIZATION.as_str()); 138 | filters::header::header::(&AUTH_HEADER) 139 | .map(move |auth_header| (required_role, auth_header)) 140 | .and_then(auth) 141 | } 142 | 143 | /// Warp-ified wrapper for `auth::authorize`. 144 | async fn auth((required_role, auth_header): (Role, String)) -> Result { 145 | authorize(required_role, auth_header).map_err(warp::reject::custom) 146 | } 147 | 148 | /// Sample handler for a user 149 | async fn user_handler(email: String) -> Result { 150 | Ok(format!("user {}", email)) 151 | } 152 | 153 | /// Sample handler for an admin 154 | async fn admin_handler(email: String) -> Result { 155 | Ok(format!("admin {}", email)) 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust SPA + Auth 2 | 3 | This project contains a Rust server that serves a single page application and 4 | has authentication + JWT-based authorization. 5 | 6 | It was written as a learning exercise and can hopefully be a useful example for 7 | a Rust-backed website that uses authentication + authorization. It's a bit more 8 | complete and closer to prodution-ready than other example code I've seen 9 | online, e.g. [JWT with warp](#special-mentions). 10 | 11 | ## Warning 12 | 13 | Though I am somewhat informed, I am not a security expert. Don't deploy this 14 | code to production. 15 | 16 | # Demo video 17 | 18 | [Click here](https://user-images.githubusercontent.com/6634136/113497053-c2505200-94b4-11eb-8010-27a132a010e9.mp4) 19 | 20 | # Dependencies 21 | 22 | - A recent version of Rust+Cargo (MSRV unknown) 23 | - A recent version of npm (minimum unknown) 24 | 25 | ## Note regarding Warp 26 | 27 | If you check [Cargo.toml](server/Cargo.toml), you'll see that the `warp` 28 | dependency is my personal warp fork. This is due to waiting on [my 29 | PR](https://github.com/seanmonstar/warp/pull/827) for more convenient rejection 30 | handling to be merged. 31 | 32 | # Notable content 33 | 34 | ## Server 35 | 36 | - Rust with a [Warp web server](https://crates.io/crates/warp) 37 | - Authentication using Argon2 password hashing to produce refresh token cookies 38 | - Authorization with 2 basic roles using JWT access tokens for claims 39 | - [Optional CORS](#serve-the-spa-separately) for more rapid client side development 40 | - Example for abstracting a data store with a trait 41 | - In-memory implementation exists 42 | 43 | ## Client 44 | 45 | - [Vue 2.X](https://vuejs.org/) framework 46 | - [Axios](https://www.npmjs.com/package/axios) for API requests 47 | - Login 48 | - Logout 49 | - Conditionally visible UI components based on JWT claims 50 | - Automatic refreshing of access tokens on 403 error 51 | 52 | I am not the most proficient client-side dev, so the structure of the client side 53 | code may not be what you want to emulate. The [API requests using 54 | axios](client/src/api/index.js) are probably the most useful to look at with 55 | regards to using the server APIs. 56 | 57 | # Note on server framework and async runtime 58 | 59 | Most of the code is hopefully not closely tied to Warp framework details — most 60 | of the Warp-specific code is in `routes.rs` with a sprinkle in `main.rs` and 61 | `error.rs`. As long as the server framework used is async capable, the rest of 62 | it should be a decent starting point for use with other server frameworks. 63 | 64 | Since the webserver uses Warp, the code uses the tokio runtime. Apart from 65 | the Warp related code, the `auth` module has a few instances where it is 66 | reliant on tokio. These are pretty minimal so it should be simple to adapt for 67 | webservers with another runtime, e.g. [Tide](https://crates.io/crates/tide). 68 | 69 | Instances of tokio reliance: 70 | 71 | - `init_default_users`: uses `block_on` to run async code in a sync function 72 | - `store_user`: spawns a blocking task to run password hashing 73 | - `authenticate`: spawns a blocking task to run password verification 74 | - `pretend_password_processing`: uses tokio sleep 75 | - `#[tokio::test]` is used for async tests 76 | 77 | # Serve the SPA with Rust 78 | 79 | The command sequence below uses an in-memory data store. To use sqlite, 80 | different commands are needed. 81 | 82 | ``` 83 | cd $(git rev-parse --show-toplevel) 84 | ./build-debug.sh 85 | cd build-output 86 | ./rust-spa-auth 87 | ``` 88 | 89 | # Serve the SPA separately 90 | 91 | To serve the SPA and the server separately for more rapid client side code 92 | development, you can use the following commands: 93 | 94 | Note - you may have to navigate to https://localhost:9090 manually and accept 95 | the certificate warning before this works. 96 | 97 | Serve client files: 98 | ``` sh 99 | cd $(git rev-parse --show-toplevel)/client 100 | npm run serve 101 | ``` 102 | 103 | Run server: 104 | ``` sh 105 | cd $(git rev-parse --show-toplevel)/server 106 | cargo run --features in_memory,dev_cors 107 | # Can omit `in_memory` from above, but a database will need to be specified 108 | # e.g. 109 | export DATABASE_URL=sqlite:///tmp/db.sql 110 | cargo run --features dev_cors -- --database $DATABASE_URL 111 | ``` 112 | 113 | # Example API Usage 114 | 115 | You can check the API functionality without your browser using cURL. 116 | 117 | See an example sequence below. 118 | 119 | ``` sh 120 | 121 | curl -v https://localhost:9090/api/login \ 122 | --cacert tls/server.rsa.crt \ 123 | -d '{"email": "user@localhost", "pw": "userpassword"}' \ 124 | -H 'Content-Type: application/json' 125 | 126 | # result is in set-cookie header: 127 | # set-cookie: refresh_token=QpOddMUkW9wk/S4B.s/a3k3JttPFH3v4j43gxx7KL+3y05Opm1rjiQBV+07z9NXacLv8PeQn6DRDoblFDerGQ9qeUp1TpaNAg5f1cYtLf3t3xnvGkHUDW2TK/mDJr4A=="; Max-Age=2592000; path=/api/auth/access; Secure; HttpOnly; SameSite=Lax; 128 | 129 | 130 | curl https://localhost:9090/api/auth/access \ 131 | --cacert tls/server.rsa.crt \ 132 | --cookie "refresh_token=QpOddMUkW9wk/S4B.s/a3k3JttPFH3v4j43gxx7KL+3y05Opm1rjiQBV+07z9NXacLv8PeQn6DRDoblFDerGQ9qeUp1TpaNAg5f1cYtLf3t3xnvGkHUDW2TK/mDJr4A==" 133 | 134 | # result: 135 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6InVzZXJAbG9jYWxob3N0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE2MTY5MjY2NTd9.kj9GR-FPUVmZh2BEvGmbqg6tAz4lsjvLxtcTXOjdDXLwD0KGZ2NrDueuuyJ1Y4z8z98q9VcpDNHYjS4veM2hYw 136 | 137 | curl https://localhost:9090/api/user \ 138 | --cacert tls/server.rsa.crt \ 139 | -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6InVzZXJAbG9jYWxob3N0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE2MTcwNjUxMDJ9.imixaRk8YgoEv8Hh33qidty_jGBAo9ewIOd7vWqAjAHiN-MZJOFeSXg25nWx86SW9Pc_QFH_qlFYaSmPG_MfRA' 140 | 141 | # result: 142 | # user user@localhost 143 | 144 | curl https://localhost:9090/api/admin \ 145 | --cacert tls/server.rsa.crt \ 146 | -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6InVzZXJAbG9jYWxob3N0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE2MTcwNjUxMDJ9.imixaRk8YgoEv8Hh33qidty_jGBAo9ewIOd7vWqAjAHiN-MZJOFeSXg25nWx86SW9Pc_QFH_qlFYaSmPG_MfRA' 147 | 148 | # result: 149 | # {"message":"no permission","status":"403 Forbidden"}⏎ 150 | 151 | curl https://localhost:9090/api/auth/logout \ 152 | -X POST \ 153 | --cacert tls/server.rsa.crt \ 154 | --cookie "refresh_token=QpOddMUkW9wk/S4B.s/a3k3JttPFH3v4j43gxx7KL+3y05Opm1rjiQBV+07z9NXacLv8PeQn6DRDoblFDerGQ9qeUp1TpaNAg5f1cYtLf3t3xnvGkHUDW2TK/mDJr4A==" 155 | 156 | ``` 157 | 158 | # Potential changes/additions 159 | 160 | - auth rate limit 161 | - http to https redirect 162 | - delete the cookie on the client on logout 163 | - not really necessary, but can do for cleanliness 164 | - lets-encrypt certificates 165 | - add APIs for add/delete user 166 | - casbin 167 | 168 | # Special mentions 169 | 170 | These sources were useful starting points. 171 | 172 | - [Hosting SPA with Warp](https://freiguy1.gitlab.io/posts/hosting-spa-with-warp.html) 173 | - [JWT authentication with Warp](https://blog.logrocket.com/jwt-authentication-in-rust/) 174 | 175 | # License 176 | 177 | This project is licensed under the [MIT license](LICENSE). 178 | 179 | # Contribution 180 | 181 | Pull requests are welcome. The goal of this project is to serve as a useful 182 | example for building a website with a Rust backend that includes some security. 183 | 184 | Unless you explicitly state otherwise, any contribution intentionally submitted 185 | for inclusion by you shall be licensed as MIT without any additional terms or 186 | conditions. 187 | -------------------------------------------------------------------------------- /server/src/auth.rs: -------------------------------------------------------------------------------- 1 | //! Provides functions for authentication and authorization. 2 | 3 | use crate::{ 4 | error::Error, 5 | storage::{self, Storage}, 6 | }; 7 | use anyhow::anyhow; 8 | use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; 9 | use argon2::Argon2; 10 | use chacha20poly1305::aead::{AeadInPlace, NewAead}; 11 | use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; 12 | use log::*; 13 | use once_cell::sync::Lazy; 14 | use rand::RngCore; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// Used for role differentiation to showcase authorization of the admin route. 18 | #[derive(Clone, Copy, Debug, PartialEq)] 19 | pub enum Role { 20 | Admin, 21 | User, 22 | } 23 | 24 | impl Role { 25 | // Doesn't use std::str::FromStr since the trait impl returns a Result and this is infallible. 26 | pub fn from_str(role: &str) -> Self { 27 | match role { 28 | "admin" => Self::Admin, 29 | _ => Self::User, 30 | } 31 | } 32 | 33 | pub fn to_str(self) -> &'static str { 34 | match self { 35 | Self::Admin => "admin", 36 | Self::User => "user", 37 | } 38 | } 39 | } 40 | 41 | static ARGON2: Lazy = Lazy::new(Argon2::default); 42 | 43 | /// FIXME: add methods to add/delete user. For now this has dead_code allowed to suppress compiler 44 | /// warnings. 45 | #[allow(dead_code)] 46 | pub async fn store_user + Send, S: Storage>( 47 | store: &S, 48 | email: &str, 49 | pw: P, 50 | role: Role, 51 | ) -> Result<(), anyhow::Error> { 52 | let hashed_pw = tokio::task::spawn_blocking(move || { 53 | ARGON2 54 | .hash_password_simple( 55 | pw.as_ref(), 56 | SaltString::generate(rand::thread_rng()).as_ref(), 57 | ) 58 | .map(|pw| pw.to_string()) 59 | }) 60 | .await 61 | .map_err(|e| anyhow!(e))? 62 | .map_err(|e| anyhow!(e))?; 63 | 64 | store 65 | .store_user(storage::User { 66 | email: email.into(), 67 | hashed_pw, 68 | role, 69 | }) 70 | .await 71 | .map_err(|e| anyhow!(e)) 72 | } 73 | 74 | // Keys are 32 bytes: 75 | // https:/kdocs.rs/chacha20poly1305/0.7.1/chacha20poly1305/type.Key.html 76 | const KEY_LEN: usize = 32; 77 | 78 | // Need to change this if refresh token persistence is desired after restarting the binary. 79 | static REFRESH_TOKEN_CIPHER: Lazy = Lazy::new(|| { 80 | let mut key_bytes = vec![0u8; KEY_LEN]; 81 | rand::thread_rng().fill_bytes(&mut key_bytes); 82 | ChaCha20Poly1305::new(Key::from_slice(&key_bytes)) 83 | }); 84 | 85 | // Nonces are 12 bytes: 86 | // https://docs.rs/chacha20poly1305/0.7.1/chacha20poly1305/type.Nonce.html 87 | const NONCE_LEN: usize = 12; 88 | 89 | /// Content of the encrypted + encoded token that is sent in an authenticate response. The 90 | /// `user_agent` field is used to mitigate against token theft. It's not a very good check since 91 | /// the header can easily be faked, but it's at least something. The `email` field is used to 92 | /// ensure that the user that created the token is still valid. The `exp` field is used to ensure 93 | /// that the token has an expiry time (good practice?) and needs to re-authenticate once in a 94 | /// while. 95 | /// 96 | /// If security is more important than convenience (mobile phones can change IP frequently), can 97 | /// use the L3 source IP address and compare against it. Though according to 98 | /// https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-sidejacking 99 | /// this might have issues with the European GDPR. 100 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 101 | pub struct RefreshToken { 102 | pub user_agent: String, 103 | pub email: String, 104 | pub exp: i64, 105 | } 106 | 107 | /// 30 days in seconds 108 | const REFRESH_TOKEN_MAX_AGE_SECS: i64 = 30 * 24 * 60 * 60; 109 | 110 | impl RefreshToken { 111 | /// Create a new refresh token. 112 | fn new(user_agent: &str, email: &str) -> Result { 113 | let exp = chrono::Utc::now() 114 | .checked_add_signed(chrono::Duration::seconds(REFRESH_TOKEN_MAX_AGE_SECS)) 115 | .ok_or_else(|| { 116 | error!("could not make timestamp"); 117 | Error::InternalError 118 | })? 119 | .timestamp(); 120 | 121 | Ok(Self { 122 | user_agent: user_agent.into(), 123 | email: email.into(), 124 | exp, 125 | }) 126 | } 127 | 128 | /// Returns a `String` to be used as a token by client-side code. The data is serialized, 129 | /// encrypted, then base64 encoded. 130 | fn encrypt_encode(&self) -> Result { 131 | let mut nonce_bytes = [0u8; NONCE_LEN]; 132 | rand::thread_rng().fill_bytes(&mut nonce_bytes); 133 | let nonce_string = base64::encode(&nonce_bytes); 134 | let mut token = bincode::serialize(&self).map_err(|_| { 135 | error!("failed to serialize refresh token"); 136 | Error::InternalError 137 | })?; 138 | 139 | let nonce = Nonce::from_slice(&nonce_bytes); 140 | REFRESH_TOKEN_CIPHER 141 | .encrypt_in_place(nonce, b"", &mut token) 142 | .map_err(|_| Error::InternalError)?; 143 | 144 | let token_string = base64::encode(&token); 145 | Ok(format!("{}.{}", nonce_string, token_string)) 146 | } 147 | } 148 | 149 | use std::str::FromStr; 150 | 151 | impl std::str::FromStr for RefreshToken { 152 | type Err = Error; 153 | 154 | fn from_str(s: &str) -> Result { 155 | let mut nonce_token_strs = s.splitn(2, '.'); 156 | let nonce_str = nonce_token_strs.next().ok_or(Error::RefreshTokenError)?; 157 | let token_str = nonce_token_strs.next().ok_or(Error::RefreshTokenError)?; 158 | let nonce_bytes = base64::decode(nonce_str).map_err(|_| Error::RefreshTokenError)?; 159 | let nonce = Nonce::from_slice(&nonce_bytes); 160 | let mut token_bytes = base64::decode(token_str).map_err(|_| Error::RefreshTokenError)?; 161 | REFRESH_TOKEN_CIPHER 162 | .decrypt_in_place(nonce, b"", &mut token_bytes) 163 | .map_err(|_| Error::RefreshTokenError)?; 164 | bincode::deserialize::(&token_bytes).map_err(|_| Error::RefreshTokenError) 165 | } 166 | } 167 | 168 | /// Add a new refresh token to the data store, returning its `String` representation for saving on 169 | /// the client side on success. 170 | async fn add_refresh_token( 171 | store: &S, 172 | user_agent: &str, 173 | email: &str, 174 | ) -> Result { 175 | let token = RefreshToken::new(user_agent, email)?; 176 | let ret = token.encrypt_encode()?; 177 | store.add_refresh_token(token).await?; 178 | Ok(ret) 179 | } 180 | 181 | #[derive(Deserialize)] 182 | pub struct AuthenticateRequest { 183 | pub email: String, 184 | pub pw: String, 185 | } 186 | 187 | /// On success, returns a refresh token cookie string that can be used to get access tokens. 188 | /// 189 | /// The refresh token is opaque and should only be able to be read by this server. 190 | /// 191 | /// This contrasts the access token which is a JWT and has claims that are publicly visible. The 192 | /// claims in an access token can be used by the client-side code to manipulate what the user can 193 | /// see based on the claims. 194 | /// 195 | /// Unlike the access token, the browser has does not need to know what's inside a refresh token — 196 | /// they just need to give it back to get their access tokens. Thus this function uses symmetric 197 | /// encryption so that the browser does not know what's inside. 198 | /// 199 | /// I am not a security professional and so am unsure if the following is correct or not: 200 | /// 201 | /// The implementation exposes the nonce and does not check for re-used nonces. My understanding is 202 | /// that the nonce is used to protect against attacks in network communication, e.g. replay 203 | /// attacks in https, ssh. 204 | /// 205 | /// For symmetric keys, accidentally re-using a nonce does not risk leaking the key. In this 206 | /// webserver, the encrypted message is intended to be replayed back, so exposing the nonce and 207 | /// allowing duplicates should be fine. 208 | /// 209 | /// The token should only be visible in the user's browser - not through network communication, 210 | /// because network comms should be encrypted via https. In addition, token theft is mitigated by 211 | /// checking the request's L3 source IP and the user agent header in the `access` function. 212 | pub async fn authenticate( 213 | store: &S, 214 | user_agent: &str, 215 | cookie_path: &str, 216 | req: AuthenticateRequest, 217 | ) -> Result { 218 | let user = match store.get_user(&req.email).await? { 219 | Some(v) => v, 220 | None => { 221 | return Err(pretend_password_processing().await); 222 | } 223 | }; 224 | 225 | // Split up ownership of `email` and `hashed_pw`. The blocking task needs `hashed_pw` and the 226 | // verification needs `email`. 227 | let email = user.email; 228 | let hashed_pw = user.hashed_pw; 229 | 230 | // Password verification is done in a blocking task because it is CPU intensive. 231 | let verification_result = tokio::task::spawn_blocking(move || { 232 | let parsed_hash = PasswordHash::new(&hashed_pw).map_err(|e| { 233 | error!("could not parse password hash: {}", e); 234 | Error::InternalError 235 | })?; 236 | ARGON2 237 | .verify_password(req.pw.as_bytes(), &parsed_hash) 238 | .map_err(|_| Error::WrongCredentialsError) 239 | }) 240 | .await 241 | .map_err(|e| { 242 | error!("tokio err {}", e); 243 | Error::InternalError 244 | })?; 245 | 246 | const SECURITY_FLAGS: &str = "Secure; HttpOnly; SameSite=Lax;"; 247 | 248 | match verification_result { 249 | Ok(()) => { 250 | log::info!( 251 | "{} authenticated with agent {}", 252 | &email, 253 | &user_agent 254 | ); 255 | Ok(format!( 256 | "refresh_token={}; Path={}; Max-Age={}; {}", 257 | add_refresh_token(store, &user_agent, &email).await?, 258 | cookie_path, 259 | REFRESH_TOKEN_MAX_AGE_SECS, 260 | SECURITY_FLAGS 261 | )) 262 | } 263 | Err(e) => Err(e), 264 | } 265 | } 266 | 267 | /// This exists to pretend that a password is being processed in the cases where it's not. This 268 | /// makes it harder to guess if a malicious request got an existing email with non-matching 269 | /// password vs. an email that does not exist. 270 | /// 271 | /// There is currently a bug: on the first time this function is called, the delay is a lot longer 272 | /// than on every other call. A workaround is to call this function at some point during 273 | /// initialization. I'm too `Lazy` to fix it, so it's declared as a `pub` function and called 274 | /// during init as the workaround. 275 | pub async fn pretend_password_processing() -> Error { 276 | static PROCESSING_TIME: Lazy = Lazy::new(|| { 277 | let salt = SaltString::generate(rand::thread_rng()); 278 | let pwhash = ARGON2 279 | .hash_password_simple(b"badpassword", salt.as_ref()) 280 | .expect("could not hash password"); 281 | let start = std::time::Instant::now(); 282 | let _ = ARGON2.verify_password(b"abcdefg", &pwhash); 283 | let end = std::time::Instant::now(); 284 | end - start 285 | }); 286 | info!("pretending to process password - should be an invalid email"); 287 | tokio::time::sleep(*PROCESSING_TIME).await; 288 | Error::WrongCredentialsError 289 | } 290 | 291 | /// On success, returns an access token that can be used to authorize with other APIs. 292 | /// 293 | /// The access request includes a refresh token that will only work for the user agent that 294 | /// originally created the token. If the provided token is used from a different user agent, then 295 | /// the token will be invalidated. 296 | pub async fn access( 297 | store: &S, 298 | user_agent: &str, 299 | refresh_token: &str, 300 | ) -> Result { 301 | let refresh_token = valid_refresh_token_from_str(store, user_agent, refresh_token).await?; 302 | 303 | let user = match store.get_user(&refresh_token.email).await? { 304 | Some(v) => v, 305 | None => { 306 | warn!("valid token for non-existent email {:?}", refresh_token); 307 | return Err(remove_bad_refresh_token(store, &refresh_token).await); 308 | } 309 | }; 310 | 311 | create_jwt(&user).map_err(|e| { 312 | error!("jwt create err: {}", e); 313 | Error::InternalError 314 | }) 315 | } 316 | 317 | /// Revokes the refresh token provided. This is done regardless of the validations, because if the 318 | /// validations fail then the token **should** be revoked anyway. 319 | pub async fn logout( 320 | store: &S, 321 | user_agent: &str, 322 | refresh_token: &str, 323 | ) -> Result<(), Error> { 324 | let refresh_token = valid_refresh_token_from_str(store, user_agent, refresh_token).await?; 325 | 326 | if store.get_user(&refresh_token.email).await?.is_none() { 327 | warn!("valid token for non-existent email {:?}", refresh_token); 328 | return Err(remove_bad_refresh_token(store, &refresh_token).await); 329 | }; 330 | 331 | store.remove_refresh_token(&refresh_token).await 332 | } 333 | 334 | /// Returns a valid `RefreshToken` on success and an error otherwise. 335 | async fn valid_refresh_token_from_str( 336 | store: &S, 337 | user_agent: &str, 338 | refresh_token: &str, 339 | ) -> Result { 340 | let refresh_token = RefreshToken::from_str(refresh_token)?; 341 | 342 | // make sure the token is known 343 | if !store.refresh_token_exists(&refresh_token).await? { 344 | warn!( 345 | "unknown refresh token provided, email {} agent {}", 346 | refresh_token.email, user_agent, 347 | ); 348 | return Err(Error::RefreshTokenError); 349 | } 350 | 351 | // remove token if expired 352 | if refresh_token.exp < chrono::Utc::now().timestamp() { 353 | warn!( 354 | "expired refresh token provided, email {} agent {}", 355 | refresh_token.email, user_agent, 356 | ); 357 | return Err(remove_bad_refresh_token(store, &refresh_token).await); 358 | } 359 | 360 | // ensure token is used by same user agent 361 | if user_agent != refresh_token.user_agent { 362 | warn!( 363 | "token used by different agent {}, token: {:?}", 364 | user_agent, &refresh_token, 365 | ); 366 | return Err(remove_bad_refresh_token(store, &refresh_token).await); 367 | } 368 | 369 | Ok(refresh_token) 370 | } 371 | 372 | /// Remove a bad refresh token from the set of known tokens. 373 | async fn remove_bad_refresh_token(store: &S, token: &RefreshToken) -> Error { 374 | if let Err(e) = store.remove_refresh_token(token).await { 375 | return e; 376 | } 377 | Error::RefreshTokenError 378 | } 379 | 380 | static MY_JWT_SECRET: Lazy<[u8; 256]> = Lazy::new(|| { 381 | let mut a = [0u8; 256]; 382 | rand::thread_rng().fill_bytes(&mut a); 383 | a 384 | }); 385 | static ENCODING_KEY: Lazy = 386 | Lazy::new(|| jsonwebtoken::EncodingKey::from_secret(MY_JWT_SECRET.as_ref())); 387 | static DECODING_KEY: Lazy = 388 | Lazy::new(|| jsonwebtoken::DecodingKey::from_secret(MY_JWT_SECRET.as_ref())); 389 | static VALIDATION_PARAMS: Lazy = 390 | Lazy::new(|| jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS512)); 391 | 392 | /// Could add this to the access token claims: 393 | /// https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-sidejacking 394 | /// but doesn't seem worthwhile considering that the refresh token serves a similar role - the access 395 | /// token is short-lived and should be kept in memory as opposed to a cookie or sessionStorage / 396 | /// localStorage. 397 | #[derive(Debug, Deserialize, Serialize)] 398 | struct Claims { 399 | email: String, 400 | role: String, 401 | exp: i64, 402 | } 403 | 404 | #[cfg(not(feature = "dev_cors"))] 405 | const ACCESS_TOKEN_DURATION: i64 = 60; 406 | 407 | // use very short duration for testing 408 | #[cfg(feature = "dev_cors")] 409 | const ACCESS_TOKEN_DURATION: i64 = 5; 410 | 411 | fn create_jwt(user: &storage::User) -> Result { 412 | let exp = chrono::Utc::now() 413 | .checked_add_signed(chrono::Duration::seconds(ACCESS_TOKEN_DURATION)) 414 | .ok_or_else(|| anyhow!("could not make timestamp"))? 415 | .timestamp(); 416 | 417 | let claims = Claims { 418 | email: user.email.clone(), 419 | role: user.role.to_str().into(), 420 | exp, 421 | }; 422 | let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS512); 423 | jsonwebtoken::encode(&header, &claims, &ENCODING_KEY).map_err(|e| anyhow!(e)) 424 | } 425 | 426 | /// Checks that a request is allowed to proceed based on the authorization header. Returns the 427 | /// email of the user on success. The `auth_header` is checked to ensure it is a valid JWT and that 428 | /// its claims satisfy `role_required`. 429 | /// 430 | /// In other words, an error will be returned if the JWT is invalid, or if the JWT is valid but the 431 | /// claimed role is insufficient. 432 | pub fn authorize(role_required: Role, auth_header: String) -> Result { 433 | const BEARER: &str = "Bearer "; 434 | 435 | if !auth_header.starts_with(BEARER) { 436 | return Err(Error::InvalidAuthHeaderError); 437 | } 438 | let jwt_str = auth_header.trim_start_matches(BEARER); 439 | 440 | let decoded_claims = 441 | jsonwebtoken::decode::(&jwt_str, &DECODING_KEY, &VALIDATION_PARAMS) 442 | .map_err(|_| Error::JwtTokenError)?; 443 | 444 | if role_required == Role::Admin && Role::from_str(&decoded_claims.claims.role) != Role::Admin { 445 | return Err(Error::NoPermissionError); 446 | } 447 | 448 | Ok(decoded_claims.claims.email) 449 | } 450 | 451 | #[cfg(test)] 452 | mod tests { 453 | use super::*; 454 | 455 | fn init_log() { 456 | static _INIT_LOG: Lazy = Lazy::new(|| { 457 | crate::init_log(); 458 | 0 459 | }); 460 | } 461 | 462 | fn token_from_cookie(t: &str) -> &str { 463 | t.split(';') 464 | .next() 465 | .expect("bad cookie") 466 | .trim_start_matches("refresh_token=") 467 | } 468 | 469 | #[cfg(feature = "in_memory")] 470 | async fn storage() -> impl Storage + Send + Sync + Clone { 471 | crate::storage::new_in_memory_storage() 472 | } 473 | 474 | #[cfg(not(feature = "in_memory"))] 475 | async fn storage() -> impl Storage + Send + Sync + Clone { 476 | let dbname = std::env::var("DATABASE_URL").expect("need DATABASE_URL variable"); 477 | crate::storage::new_db_storage(&dbname) 478 | .await 479 | .expect("no db available") 480 | } 481 | 482 | #[tokio::test(flavor = "multi_thread")] 483 | async fn test_good_authenticate_access_authorize() { 484 | init_log(); 485 | let s = storage().await; 486 | assert!(store_user(&s, "admin", "goodpasswordgoeshere", Role::Admin) 487 | .await 488 | .is_ok()); 489 | 490 | let refresh_cookie = authenticate( 491 | &s, 492 | "cargo test", 493 | "/path", 494 | AuthenticateRequest { 495 | email: "admin".into(), 496 | pw: "goodpasswordgoeshere".into(), 497 | }, 498 | ) 499 | .await 500 | .expect("authenticate failed"); 501 | 502 | let refresh_token = token_from_cookie(&refresh_cookie); 503 | let access_token = access(&s, "cargo test", &refresh_token) 504 | .await 505 | .expect("access failed"); 506 | 507 | assert_eq!( 508 | authorize(Role::Admin, format!("Bearer {}", access_token)), 509 | Ok("admin".into()) 510 | ); 511 | } 512 | 513 | #[tokio::test(flavor = "multi_thread")] 514 | async fn test_bad_authenticate() { 515 | init_log(); 516 | let s = storage().await; 517 | assert!(store_user(&s, "admin", "goodpasswordgoeshere", Role::Admin) 518 | .await 519 | .is_ok()); 520 | 521 | assert!(authenticate( 522 | &s, 523 | "cargo test", 524 | "/path", 525 | AuthenticateRequest { 526 | email: "noexist".into(), 527 | pw: "goodpasswordgoeshere".into(), 528 | }, 529 | ) 530 | .await 531 | .is_err()); 532 | 533 | assert!(authenticate( 534 | &s, 535 | "cargo test", 536 | "/path", 537 | AuthenticateRequest { 538 | email: "admin".into(), 539 | pw: "incorrectpassword".into(), 540 | }, 541 | ) 542 | .await 543 | .is_err()); 544 | } 545 | 546 | #[tokio::test(flavor = "multi_thread")] 547 | async fn test_bad_access() { 548 | init_log(); 549 | let s = storage().await; 550 | assert!(store_user(&s, "admin", "goodpasswordgoeshere", Role::Admin) 551 | .await 552 | .is_ok()); 553 | 554 | let refresh_cookie = authenticate( 555 | &s, 556 | "cargo test", 557 | "/path", 558 | AuthenticateRequest { 559 | email: "admin".into(), 560 | pw: "goodpasswordgoeshere".into(), 561 | }, 562 | ) 563 | .await 564 | .expect("authenticate failed"); 565 | 566 | let refresh_token = token_from_cookie(&refresh_cookie); 567 | 568 | // use different user agent 569 | assert!(access(&s, "not cargo test", &refresh_token).await.is_err()); 570 | // use correct user agent - no longer works 571 | assert!(access(&s, "cargo test", &refresh_token).await.is_err()); 572 | } 573 | 574 | #[tokio::test(flavor = "multi_thread")] 575 | async fn test_bad_authorize() { 576 | init_log(); 577 | let s = storage().await; 578 | assert!( 579 | store_user(&s, "notadmin", "goodpasswordgoeshere", Role::User) 580 | .await 581 | .is_ok() 582 | ); 583 | 584 | let refresh_cookie = authenticate( 585 | &s, 586 | "cargo test", 587 | "/path", 588 | AuthenticateRequest { 589 | email: "notadmin".into(), 590 | pw: "goodpasswordgoeshere".into(), 591 | }, 592 | ) 593 | .await 594 | .expect("authenticate failed"); 595 | 596 | let refresh_token = token_from_cookie(&refresh_cookie); 597 | let access_token = access(&s, "cargo test", &refresh_token) 598 | .await 599 | .expect("access failed"); 600 | 601 | assert!(authorize(Role::Admin, format!("Bearer {}", access_token)).is_err()); 602 | assert_eq!( 603 | authorize(Role::User, format!("Bearer {}", access_token)), 604 | Ok("notadmin".into()) 605 | ); 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /server/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aead" 5 | version = "0.3.2" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" 8 | dependencies = [ 9 | "generic-array", 10 | ] 11 | 12 | [[package]] 13 | name = "ahash" 14 | version = "0.4.7" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" 17 | 18 | [[package]] 19 | name = "ahash" 20 | version = "0.7.2" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "7f200cbb1e856866d9eade941cf3aa0c5d7dd36f74311c4273b494f4ef036957" 23 | dependencies = [ 24 | "getrandom 0.2.2", 25 | "once_cell", 26 | "version_check", 27 | ] 28 | 29 | [[package]] 30 | name = "aho-corasick" 31 | version = "0.7.15" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 34 | dependencies = [ 35 | "memchr", 36 | ] 37 | 38 | [[package]] 39 | name = "ansi_term" 40 | version = "0.11.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 43 | dependencies = [ 44 | "winapi", 45 | ] 46 | 47 | [[package]] 48 | name = "anyhow" 49 | version = "1.0.40" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" 52 | 53 | [[package]] 54 | name = "argon2" 55 | version = "0.1.4" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "7ee15f9f5e0f846cab0aa13d5dd1edbc49331dcae95a2e43b84ccc0b406dcda4" 58 | dependencies = [ 59 | "blake2", 60 | "password-hash", 61 | ] 62 | 63 | [[package]] 64 | name = "arrayref" 65 | version = "0.3.6" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 68 | 69 | [[package]] 70 | name = "arrayvec" 71 | version = "0.5.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 74 | 75 | [[package]] 76 | name = "async-trait" 77 | version = "0.1.48" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" 80 | dependencies = [ 81 | "proc-macro2", 82 | "quote", 83 | "syn", 84 | ] 85 | 86 | [[package]] 87 | name = "atoi" 88 | version = "0.4.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" 91 | dependencies = [ 92 | "num-traits", 93 | ] 94 | 95 | [[package]] 96 | name = "atty" 97 | version = "0.2.14" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 100 | dependencies = [ 101 | "hermit-abi", 102 | "libc", 103 | "winapi", 104 | ] 105 | 106 | [[package]] 107 | name = "autocfg" 108 | version = "0.1.7" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 111 | 112 | [[package]] 113 | name = "autocfg" 114 | version = "1.0.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 117 | 118 | [[package]] 119 | name = "base-x" 120 | version = "0.2.8" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" 123 | 124 | [[package]] 125 | name = "base64" 126 | version = "0.12.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 129 | 130 | [[package]] 131 | name = "base64" 132 | version = "0.13.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 135 | 136 | [[package]] 137 | name = "base64ct" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5" 141 | 142 | [[package]] 143 | name = "bigdecimal" 144 | version = "0.2.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "cc403c26e6b03005522e6e8053384c4e881dfe5b2bf041c0c2c49be33d64a539" 147 | dependencies = [ 148 | "num-bigint 0.3.2", 149 | "num-integer", 150 | "num-traits", 151 | ] 152 | 153 | [[package]] 154 | name = "bincode" 155 | version = "1.3.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" 158 | dependencies = [ 159 | "byteorder", 160 | "serde", 161 | ] 162 | 163 | [[package]] 164 | name = "bit-vec" 165 | version = "0.6.3" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 168 | 169 | [[package]] 170 | name = "bitflags" 171 | version = "1.2.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 174 | 175 | [[package]] 176 | name = "bitvec" 177 | version = "0.19.5" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" 180 | dependencies = [ 181 | "funty", 182 | "radium", 183 | "tap", 184 | "wyz", 185 | ] 186 | 187 | [[package]] 188 | name = "blake2" 189 | version = "0.9.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" 192 | dependencies = [ 193 | "crypto-mac 0.8.0", 194 | "digest", 195 | "opaque-debug", 196 | ] 197 | 198 | [[package]] 199 | name = "blake2b_simd" 200 | version = "0.5.11" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 203 | dependencies = [ 204 | "arrayref", 205 | "arrayvec", 206 | "constant_time_eq", 207 | ] 208 | 209 | [[package]] 210 | name = "block-buffer" 211 | version = "0.9.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 214 | dependencies = [ 215 | "generic-array", 216 | ] 217 | 218 | [[package]] 219 | name = "bstr" 220 | version = "0.2.15" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" 223 | dependencies = [ 224 | "memchr", 225 | ] 226 | 227 | [[package]] 228 | name = "buf_redux" 229 | version = "0.8.4" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 232 | dependencies = [ 233 | "memchr", 234 | "safemem", 235 | ] 236 | 237 | [[package]] 238 | name = "build_const" 239 | version = "0.2.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" 242 | 243 | [[package]] 244 | name = "bumpalo" 245 | version = "3.6.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" 248 | 249 | [[package]] 250 | name = "byteorder" 251 | version = "1.4.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 254 | 255 | [[package]] 256 | name = "bytes" 257 | version = "1.0.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 260 | 261 | [[package]] 262 | name = "cc" 263 | version = "1.0.67" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 266 | dependencies = [ 267 | "jobserver", 268 | ] 269 | 270 | [[package]] 271 | name = "cfg-if" 272 | version = "1.0.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 275 | 276 | [[package]] 277 | name = "chacha20" 278 | version = "0.6.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "ed8738f14471a99f0e316c327e68fc82a3611cc2895fcb604b89eedaf8f39d95" 281 | dependencies = [ 282 | "cipher", 283 | "zeroize", 284 | ] 285 | 286 | [[package]] 287 | name = "chacha20poly1305" 288 | version = "0.7.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "af1fc18e6d90c40164bf6c317476f2a98f04661e310e79830366b7e914c58a8e" 291 | dependencies = [ 292 | "aead", 293 | "chacha20", 294 | "cipher", 295 | "poly1305", 296 | "zeroize", 297 | ] 298 | 299 | [[package]] 300 | name = "chrono" 301 | version = "0.4.19" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 304 | dependencies = [ 305 | "libc", 306 | "num-integer", 307 | "num-traits", 308 | "time 0.1.43", 309 | "winapi", 310 | ] 311 | 312 | [[package]] 313 | name = "cipher" 314 | version = "0.2.5" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" 317 | dependencies = [ 318 | "generic-array", 319 | ] 320 | 321 | [[package]] 322 | name = "clap" 323 | version = "2.33.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 326 | dependencies = [ 327 | "ansi_term", 328 | "atty", 329 | "bitflags", 330 | "strsim", 331 | "textwrap", 332 | "unicode-width", 333 | "vec_map", 334 | ] 335 | 336 | [[package]] 337 | name = "const_fn" 338 | version = "0.4.6" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" 341 | 342 | [[package]] 343 | name = "constant_time_eq" 344 | version = "0.1.5" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 347 | 348 | [[package]] 349 | name = "cpuid-bool" 350 | version = "0.1.2" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" 353 | 354 | [[package]] 355 | name = "cpuid-bool" 356 | version = "0.2.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" 359 | 360 | [[package]] 361 | name = "crc" 362 | version = "1.8.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" 365 | dependencies = [ 366 | "build_const", 367 | ] 368 | 369 | [[package]] 370 | name = "crossbeam-channel" 371 | version = "0.5.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 374 | dependencies = [ 375 | "cfg-if", 376 | "crossbeam-utils", 377 | ] 378 | 379 | [[package]] 380 | name = "crossbeam-queue" 381 | version = "0.3.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" 384 | dependencies = [ 385 | "cfg-if", 386 | "crossbeam-utils", 387 | ] 388 | 389 | [[package]] 390 | name = "crossbeam-utils" 391 | version = "0.8.3" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" 394 | dependencies = [ 395 | "autocfg 1.0.1", 396 | "cfg-if", 397 | "lazy_static", 398 | ] 399 | 400 | [[package]] 401 | name = "crypto-mac" 402 | version = "0.8.0" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" 405 | dependencies = [ 406 | "generic-array", 407 | "subtle", 408 | ] 409 | 410 | [[package]] 411 | name = "crypto-mac" 412 | version = "0.10.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" 415 | dependencies = [ 416 | "generic-array", 417 | "subtle", 418 | ] 419 | 420 | [[package]] 421 | name = "digest" 422 | version = "0.9.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 425 | dependencies = [ 426 | "generic-array", 427 | ] 428 | 429 | [[package]] 430 | name = "dirs" 431 | version = "3.0.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" 434 | dependencies = [ 435 | "dirs-sys", 436 | ] 437 | 438 | [[package]] 439 | name = "dirs-sys" 440 | version = "0.3.5" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 443 | dependencies = [ 444 | "libc", 445 | "redox_users", 446 | "winapi", 447 | ] 448 | 449 | [[package]] 450 | name = "discard" 451 | version = "1.0.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" 454 | 455 | [[package]] 456 | name = "dotenv" 457 | version = "0.15.0" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 460 | 461 | [[package]] 462 | name = "either" 463 | version = "1.6.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 466 | dependencies = [ 467 | "serde", 468 | ] 469 | 470 | [[package]] 471 | name = "encoding_rs" 472 | version = "0.8.28" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" 475 | dependencies = [ 476 | "cfg-if", 477 | ] 478 | 479 | [[package]] 480 | name = "env_logger" 481 | version = "0.8.3" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" 484 | dependencies = [ 485 | "atty", 486 | "humantime", 487 | "log", 488 | "regex", 489 | "termcolor", 490 | ] 491 | 492 | [[package]] 493 | name = "fnv" 494 | version = "1.0.7" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 497 | 498 | [[package]] 499 | name = "form_urlencoded" 500 | version = "1.0.1" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 503 | dependencies = [ 504 | "matches", 505 | "percent-encoding", 506 | ] 507 | 508 | [[package]] 509 | name = "funty" 510 | version = "1.1.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" 513 | 514 | [[package]] 515 | name = "futures" 516 | version = "0.3.13" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" 519 | dependencies = [ 520 | "futures-channel", 521 | "futures-core", 522 | "futures-executor", 523 | "futures-io", 524 | "futures-sink", 525 | "futures-task", 526 | "futures-util", 527 | ] 528 | 529 | [[package]] 530 | name = "futures-channel" 531 | version = "0.3.13" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" 534 | dependencies = [ 535 | "futures-core", 536 | "futures-sink", 537 | ] 538 | 539 | [[package]] 540 | name = "futures-core" 541 | version = "0.3.13" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" 544 | 545 | [[package]] 546 | name = "futures-executor" 547 | version = "0.3.13" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" 550 | dependencies = [ 551 | "futures-core", 552 | "futures-task", 553 | "futures-util", 554 | ] 555 | 556 | [[package]] 557 | name = "futures-io" 558 | version = "0.3.13" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" 561 | 562 | [[package]] 563 | name = "futures-macro" 564 | version = "0.3.13" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" 567 | dependencies = [ 568 | "proc-macro-hack", 569 | "proc-macro2", 570 | "quote", 571 | "syn", 572 | ] 573 | 574 | [[package]] 575 | name = "futures-sink" 576 | version = "0.3.13" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" 579 | 580 | [[package]] 581 | name = "futures-task" 582 | version = "0.3.13" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" 585 | 586 | [[package]] 587 | name = "futures-util" 588 | version = "0.3.13" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" 591 | dependencies = [ 592 | "futures-channel", 593 | "futures-core", 594 | "futures-io", 595 | "futures-macro", 596 | "futures-sink", 597 | "futures-task", 598 | "memchr", 599 | "pin-project-lite", 600 | "pin-utils", 601 | "proc-macro-hack", 602 | "proc-macro-nested", 603 | "slab", 604 | ] 605 | 606 | [[package]] 607 | name = "generic-array" 608 | version = "0.14.4" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 611 | dependencies = [ 612 | "typenum", 613 | "version_check", 614 | ] 615 | 616 | [[package]] 617 | name = "getrandom" 618 | version = "0.1.16" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 621 | dependencies = [ 622 | "cfg-if", 623 | "libc", 624 | "wasi 0.9.0+wasi-snapshot-preview1", 625 | ] 626 | 627 | [[package]] 628 | name = "getrandom" 629 | version = "0.2.2" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" 632 | dependencies = [ 633 | "cfg-if", 634 | "libc", 635 | "wasi 0.10.2+wasi-snapshot-preview1", 636 | ] 637 | 638 | [[package]] 639 | name = "git2" 640 | version = "0.13.17" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "1d250f5f82326884bd39c2853577e70a121775db76818ffa452ed1e80de12986" 643 | dependencies = [ 644 | "bitflags", 645 | "libc", 646 | "libgit2-sys", 647 | "log", 648 | "url", 649 | ] 650 | 651 | [[package]] 652 | name = "h2" 653 | version = "0.3.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" 656 | dependencies = [ 657 | "bytes", 658 | "fnv", 659 | "futures-core", 660 | "futures-sink", 661 | "futures-util", 662 | "http", 663 | "indexmap", 664 | "slab", 665 | "tokio", 666 | "tokio-util", 667 | "tracing", 668 | ] 669 | 670 | [[package]] 671 | name = "hashbrown" 672 | version = "0.9.1" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 675 | dependencies = [ 676 | "ahash 0.4.7", 677 | ] 678 | 679 | [[package]] 680 | name = "hashlink" 681 | version = "0.6.0" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" 684 | dependencies = [ 685 | "hashbrown", 686 | ] 687 | 688 | [[package]] 689 | name = "headers" 690 | version = "0.3.4" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" 693 | dependencies = [ 694 | "base64 0.13.0", 695 | "bitflags", 696 | "bytes", 697 | "headers-core", 698 | "http", 699 | "mime", 700 | "sha-1", 701 | "time 0.1.43", 702 | ] 703 | 704 | [[package]] 705 | name = "headers-core" 706 | version = "0.2.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 709 | dependencies = [ 710 | "http", 711 | ] 712 | 713 | [[package]] 714 | name = "heck" 715 | version = "0.3.2" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 718 | dependencies = [ 719 | "unicode-segmentation", 720 | ] 721 | 722 | [[package]] 723 | name = "hermit-abi" 724 | version = "0.1.18" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 727 | dependencies = [ 728 | "libc", 729 | ] 730 | 731 | [[package]] 732 | name = "hex" 733 | version = "0.4.3" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 736 | 737 | [[package]] 738 | name = "hmac" 739 | version = "0.10.1" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" 742 | dependencies = [ 743 | "crypto-mac 0.10.0", 744 | "digest", 745 | ] 746 | 747 | [[package]] 748 | name = "http" 749 | version = "0.2.3" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" 752 | dependencies = [ 753 | "bytes", 754 | "fnv", 755 | "itoa", 756 | ] 757 | 758 | [[package]] 759 | name = "http-body" 760 | version = "0.4.1" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "5dfb77c123b4e2f72a2069aeae0b4b4949cc7e966df277813fc16347e7549737" 763 | dependencies = [ 764 | "bytes", 765 | "http", 766 | "pin-project-lite", 767 | ] 768 | 769 | [[package]] 770 | name = "httparse" 771 | version = "1.3.5" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" 774 | 775 | [[package]] 776 | name = "httpdate" 777 | version = "0.3.2" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" 780 | 781 | [[package]] 782 | name = "humantime" 783 | version = "2.1.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 786 | 787 | [[package]] 788 | name = "hyper" 789 | version = "0.14.5" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" 792 | dependencies = [ 793 | "bytes", 794 | "futures-channel", 795 | "futures-core", 796 | "futures-util", 797 | "h2", 798 | "http", 799 | "http-body", 800 | "httparse", 801 | "httpdate", 802 | "itoa", 803 | "pin-project", 804 | "socket2", 805 | "tokio", 806 | "tower-service", 807 | "tracing", 808 | "want", 809 | ] 810 | 811 | [[package]] 812 | name = "idna" 813 | version = "0.2.2" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" 816 | dependencies = [ 817 | "matches", 818 | "unicode-bidi", 819 | "unicode-normalization", 820 | ] 821 | 822 | [[package]] 823 | name = "indexmap" 824 | version = "1.6.2" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" 827 | dependencies = [ 828 | "autocfg 1.0.1", 829 | "hashbrown", 830 | ] 831 | 832 | [[package]] 833 | name = "input_buffer" 834 | version = "0.4.0" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" 837 | dependencies = [ 838 | "bytes", 839 | ] 840 | 841 | [[package]] 842 | name = "instant" 843 | version = "0.1.9" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 846 | dependencies = [ 847 | "cfg-if", 848 | ] 849 | 850 | [[package]] 851 | name = "ipnetwork" 852 | version = "0.17.0" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" 855 | 856 | [[package]] 857 | name = "itoa" 858 | version = "0.4.7" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 861 | 862 | [[package]] 863 | name = "jobserver" 864 | version = "0.1.21" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" 867 | dependencies = [ 868 | "libc", 869 | ] 870 | 871 | [[package]] 872 | name = "js-sys" 873 | version = "0.3.50" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" 876 | dependencies = [ 877 | "wasm-bindgen", 878 | ] 879 | 880 | [[package]] 881 | name = "jsonwebtoken" 882 | version = "7.2.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32" 885 | dependencies = [ 886 | "base64 0.12.3", 887 | "pem", 888 | "ring", 889 | "serde", 890 | "serde_json", 891 | "simple_asn1 0.4.1", 892 | ] 893 | 894 | [[package]] 895 | name = "lazy_static" 896 | version = "1.4.0" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 899 | dependencies = [ 900 | "spin", 901 | ] 902 | 903 | [[package]] 904 | name = "lexical-core" 905 | version = "0.7.5" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" 908 | dependencies = [ 909 | "arrayvec", 910 | "bitflags", 911 | "cfg-if", 912 | "ryu", 913 | "static_assertions", 914 | ] 915 | 916 | [[package]] 917 | name = "libc" 918 | version = "0.2.92" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" 921 | 922 | [[package]] 923 | name = "libgit2-sys" 924 | version = "0.12.18+1.1.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "3da6a42da88fc37ee1ecda212ffa254c25713532980005d5f7c0b0fbe7e6e885" 927 | dependencies = [ 928 | "cc", 929 | "libc", 930 | "libz-sys", 931 | "pkg-config", 932 | ] 933 | 934 | [[package]] 935 | name = "libm" 936 | version = "0.2.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" 939 | 940 | [[package]] 941 | name = "libsqlite3-sys" 942 | version = "0.22.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "2f6332d94daa84478d55a6aa9dbb3b305ed6500fb0cb9400cb9e1525d0e0e188" 945 | dependencies = [ 946 | "cc", 947 | "pkg-config", 948 | "vcpkg", 949 | ] 950 | 951 | [[package]] 952 | name = "libz-sys" 953 | version = "1.1.2" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" 956 | dependencies = [ 957 | "cc", 958 | "libc", 959 | "pkg-config", 960 | "vcpkg", 961 | ] 962 | 963 | [[package]] 964 | name = "lock_api" 965 | version = "0.4.3" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" 968 | dependencies = [ 969 | "scopeguard", 970 | ] 971 | 972 | [[package]] 973 | name = "log" 974 | version = "0.4.14" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 977 | dependencies = [ 978 | "cfg-if", 979 | ] 980 | 981 | [[package]] 982 | name = "maplit" 983 | version = "1.0.2" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 986 | 987 | [[package]] 988 | name = "matches" 989 | version = "0.1.8" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 992 | 993 | [[package]] 994 | name = "md-5" 995 | version = "0.9.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" 998 | dependencies = [ 999 | "block-buffer", 1000 | "digest", 1001 | "opaque-debug", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "memchr" 1006 | version = "2.3.4" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 1009 | 1010 | [[package]] 1011 | name = "mime" 1012 | version = "0.3.16" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 1015 | 1016 | [[package]] 1017 | name = "mime_guess" 1018 | version = "2.0.3" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 1021 | dependencies = [ 1022 | "mime", 1023 | "unicase", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "mio" 1028 | version = "0.7.11" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" 1031 | dependencies = [ 1032 | "libc", 1033 | "log", 1034 | "miow", 1035 | "ntapi", 1036 | "winapi", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "miow" 1041 | version = "0.3.7" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 1044 | dependencies = [ 1045 | "winapi", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "multipart" 1050 | version = "0.17.1" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" 1053 | dependencies = [ 1054 | "buf_redux", 1055 | "httparse", 1056 | "log", 1057 | "mime", 1058 | "mime_guess", 1059 | "quick-error", 1060 | "rand 0.7.3", 1061 | "safemem", 1062 | "tempfile", 1063 | "twoway", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "nom" 1068 | version = "6.1.2" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" 1071 | dependencies = [ 1072 | "bitvec", 1073 | "funty", 1074 | "lexical-core", 1075 | "memchr", 1076 | "version_check", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "ntapi" 1081 | version = "0.3.6" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 1084 | dependencies = [ 1085 | "winapi", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "num-bigint" 1090 | version = "0.2.6" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" 1093 | dependencies = [ 1094 | "autocfg 1.0.1", 1095 | "num-integer", 1096 | "num-traits", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "num-bigint" 1101 | version = "0.3.2" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" 1104 | dependencies = [ 1105 | "autocfg 1.0.1", 1106 | "num-integer", 1107 | "num-traits", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "num-bigint-dig" 1112 | version = "0.7.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" 1115 | dependencies = [ 1116 | "autocfg 0.1.7", 1117 | "byteorder", 1118 | "lazy_static", 1119 | "libm", 1120 | "num-integer", 1121 | "num-iter", 1122 | "num-traits", 1123 | "rand 0.8.3", 1124 | "smallvec", 1125 | "zeroize", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "num-integer" 1130 | version = "0.1.44" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 1133 | dependencies = [ 1134 | "autocfg 1.0.1", 1135 | "num-traits", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "num-iter" 1140 | version = "0.1.42" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 1143 | dependencies = [ 1144 | "autocfg 1.0.1", 1145 | "num-integer", 1146 | "num-traits", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "num-traits" 1151 | version = "0.2.14" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 1154 | dependencies = [ 1155 | "autocfg 1.0.1", 1156 | "libm", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "num_cpus" 1161 | version = "1.13.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 1164 | dependencies = [ 1165 | "hermit-abi", 1166 | "libc", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "once_cell" 1171 | version = "1.7.2" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 1174 | 1175 | [[package]] 1176 | name = "opaque-debug" 1177 | version = "0.3.0" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 1180 | 1181 | [[package]] 1182 | name = "parking_lot" 1183 | version = "0.11.1" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 1186 | dependencies = [ 1187 | "instant", 1188 | "lock_api", 1189 | "parking_lot_core", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "parking_lot_core" 1194 | version = "0.8.3" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" 1197 | dependencies = [ 1198 | "cfg-if", 1199 | "instant", 1200 | "libc", 1201 | "redox_syscall 0.2.5", 1202 | "smallvec", 1203 | "winapi", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "password-hash" 1208 | version = "0.1.2" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "85d8faea6c018131952a192ee55bd9394c51fc6f63294b668d97636e6f842d40" 1211 | dependencies = [ 1212 | "base64ct", 1213 | "rand_core 0.6.2", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "paw" 1218 | version = "1.0.0" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9" 1221 | dependencies = [ 1222 | "paw-attributes", 1223 | "paw-raw", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "paw-attributes" 1228 | version = "1.0.2" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "0f35583365be5d148e959284f42526841917b7bfa09e2d1a7ad5dde2cf0eaa39" 1231 | dependencies = [ 1232 | "proc-macro2", 1233 | "quote", 1234 | "syn", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "paw-raw" 1239 | version = "1.0.0" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "7f0b59668fe80c5afe998f0c0bf93322bf2cd66cafeeb80581f291716f3467f2" 1242 | 1243 | [[package]] 1244 | name = "pem" 1245 | version = "0.8.3" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" 1248 | dependencies = [ 1249 | "base64 0.13.0", 1250 | "once_cell", 1251 | "regex", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "percent-encoding" 1256 | version = "2.1.0" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 1259 | 1260 | [[package]] 1261 | name = "pin-project" 1262 | version = "1.0.6" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" 1265 | dependencies = [ 1266 | "pin-project-internal", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "pin-project-internal" 1271 | version = "1.0.6" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" 1274 | dependencies = [ 1275 | "proc-macro2", 1276 | "quote", 1277 | "syn", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "pin-project-lite" 1282 | version = "0.2.6" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 1285 | 1286 | [[package]] 1287 | name = "pin-utils" 1288 | version = "0.1.0" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1291 | 1292 | [[package]] 1293 | name = "pkg-config" 1294 | version = "0.3.19" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 1297 | 1298 | [[package]] 1299 | name = "poly1305" 1300 | version = "0.6.2" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "4b7456bc1ad2d4cf82b3a016be4c2ac48daf11bf990c1603ebd447fe6f30fca8" 1303 | dependencies = [ 1304 | "cpuid-bool 0.2.0", 1305 | "universal-hash", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "ppv-lite86" 1310 | version = "0.2.10" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 1313 | 1314 | [[package]] 1315 | name = "proc-macro-error" 1316 | version = "1.0.4" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1319 | dependencies = [ 1320 | "proc-macro-error-attr", 1321 | "proc-macro2", 1322 | "quote", 1323 | "syn", 1324 | "version_check", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "proc-macro-error-attr" 1329 | version = "1.0.4" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1332 | dependencies = [ 1333 | "proc-macro2", 1334 | "quote", 1335 | "version_check", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "proc-macro-hack" 1340 | version = "0.5.19" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 1343 | 1344 | [[package]] 1345 | name = "proc-macro-nested" 1346 | version = "0.1.7" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 1349 | 1350 | [[package]] 1351 | name = "proc-macro2" 1352 | version = "1.0.26" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" 1355 | dependencies = [ 1356 | "unicode-xid", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "quick-error" 1361 | version = "1.2.3" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1364 | 1365 | [[package]] 1366 | name = "quote" 1367 | version = "1.0.9" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 1370 | dependencies = [ 1371 | "proc-macro2", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "radium" 1376 | version = "0.5.3" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" 1379 | 1380 | [[package]] 1381 | name = "rand" 1382 | version = "0.7.3" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 1385 | dependencies = [ 1386 | "getrandom 0.1.16", 1387 | "libc", 1388 | "rand_chacha 0.2.2", 1389 | "rand_core 0.5.1", 1390 | "rand_hc 0.2.0", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "rand" 1395 | version = "0.8.3" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" 1398 | dependencies = [ 1399 | "libc", 1400 | "rand_chacha 0.3.0", 1401 | "rand_core 0.6.2", 1402 | "rand_hc 0.3.0", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "rand_chacha" 1407 | version = "0.2.2" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 1410 | dependencies = [ 1411 | "ppv-lite86", 1412 | "rand_core 0.5.1", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "rand_chacha" 1417 | version = "0.3.0" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 1420 | dependencies = [ 1421 | "ppv-lite86", 1422 | "rand_core 0.6.2", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "rand_core" 1427 | version = "0.5.1" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 1430 | dependencies = [ 1431 | "getrandom 0.1.16", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "rand_core" 1436 | version = "0.6.2" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" 1439 | dependencies = [ 1440 | "getrandom 0.2.2", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "rand_hc" 1445 | version = "0.2.0" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1448 | dependencies = [ 1449 | "rand_core 0.5.1", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "rand_hc" 1454 | version = "0.3.0" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 1457 | dependencies = [ 1458 | "rand_core 0.6.2", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "redox_syscall" 1463 | version = "0.1.57" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 1466 | 1467 | [[package]] 1468 | name = "redox_syscall" 1469 | version = "0.2.5" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" 1472 | dependencies = [ 1473 | "bitflags", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "redox_users" 1478 | version = "0.3.5" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 1481 | dependencies = [ 1482 | "getrandom 0.1.16", 1483 | "redox_syscall 0.1.57", 1484 | "rust-argon2", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "regex" 1489 | version = "1.4.5" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" 1492 | dependencies = [ 1493 | "aho-corasick", 1494 | "memchr", 1495 | "regex-syntax", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "regex-syntax" 1500 | version = "0.6.23" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" 1503 | 1504 | [[package]] 1505 | name = "remove_dir_all" 1506 | version = "0.5.3" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1509 | dependencies = [ 1510 | "winapi", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "ring" 1515 | version = "0.16.20" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1518 | dependencies = [ 1519 | "cc", 1520 | "libc", 1521 | "once_cell", 1522 | "spin", 1523 | "untrusted", 1524 | "web-sys", 1525 | "winapi", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "rsa" 1530 | version = "0.4.0" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" 1533 | dependencies = [ 1534 | "byteorder", 1535 | "digest", 1536 | "lazy_static", 1537 | "num-bigint-dig", 1538 | "num-integer", 1539 | "num-iter", 1540 | "num-traits", 1541 | "pem", 1542 | "rand 0.8.3", 1543 | "simple_asn1 0.5.1", 1544 | "subtle", 1545 | "zeroize", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "rust-argon2" 1550 | version = "0.8.3" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 1553 | dependencies = [ 1554 | "base64 0.13.0", 1555 | "blake2b_simd", 1556 | "constant_time_eq", 1557 | "crossbeam-utils", 1558 | ] 1559 | 1560 | [[package]] 1561 | name = "rust-spa-auth" 1562 | version = "0.1.0" 1563 | dependencies = [ 1564 | "anyhow", 1565 | "argon2", 1566 | "async-trait", 1567 | "base64 0.13.0", 1568 | "bincode", 1569 | "chacha20poly1305", 1570 | "chrono", 1571 | "env_logger", 1572 | "jsonwebtoken", 1573 | "log", 1574 | "once_cell", 1575 | "parking_lot", 1576 | "paw", 1577 | "rand 0.8.3", 1578 | "serde", 1579 | "sqlx", 1580 | "structopt", 1581 | "thiserror", 1582 | "tokio", 1583 | "warp", 1584 | ] 1585 | 1586 | [[package]] 1587 | name = "rust_decimal" 1588 | version = "1.10.3" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "dc7f5b8840fb1f83869a3e1dfd06d93db79ea05311ac5b42b8337d3371caa4f1" 1591 | dependencies = [ 1592 | "arrayvec", 1593 | "num-traits", 1594 | "serde", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "rustc_version" 1599 | version = "0.2.3" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 1602 | dependencies = [ 1603 | "semver", 1604 | ] 1605 | 1606 | [[package]] 1607 | name = "rustls" 1608 | version = "0.19.0" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" 1611 | dependencies = [ 1612 | "base64 0.13.0", 1613 | "log", 1614 | "ring", 1615 | "sct", 1616 | "webpki", 1617 | ] 1618 | 1619 | [[package]] 1620 | name = "ryu" 1621 | version = "1.0.5" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1624 | 1625 | [[package]] 1626 | name = "safemem" 1627 | version = "0.3.3" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1630 | 1631 | [[package]] 1632 | name = "scoped-tls" 1633 | version = "1.0.0" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 1636 | 1637 | [[package]] 1638 | name = "scopeguard" 1639 | version = "1.1.0" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1642 | 1643 | [[package]] 1644 | name = "sct" 1645 | version = "0.6.0" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" 1648 | dependencies = [ 1649 | "ring", 1650 | "untrusted", 1651 | ] 1652 | 1653 | [[package]] 1654 | name = "semver" 1655 | version = "0.9.0" 1656 | source = "registry+https://github.com/rust-lang/crates.io-index" 1657 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 1658 | dependencies = [ 1659 | "semver-parser", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "semver-parser" 1664 | version = "0.7.0" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 1667 | 1668 | [[package]] 1669 | name = "serde" 1670 | version = "1.0.125" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" 1673 | dependencies = [ 1674 | "serde_derive", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "serde_derive" 1679 | version = "1.0.125" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" 1682 | dependencies = [ 1683 | "proc-macro2", 1684 | "quote", 1685 | "syn", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "serde_json" 1690 | version = "1.0.64" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 1693 | dependencies = [ 1694 | "indexmap", 1695 | "itoa", 1696 | "ryu", 1697 | "serde", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "serde_urlencoded" 1702 | version = "0.7.0" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1705 | dependencies = [ 1706 | "form_urlencoded", 1707 | "itoa", 1708 | "ryu", 1709 | "serde", 1710 | ] 1711 | 1712 | [[package]] 1713 | name = "sha-1" 1714 | version = "0.9.4" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" 1717 | dependencies = [ 1718 | "block-buffer", 1719 | "cfg-if", 1720 | "cpuid-bool 0.1.2", 1721 | "digest", 1722 | "opaque-debug", 1723 | ] 1724 | 1725 | [[package]] 1726 | name = "sha1" 1727 | version = "0.6.0" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" 1730 | 1731 | [[package]] 1732 | name = "sha2" 1733 | version = "0.9.3" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" 1736 | dependencies = [ 1737 | "block-buffer", 1738 | "cfg-if", 1739 | "cpuid-bool 0.1.2", 1740 | "digest", 1741 | "opaque-debug", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "simple_asn1" 1746 | version = "0.4.1" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" 1749 | dependencies = [ 1750 | "chrono", 1751 | "num-bigint 0.2.6", 1752 | "num-traits", 1753 | ] 1754 | 1755 | [[package]] 1756 | name = "simple_asn1" 1757 | version = "0.5.1" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "db8d597fce66eb0f19dd129b9956e4054cba21aeaf97d4116595027b670fac50" 1760 | dependencies = [ 1761 | "chrono", 1762 | "num-bigint 0.3.2", 1763 | "num-traits", 1764 | "thiserror", 1765 | ] 1766 | 1767 | [[package]] 1768 | name = "slab" 1769 | version = "0.4.2" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1772 | 1773 | [[package]] 1774 | name = "smallvec" 1775 | version = "1.6.1" 1776 | source = "registry+https://github.com/rust-lang/crates.io-index" 1777 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 1778 | 1779 | [[package]] 1780 | name = "socket2" 1781 | version = "0.4.0" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" 1784 | dependencies = [ 1785 | "libc", 1786 | "winapi", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "spin" 1791 | version = "0.5.2" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1794 | 1795 | [[package]] 1796 | name = "sqlformat" 1797 | version = "0.1.6" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" 1800 | dependencies = [ 1801 | "lazy_static", 1802 | "maplit", 1803 | "nom", 1804 | "regex", 1805 | "unicode_categories", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "sqlx" 1810 | version = "0.5.2" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "d582b9bc04ec6c03084196efc42c2226b018e9941f03ee62bd88921d500917c0" 1813 | dependencies = [ 1814 | "sqlx-core", 1815 | "sqlx-macros", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "sqlx-core" 1820 | version = "0.5.2" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "de52d1d473cebb2abb79c886ef6a8023e965e34c0676a99cfeac2cc7f0fde4c1" 1823 | dependencies = [ 1824 | "ahash 0.7.2", 1825 | "atoi", 1826 | "base64 0.13.0", 1827 | "bigdecimal", 1828 | "bit-vec", 1829 | "bitflags", 1830 | "bstr", 1831 | "byteorder", 1832 | "bytes", 1833 | "chrono", 1834 | "crc", 1835 | "crossbeam-channel", 1836 | "crossbeam-queue", 1837 | "crossbeam-utils", 1838 | "digest", 1839 | "dirs", 1840 | "either", 1841 | "encoding_rs", 1842 | "futures-channel", 1843 | "futures-core", 1844 | "futures-util", 1845 | "generic-array", 1846 | "git2", 1847 | "hashlink", 1848 | "hex", 1849 | "hmac", 1850 | "ipnetwork", 1851 | "itoa", 1852 | "libc", 1853 | "libsqlite3-sys", 1854 | "log", 1855 | "md-5", 1856 | "memchr", 1857 | "num-bigint 0.3.2", 1858 | "once_cell", 1859 | "parking_lot", 1860 | "percent-encoding", 1861 | "rand 0.8.3", 1862 | "regex", 1863 | "rsa", 1864 | "rust_decimal", 1865 | "rustls", 1866 | "serde", 1867 | "serde_json", 1868 | "sha-1", 1869 | "sha2", 1870 | "smallvec", 1871 | "sqlformat", 1872 | "sqlx-rt", 1873 | "stringprep", 1874 | "thiserror", 1875 | "time 0.2.26", 1876 | "tokio-stream", 1877 | "url", 1878 | "uuid", 1879 | "webpki", 1880 | "webpki-roots", 1881 | "whoami", 1882 | ] 1883 | 1884 | [[package]] 1885 | name = "sqlx-macros" 1886 | version = "0.5.2" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "1a40f0be97e704d3fbf059e7e3333c3735639146a72d586c5534c70e79da88a4" 1889 | dependencies = [ 1890 | "dotenv", 1891 | "either", 1892 | "futures", 1893 | "heck", 1894 | "hex", 1895 | "once_cell", 1896 | "proc-macro2", 1897 | "quote", 1898 | "serde", 1899 | "serde_json", 1900 | "sha2", 1901 | "sqlx-core", 1902 | "sqlx-rt", 1903 | "syn", 1904 | "url", 1905 | ] 1906 | 1907 | [[package]] 1908 | name = "sqlx-rt" 1909 | version = "0.5.2" 1910 | source = "registry+https://github.com/rust-lang/crates.io-index" 1911 | checksum = "b6ae97ab05063ed515cdc23d90253213aa24dda0a288c5ec079af3d10f9771bc" 1912 | dependencies = [ 1913 | "once_cell", 1914 | "tokio", 1915 | "tokio-rustls", 1916 | ] 1917 | 1918 | [[package]] 1919 | name = "standback" 1920 | version = "0.2.17" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" 1923 | dependencies = [ 1924 | "version_check", 1925 | ] 1926 | 1927 | [[package]] 1928 | name = "static_assertions" 1929 | version = "1.1.0" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1932 | 1933 | [[package]] 1934 | name = "stdweb" 1935 | version = "0.4.20" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" 1938 | dependencies = [ 1939 | "discard", 1940 | "rustc_version", 1941 | "stdweb-derive", 1942 | "stdweb-internal-macros", 1943 | "stdweb-internal-runtime", 1944 | "wasm-bindgen", 1945 | ] 1946 | 1947 | [[package]] 1948 | name = "stdweb-derive" 1949 | version = "0.5.3" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" 1952 | dependencies = [ 1953 | "proc-macro2", 1954 | "quote", 1955 | "serde", 1956 | "serde_derive", 1957 | "syn", 1958 | ] 1959 | 1960 | [[package]] 1961 | name = "stdweb-internal-macros" 1962 | version = "0.2.9" 1963 | source = "registry+https://github.com/rust-lang/crates.io-index" 1964 | checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" 1965 | dependencies = [ 1966 | "base-x", 1967 | "proc-macro2", 1968 | "quote", 1969 | "serde", 1970 | "serde_derive", 1971 | "serde_json", 1972 | "sha1", 1973 | "syn", 1974 | ] 1975 | 1976 | [[package]] 1977 | name = "stdweb-internal-runtime" 1978 | version = "0.1.5" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" 1981 | 1982 | [[package]] 1983 | name = "stringprep" 1984 | version = "0.1.2" 1985 | source = "registry+https://github.com/rust-lang/crates.io-index" 1986 | checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" 1987 | dependencies = [ 1988 | "unicode-bidi", 1989 | "unicode-normalization", 1990 | ] 1991 | 1992 | [[package]] 1993 | name = "strsim" 1994 | version = "0.8.0" 1995 | source = "registry+https://github.com/rust-lang/crates.io-index" 1996 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1997 | 1998 | [[package]] 1999 | name = "structopt" 2000 | version = "0.3.21" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" 2003 | dependencies = [ 2004 | "clap", 2005 | "lazy_static", 2006 | "paw", 2007 | "structopt-derive", 2008 | ] 2009 | 2010 | [[package]] 2011 | name = "structopt-derive" 2012 | version = "0.4.14" 2013 | source = "registry+https://github.com/rust-lang/crates.io-index" 2014 | checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" 2015 | dependencies = [ 2016 | "heck", 2017 | "proc-macro-error", 2018 | "proc-macro2", 2019 | "quote", 2020 | "syn", 2021 | ] 2022 | 2023 | [[package]] 2024 | name = "subtle" 2025 | version = "2.4.0" 2026 | source = "registry+https://github.com/rust-lang/crates.io-index" 2027 | checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" 2028 | 2029 | [[package]] 2030 | name = "syn" 2031 | version = "1.0.68" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" 2034 | dependencies = [ 2035 | "proc-macro2", 2036 | "quote", 2037 | "unicode-xid", 2038 | ] 2039 | 2040 | [[package]] 2041 | name = "synstructure" 2042 | version = "0.12.4" 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" 2044 | checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" 2045 | dependencies = [ 2046 | "proc-macro2", 2047 | "quote", 2048 | "syn", 2049 | "unicode-xid", 2050 | ] 2051 | 2052 | [[package]] 2053 | name = "tap" 2054 | version = "1.0.1" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 2057 | 2058 | [[package]] 2059 | name = "tempfile" 2060 | version = "3.2.0" 2061 | source = "registry+https://github.com/rust-lang/crates.io-index" 2062 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 2063 | dependencies = [ 2064 | "cfg-if", 2065 | "libc", 2066 | "rand 0.8.3", 2067 | "redox_syscall 0.2.5", 2068 | "remove_dir_all", 2069 | "winapi", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "termcolor" 2074 | version = "1.1.2" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 2077 | dependencies = [ 2078 | "winapi-util", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "textwrap" 2083 | version = "0.11.0" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 2086 | dependencies = [ 2087 | "unicode-width", 2088 | ] 2089 | 2090 | [[package]] 2091 | name = "thiserror" 2092 | version = "1.0.24" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" 2095 | dependencies = [ 2096 | "thiserror-impl", 2097 | ] 2098 | 2099 | [[package]] 2100 | name = "thiserror-impl" 2101 | version = "1.0.24" 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" 2103 | checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" 2104 | dependencies = [ 2105 | "proc-macro2", 2106 | "quote", 2107 | "syn", 2108 | ] 2109 | 2110 | [[package]] 2111 | name = "time" 2112 | version = "0.1.43" 2113 | source = "registry+https://github.com/rust-lang/crates.io-index" 2114 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 2115 | dependencies = [ 2116 | "libc", 2117 | "winapi", 2118 | ] 2119 | 2120 | [[package]] 2121 | name = "time" 2122 | version = "0.2.26" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" 2125 | dependencies = [ 2126 | "const_fn", 2127 | "libc", 2128 | "standback", 2129 | "stdweb", 2130 | "time-macros", 2131 | "version_check", 2132 | "winapi", 2133 | ] 2134 | 2135 | [[package]] 2136 | name = "time-macros" 2137 | version = "0.1.1" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" 2140 | dependencies = [ 2141 | "proc-macro-hack", 2142 | "time-macros-impl", 2143 | ] 2144 | 2145 | [[package]] 2146 | name = "time-macros-impl" 2147 | version = "0.1.1" 2148 | source = "registry+https://github.com/rust-lang/crates.io-index" 2149 | checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" 2150 | dependencies = [ 2151 | "proc-macro-hack", 2152 | "proc-macro2", 2153 | "quote", 2154 | "standback", 2155 | "syn", 2156 | ] 2157 | 2158 | [[package]] 2159 | name = "tinyvec" 2160 | version = "1.2.0" 2161 | source = "registry+https://github.com/rust-lang/crates.io-index" 2162 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 2163 | dependencies = [ 2164 | "tinyvec_macros", 2165 | ] 2166 | 2167 | [[package]] 2168 | name = "tinyvec_macros" 2169 | version = "0.1.0" 2170 | source = "registry+https://github.com/rust-lang/crates.io-index" 2171 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 2172 | 2173 | [[package]] 2174 | name = "tokio" 2175 | version = "1.4.0" 2176 | source = "registry+https://github.com/rust-lang/crates.io-index" 2177 | checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" 2178 | dependencies = [ 2179 | "autocfg 1.0.1", 2180 | "bytes", 2181 | "libc", 2182 | "memchr", 2183 | "mio", 2184 | "num_cpus", 2185 | "pin-project-lite", 2186 | "tokio-macros", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "tokio-macros" 2191 | version = "1.1.0" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" 2194 | dependencies = [ 2195 | "proc-macro2", 2196 | "quote", 2197 | "syn", 2198 | ] 2199 | 2200 | [[package]] 2201 | name = "tokio-rustls" 2202 | version = "0.22.0" 2203 | source = "registry+https://github.com/rust-lang/crates.io-index" 2204 | checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" 2205 | dependencies = [ 2206 | "rustls", 2207 | "tokio", 2208 | "webpki", 2209 | ] 2210 | 2211 | [[package]] 2212 | name = "tokio-stream" 2213 | version = "0.1.5" 2214 | source = "registry+https://github.com/rust-lang/crates.io-index" 2215 | checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" 2216 | dependencies = [ 2217 | "futures-core", 2218 | "pin-project-lite", 2219 | "tokio", 2220 | ] 2221 | 2222 | [[package]] 2223 | name = "tokio-tungstenite" 2224 | version = "0.13.0" 2225 | source = "registry+https://github.com/rust-lang/crates.io-index" 2226 | checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" 2227 | dependencies = [ 2228 | "futures-util", 2229 | "log", 2230 | "pin-project", 2231 | "tokio", 2232 | "tungstenite", 2233 | ] 2234 | 2235 | [[package]] 2236 | name = "tokio-util" 2237 | version = "0.6.5" 2238 | source = "registry+https://github.com/rust-lang/crates.io-index" 2239 | checksum = "5143d049e85af7fbc36f5454d990e62c2df705b3589f123b71f441b6b59f443f" 2240 | dependencies = [ 2241 | "bytes", 2242 | "futures-core", 2243 | "futures-sink", 2244 | "log", 2245 | "pin-project-lite", 2246 | "tokio", 2247 | ] 2248 | 2249 | [[package]] 2250 | name = "tower-service" 2251 | version = "0.3.1" 2252 | source = "registry+https://github.com/rust-lang/crates.io-index" 2253 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 2254 | 2255 | [[package]] 2256 | name = "tracing" 2257 | version = "0.1.25" 2258 | source = "registry+https://github.com/rust-lang/crates.io-index" 2259 | checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" 2260 | dependencies = [ 2261 | "cfg-if", 2262 | "log", 2263 | "pin-project-lite", 2264 | "tracing-core", 2265 | ] 2266 | 2267 | [[package]] 2268 | name = "tracing-core" 2269 | version = "0.1.17" 2270 | source = "registry+https://github.com/rust-lang/crates.io-index" 2271 | checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" 2272 | dependencies = [ 2273 | "lazy_static", 2274 | ] 2275 | 2276 | [[package]] 2277 | name = "try-lock" 2278 | version = "0.2.3" 2279 | source = "registry+https://github.com/rust-lang/crates.io-index" 2280 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 2281 | 2282 | [[package]] 2283 | name = "tungstenite" 2284 | version = "0.12.0" 2285 | source = "registry+https://github.com/rust-lang/crates.io-index" 2286 | checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" 2287 | dependencies = [ 2288 | "base64 0.13.0", 2289 | "byteorder", 2290 | "bytes", 2291 | "http", 2292 | "httparse", 2293 | "input_buffer", 2294 | "log", 2295 | "rand 0.8.3", 2296 | "sha-1", 2297 | "url", 2298 | "utf-8", 2299 | ] 2300 | 2301 | [[package]] 2302 | name = "twoway" 2303 | version = "0.1.8" 2304 | source = "registry+https://github.com/rust-lang/crates.io-index" 2305 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 2306 | dependencies = [ 2307 | "memchr", 2308 | ] 2309 | 2310 | [[package]] 2311 | name = "typenum" 2312 | version = "1.13.0" 2313 | source = "registry+https://github.com/rust-lang/crates.io-index" 2314 | checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" 2315 | 2316 | [[package]] 2317 | name = "unicase" 2318 | version = "2.6.0" 2319 | source = "registry+https://github.com/rust-lang/crates.io-index" 2320 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 2321 | dependencies = [ 2322 | "version_check", 2323 | ] 2324 | 2325 | [[package]] 2326 | name = "unicode-bidi" 2327 | version = "0.3.4" 2328 | source = "registry+https://github.com/rust-lang/crates.io-index" 2329 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 2330 | dependencies = [ 2331 | "matches", 2332 | ] 2333 | 2334 | [[package]] 2335 | name = "unicode-normalization" 2336 | version = "0.1.17" 2337 | source = "registry+https://github.com/rust-lang/crates.io-index" 2338 | checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" 2339 | dependencies = [ 2340 | "tinyvec", 2341 | ] 2342 | 2343 | [[package]] 2344 | name = "unicode-segmentation" 2345 | version = "1.7.1" 2346 | source = "registry+https://github.com/rust-lang/crates.io-index" 2347 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 2348 | 2349 | [[package]] 2350 | name = "unicode-width" 2351 | version = "0.1.8" 2352 | source = "registry+https://github.com/rust-lang/crates.io-index" 2353 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 2354 | 2355 | [[package]] 2356 | name = "unicode-xid" 2357 | version = "0.2.1" 2358 | source = "registry+https://github.com/rust-lang/crates.io-index" 2359 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 2360 | 2361 | [[package]] 2362 | name = "unicode_categories" 2363 | version = "0.1.1" 2364 | source = "registry+https://github.com/rust-lang/crates.io-index" 2365 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 2366 | 2367 | [[package]] 2368 | name = "universal-hash" 2369 | version = "0.4.0" 2370 | source = "registry+https://github.com/rust-lang/crates.io-index" 2371 | checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" 2372 | dependencies = [ 2373 | "generic-array", 2374 | "subtle", 2375 | ] 2376 | 2377 | [[package]] 2378 | name = "untrusted" 2379 | version = "0.7.1" 2380 | source = "registry+https://github.com/rust-lang/crates.io-index" 2381 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 2382 | 2383 | [[package]] 2384 | name = "url" 2385 | version = "2.2.1" 2386 | source = "registry+https://github.com/rust-lang/crates.io-index" 2387 | checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" 2388 | dependencies = [ 2389 | "form_urlencoded", 2390 | "idna", 2391 | "matches", 2392 | "percent-encoding", 2393 | ] 2394 | 2395 | [[package]] 2396 | name = "utf-8" 2397 | version = "0.7.5" 2398 | source = "registry+https://github.com/rust-lang/crates.io-index" 2399 | checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" 2400 | 2401 | [[package]] 2402 | name = "uuid" 2403 | version = "0.8.2" 2404 | source = "registry+https://github.com/rust-lang/crates.io-index" 2405 | checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" 2406 | 2407 | [[package]] 2408 | name = "vcpkg" 2409 | version = "0.2.11" 2410 | source = "registry+https://github.com/rust-lang/crates.io-index" 2411 | checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" 2412 | 2413 | [[package]] 2414 | name = "vec_map" 2415 | version = "0.8.2" 2416 | source = "registry+https://github.com/rust-lang/crates.io-index" 2417 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 2418 | 2419 | [[package]] 2420 | name = "version_check" 2421 | version = "0.9.3" 2422 | source = "registry+https://github.com/rust-lang/crates.io-index" 2423 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 2424 | 2425 | [[package]] 2426 | name = "want" 2427 | version = "0.3.0" 2428 | source = "registry+https://github.com/rust-lang/crates.io-index" 2429 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2430 | dependencies = [ 2431 | "log", 2432 | "try-lock", 2433 | ] 2434 | 2435 | [[package]] 2436 | name = "warp" 2437 | version = "0.3.1" 2438 | source = "git+https://github.com/jtroo/warp#45c8ae942b205cf64f7adee2fb9f2c8cde6ecf95" 2439 | dependencies = [ 2440 | "bytes", 2441 | "futures", 2442 | "headers", 2443 | "http", 2444 | "hyper", 2445 | "log", 2446 | "mime", 2447 | "mime_guess", 2448 | "multipart", 2449 | "percent-encoding", 2450 | "pin-project", 2451 | "scoped-tls", 2452 | "serde", 2453 | "serde_json", 2454 | "serde_urlencoded", 2455 | "tokio", 2456 | "tokio-rustls", 2457 | "tokio-stream", 2458 | "tokio-tungstenite", 2459 | "tokio-util", 2460 | "tower-service", 2461 | "tracing", 2462 | ] 2463 | 2464 | [[package]] 2465 | name = "wasi" 2466 | version = "0.9.0+wasi-snapshot-preview1" 2467 | source = "registry+https://github.com/rust-lang/crates.io-index" 2468 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 2469 | 2470 | [[package]] 2471 | name = "wasi" 2472 | version = "0.10.2+wasi-snapshot-preview1" 2473 | source = "registry+https://github.com/rust-lang/crates.io-index" 2474 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 2475 | 2476 | [[package]] 2477 | name = "wasm-bindgen" 2478 | version = "0.2.73" 2479 | source = "registry+https://github.com/rust-lang/crates.io-index" 2480 | checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" 2481 | dependencies = [ 2482 | "cfg-if", 2483 | "wasm-bindgen-macro", 2484 | ] 2485 | 2486 | [[package]] 2487 | name = "wasm-bindgen-backend" 2488 | version = "0.2.73" 2489 | source = "registry+https://github.com/rust-lang/crates.io-index" 2490 | checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" 2491 | dependencies = [ 2492 | "bumpalo", 2493 | "lazy_static", 2494 | "log", 2495 | "proc-macro2", 2496 | "quote", 2497 | "syn", 2498 | "wasm-bindgen-shared", 2499 | ] 2500 | 2501 | [[package]] 2502 | name = "wasm-bindgen-macro" 2503 | version = "0.2.73" 2504 | source = "registry+https://github.com/rust-lang/crates.io-index" 2505 | checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" 2506 | dependencies = [ 2507 | "quote", 2508 | "wasm-bindgen-macro-support", 2509 | ] 2510 | 2511 | [[package]] 2512 | name = "wasm-bindgen-macro-support" 2513 | version = "0.2.73" 2514 | source = "registry+https://github.com/rust-lang/crates.io-index" 2515 | checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" 2516 | dependencies = [ 2517 | "proc-macro2", 2518 | "quote", 2519 | "syn", 2520 | "wasm-bindgen-backend", 2521 | "wasm-bindgen-shared", 2522 | ] 2523 | 2524 | [[package]] 2525 | name = "wasm-bindgen-shared" 2526 | version = "0.2.73" 2527 | source = "registry+https://github.com/rust-lang/crates.io-index" 2528 | checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" 2529 | 2530 | [[package]] 2531 | name = "web-sys" 2532 | version = "0.3.50" 2533 | source = "registry+https://github.com/rust-lang/crates.io-index" 2534 | checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" 2535 | dependencies = [ 2536 | "js-sys", 2537 | "wasm-bindgen", 2538 | ] 2539 | 2540 | [[package]] 2541 | name = "webpki" 2542 | version = "0.21.4" 2543 | source = "registry+https://github.com/rust-lang/crates.io-index" 2544 | checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" 2545 | dependencies = [ 2546 | "ring", 2547 | "untrusted", 2548 | ] 2549 | 2550 | [[package]] 2551 | name = "webpki-roots" 2552 | version = "0.21.1" 2553 | source = "registry+https://github.com/rust-lang/crates.io-index" 2554 | checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" 2555 | dependencies = [ 2556 | "webpki", 2557 | ] 2558 | 2559 | [[package]] 2560 | name = "whoami" 2561 | version = "1.1.2" 2562 | source = "registry+https://github.com/rust-lang/crates.io-index" 2563 | checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" 2564 | dependencies = [ 2565 | "wasm-bindgen", 2566 | "web-sys", 2567 | ] 2568 | 2569 | [[package]] 2570 | name = "winapi" 2571 | version = "0.3.9" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2574 | dependencies = [ 2575 | "winapi-i686-pc-windows-gnu", 2576 | "winapi-x86_64-pc-windows-gnu", 2577 | ] 2578 | 2579 | [[package]] 2580 | name = "winapi-i686-pc-windows-gnu" 2581 | version = "0.4.0" 2582 | source = "registry+https://github.com/rust-lang/crates.io-index" 2583 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2584 | 2585 | [[package]] 2586 | name = "winapi-util" 2587 | version = "0.1.5" 2588 | source = "registry+https://github.com/rust-lang/crates.io-index" 2589 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2590 | dependencies = [ 2591 | "winapi", 2592 | ] 2593 | 2594 | [[package]] 2595 | name = "winapi-x86_64-pc-windows-gnu" 2596 | version = "0.4.0" 2597 | source = "registry+https://github.com/rust-lang/crates.io-index" 2598 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2599 | 2600 | [[package]] 2601 | name = "wyz" 2602 | version = "0.2.0" 2603 | source = "registry+https://github.com/rust-lang/crates.io-index" 2604 | checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" 2605 | 2606 | [[package]] 2607 | name = "zeroize" 2608 | version = "1.2.0" 2609 | source = "registry+https://github.com/rust-lang/crates.io-index" 2610 | checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" 2611 | dependencies = [ 2612 | "zeroize_derive", 2613 | ] 2614 | 2615 | [[package]] 2616 | name = "zeroize_derive" 2617 | version = "1.0.1" 2618 | source = "registry+https://github.com/rust-lang/crates.io-index" 2619 | checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" 2620 | dependencies = [ 2621 | "proc-macro2", 2622 | "quote", 2623 | "syn", 2624 | "synstructure", 2625 | ] 2626 | --------------------------------------------------------------------------------