Skip to content

Commit

Permalink
env var to allow unauthed clients
Browse files Browse the repository at this point in the history
  • Loading branch information
agrinman committed Apr 19, 2020
1 parent 5f7bf27 commit 4daf3ed
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 40 deletions.
26 changes: 15 additions & 11 deletions src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub struct Config {
pub host: String,
pub local_port: String,
pub sub_domain: String,
pub secret_key: SecretKey,
pub secret_key: Option<SecretKey>,
pub tls_off: bool
}

Expand Down Expand Up @@ -91,16 +91,20 @@ impl Config {
},
SubCommand::Start { key, sub_domain, port } => {
(match key {
Some(key) => key,
Some(key) => Some(key),
None => {
let key_file_path = match dirs::home_dir().map(|h| h.join(WORMHOLE_DIR).join(SECRET_KEY_FILE)) {
Some(path) => path,
None => {
panic!("Missing authentication key file. Could not find home directory.")
}
};

std::fs::read_to_string(key_file_path).expect("Missing authentication token. Try running the `auth` command.")
dirs::home_dir()
.map(|h| h.join(WORMHOLE_DIR).join(SECRET_KEY_FILE))
.map(|path| {
if path.exists() {
std::fs::read_to_string(path)
.map_err(|e| error!("Error reading authentication token: {:?}", e))
.ok()
} else {
None
}
})
.unwrap_or(None)
}
}, sub_domain, port)
}
Expand Down Expand Up @@ -128,7 +132,7 @@ impl Config {
host,
local_port,
sub_domain: sub_domain.unwrap_or(ServerHello::random_domain()),
secret_key: SecretKey(secret_key),
secret_key: secret_key.map(|s| SecretKey(s)),
tls_off,
})
}
Expand Down
32 changes: 26 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ impl SecretKey {
rand::thread_rng().fill_bytes(&mut key);
Self(base64::encode_config(&key, base64::URL_SAFE_NO_PAD))
}

