1
0
mirror of https://github.com/tonarino/innernet.git synced 2025-01-04 03:48:20 +02:00

server: switch from using warp directly to hyper (#67)

Closes #53
This commit is contained in:
Jake McGinty 2021-05-06 12:32:54 +09:00 committed by GitHub
parent d8de58c8a8
commit c01c2be4bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 561 additions and 993 deletions

324
Cargo.lock generated
View File

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.4.7"
@ -80,15 +78,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.4.3"
@ -200,81 +189,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "cpufeatures"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd5a7748210e7ec1a9696610b1015e6e31fbf58f77a160801f124bd1c36592a"
[[package]]
name = "crossbeam"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80"
dependencies = [
"cfg-if",
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94"
dependencies = [
"cfg-if",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
dependencies = [
"autocfg",
"cfg-if",
"lazy_static",
]
[[package]]
name = "curve25519-dalek"
version = "3.0.2"
@ -287,16 +201,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "dashmap"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
dependencies = [
"cfg-if",
"num_cpus",
]
[[package]]
name = "dialoguer"
version = "0.8.0"
@ -365,20 +269,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.14"
@ -386,7 +276,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -395,18 +284,6 @@ version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815"
[[package]]
name = "futures-io"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04"
[[package]]
name = "futures-sink"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23"
[[package]]
name = "futures-task"
version = "0.3.14"
@ -420,7 +297,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025"
dependencies = [
"futures-core",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
@ -453,25 +329,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "h2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
@ -490,31 +347,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "headers"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855"
dependencies = [
"base64",
"bitflags",
"bytes",
"headers-core",
"http",
"mime",
"sha-1",
"time",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.3.2"
@ -599,7 +431,6 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
@ -624,16 +455,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "indoc"
version = "1.0.3"
@ -724,31 +545,6 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "0.7.11"
@ -806,12 +602,6 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "parking_lot"
version = "0.11.1"
@ -1053,12 +843,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "scoped-tls"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1096,26 +880,13 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "server"
version = "1.2.0"
dependencies = [
"anyhow",
"bytes",
"colored",
"crossbeam",
"dashmap",
"dialoguer",
"hyper",
"indoc",
@ -1139,23 +910,9 @@ dependencies = [
"toml",
"ureq",
"url",
"warp",
"wgctrl",
]
[[package]]
name = "sha-1"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659df5fc3ce22274daac600ffb845300bd2125bcfaec047823075afdab81c00"
dependencies = [
"block-buffer",
"cfg-if",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "shared"
version = "1.2.0"
@ -1180,12 +937,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
[[package]]
name = "slab"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
[[package]]
name = "smallvec"
version = "1.6.1"
@ -1323,16 +1074,6 @@ dependencies = [
"syn",
]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.2.0"
@ -1355,9 +1096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"pin-project-lite",
@ -1375,31 +1114,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-stream"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.5.8"
@ -1422,7 +1136,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-core",
]
@ -1448,15 +1161,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.5"
@ -1554,32 +1258,6 @@ dependencies = [
"try-lock",
]
[[package]]
name = "warp"
version = "0.3.1"
source = "git+https://github.com/tonarino/warp#bd70fb6249810eb63e9acdfe2dcc79e41ab53304"
dependencies = [
"bytes",
"futures",
"headers",
"http",
"hyper",
"log",
"mime",
"mime_guess",
"percent-encoding",
"pin-project",
"scoped-tls",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
"tracing",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"

View File

@ -13,11 +13,10 @@ name = "innernet-server"
path = "src/main.rs"
[dependencies]
bytes = "1"
colored = "2"
crossbeam = "0.8"
dashmap = "4"
dialoguer = "0.8"
hyper = "0.14"
hyper = { version = "0.14", default-features = false, features = ["http1", "server", "runtime", "stream"] }
indoc = "1"
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
libc = "0.2"
@ -33,11 +32,10 @@ shared = { path = "../shared" }
structopt = "0.3"
subtle = "2"
thiserror = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
toml = "0.5"
ureq = { version = "2", default-features = false }
url = "2"
warp = { git = "https://github.com/tonarino/warp", default-features = false } # pending https://github.com/seanmonstar/warp/issues/830
wgctrl = { path = "../wgctrl-rs" }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@ -2,82 +2,60 @@
//!
//! A peer belongs to one parent CIDR, and can by default see all peers within that parent.
use crate::{db::DatabaseAssociation, form_body, with_admin_session, AdminSession, Context};
use std::collections::VecDeque;
use crate::{
db::DatabaseAssociation,
util::{form_body, json_response, status_response},
ServerError, Session,
};
use hyper::{Body, Method, Request, Response, StatusCode};
use shared::AssociationContents;
use warp::{http::StatusCode, Filter};
pub mod routes {
use super::*;
pub fn all(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path("associations").and(
list(context.clone())
.or(create(context.clone()))
.or(delete(context)),
)
}
pub fn list(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::get())
.and(with_admin_session(context))
.and_then(handlers::list)
}
pub fn create(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::post())
.and(form_body())
.and(with_admin_session(context))
.and_then(handlers::create)
}
pub fn delete(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path::param()
.and(warp::path::end())
.and(warp::delete())
.and(with_admin_session(context))
.and_then(handlers::delete)
pub async fn routes(
req: Request<Body>,
mut components: VecDeque<String>,
session: Session,
) -> Result<Response<Body>, ServerError> {
match (req.method(), components.pop_front().as_deref()) {
(&Method::GET, None) => handlers::list(session).await,
(&Method::POST, None) => {
let form = form_body(req).await?;
handlers::create(form, session).await
},
(&Method::DELETE, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
handlers::delete(id, session).await
},
_ => Err(ServerError::NotFound),
}
}
mod handlers {
use super::*;
pub async fn create(
contents: AssociationContents,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
DatabaseAssociation::create(&conn, contents)?;
Ok(StatusCode::CREATED)
status_response(StatusCode::CREATED)
}
pub async fn list(session: AdminSession) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn list(session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let auths = DatabaseAssociation::list(&conn)?;
Ok(warp::reply::json(&auths))
json_response(&auths)
}
pub async fn delete(
id: i64,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn delete(id: i64, session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
DatabaseAssociation::delete(&conn, id)?;
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
}

View File

@ -1,86 +1,60 @@
use crate::{db::DatabaseCidr, form_body, with_admin_session, AdminSession, Context};
use shared::CidrContents;
use warp::{
http::{response::Response, StatusCode},
Filter,
use std::collections::VecDeque;
use crate::{
db::DatabaseCidr,
util::{form_body, json_response, status_response},
ServerError, Session,
};
use hyper::{Body, Method, Request, Response, StatusCode};
use shared::CidrContents;
pub mod routes {
use super::*;
pub fn all(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("cidrs").and(
list(context.clone())
.or(create(context.clone()))
.or(delete(context)),
)
}
pub fn list(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::get())
.and(with_admin_session(context))
.and_then(handlers::list)
}
pub fn create(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::post())
.and(form_body())
.and(with_admin_session(context))
.and_then(handlers::create)
}
pub fn delete(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::param()
.and(warp::path::end())
.and(warp::delete())
.and(with_admin_session(context))
.and_then(handlers::delete)
pub async fn routes(
req: Request<Body>,
mut components: VecDeque<String>,
session: Session,
) -> Result<Response<Body>, ServerError> {
match (req.method(), components.pop_front().as_deref()) {
(&Method::GET, None) => handlers::list(session).await,
(&Method::POST, None) => {
let form = form_body(req).await?;
handlers::create(form, session).await
},
(&Method::DELETE, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
handlers::delete(id, session).await
},
_ => Err(ServerError::NotFound),
}
}
mod handlers {
use crate::util::json_status_response;
use super::*;
pub async fn create(
contents: CidrContents,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let cidr = DatabaseCidr::create(&conn, contents)?;
let response = Response::builder()
.status(StatusCode::CREATED)
.body(serde_json::to_string(&cidr).unwrap())
.unwrap();
Ok(response)
json_status_response(&cidr, StatusCode::CREATED)
}
pub async fn list(session: AdminSession) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn list(session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let cidrs = DatabaseCidr::list(&conn)?;
Ok(warp::reply::json(&cidrs))
json_response(&cidrs)
}
pub async fn delete(
id: i64,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn delete(id: i64, session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
DatabaseCidr::delete(&conn, id)?;
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
}
@ -89,6 +63,7 @@ mod tests {
use super::*;
use crate::{test, DatabasePeer};
use anyhow::Result;
use bytes::Buf;
use shared::Cidr;
#[tokio::test]
@ -103,17 +78,14 @@ mod tests {
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert_eq!(res.status(), 201);
let cidr_res: Cidr = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let cidr_res: Cidr = serde_json::from_reader(whole_body.reader())?;
assert_eq!(contents, cidr_res.contents);
let new_cidrs = DatabaseCidr::list(&server.db().lock())?;
@ -132,15 +104,12 @@ mod tests {
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert!(res.status().is_success());
let cidr_res: Cidr = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let cidr_res: Cidr = serde_json::from_reader(whole_body.reader())?;
let contents = CidrContents {
name: "experimental".to_string(),
@ -148,10 +117,7 @@ mod tests {
parent: Some(cidr_res.id),
};
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert!(!res.status().is_success());
@ -168,12 +134,8 @@ mod tests {
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::USER1_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::USER1_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert!(!res.status().is_success());
@ -189,12 +151,8 @@ mod tests {
cidr: test::EXPERIMENTAL_CIDR.parse()?,
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert!(res.status().is_success());
@ -204,12 +162,8 @@ mod tests {
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert!(!res.status().is_success());
@ -225,12 +179,8 @@ mod tests {
cidr: "10.80.1.0/21".parse()?,
parent: Some(test::ROOT_CIDR_ID),
};
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/cidrs")
.body(serde_json::to_string(&contents)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -258,31 +208,32 @@ mod tests {
},
)?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/cidrs/{}", experimental_cidr.id),
)
.await;
// Should fail because child CIDR exists.
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/cidrs/{}", experimental_subcidr.id))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/cidrs/{}", experimental_subcidr.id),
)
.await;
// Deleting child "leaf" CIDR should fail because peer exists inside it.
assert_eq!(res.status(), StatusCode::NO_CONTENT);
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/cidrs/{}", experimental_cidr.id),
)
.await;
// Now deleting parent CIDR should work because child is gone.
assert_eq!(res.status(), StatusCode::NO_CONTENT);
@ -312,13 +263,12 @@ mod tests {
)?,
)?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/cidrs/{}", experimental_cidr.id),
)
.await;
// Deleting CIDR should fail because peer exists inside it.
assert_eq!(res.status(), StatusCode::BAD_REQUEST);

View File

@ -1,17 +1,26 @@
use warp::Filter;
use std::collections::VecDeque;
use crate::Context;
use hyper::{Body, Request, Response};
use crate::{ServerError, Session};
pub mod association;
pub mod cidr;
pub mod peer;
pub fn routes(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("admin").and(
association::routes::all(context.clone())
.or(cidr::routes::all(context.clone()))
.or(peer::routes::all(context.clone())),
)
pub async fn routes(
req: Request<Body>,
mut components: VecDeque<String>,
session: Session,
) -> Result<Response<Body>, ServerError> {
if !session.admin_capable() {
return Err(ServerError::Unauthorized);
}
match components.pop_front().as_deref() {
Some("associations") => association::routes(req, components, session).await,
Some("cidrs") => cidr::routes(req, components, session).await,
Some("peers") => peer::routes(req, components, session).await,
_ => Err(ServerError::NotFound),
}
}

View File

@ -1,82 +1,47 @@
use std::collections::VecDeque;
use crate::{
api::inject_endpoints, db::DatabasePeer, with_admin_session, AdminSession, Context, ServerError,
api::inject_endpoints,
db::DatabasePeer,
util::{form_body, json_response, json_status_response, status_response},
ServerError, Session,
};
use hyper::{Body, Method, Request, Response, StatusCode};
use shared::PeerContents;
use warp::{
http::{response::Response, StatusCode},
Filter,
};
use wgctrl::DeviceConfigBuilder;
pub mod routes {
use crate::form_body;
use super::*;
pub fn all(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("peers").and(
list(context.clone())
.or(list(context.clone()))
.or(create(context.clone()))
.or(update(context.clone()))
.or(delete(context)),
)
}
// POST /v1/admin/peers
pub fn create(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::post())
.and(form_body())
.and(with_admin_session(context))
.and_then(handlers::create)
}
// PUT /v1/admin/peers/:id
pub fn update(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::param()
.and(warp::path::end())
.and(warp::put())
.and(form_body())
.and(with_admin_session(context))
.and_then(handlers::update)
}
// GET /v1/admin/peers
pub fn list(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::end()
.and(warp::get())
.and(with_admin_session(context))
.and_then(handlers::list)
}
// DELETE /v1/admin/peers/:id
pub fn delete(
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path::param()
.and(warp::path::end())
.and(warp::delete())
.and(with_admin_session(context))
.and_then(handlers::delete)
pub async fn routes(
req: Request<Body>,
mut components: VecDeque<String>,
session: Session,
) -> Result<Response<Body>, ServerError> {
match (req.method(), components.pop_front().as_deref()) {
(&Method::GET, None) => handlers::list(session).await,
(&Method::POST, None) => {
let form = form_body(req).await?;
handlers::create(form, session).await
},
(&Method::PUT, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
let form = form_body(req).await?;
handlers::update(id, form, session).await
},
(&Method::DELETE, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
handlers::delete(id, session).await
},
_ => Err(ServerError::NotFound),
}
}
mod handlers {
use super::*;
pub async fn create(
form: PeerContents,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let peer = DatabasePeer::create(&conn, form)?;
@ -91,43 +56,37 @@ mod handlers {
log::info!("updated WireGuard interface, adding {}", &*peer);
}
let response = Response::builder()
.status(StatusCode::CREATED)
.body(serde_json::to_string(&*peer).unwrap());
Ok(response)
json_status_response(&*peer, StatusCode::CREATED)
}
pub async fn update(
id: i64,
form: PeerContents,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let mut peer = DatabasePeer::get(&conn, id)?;
peer.update(&conn, form)?;
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
/// List all peers, including disabled ones. This is an admin-only endpoint.
pub async fn list(session: AdminSession) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn list(session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let mut peers = DatabasePeer::list(&conn)?
.into_iter()
.map(|peer| peer.inner)
.collect::<Vec<_>>();
inject_endpoints(&session, &mut peers);
Ok(warp::reply::json(&peers))
json_response(&peers)
}
pub async fn delete(
id: i64,
session: AdminSession,
) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn delete(id: i64, session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
DatabasePeer::disable(&conn, id)?;
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
}
@ -136,6 +95,7 @@ mod tests {
use super::*;
use crate::test;
use anyhow::Result;
use bytes::Buf;
use shared::Peer;
#[tokio::test]
@ -146,17 +106,15 @@ mod tests {
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::CREATED);
// The response contains the new peer information.
let peer_res: Peer = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let peer_res: Peer = serde_json::from_reader(whole_body.reader())?;
assert_eq!(peer, peer_res.contents);
// The number of peer entries in the database increased by 1.
@ -172,12 +130,8 @@ mod tests {
let peer = test::developer_peer_contents("devel oper", "10.80.64.4")?;
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -193,12 +147,8 @@ mod tests {
// Try to add a peer with a name that is already taken.
let peer = test::developer_peer_contents("developer2", "10.80.64.4")?;
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -219,12 +169,8 @@ mod tests {
// Try to add a peer with an IP that is already taken.
let peer = test::developer_peer_contents("developer3", "10.80.64.3")?;
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -239,37 +185,27 @@ mod tests {
#[tokio::test]
async fn test_add_peer_with_outside_cidr_range_ip() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let old_peers = DatabasePeer::list(&server.db().lock())?;
// Try to add IP outside of the CIDR network.
let peer = test::developer_peer_contents("developer3", "10.80.65.4")?;
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Try to use the network address as peer IP.
let peer = test::developer_peer_contents("developer3", "10.80.64.0")?;
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Try to use the broadcast address as peer IP.
let peer = test::developer_peer_contents("developer3", "10.80.64.255")?;
let res = server
.post_request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
@ -287,12 +223,8 @@ mod tests {
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
// Try to create a new developer peer from a user peer.
let filter = crate::routes(server.context());
let res = server
.post_request_from_ip(test::USER1_PEER_IP)
.path("/v1/admin/peers")
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(test::USER1_PEER_IP, "POST", "/v1/admin/peers", &peer)
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -311,12 +243,13 @@ mod tests {
};
// Try to create a new developer peer from a user peer.
let filter = crate::routes(server.context());
let res = server
.put_request_from_ip(test::ADMIN_PEER_IP)
.path(&format!("/v1/admin/peers/{}", test::DEVELOPER1_PEER_ID))
.body(serde_json::to_string(&change)?)
.reply(&filter)
.form_request(
test::ADMIN_PEER_IP,
"PUT",
&format!("/v1/admin/peers/{}", test::DEVELOPER1_PEER_ID),
&change,
)
.await;
assert_eq!(res.status(), StatusCode::NO_CONTENT);
@ -333,12 +266,13 @@ mod tests {
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
// Try to create a new developer peer from a user peer.
let filter = crate::routes(server.context());
let res = server
.put_request_from_ip(test::USER1_PEER_IP)
.path(&format!("/v1/admin/peers/{}", test::ADMIN_PEER_ID))
.body(serde_json::to_string(&peer)?)
.reply(&filter)
.form_request(
test::USER1_PEER_IP,
"PUT",
&format!("/v1/admin/peers/{}", test::DEVELOPER1_PEER_ID),
&peer,
)
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -349,16 +283,14 @@ mod tests {
#[tokio::test]
async fn test_list_all_peers_from_admin() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.path("/v1/admin/peers")
.reply(&filter)
.request(test::ADMIN_PEER_IP, "GET", "/v1/admin/peers")
.await;
assert_eq!(res.status(), StatusCode::OK);
let peers: Vec<Peer> = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let peers: Vec<Peer> = serde_json::from_reader(whole_body.reader())?;
let peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
// An admin peer should see all the peers.
assert_eq!(
@ -379,11 +311,8 @@ mod tests {
#[tokio::test]
async fn test_list_all_peers_from_non_admin() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/admin/peers")
.reply(&filter)
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/admin/peers")
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -394,15 +323,14 @@ mod tests {
#[tokio::test]
async fn test_delete() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let old_peers = DatabasePeer::list(&server.db().lock())?;
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/peers/{}", test::USER1_PEER_ID),
)
.await;
assert!(res.status().is_success());
@ -418,15 +346,15 @@ mod tests {
#[tokio::test]
async fn test_delete_from_non_admin() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let old_peers = DatabasePeer::list(&server.db().lock())?;
let res = server
.request_from_ip(test::DEVELOPER1_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID))
.reply(&filter)
.request(
test::DEVELOPER1_PEER_IP,
"DELETE",
&format!("/v1/admin/peers/{}", test::USER1_PEER_ID),
)
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -441,13 +369,13 @@ mod tests {
#[tokio::test]
async fn test_delete_unknown_id() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::ADMIN_PEER_IP)
.method("DELETE")
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID + 100))
.reply(&filter)
.request(
test::ADMIN_PEER_IP,
"DELETE",
&format!("/v1/admin/peers/{}", test::USER1_PEER_ID + 100),
)
.await;
// Trying to delete a peer of non-existing ID will result in error.

View File

@ -10,7 +10,7 @@ pub mod user;
pub fn inject_endpoints(session: &Session, peers: &mut Vec<Peer>) {
for mut peer in peers {
if peer.contents.endpoint.is_none() {
if let Some(endpoint) = session.context.endpoints.get(&peer.public_key) {
if let Some(endpoint) = session.context.endpoints.read().get(&peer.public_key) {
peer.contents.endpoint = Some(endpoint.to_owned().into());
}
}

View File

@ -1,57 +1,42 @@
use std::collections::VecDeque;
use crate::{
api::inject_endpoints,
db::{DatabaseCidr, DatabasePeer},
form_body, with_session, with_unredeemed_session, Context, ServerError, Session,
UnredeemedSession,
util::{form_body, json_response, status_response},
ServerError, Session,
};
use hyper::StatusCode;
use hyper::{Body, Method, Request, Response, StatusCode};
use shared::{EndpointContents, PeerContents, RedeemContents, State, REDEEM_TRANSITION_WAIT};
use warp::Filter;
use wgctrl::DeviceConfigBuilder;
pub fn routes(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path("user").and(
routes::state(context.clone())
.or(routes::redeem(context.clone()))
.or(routes::override_endpoint(context.clone())),
)
}
pub mod routes {
use super::*;
pub fn state(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path("state")
.and(warp::path::end())
.and(warp::get())
.and(with_session(context))
.and_then(handlers::state)
}
pub fn redeem(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path("redeem")
.and(warp::path::end())
.and(warp::post())
.and(form_body())
.and(with_unredeemed_session(context))
.and_then(handlers::redeem)
}
pub fn override_endpoint(
context: Context,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path("endpoint")
.and(warp::path::end())
.and(warp::put())
.and(form_body())
.and(with_session(context))
.and_then(handlers::endpoint)
pub async fn routes(
req: Request<Body>,
mut components: VecDeque<String>,
session: Session,
) -> Result<Response<Body>, ServerError> {
match (req.method(), components.pop_front().as_deref()) {
(&Method::GET, Some("state")) => {
if !session.user_capable() {
return Err(ServerError::Unauthorized);
}
handlers::state(session).await
},
(&Method::POST, Some("redeem")) => {
if !session.redeemable() {
return Err(ServerError::Unauthorized);
}
let form = form_body(req).await?;
handlers::redeem(form, session).await
},
(&Method::PUT, Some("endpoint")) => {
if !session.user_capable() {
return Err(ServerError::Unauthorized);
}
let form = form_body(req).await?;
handlers::endpoint(form, session).await
},
_ => Err(ServerError::NotFound),
}
}
@ -62,7 +47,7 @@ mod handlers {
///
/// This endpoint returns the visible CIDRs and Peers, providing all the necessary
/// information for the peer to create connections to all of them.
pub async fn state(session: Session) -> Result<impl warp::Reply, warp::Rejection> {
pub async fn state(session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let selected_peer = DatabasePeer::get(&conn, session.peer.id)?;
@ -74,8 +59,7 @@ mod handlers {
.map(|p| p.inner)
.collect();
inject_endpoints(&session, &mut peers);
Ok(warp::reply::json(&State { cidrs, peers }))
json_response(State { peers, cidrs })
}
/// Redeems an invitation. An invitation includes a WireGuard keypair generated by either the server
@ -88,8 +72,8 @@ mod handlers {
/// it is called and succeeds, it cannot be called again.
pub async fn redeem(
form: RedeemContents,
session: UnredeemedSession,
) -> Result<impl warp::Reply, warp::Rejection> {
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let mut selected_peer = DatabasePeer::get(&conn, session.peer.id)?;
@ -97,12 +81,14 @@ mod handlers {
.map_err(|_| ServerError::WireGuard)?;
if selected_peer.is_redeemed {
Ok(StatusCode::GONE)
Ok(Response::builder()
.status(StatusCode::GONE)
.body(Body::empty())?)
} else {
selected_peer.redeem(&conn, &form.public_key)?;
if cfg!(not(test)) {
let interface = session.context.interface.clone();
let interface = session.context.interface;
// If we were to modify the WireGuard interface immediately, the HTTP response wouldn't
// get through. Instead, we need to wait a reasonable amount for the HTTP response to
@ -129,7 +115,7 @@ mod handlers {
.ok();
});
}
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
}
@ -144,7 +130,7 @@ mod handlers {
pub async fn endpoint(
contents: EndpointContents,
session: Session,
) -> Result<impl warp::Reply, warp::Rejection> {
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let mut selected_peer = DatabasePeer::get(&conn, session.peer.id)?;
selected_peer.update(
@ -155,7 +141,7 @@ mod handlers {
},
)?;
Ok(StatusCode::NO_CONTENT)
status_response(StatusCode::NO_CONTENT)
}
}
@ -164,22 +150,20 @@ mod tests {
use super::*;
use crate::{db::DatabaseAssociation, test};
use anyhow::Result;
use bytes::Buf;
use shared::{AssociationContents, CidrContents, EndpointContents};
use warp::http::StatusCode;
#[tokio::test]
async fn test_get_state_from_developer1() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
let res = server
.request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/user/state")
.reply(&filter)
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/user/state")
.await;
assert_eq!(res.status(), StatusCode::OK);
let State { peers, .. } = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
let mut peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
peer_names.sort();
// Developers should see only peers in infra CIDR and developer CIDR.
@ -194,15 +178,14 @@ mod tests {
#[tokio::test]
async fn test_override_endpoint() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
assert_eq!(
server
.put_request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/user/endpoint")
.body(serde_json::to_string(&EndpointContents::Set(
"1.1.1.1:51820".parse().unwrap()
))?)
.reply(&filter)
.form_request(
test::DEVELOPER1_PEER_IP,
"PUT",
"/v1/user/endpoint",
&EndpointContents::Set("1.1.1.1:51820".parse().unwrap())
)
.await
.status(),
StatusCode::NO_CONTENT
@ -211,10 +194,12 @@ mod tests {
println!("{}", serde_json::to_string(&EndpointContents::Unset)?);
assert_eq!(
server
.put_request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/user/endpoint")
.body(serde_json::to_string(&EndpointContents::Unset)?)
.reply(&filter)
.form_request(
test::DEVELOPER1_PEER_IP,
"PUT",
"/v1/user/endpoint",
&EndpointContents::Unset,
)
.await
.status(),
StatusCode::NO_CONTENT
@ -222,10 +207,12 @@ mod tests {
assert_eq!(
server
.put_request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/user/endpoint")
.body("endpoint=blah")
.reply(&filter)
.form_request(
test::DEVELOPER1_PEER_IP,
"PUT",
"/v1/user/endpoint",
"endpoint=blah",
)
.await
.status(),
StatusCode::BAD_REQUEST
@ -237,14 +224,9 @@ mod tests {
#[tokio::test]
async fn test_list_peers_from_unknown_ip() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
// Request comes from an unknown IP.
let res = server
.request_from_ip("10.80.80.80")
.path("/v1/user/state")
.reply(&filter)
.await;
let res = server.request("10.80.80.80", "GET", "/v1/user/state").await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -254,7 +236,6 @@ mod tests {
#[tokio::test]
async fn test_list_peers_for_developer_subcidr() -> Result<()> {
let server = test::Server::new()?;
let filter = crate::routes(server.context());
{
let db = server.db.lock();
let cidr = DatabaseCidr::create(
@ -301,13 +282,10 @@ mod tests {
}
for ip in &[test::DEVELOPER1_PEER_IP, test::EXPERIMENT_SUBCIDR_PEER_IP] {
let res = server
.request_from_ip(ip)
.path("/v1/user/state")
.reply(&filter)
.await;
let res = server.request(ip, "GET", "/v1/user/state").await;
assert_eq!(res.status(), StatusCode::OK);
let State { peers, .. } = serde_json::from_slice(&res.body())?;
let whole_body = hyper::body::aggregate(res).await?;
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
let mut peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
peer_names.sort();
// Developers should see only peers in infra CIDR and developer CIDR.
@ -347,13 +325,9 @@ mod tests {
peer_contents.is_redeemed = false;
let _experiment_peer = DatabasePeer::create(&server.db().lock(), peer_contents)?;
let filter = crate::routes(server.context());
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
let res = server
.request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
.path("/v1/user/state")
.reply(&filter)
.request(test::EXPERIMENT_SUBCIDR_PEER_IP, "GET", "/v1/user/state")
.await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
@ -362,27 +336,29 @@ mod tests {
public_key: "YBVIgpfLbi/knrMCTEb0L6eVy0daiZnJJQkxBK9s+2I=".into(),
};
let res = server
.post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
.path("/v1/user/redeem")
.body(serde_json::to_string(&body)?)
.reply(&filter)
.form_request(
test::EXPERIMENT_SUBCIDR_PEER_IP,
"POST",
"/v1/user/redeem",
&body,
)
.await;
assert!(res.status().is_success());
// Step 3: Ensure that a second attempt at redemption DOESN'T work.
let res = server
.post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
.path("/v1/user/redeem")
.body(serde_json::to_string(&body)?)
.reply(&filter)
.form_request(
test::EXPERIMENT_SUBCIDR_PEER_IP,
"POST",
"/v1/user/redeem",
&body,
)
.await;
assert!(res.status().is_client_error());
// Step 3: Ensure that after redemption, fetching state works.
let res = server
.request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
.path("/v1/user/state")
.reply(&filter)
.request(test::EXPERIMENT_SUBCIDR_PEER_IP, "GET", "/v1/user/state")
.await;
assert_eq!(res.status(), StatusCode::OK);
Ok(())

View File

@ -201,9 +201,9 @@ impl DatabasePeer {
cidr_id,
public_key,
endpoint,
persistent_keepalive_interval,
is_admin,
is_disabled,
persistent_keepalive_interval,
is_redeemed,
},
}
@ -223,7 +223,7 @@ impl DatabasePeer {
Ok(result)
}
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, ServerError> {
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, rusqlite::Error> {
let result = conn.query_row(
"SELECT
id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed

View File

@ -1,16 +1,25 @@
use crossbeam::channel::{self, select};
use dashmap::DashMap;
use parking_lot::RwLock;
use wgctrl::{DeviceInfo, InterfaceName};
use std::{io, net::SocketAddr, sync::Arc, thread, time::Duration};
use std::{
collections::HashMap,
io,
net::SocketAddr,
sync::{
mpsc::{sync_channel, SyncSender, TryRecvError},
Arc,
},
thread,
time::Duration,
};
pub struct Endpoints {
pub endpoints: Arc<DashMap<String, SocketAddr>>,
stop_tx: channel::Sender<()>,
pub endpoints: Arc<RwLock<HashMap<String, SocketAddr>>>,
stop_tx: SyncSender<()>,
}
impl std::ops::Deref for Endpoints {
type Target = DashMap<String, SocketAddr>;
type Target = RwLock<HashMap<String, SocketAddr>>;
fn deref(&self) -> &Self::Target {
&self.endpoints
@ -19,30 +28,28 @@ impl std::ops::Deref for Endpoints {
impl Endpoints {
pub fn new(iface: &InterfaceName) -> Result<Self, io::Error> {
let endpoints = Arc::new(DashMap::new());
let (stop_tx, stop_rx) = channel::bounded(1);
let endpoints = Arc::new(RwLock::new(HashMap::new()));
let (stop_tx, stop_rx) = sync_channel(1);
let iface = iface.to_owned();
let thread_endpoints = endpoints.clone();
log::info!("spawning endpoint watch thread.");
if cfg!(not(test)) {
thread::spawn(move || loop {
select! {
recv(stop_rx) -> _ => {
break;
},
default => {
if let Ok(info) = DeviceInfo::get_by_name(&iface) {
for peer in info.peers {
if let Some(endpoint) = peer.config.endpoint {
thread_endpoints.insert(peer.config.public_key.to_base64(), endpoint);
}
}
if matches!(stop_rx.try_recv(), Ok(_) | Err(TryRecvError::Disconnected)) {
break;
}
if let Ok(info) = DeviceInfo::get_by_name(&iface) {
for peer in info.peers {
if let Some(endpoint) = peer.config.endpoint {
thread_endpoints
.write()
.insert(peer.config.public_key.to_base64(), endpoint);
}
thread::sleep(Duration::from_secs(1));
}
}
thread::sleep(Duration::from_secs(1));
});
}
Ok(Self { endpoints, stop_tx })

View File

@ -1,5 +1,7 @@
use std::convert::TryFrom;
use hyper::{http, Body, Response, StatusCode};
use thiserror::Error;
use warp::{http::StatusCode, reject::Rejection};
#[derive(Error, Debug)]
pub enum ServerError {
@ -20,17 +22,15 @@ pub enum ServerError {
#[error("internal I/O error")]
Io(#[from] std::io::Error),
}
impl warp::reject::Reject for ServerError {}
#[error("JSON parsing/serialization error")]
Json(#[from] serde_json::Error),
pub async fn handle_rejection(err: Rejection) -> Result<StatusCode, warp::Rejection> {
eprintln!("rejection: {:?}", err);
if let Some(error) = err.find::<ServerError>() {
Ok(error.into())
} else {
Err(err)
}
#[error("Generic HTTP error")]
Http(#[from] http::Error),
#[error("Generic Hyper error")]
Hyper(#[from] hyper::Error),
}
impl<'a> From<&'a ServerError> for StatusCode {
@ -39,7 +39,7 @@ impl<'a> From<&'a ServerError> for StatusCode {
match error {
Unauthorized => StatusCode::UNAUTHORIZED,
NotFound => StatusCode::NOT_FOUND,
InvalidQuery => StatusCode::BAD_REQUEST,
InvalidQuery | Json(_) => StatusCode::BAD_REQUEST,
// Special-case the constraint violation situation.
Database(rusqlite::Error::SqliteFailure(libsqlite3_sys::Error { code, .. }, ..))
if *code == libsqlite3_sys::ErrorCode::ConstraintViolation =>
@ -47,7 +47,19 @@ impl<'a> From<&'a ServerError> for StatusCode {
StatusCode::BAD_REQUEST
},
Database(rusqlite::Error::QueryReturnedNoRows) => StatusCode::NOT_FOUND,
WireGuard | Io(_) | Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
WireGuard | Io(_) | Database(_) | Http(_) | Hyper(_) => {
StatusCode::INTERNAL_SERVER_ERROR
},
}
}
}
impl TryFrom<ServerError> for Response<Body> {
type Error = http::Error;
fn try_from(e: ServerError) -> Result<Self, Self::Error> {
Response::builder()
.status(StatusCode::from(&e))
.body(Body::empty())
}
}

View File

@ -105,7 +105,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
})?;
let name: String = if let Some(name) = opts.network_name {
name.clone()
name
} else {
println!("Here you'll specify the network CIDR, which will encompass the entire network.");
Input::with_theme(&theme)
@ -115,7 +115,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
};
let root_cidr: IpNetwork = if let Some(cidr) = opts.network_cidr {
cidr.clone()
cidr
} else {
Input::with_theme(&theme)
.with_prompt("Network CIDR")
@ -127,7 +127,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
let name = name.parse()?;
let endpoint: Endpoint = if let Some(endpoint) = opts.external_endpoint {
endpoint.clone()
endpoint
} else {
let external_ip: Option<IpAddr> = ureq::get("http://4.icanhazip.com")
.call()

View File

@ -1,15 +1,15 @@
use colored::*;
use dialoguer::Confirm;
use error::handle_rejection;
use hyper::{server::conn::AddrStream, Body, Request};
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
use indoc::printdoc;
use ipnetwork::IpNetwork;
use parking_lot::Mutex;
use rusqlite::Connection;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, INNERNET_PUBKEY_HEADER};
use std::{
convert::Infallible,
collections::VecDeque,
convert::TryInto,
env,
fs::File,
io::prelude::*,
@ -20,7 +20,6 @@ use std::{
};
use structopt::StructOpt;
use subtle::ConstantTimeEq;
use warp::{filters, Filter};
use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuilder};
pub mod api;
@ -29,6 +28,7 @@ pub mod endpoints;
pub mod error;
#[cfg(test)]
mod test;
pub mod util;
mod initialize;
@ -95,21 +95,17 @@ pub struct Session {
pub peer: DatabasePeer,
}
pub struct AdminSession(Session);
impl Deref for AdminSession {
type Target = Session;
fn deref(&self) -> &Self::Target {
&self.0
impl Session {
pub fn admin_capable(&self) -> bool {
self.peer.is_admin && self.user_capable()
}
}
pub struct UnredeemedSession(Session);
impl Deref for UnredeemedSession {
type Target = Session;
pub fn user_capable(&self) -> bool {
!self.peer.is_disabled && self.peer.is_redeemed
}
fn deref(&self) -> &Self::Target {
&self.0
pub fn redeemable(&self) -> bool {
!self.peer.is_disabled && !self.peer.is_redeemed
}
}
@ -231,7 +227,11 @@ fn open_database_connection(
.into());
}
Ok(Connection::open(&database_path)?)
let conn = Connection::open(&database_path)?;
// Foreign key constraints aren't on in SQLite by default. Enable.
conn.pragma_update(None, "foreign_keys", &1)?;
Ok(conn)
}
fn add_peer(
@ -304,11 +304,39 @@ fn add_cidr(
Ok(())
}
fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
if Confirm::with_theme(&*prompts::THEME)
.with_prompt(&format!(
"Permanently delete network \"{}\"?",
interface.as_str_lossy().yellow()
))
.default(false)
.interact()?
{
println!("{} bringing down interface (if up).", "[*]".dimmed());
wg::down(interface).ok();
let config = conf.config_path(interface);
let data = conf.database_path(interface);
std::fs::remove_file(&config)
.with_path(&config)
.map_err(|e| println!("[!] {}", e.to_string().yellow()))
.ok();
std::fs::remove_file(&data)
.with_path(&data)
.map_err(|e| println!("[!] {}", e.to_string().yellow()))
.ok();
println!(
"{} network {} is uninstalled.",
"[*]".dimmed(),
interface.as_str_lossy().yellow()
);
}
Ok(())
}
async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
let config = ConfigFile::from_file(conf.config_path(interface))?;
let conn = open_database_connection(interface, conf)?;
// Foreign key constraints aren't on in SQLite by default. Enable.
conn.pragma_update(None, "foreign_keys", &1)?;
let peers = DatabasePeer::list(&conn)?;
let peer_configs = peers
@ -343,55 +371,23 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
};
log::info!("innernet-server {} starting.", VERSION);
let routes = routes(context.clone()).with(warp::log("warp")).boxed();
let listener = get_listener((config.address, config.listen_port).into(), interface)?;
let warp_svc = warp::service(routes);
let make_svc = hyper::service::make_service_fn(move |socket: &AddrStream| {
let remote_addr = socket.remote_addr();
let warp_svc = warp_svc.clone();
let context = context.clone();
async move {
let svc = hyper::service::service_fn(move |req: Request<Body>| {
let warp_svc = warp_svc.clone();
async move { warp_svc.call_with_addr(req, Some(remote_addr)).await }
});
Ok::<_, Infallible>(svc)
Ok::<_, http::Error>(hyper::service::service_fn(move |req: Request<Body>| {
hyper_service(req, context.clone(), remote_addr)
}))
}
});
hyper::Server::from_tcp(listener)?.serve(make_svc).await?;
let server = hyper::Server::from_tcp(listener)?.serve(make_svc);
Ok(())
}
server.await?;
fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
if Confirm::with_theme(&*prompts::THEME)
.with_prompt(&format!(
"Permanently delete network \"{}\"?",
interface.as_str_lossy().yellow()
))
.default(false)
.interact()?
{
println!("{} bringing down interface (if up).", "[*]".dimmed());
wg::down(interface).ok();
let config = conf.config_path(interface);
let data = conf.database_path(interface);
std::fs::remove_file(&config)
.with_path(&config)
.map_err(|e| println!("[!] {}", e.to_string().yellow()))
.ok();
std::fs::remove_file(&data)
.with_path(&data)
.map_err(|e| println!("[!] {}", e.to_string().yellow()))
.ok();
println!(
"{} network {} is uninstalled.",
"[*]".dimmed(),
interface.as_str_lossy().yellow()
);
}
Ok(())
}
@ -423,103 +419,68 @@ fn get_listener(addr: SocketAddr, _interface: &InterfaceName) -> Result<TcpListe
Ok(listener)
}
pub fn routes(
pub(crate) async fn hyper_service(
req: Request<Body>,
context: Context,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("v1")
.and(api::admin::routes(context.clone()).or(api::user::routes(context)))
.recover(handle_rejection)
remote_addr: SocketAddr,
) -> Result<Response<Body>, http::Error> {
// Break the path into components.
let components: VecDeque<_> = req
.uri()
.path()
.trim_start_matches('/')
.split('/')
.map(String::from)
.collect();
routes(req, context, remote_addr, components)
.await
.or_else(TryInto::try_into)
}
pub fn form_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone
where
T: DeserializeOwned + Send,
{
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
async fn routes(
req: Request<Body>,
context: Context,
remote_addr: SocketAddr,
mut components: VecDeque<String>,
) -> Result<Response<Body>, ServerError> {
// Must be "/v1/[something]"
if components.pop_front().as_deref() != Some("v1") {
Err(ServerError::NotFound)
} else {
let session = get_session(&req, context, remote_addr.ip())?;
let component = components.pop_front();
match component.as_deref() {
Some("user") => api::user::routes(req, components, session).await,
Some("admin") => api::admin::routes(req, components, session).await,
_ => Err(ServerError::NotFound),
}
}
}
pub fn with_unredeemed_session(
fn get_session(
req: &Request<Body>,
context: Context,
) -> impl Filter<Extract = (UnredeemedSession,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, pubkey: String| {
get_session(
context.clone(),
addr.map(|addr| addr.ip()),
pubkey,
false,
false,
)
})
.map(|session| UnredeemedSession(session))
}
pub fn with_session(
context: Context,
) -> impl Filter<Extract = (Session,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, pubkey: String| {
get_session(
context.clone(),
addr.map(|addr| addr.ip()),
pubkey,
false,
true,
)
})
}
pub fn with_admin_session(
context: Context,
) -> impl Filter<Extract = (AdminSession,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, pubkey: String| {
get_session(
context.clone(),
addr.map(|addr| addr.ip()),
pubkey,
true,
true,
)
})
.map(|session| AdminSession(session))
}
async fn get_session(
context: Context,
addr: Option<IpAddr>,
pubkey: String,
admin_only: bool,
redeemed_only: bool,
) -> Result<Session, warp::Rejection> {
_get_session(context, addr, pubkey, admin_only, redeemed_only)
.map_err(|_| warp::reject::custom(ServerError::Unauthorized))
}
fn _get_session(
context: Context,
addr: Option<IpAddr>,
pubkey: String,
admin_only: bool,
redeemed_only: bool,
) -> Result<Session, Error> {
let pubkey = Key::from_base64(&pubkey)?;
addr: IpAddr,
) -> Result<Session, ServerError> {
let pubkey = req
.headers()
.get(INNERNET_PUBKEY_HEADER)
.ok_or(ServerError::Unauthorized)?;
let pubkey = pubkey.to_str().map_err(|_| ServerError::Unauthorized)?;
let pubkey = Key::from_base64(&pubkey).map_err(|_| ServerError::Unauthorized)?;
if pubkey.0.ct_eq(&context.public_key.0).into() {
let addr = addr.ok_or(ServerError::NotFound)?;
let peer = DatabasePeer::get_from_ip(&context.db.lock(), addr)?;
let peer = DatabasePeer::get_from_ip(&context.db.lock(), addr).map_err(|e| match e {
rusqlite::Error::QueryReturnedNoRows => ServerError::Unauthorized,
e => ServerError::Database(e),
})?;
if !peer.is_disabled
&& (!admin_only || peer.is_admin)
&& (!redeemed_only || peer.is_redeemed)
{
if !peer.is_disabled {
return Ok(Session { context, peer });
}
}
Err(ServerError::Unauthorized.into())
Err(ServerError::Unauthorized)
}
#[cfg(test)]
@ -527,8 +488,8 @@ mod tests {
use super::*;
use crate::test;
use anyhow::Result;
use hyper::StatusCode;
use std::path::Path;
use warp::http::StatusCode;
#[test]
fn test_init_wizard() -> Result<()> {
@ -543,17 +504,17 @@ mod tests {
#[tokio::test]
async fn test_with_session_disguised_with_headers() -> Result<()> {
let server = test::Server::new()?;
let filter = routes(server.context());
// Request from an unknown IP, trying to disguise as an admin using HTTP headers.
let res = server
.request_from_ip("10.80.80.80")
.path("/v1/admin/peers")
let req = Request::builder()
.uri(format!("http://{}/v1/admin/peers", test::WG_MANAGE_PEER_IP))
.header("Forwarded", format!("for={}", test::ADMIN_PEER_IP))
.header("X-Forwarded-For", test::ADMIN_PEER_IP)
.header("X-Real-IP", test::ADMIN_PEER_IP)
.reply(&filter)
.await;
.body(Body::empty())
.unwrap();
// Request from an unknown IP, trying to disguise as an admin using HTTP headers.
let res = server.raw_request("10.80.80.80", req).await;
// addr::remote() filter only look at remote_addr from TCP socket.
// HTTP headers are not considered. This also means that innernet
@ -566,17 +527,16 @@ mod tests {
#[tokio::test]
async fn test_incorrect_public_key() -> Result<()> {
let server = test::Server::new()?;
let filter = routes(server.context());
let key = Key::generate_private().generate_public();
// Request from an unknown IP, trying to disguise as an admin using HTTP headers.
let res = server
.request_from_ip("10.80.80.80")
.path("/v1/admin/peers")
let req = Request::builder()
.uri(format!("http://{}/v1/admin/peers", test::WG_MANAGE_PEER_IP))
.header(shared::INNERNET_PUBKEY_HEADER, key.to_base64())
.reply(&filter)
.await;
.body(Body::empty())
.unwrap();
let res = server.raw_request("10.80.80.80", req).await;
// addr::remote() filter only look at remote_addr from TCP socket.
// HTTP headers are not considered. This also means that innernet
@ -589,15 +549,13 @@ mod tests {
#[tokio::test]
async fn test_unparseable_public_key() -> Result<()> {
let server = test::Server::new()?;
let filter = routes(server.context());
// Request from an unknown IP, trying to disguise as an admin using HTTP headers.
let res = server
.request_from_ip("10.80.80.80")
.path("/v1/admin/peers")
.header(shared::INNERNET_PUBKEY_HEADER, "")
.reply(&filter)
.await;
let req = Request::builder()
.uri(format!("http://{}/v1/admin/peers", test::WG_MANAGE_PEER_IP))
.header(shared::INNERNET_PUBKEY_HEADER, "!!!")
.body(Body::empty())
.unwrap();
let res = server.raw_request("10.80.80.80", req).await;
// addr::remote() filter only look at remote_addr from TCP socket.
// HTTP headers are not considered. This also means that innernet

View File

@ -6,12 +6,13 @@ use crate::{
Context, ServerConfig,
};
use anyhow::{anyhow, Result};
use hyper::{header::HeaderValue, http, Body, Request, Response};
use parking_lot::Mutex;
use rusqlite::Connection;
use serde::Serialize;
use shared::{Cidr, CidrContents, PeerContents};
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use tempfile::TempDir;
use warp::test::RequestBuilder;
use wgctrl::{InterfaceName, Key, KeyPair};
pub const ROOT_CIDR: &str = "10.80.0.0/15";
@ -144,23 +145,50 @@ impl Server {
self.conf.config_path(&self.interface)
}
pub fn request_from_ip(&self, ip_str: &str) -> RequestBuilder {
pub async fn raw_request(&self, ip_str: &str, req: Request<Body>) -> Response<Body> {
let port = 54321u16;
warp::test::request()
.remote_addr(SocketAddr::new(ip_str.parse().unwrap(), port))
.header(shared::INNERNET_PUBKEY_HEADER, self.public_key.to_base64())
crate::hyper_service(
req,
self.context(),
SocketAddr::new(ip_str.parse().unwrap(), port),
)
.await
.unwrap()
}
pub fn post_request_from_ip(&self, ip_str: &str) -> RequestBuilder {
self.request_from_ip(ip_str)
.method("POST")
.header("Content-Type", "application/json")
fn base_request_builder(&self, verb: &str, path: &str) -> http::request::Builder {
Request::builder()
.uri(format!("http://{}{}", WG_MANAGE_PEER_IP, path))
.method(verb)
.header(
shared::INNERNET_PUBKEY_HEADER,
HeaderValue::from_str(&self.public_key.to_base64()).unwrap(),
)
}
pub fn put_request_from_ip(&self, ip_str: &str) -> RequestBuilder {
self.request_from_ip(ip_str)
.method("PUT")
pub async fn request(&self, ip_str: &str, verb: &str, path: &str) -> Response<Body> {
let req = self
.base_request_builder(verb, path)
.body(Body::empty())
.unwrap();
self.raw_request(ip_str, req).await
}
pub async fn form_request<F: Serialize>(
&self,
ip_str: &str,
verb: &str,
path: &str,
form: F,
) -> Response<Body> {
let json = serde_json::to_string(&form).unwrap();
let req = self
.base_request_builder(verb, path)
.header("Content-Type", "application/json")
.header("Content-Length", json.len().to_string())
.body(Body::from(json))
.unwrap();
self.raw_request(ip_str, req).await
}
}

45
server/src/util.rs Normal file
View File

@ -0,0 +1,45 @@
use bytes::Buf;
use hyper::{header, Body, Request, Response, StatusCode};
use serde::{de::DeserializeOwned, Serialize};
use crate::ServerError;
pub async fn form_body<F: DeserializeOwned>(req: Request<Body>) -> Result<F, ServerError> {
let content_len: usize = req
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|header| header.to_str().ok())
.and_then(|header| header.parse().ok())
.ok_or(ServerError::InvalidQuery)?;
if content_len > 16 * 1024 {
return Err(ServerError::InvalidQuery);
}
let whole_body = hyper::body::aggregate(req).await?;
serde_json::from_reader(whole_body.reader()).map_err(Into::into)
}
pub fn json_response<F: Serialize>(form: F) -> Result<Response<Body>, ServerError> {
let json = serde_json::to_string(&form)?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(json))?)
}
pub fn json_status_response<F: Serialize>(
form: F,
status: StatusCode,
) -> Result<Response<Body>, ServerError> {
let json = serde_json::to_string(&form)?;
Ok(Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(json))?)
}
pub fn status_response(status: StatusCode) -> Result<Response<Body>, ServerError> {
Ok(Response::builder().status(status).body(Body::empty())?)
}

View File

@ -23,6 +23,7 @@ pub fn is_valid_hostname(name: &str) -> bool {
name.len() < 64 && PEER_NAME_REGEX.is_match(name)
}
#[allow(clippy::ptr_arg)]
pub fn hostname_validator(name: &String) -> Result<(), &'static str> {
if is_valid_hostname(name) {
Ok(())
@ -119,8 +120,8 @@ pub fn choose_association<'a>(
}
pub fn add_association(cidrs: &[Cidr]) -> Result<Option<(&Cidr, &Cidr)>, Error> {
let cidr1 = choose_cidr(&cidrs[..], "First CIDR")?;
let cidr2 = choose_cidr(&cidrs[..], "Second CIDR")?;
let cidr1 = choose_cidr(cidrs, "First CIDR")?;
let cidr2 = choose_cidr(cidrs, "Second CIDR")?;
Ok(
if Confirm::with_theme(&*THEME)
@ -178,7 +179,7 @@ pub fn add_peer(
let mut available_ip = None;
let candidate_ips = cidr.iter().filter(|ip| cidr.is_assignable(*ip));
for ip in candidate_ips {
if peers.iter().find(|peer| peer.ip == ip).is_none() {
if !peers.iter().any(|peer| peer.ip == ip) {
available_ip = Some(ip);
break;
}
@ -388,7 +389,7 @@ pub fn ask_endpoint(external_ip: Option<IpAddr>) -> Result<Endpoint, Error> {
endpoint_builder
.with_prompt("External endpoint")
.interact()
.map_err(|e| Error::from(e))
.map_err(Into::into)
}
pub fn override_endpoint(unset: bool) -> Result<Option<Option<Endpoint>>, Error> {

View File

@ -136,11 +136,11 @@ pub enum EndpointContents {
Unset,
}
impl Into<Option<Endpoint>> for EndpointContents {
fn into(self) -> Option<Endpoint> {
match self {
Self::Set(addr) => Some(addr),
Self::Unset => None,
impl From<EndpointContents> for Option<Endpoint> {
fn from(endpoint: EndpointContents) -> Self {
match endpoint {
EndpointContents::Set(addr) => Some(addr),
EndpointContents::Unset => None,
}
}
}

View File

@ -259,7 +259,7 @@ pub fn get_by_name(name: &InterfaceName) -> Result<DeviceInfo, io::Error> {
/// available.
fn get_userspace_implementation() -> String {
std::env::var("WG_USERSPACE_IMPLEMENTATION")
.or(std::env::var("WG_QUICK_USERSPACE_IMPLEMENTATION"))
.or_else(|_| std::env::var("WG_QUICK_USERSPACE_IMPLEMENTATION"))
.unwrap_or_else(|_| "wireguard-go".to_string())
}