use rand::Rng;
use std::{fs, io, net, path};
use crate::error::Error;
const GEN_CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
const GEN_LENGTH: usize = 25;
const LT: &str = "bwt::auth";
pub enum AuthMethod {
UserProvided(String),
Cookie(path::PathBuf),
Ephemeral,
None,
}
impl AuthMethod {
pub fn get_token(self) -> Result<Option<String>, io::Error> {
Ok(match self {
AuthMethod::UserProvided(token) => Some(token),
AuthMethod::Cookie(file) => Some(read_write_cookie(&file)?),
AuthMethod::Ephemeral => Some(generate_token()),
AuthMethod::None => None,
})
}
}
fn generate_token() -> String {
let mut rng = rand::thread_rng();
(0..GEN_LENGTH)
.map(|_| {
let idx = rng.gen_range(0..GEN_CHARSET.len());
GEN_CHARSET[idx] as char
})
.collect()
}
fn read_write_cookie(file: &path::Path) -> Result<String, io::Error> {
if file.exists() {
info!(
target: LT,
"Reading from cookie file: {}",
file.to_string_lossy()
);
fs::read_to_string(file)
} else {
info!(
target: LT,
"Writing generated token to cookie file: {}",
file.to_string_lossy()
);
let token = generate_token();
fs::write(file, token.clone())?;
Ok(token)
}
}
#[cfg(feature = "electrum")]
pub fn electrum_socks5_auth(
mut stream: net::TcpStream,
access_token: &str,
) -> Result<net::TcpStream, Error> {
use std::io::{BufReader, Read, Write};
const SOCKS5: u8 = 0x05;
const AUTH_VER: u8 = 0x01;
const AUTH_NONE: u8 = 0x00;
const AUTH_USERPWD: u8 = 0x02;
const SUCCESS: u8 = 0x00;
const CONNECT: u8 = 0x01;
const RSV: u8 = 0x00;
const ADDR_IPV4: u8 = 0x01;
const ADDR_DOMAIN: u8 = 0x03;
let read_byte = |reader: &mut BufReader<_>| -> io::Result<_> {
let mut buf = [0; 1];
reader.read_exact(&mut buf)?;
Ok(buf[0])
};
let read_var = |reader: &mut BufReader<_>| -> Result<_, Error> {
let len = read_byte(reader)? as u64;
let mut buf = vec![];
let mut chunk = reader.take(len);
chunk.read_to_end(&mut buf)?;
ensure!(buf.len() as u64 == len, "unexpected EOF");
Ok(buf)
};
let mut reader = BufReader::new(stream.try_clone().expect("failed to clone TcpStream"));
ensure!(read_byte(&mut reader)? == SOCKS5, "invalid version");
let auth_methods = read_var(&mut reader)?;
let mut authenticated = false;
if auth_methods.contains(&AUTH_USERPWD) {
stream.write_all(&[SOCKS5, AUTH_USERPWD])?;
ensure!(read_byte(&mut reader)? == AUTH_VER, "invalid auth version");
let _username = read_var(&mut reader)?; let password = String::from_utf8(read_var(&mut reader)?)?;
ensure!(password == access_token, "invalid token (userpwd)");
authenticated = true;
stream.write_all(&[AUTH_VER, SUCCESS])?;
} else if auth_methods.contains(&AUTH_NONE) {
stream.write_all(&[SOCKS5, AUTH_NONE])?;
} else {
bail!("incompatible auth methods");
}
let mut buf = [0; 4];
reader.read_exact(&mut buf)?;
ensure!(buf[0..3] == [SOCKS5, CONNECT, RSV], "invalid connect");
match buf[3] {
ADDR_IPV4 => reader.read_exact(&mut [0; 4])?,
ADDR_DOMAIN => {
let hostname = String::from_utf8(read_var(&mut reader)?)?;
ensure!(authenticated || hostname == access_token, "invalid token");
authenticated = true;
}
_ => bail!("invalid socks5 address type"),
};
ensure!(authenticated, "no token was offered");
reader.read_exact(&mut [0; 2])?;
stream.write_all(&[
SOCKS5, SUCCESS, RSV, ADDR_IPV4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
])?;
Ok(stream)
}
#[cfg(feature = "http")]
pub fn http_basic_auth(
access_token: Option<String>,
) -> warp::filters::BoxedFilter<(Result<(), Error>,)> {
use bitcoin::base64;
use std::sync::Arc;
use warp::http::StatusCode;
use warp::Filter;
fn parse_header(header_val: String) -> Option<(String, String)> {
if header_val.to_ascii_lowercase().starts_with("basic ") {
let auth_base64 = &header_val[6..];
let auth_decoded = String::from_utf8(base64::decode(&auth_base64).ok()?).ok()?;
let mut parts = auth_decoded.splitn(2, ':');
Some((parts.next()?.into(), parts.next()?.into()))
} else {
None
}
}
if let Some(access_token) = access_token {
let access_token = Arc::new(access_token);
warp::any()
.and(warp::any().map(move || access_token.clone()))
.and(warp::header::optional("authorization"))
.map(|access_token: Arc<String>, auth_header: Option<String>| {
let password = auth_header.and_then(parse_header).map(|creds| creds.1);
ensure!(
password == Some(access_token.to_string()),
StatusCode::UNAUTHORIZED
);
Ok(())
})
.boxed()
} else {
warp::any().map(|| Ok(())).boxed()
}
}