#[allow(unused)]
pub fn anonymous_key() -> Self {
let mut key = [0u8; 32];
Self(base64::encode_config(&key, base64::URL_SAFE_NO_PAD))
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -43,34 +49,48 @@ const CLIENT_HELLO_TTL_SECONDS:i64 = 300;
pub struct ClientHello {
pub id: ClientId,
pub sub_domain: Option<String>,
pub is_anonymous: bool,
// epoch
unix_seconds: i64,
//hex encoded
signature: String,
}

impl ClientHello {
pub fn generate(id: ClientId, secret_key: &SecretKey, sub_domain: Option<String>) -> (Self, ClientId) {
pub fn generate(id: ClientId, secret_key: &Option<SecretKey>, sub_domain: Option<String>) -> (Self, ClientId) {
let unix_seconds = Utc::now().timestamp();

let input = format!("{}", unix_seconds);
let signature = hmac_sha256::HMAC::mac(input.as_bytes(), secret_key.0.as_bytes());
let signature = match secret_key {
Some(key) => hmac_sha256::HMAC::mac(input.as_bytes(), key.0.as_bytes()),
None => hmac_sha256::HMAC::mac(input.as_bytes(), SecretKey::anonymous_key().0.as_bytes()),
};

(ClientHello {
id: id.clone(), sub_domain, unix_seconds, signature: hex::encode(signature)
id: id.clone(), sub_domain, unix_seconds, signature: hex::encode(signature), is_anonymous: secret_key.is_none()
}, id)
}

#[allow(unused)]
pub fn verify(secret_key: &SecretKey, data: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
pub fn verify(secret_key: &SecretKey, data: &[u8], allow_unknown: bool) -> Result<Self, Box<dyn std::error::Error>> {
let client_hello:ClientHello = serde_json::from_slice(&data)?;

// check the time
if (Utc::now().timestamp() - client_hello.unix_seconds).abs() > CLIENT_HELLO_TTL_SECONDS {
return Err("Expired client hello".into())
}

// check that anonymous is allowed
if !allow_unknown && client_hello.is_anonymous {
return Err("Anonymous clients are not allowed".into())
}

let input = format!("{}", client_hello.unix_seconds);
let expected = hmac_sha256::HMAC::mac(input.as_bytes(), secret_key.0.as_bytes());

let expected = if client_hello.is_anonymous {
hmac_sha256::HMAC::mac(input.as_bytes(), SecretKey::anonymous_key().0.as_bytes())
} else {
hmac_sha256::HMAC::mac(input.as_bytes(), secret_key.0.as_bytes())
};

if hex::encode(expected) != client_hello.signature {
return Err("Bad signature in client hello".into())
Expand Down
53 changes: 30 additions & 23 deletions src/server/control_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,41 @@ async fn try_client_handshake(mut websocket: WebSocket) -> Option<(WebSocket, Cl
},
};

let client_hello = ClientHello::verify(&SECRET_KEY, client_hello_data.as_bytes()).map_err(|e| format!("{:?}", e));
let client_hello = ClientHello::verify(&SECRET_KEY, client_hello_data.as_bytes(), allow_unknown_clients())
.map_err(|e| format!("{:?}", e));

let (client_hello, sub_domain) = match client_hello {
Ok(ch) => {
// check that the subdomain is available and valid
let sub_domain = if let Some(sub_domain) = &ch.sub_domain {
// ignore uppercase
let sub_domain = sub_domain.to_lowercase();

if sub_domain.chars().filter(|c| !c.is_alphanumeric()).count() > 0 {
error!("invalid client hello: only alphanumeric chars allowed!");
let data = serde_json::to_vec(&ServerHello::InvalidSubDomain).unwrap_or_default();
let _ = websocket.send(Message::binary(data)).await;
return None
}

let existing_client = Connections::client_for_host(&sub_domain);
if existing_client.is_some() && Some(&ch.id) != existing_client.as_ref() {
error!("invalid client hello: requested sub domain in use already!");
let data = serde_json::to_vec(&ServerHello::SubDomainInUse).unwrap_or_default();
let _ = websocket.send(Message::binary(data)).await;
return None
let sub_domain = match (ch.is_anonymous, &ch.sub_domain) {
// don't allow anonymous clients to pick subdomains
(true, _) | (_, None) => ServerHello::random_domain(),

// otherwise, try to assign the sub domain
(false, Some(sub_domain)) => {
// ignore uppercase
let sub_domain = sub_domain.to_lowercase();

if sub_domain.chars().filter(|c| !c.is_alphanumeric()).count() > 0 {
error!("invalid client hello: only alphanumeric chars allowed!");
let data = serde_json::to_vec(&ServerHello::InvalidSubDomain).unwrap_or_default();
let _ = websocket.send(Message::binary(data)).await;
return None
}

let existing_client = Connections::client_for_host(&sub_domain);
if existing_client.is_some() && Some(&ch.id) != existing_client.as_ref() {
error!("invalid client hello: requested sub domain in use already!");
let data = serde_json::to_vec(&ServerHello::SubDomainInUse).unwrap_or_default();
let _ = websocket.send(Message::binary(data)).await;
return None
}

sub_domain
}

sub_domain
} else {
ServerHello::random_domain()
};


(ch, sub_domain)
},
Err(e) => {
Expand All @@ -91,7 +98,7 @@ async fn try_client_handshake(mut websocket: WebSocket) -> Option<(WebSocket, Cl
return None
}

info!("new client connected: {:?}", &client_hello.id);
info!("new client connected: {:?}{}", &client_hello.id, if client_hello.is_anonymous { " (anonymous)"} else { "" });
Some((websocket, client_hello.id, sub_domain))
}

Expand Down
12 changes: 12 additions & 0 deletions src/server/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ lazy_static! {
pub static ref ALLOWED_HOSTS:Vec<String> = allowed_host_suffixes();
}

/// TODO: add support for client registration and per-client api keys
/// For now this admin key is only for locking down custom deployments
/// See `allow_non_authenticated` for more.
pub fn load_secret_key() -> SecretKey {
match std::env::var("SECRET_KEY") {
Ok(key) => SecretKey(key),
Expand All @@ -40,12 +43,21 @@ pub fn load_secret_key() -> SecretKey {
}
}

/// What hosts do we allow tunnels on:
/// i.e: baz.com => *.baz.com
/// foo.bar => *.foo.bar
pub fn allowed_host_suffixes() -> Vec<String> {
std::env::var("ALLOWED_HOSTS")
.map(|s| s.split(",").map(String::from).collect())
.unwrap_or(vec![])
}

/// For demo purposes, allow unknown client connections
/// controlled by an env below
pub fn allow_unknown_clients() -> bool {
std::env::var("ALLOW_UNKNOWN_CLIENTS").is_ok()
}

#[tokio::main]
async fn main() {
pretty_env_logger::init();
Expand Down

0 comments on commit 4daf3ed

Please sign in to comment.