{client,server}: send and require a header that contains the server public key
This is a stop-gap CSRF protection mechanism from unsophisticated attacks. It's to be considered a temporary solution until a more complete one can be implemented, but it should be sufficient in most cases for the time being. See https://github.com/tonarino/innernet/issues/38 for further discussion.pull/48/head
parent
bcd68df772
commit
a87d56cfc9
|
@ -1132,6 +1132,7 @@ dependencies = [
|
|||
"shared",
|
||||
"socket2",
|
||||
"structopt",
|
||||
"subtle",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
|
@ -21,7 +21,7 @@ mod util;
|
|||
|
||||
use data_store::DataStore;
|
||||
use shared::{wg, Error};
|
||||
use util::{http_delete, http_get, http_post, http_put, human_duration, human_size};
|
||||
use util::{human_duration, human_size, Api};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "innernet", about)]
|
||||
|
@ -216,8 +216,8 @@ fn install(invite: &Path, hosts_file: Option<PathBuf>) -> Result<(), Error> {
|
|||
"[*]".dimmed(),
|
||||
&config.server.internal_endpoint
|
||||
);
|
||||
http_post(
|
||||
&config.server.internal_endpoint,
|
||||
Api::new(&config.server).http_form(
|
||||
"POST",
|
||||
"/user/redeem",
|
||||
RedeemContents {
|
||||
public_key: keypair.public.to_base64(),
|
||||
|
@ -334,7 +334,7 @@ fn fetch(
|
|||
|
||||
println!("{} fetching state from server.", "[*]".dimmed());
|
||||
let mut store = DataStore::open_or_create(&interface)?;
|
||||
let State { peers, cidrs } = http_get(&config.server.internal_endpoint, "/user/state")?;
|
||||
let State { peers, cidrs } = Api::new(&config.server).http("GET", "/user/state")?;
|
||||
|
||||
let device_info = DeviceInfo::get_by_name(&interface)?;
|
||||
let interface_public_key = device_info
|
||||
|
@ -419,12 +419,13 @@ fn fetch(
|
|||
fn add_cidr(interface: &InterfaceName) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
println!("Fetching CIDRs");
|
||||
let cidrs: Vec<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
|
||||
let api = Api::new(&server);
|
||||
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
|
||||
|
||||
let cidr_request = prompts::add_cidr(&cidrs)?;
|
||||
|
||||
println!("Creating CIDR...");
|
||||
let cidr: Cidr = http_post(&server.internal_endpoint, "/admin/cidrs", cidr_request)?;
|
||||
let cidr: Cidr = api.http_form("POST", "/admin/cidrs", cidr_request)?;
|
||||
|
||||
printdoc!(
|
||||
"
|
||||
|
@ -443,15 +444,17 @@ fn add_cidr(interface: &InterfaceName) -> Result<(), Error> {
|
|||
|
||||
fn add_peer(interface: &InterfaceName) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
let api = Api::new(&server);
|
||||
|
||||
println!("Fetching CIDRs");
|
||||
let cidrs: Vec<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
|
||||
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
|
||||
println!("Fetching peers");
|
||||
let peers: Vec<Peer> = http_get(&server.internal_endpoint, "/admin/peers")?;
|
||||
let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
|
||||
let cidr_tree = CidrTree::new(&cidrs[..]);
|
||||
|
||||
if let Some((peer_request, keypair)) = prompts::add_peer(&peers, &cidr_tree)? {
|
||||
println!("Creating peer...");
|
||||
let peer: Peer = http_post(&server.internal_endpoint, "/admin/peers", peer_request)?;
|
||||
let peer: Peer = api.http_form("POST", "/admin/peers", peer_request)?;
|
||||
let server_peer = peers.iter().find(|p| p.id == 1).unwrap();
|
||||
prompts::save_peer_invitation(
|
||||
interface,
|
||||
|
@ -470,17 +473,15 @@ fn add_peer(interface: &InterfaceName) -> Result<(), Error> {
|
|||
|
||||
fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
let api = Api::new(&server);
|
||||
|
||||
println!("Fetching peers.");
|
||||
let peers: Vec<Peer> = http_get(&server.internal_endpoint, "/admin/peers")?;
|
||||
let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
|
||||
|
||||
if let Some(peer) = prompts::enable_or_disable_peer(&peers[..], enable)? {
|
||||
let Peer { id, mut contents } = peer;
|
||||
contents.is_disabled = !enable;
|
||||
http_put(
|
||||
&server.internal_endpoint,
|
||||
&format!("/admin/peers/{}", id),
|
||||
contents,
|
||||
)?;
|
||||
api.http_form("PUT", &format!("/admin/peers/{}", id), contents)?;
|
||||
} else {
|
||||
println!("exited without disabling peer.");
|
||||
}
|
||||
|
@ -490,13 +491,14 @@ fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(),
|
|||
|
||||
fn add_association(interface: &InterfaceName) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
let api = Api::new(&server);
|
||||
|
||||
println!("Fetching CIDRs");
|
||||
let cidrs: Vec<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
|
||||
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
|
||||
|
||||
if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? {
|
||||
http_post(
|
||||
&server.internal_endpoint,
|
||||
api.http_form(
|
||||
"POST",
|
||||
"/admin/associations",
|
||||
AssociationContents {
|
||||
cidr_id_1: cidr1.id,
|
||||
|
@ -512,18 +514,15 @@ fn add_association(interface: &InterfaceName) -> Result<(), Error> {
|
|||
|
||||
fn delete_association(interface: &InterfaceName) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
let api = Api::new(&server);
|
||||
|
||||
println!("Fetching CIDRs");
|
||||
let cidrs: Vec<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
|
||||
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
|
||||
println!("Fetching associations");
|
||||
let associations: Vec<Association> =
|
||||
http_get(&server.internal_endpoint, "/admin/associations")?;
|
||||
let associations: Vec<Association> = api.http("GET", "/admin/associations")?;
|
||||
|
||||
if let Some(association) = prompts::delete_association(&associations[..], &cidrs[..])? {
|
||||
http_delete(
|
||||
&server.internal_endpoint,
|
||||
&format!("/admin/associations/{}", association.id),
|
||||
)?;
|
||||
api.http("DELETE", &format!("/admin/associations/{}", association.id))?;
|
||||
} else {
|
||||
println!("exited without adding association.");
|
||||
}
|
||||
|
@ -533,11 +532,12 @@ fn delete_association(interface: &InterfaceName) -> Result<(), Error> {
|
|||
|
||||
fn list_associations(interface: &InterfaceName) -> Result<(), Error> {
|
||||
let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
|
||||
let api = Api::new(&server);
|
||||
|
||||
println!("Fetching CIDRs");
|
||||
let cidrs: Vec<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
|
||||
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
|
||||
println!("Fetching associations");
|
||||
let associations: Vec<Association> =
|
||||
http_get(&server.internal_endpoint, "/admin/associations")?;
|
||||
let associations: Vec<Association> = api.http("GET", "/admin/associations")?;
|
||||
|
||||
for association in associations {
|
||||
println!(
|
||||
|
@ -590,8 +590,8 @@ fn override_endpoint(interface: &InterfaceName, unset: bool) -> Result<(), Error
|
|||
|
||||
if let Some(endpoint) = prompts::override_endpoint(unset)? {
|
||||
println!("Updating endpoint.");
|
||||
http_put(
|
||||
&config.server.internal_endpoint,
|
||||
Api::new(&config.server).http_form(
|
||||
"PUT",
|
||||
"/user/endpoint",
|
||||
EndpointContents::from(endpoint),
|
||||
)?;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::{ClientError, Error};
|
||||
use colored::*;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
use shared::{interface_config::ServerInfo, INNERNET_PUBKEY_HEADER};
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn human_duration(duration: Duration) -> String {
|
||||
match duration.as_secs() {
|
||||
|
@ -46,41 +47,56 @@ pub fn human_size(bytes: u64) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn http_get<T: DeserializeOwned>(server: &SocketAddr, endpoint: &str) -> Result<T, Error> {
|
||||
let response = ureq::get(&format!("http://{}/v1{}", server, endpoint)).call()?;
|
||||
process_response(response)
|
||||
pub struct Api<'a> {
|
||||
server: &'a ServerInfo,
|
||||
}
|
||||
|
||||
pub fn http_delete(server: &SocketAddr, endpoint: &str) -> Result<(), Error> {
|
||||
ureq::get(&format!("http://{}/v1{}", server, endpoint)).call()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn http_post<S: Serialize, D: DeserializeOwned>(
|
||||
server: &SocketAddr,
|
||||
endpoint: &str,
|
||||
form: S,
|
||||
) -> Result<D, Error> {
|
||||
let response = ureq::post(&format!("http://{}/v1{}", server, endpoint))
|
||||
.send_json(serde_json::to_value(form)?)?;
|
||||
process_response(response)
|
||||
}
|
||||
|
||||
pub fn http_put<S: Serialize>(server: &SocketAddr, endpoint: &str, form: S) -> Result<(), Error> {
|
||||
ureq::put(&format!("http://{}/v1{}", server, endpoint))
|
||||
.send_json(serde_json::to_value(form)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_response<T: DeserializeOwned>(response: ureq::Response) -> Result<T, Error> {
|
||||
let mut response = response.into_string()?;
|
||||
if response.is_empty() {
|
||||
response = "null".into();
|
||||
impl<'a> Api<'a> {
|
||||
pub fn new(server: &'a ServerInfo) -> Self {
|
||||
Self { server }
|
||||
}
|
||||
|
||||
pub fn http<T: DeserializeOwned>(&self, verb: &str, endpoint: &str) -> Result<T, Error> {
|
||||
self.request::<(), _>(verb, endpoint, None)
|
||||
}
|
||||
|
||||
pub fn http_form<S: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
verb: &str,
|
||||
endpoint: &str,
|
||||
form: S,
|
||||
) -> Result<T, Error> {
|
||||
self.request(verb, endpoint, Some(form))
|
||||
}
|
||||
|
||||
fn request<S: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
verb: &str,
|
||||
endpoint: &str,
|
||||
form: Option<S>,
|
||||
) -> Result<T, Error> {
|
||||
let request = ureq::request(
|
||||
verb,
|
||||
&format!("http://{}/v1{}", self.server.internal_endpoint, endpoint),
|
||||
)
|
||||
.set(INNERNET_PUBKEY_HEADER, &self.server.public_key);
|
||||
|
||||
let response = if let Some(form) = form {
|
||||
request.send_json(serde_json::to_value(form)?)?
|
||||
} else {
|
||||
request.call()?
|
||||
};
|
||||
|
||||
let mut response = response.into_string()?;
|
||||
// A little trick for serde to parse an empty response as `()`.
|
||||
if response.is_empty() {
|
||||
response = "null".into();
|
||||
}
|
||||
Ok(serde_json::from_str(&response).map_err(|e| {
|
||||
ClientError(format!(
|
||||
"failed to deserialize JSON response from the server: {}, response={}",
|
||||
e, &response
|
||||
))
|
||||
})?)
|
||||
}
|
||||
Ok(serde_json::from_str(&response).map_err(|e| {
|
||||
ClientError(format!(
|
||||
"failed to deserialize JSON response from the server: {}, response={}",
|
||||
e, &response
|
||||
))
|
||||
})?)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ rusqlite = "0.24"
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
shared = { path = "../shared" }
|
||||
subtle = "2"
|
||||
structopt = "0.3"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
|
|
|
@ -104,7 +104,8 @@ mod tests {
|
|||
};
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -132,7 +133,8 @@ mod tests {
|
|||
};
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -145,7 +147,8 @@ mod tests {
|
|||
cidr: test::EXPERIMENTAL_SUBCIDR.parse()?,
|
||||
parent: Some(cidr_res.id),
|
||||
};
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -166,7 +169,8 @@ mod tests {
|
|||
};
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::USER1_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::USER1_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -186,7 +190,8 @@ mod tests {
|
|||
parent: Some(test::ROOT_CIDR_ID),
|
||||
};
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -200,7 +205,8 @@ mod tests {
|
|||
};
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -220,7 +226,8 @@ mod tests {
|
|||
parent: Some(test::ROOT_CIDR_ID),
|
||||
};
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/cidrs")
|
||||
.body(serde_json::to_string(&contents)?)
|
||||
.reply(&filter)
|
||||
|
@ -253,7 +260,8 @@ mod tests {
|
|||
|
||||
let filter = crate::routes(server.context());
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
|
||||
.reply(&filter)
|
||||
|
@ -261,7 +269,8 @@ mod tests {
|
|||
// Should fail because child CIDR exists.
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/cidrs/{}", experimental_subcidr.id))
|
||||
.reply(&filter)
|
||||
|
@ -269,7 +278,8 @@ mod tests {
|
|||
// Deleting child "leaf" CIDR should fail because peer exists inside it.
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
|
||||
.reply(&filter)
|
||||
|
@ -304,7 +314,8 @@ mod tests {
|
|||
|
||||
let filter = crate::routes(server.context());
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id))
|
||||
.reply(&filter)
|
||||
|
|
|
@ -147,7 +147,8 @@ mod tests {
|
|||
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -172,7 +173,8 @@ mod tests {
|
|||
let peer = test::developer_peer_contents("devel oper", "10.80.64.4")?;
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -192,7 +194,8 @@ mod tests {
|
|||
let peer = test::developer_peer_contents("developer2", "10.80.64.4")?;
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -217,7 +220,8 @@ mod tests {
|
|||
let peer = test::developer_peer_contents("developer3", "10.80.64.3")?;
|
||||
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -241,7 +245,8 @@ mod tests {
|
|||
|
||||
// Try to add IP outside of the CIDR network.
|
||||
let peer = test::developer_peer_contents("developer3", "10.80.65.4")?;
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -250,7 +255,8 @@ mod tests {
|
|||
|
||||
// Try to use the network address as peer IP.
|
||||
let peer = test::developer_peer_contents("developer3", "10.80.64.0")?;
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -259,7 +265,8 @@ mod tests {
|
|||
|
||||
// Try to use the broadcast address as peer IP.
|
||||
let peer = test::developer_peer_contents("developer3", "10.80.64.255")?;
|
||||
let res = test::post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -281,7 +288,8 @@ mod tests {
|
|||
|
||||
// Try to create a new developer peer from a user peer.
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::post_request_from_ip(test::USER1_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::USER1_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.body(serde_json::to_string(&peer)?)
|
||||
.reply(&filter)
|
||||
|
@ -304,7 +312,8 @@ mod tests {
|
|||
|
||||
// Try to create a new developer peer from a user peer.
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::put_request_from_ip(test::ADMIN_PEER_IP)
|
||||
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)
|
||||
|
@ -325,7 +334,8 @@ mod tests {
|
|||
|
||||
// Try to create a new developer peer from a user peer.
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::put_request_from_ip(test::USER1_PEER_IP)
|
||||
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)
|
||||
|
@ -340,7 +350,8 @@ mod tests {
|
|||
async fn test_list_all_peers_from_admin() -> Result<()> {
|
||||
let server = test::Server::new()?;
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -369,7 +380,8 @@ mod tests {
|
|||
async fn test_list_all_peers_from_non_admin() -> Result<()> {
|
||||
let server = test::Server::new()?;
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
.path("/v1/admin/peers")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -386,7 +398,8 @@ mod tests {
|
|||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID))
|
||||
.reply(&filter)
|
||||
|
@ -409,7 +422,8 @@ mod tests {
|
|||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
|
||||
let res = test::request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID))
|
||||
.reply(&filter)
|
||||
|
@ -429,7 +443,8 @@ mod tests {
|
|||
let server = test::Server::new()?;
|
||||
let filter = crate::routes(server.context());
|
||||
|
||||
let res = test::request_from_ip(test::ADMIN_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::ADMIN_PEER_IP)
|
||||
.method("DELETE")
|
||||
.path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID + 100))
|
||||
.reply(&filter)
|
||||
|
|
|
@ -171,7 +171,8 @@ mod tests {
|
|||
async fn test_get_state_from_developer1() -> Result<()> {
|
||||
let server = test::Server::new()?;
|
||||
let filter = crate::routes(server.context());
|
||||
let res = test::request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
.path("/v1/user/state")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -195,7 +196,8 @@ mod tests {
|
|||
let server = test::Server::new()?;
|
||||
let filter = crate::routes(server.context());
|
||||
assert_eq!(
|
||||
test::put_request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
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()?
|
||||
|
@ -208,7 +210,8 @@ mod tests {
|
|||
|
||||
println!("{}", serde_json::to_string(&EndpointContents::Unset)?);
|
||||
assert_eq!(
|
||||
test::put_request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
server
|
||||
.put_request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
.path("/v1/user/endpoint")
|
||||
.body(serde_json::to_string(&EndpointContents::Unset)?)
|
||||
.reply(&filter)
|
||||
|
@ -218,7 +221,8 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
test::put_request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
server
|
||||
.put_request_from_ip(test::DEVELOPER1_PEER_IP)
|
||||
.path("/v1/user/endpoint")
|
||||
.body("endpoint=blah")
|
||||
.reply(&filter)
|
||||
|
@ -236,7 +240,8 @@ mod tests {
|
|||
let filter = crate::routes(server.context());
|
||||
|
||||
// Request comes from an unknown IP.
|
||||
let res = test::request_from_ip("10.80.80.80")
|
||||
let res = server
|
||||
.request_from_ip("10.80.80.80")
|
||||
.path("/v1/user/state")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -296,7 +301,8 @@ mod tests {
|
|||
}
|
||||
|
||||
for ip in &[test::DEVELOPER1_PEER_IP, test::EXPERIMENT_SUBCIDR_PEER_IP] {
|
||||
let res = test::request_from_ip(ip)
|
||||
let res = server
|
||||
.request_from_ip(ip)
|
||||
.path("/v1/user/state")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -344,7 +350,8 @@ mod tests {
|
|||
let filter = crate::routes(server.context());
|
||||
|
||||
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
|
||||
let res = test::request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
.path("/v1/user/state")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
@ -354,7 +361,8 @@ mod tests {
|
|||
let body = RedeemContents {
|
||||
public_key: "YBVIgpfLbi/knrMCTEb0L6eVy0daiZnJJQkxBK9s+2I=".into(),
|
||||
};
|
||||
let res = test::post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
.path("/v1/user/redeem")
|
||||
.body(serde_json::to_string(&body)?)
|
||||
.reply(&filter)
|
||||
|
@ -362,7 +370,8 @@ mod tests {
|
|||
assert!(res.status().is_success());
|
||||
|
||||
// Step 3: Ensure that a second attempt at redemption DOESN'T work.
|
||||
let res = test::post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
let res = server
|
||||
.post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
.path("/v1/user/redeem")
|
||||
.body(serde_json::to_string(&body)?)
|
||||
.reply(&filter)
|
||||
|
@ -370,7 +379,8 @@ mod tests {
|
|||
assert!(res.status().is_client_error());
|
||||
|
||||
// Step 3: Ensure that after redemption, fetching state works.
|
||||
let res = test::request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
let res = server
|
||||
.request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP)
|
||||
.path("/v1/user/state")
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
|
|
@ -6,7 +6,7 @@ use ipnetwork::IpNetwork;
|
|||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use shared::IoErrorContext;
|
||||
use shared::{IoErrorContext, INNERNET_PUBKEY_HEADER};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
env,
|
||||
|
@ -18,8 +18,9 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use warp::Filter;
|
||||
use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, PeerConfigBuilder};
|
||||
use subtle::ConstantTimeEq;
|
||||
use warp::{filters, Filter};
|
||||
use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuilder};
|
||||
|
||||
pub mod api;
|
||||
pub mod db;
|
||||
|
@ -68,6 +69,7 @@ pub struct Context {
|
|||
pub db: Db,
|
||||
pub endpoints: Arc<Endpoints>,
|
||||
pub interface: InterfaceName,
|
||||
pub public_key: Key,
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
|
@ -297,11 +299,13 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
|
|||
|
||||
log::info!("{} peers added to wireguard interface.", peers.len());
|
||||
|
||||
let public_key = wgctrl::Key::from_base64(&config.private_key)?.generate_public();
|
||||
let db = Arc::new(Mutex::new(conn));
|
||||
let context = Context {
|
||||
db,
|
||||
interface: *interface,
|
||||
endpoints,
|
||||
public_key,
|
||||
};
|
||||
|
||||
log::info!("innernet-server {} starting.", VERSION);
|
||||
|
@ -363,34 +367,6 @@ pub fn routes(
|
|||
.recover(handle_rejection)
|
||||
}
|
||||
|
||||
pub fn with_unredeemed_session(
|
||||
context: Context,
|
||||
) -> impl Filter<Extract = (UnredeemedSession,), Error = warp::Rejection> + Clone {
|
||||
warp::filters::addr::remote()
|
||||
.and_then(move |addr: Option<SocketAddr>| {
|
||||
get_session(context.clone(), addr.map(|addr| addr.ip()), false, false)
|
||||
})
|
||||
.map(|session| UnredeemedSession(session))
|
||||
}
|
||||
|
||||
pub fn with_session(
|
||||
context: Context,
|
||||
) -> impl Filter<Extract = (Session,), Error = warp::Rejection> + Clone {
|
||||
warp::filters::addr::remote().and_then(move |addr: Option<SocketAddr>| {
|
||||
get_session(context.clone(), addr.map(|addr| addr.ip()), false, true)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_admin_session(
|
||||
context: Context,
|
||||
) -> impl Filter<Extract = (AdminSession,), Error = warp::Rejection> + Clone {
|
||||
warp::filters::addr::remote()
|
||||
.and_then(move |addr: Option<SocketAddr>| {
|
||||
get_session(context.clone(), addr.map(|addr| addr.ip()), true, true)
|
||||
})
|
||||
.map(|session| AdminSession(session))
|
||||
}
|
||||
|
||||
pub fn form_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone
|
||||
where
|
||||
T: DeserializeOwned + Send,
|
||||
|
@ -398,24 +374,88 @@ where
|
|||
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
|
||||
}
|
||||
|
||||
pub fn with_unredeemed_session(
|
||||
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> {
|
||||
addr.map(|addr| -> Result<Session, ServerError> {
|
||||
_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)?;
|
||||
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)?;
|
||||
|
||||
if !peer.is_disabled && (!admin_only || peer.is_admin) && (!redeemed_only || peer.is_redeemed) {
|
||||
Ok(Session { context, peer })
|
||||
} else {
|
||||
Err(ServerError::Unauthorized)
|
||||
if !peer.is_disabled
|
||||
&& (!admin_only || peer.is_admin)
|
||||
&& (!redeemed_only || peer.is_redeemed)
|
||||
{
|
||||
return Ok(Session { context, peer });
|
||||
}
|
||||
})
|
||||
.map(|session| session.ok())
|
||||
.flatten() // If no IP address is found, reject.
|
||||
.ok_or_else(|| { warp::reject::custom(ServerError::Unauthorized)})
|
||||
}
|
||||
|
||||
Err(ServerError::Unauthorized.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -442,7 +482,8 @@ mod tests {
|
|||
let filter = routes(server.context());
|
||||
|
||||
// Request from an unknown IP, trying to disguise as an admin using HTTP headers.
|
||||
let res = test::request_from_ip("10.80.80.80")
|
||||
let res = server
|
||||
.request_from_ip("10.80.80.80")
|
||||
.path("/v1/admin/peers")
|
||||
.header("Forwarded", format!("for={}", test::ADMIN_PEER_IP))
|
||||
.header("X-Forwarded-For", test::ADMIN_PEER_IP)
|
||||
|
@ -457,4 +498,48 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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")
|
||||
.header(shared::INNERNET_PUBKEY_HEADER, key.to_base64())
|
||||
.reply(&filter)
|
||||
.await;
|
||||
|
||||
// addr::remote() filter only look at remote_addr from TCP socket.
|
||||
// HTTP headers are not considered. This also means that innernet
|
||||
// server would not function behind an HTTP proxy.
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
// addr::remote() filter only look at remote_addr from TCP socket.
|
||||
// HTTP headers are not considered. This also means that innernet
|
||||
// server would not function behind an HTTP proxy.
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use shared::{Cidr, CidrContents, PeerContents};
|
|||
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
use tempfile::TempDir;
|
||||
use warp::test::RequestBuilder;
|
||||
use wgctrl::{InterfaceName, KeyPair};
|
||||
use wgctrl::{InterfaceName, Key, KeyPair};
|
||||
|
||||
pub const ROOT_CIDR: &str = "10.80.0.0/15";
|
||||
pub const SERVER_CIDR: &str = "10.80.0.1/32";
|
||||
|
@ -47,6 +47,7 @@ pub struct Server {
|
|||
endpoints: Arc<Endpoints>,
|
||||
interface: InterfaceName,
|
||||
conf: ServerConfig,
|
||||
public_key: Key,
|
||||
// The directory will be removed during destruction.
|
||||
_test_dir: TempDir,
|
||||
}
|
||||
|
@ -56,6 +57,7 @@ impl Server {
|
|||
let test_dir = tempfile::tempdir()?;
|
||||
let test_dir_path = test_dir.path();
|
||||
|
||||
let public_key = Key::generate_private().generate_public();
|
||||
// Run the init wizard to initialize the database and create basic
|
||||
// cidrs and peers.
|
||||
let interface = "test".to_string();
|
||||
|
@ -116,6 +118,7 @@ impl Server {
|
|||
db,
|
||||
endpoints,
|
||||
interface,
|
||||
public_key,
|
||||
_test_dir: test_dir,
|
||||
})
|
||||
}
|
||||
|
@ -129,12 +132,32 @@ impl Server {
|
|||
db: self.db.clone(),
|
||||
interface: self.interface.clone(),
|
||||
endpoints: self.endpoints.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wg_conf_path(&self) -> PathBuf {
|
||||
self.conf.config_path(&self.interface)
|
||||
}
|
||||
|
||||
pub fn request_from_ip(&self, ip_str: &str) -> RequestBuilder {
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn post_request_from_ip(&self, ip_str: &str) -> RequestBuilder {
|
||||
self.request_from_ip(ip_str)
|
||||
.method("POST")
|
||||
.header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
pub fn put_request_from_ip(&self, ip_str: &str) -> RequestBuilder {
|
||||
self.request_from_ip(ip_str)
|
||||
.method("PUT")
|
||||
.header("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result<Cidr> {
|
||||
|
@ -154,23 +177,6 @@ pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result<Cidr>
|
|||
// Below are helper functions for writing tests.
|
||||
//
|
||||
|
||||
pub fn request_from_ip(ip_str: &str) -> RequestBuilder {
|
||||
let port = 54321u16;
|
||||
warp::test::request().remote_addr(SocketAddr::new(ip_str.parse().unwrap(), port))
|
||||
}
|
||||
|
||||
pub fn post_request_from_ip(ip_str: &str) -> RequestBuilder {
|
||||
request_from_ip(ip_str)
|
||||
.method("POST")
|
||||
.header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
pub fn put_request_from_ip(ip_str: &str) -> RequestBuilder {
|
||||
request_from_ip(ip_str)
|
||||
.method("PUT")
|
||||
.header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
pub fn peer_contents(
|
||||
name: &str,
|
||||
ip_str: &str,
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
};
|
||||
use wgctrl::InterfaceName;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct InterfaceConfig {
|
||||
/// The information to bring up the interface.
|
||||
|
@ -21,7 +21,7 @@ pub struct InterfaceConfig {
|
|||
pub server: ServerInfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct InterfaceInfo {
|
||||
/// The interface name (i.e. "tonari")
|
||||
|
@ -38,7 +38,7 @@ pub struct InterfaceInfo {
|
|||
pub listen_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ServerInfo {
|
||||
/// The server's WireGuard public key
|
||||
|
|
|
@ -27,7 +27,8 @@ lazy_static! {
|
|||
pub static ref REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5);
|
||||
}
|
||||
|
||||
pub static PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
|
||||
pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
|
||||
pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
|
||||
|
||||
pub type Error = Box<dyn std::error::Error>;
|
||||
|
||||
|
|
|
@ -366,7 +366,7 @@ pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result<
|
|||
/// `Key`s, especially ones created from external data.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub struct Key([u8; 32]);
|
||||
pub struct Key(pub [u8; 32]);
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl Key {
|
||||
|
|
Loading…
Reference in New Issue