use std::{
future::Future,
io,
net::{Ipv4Addr, SocketAddr},
path::{Path, PathBuf},
};
use base64::Engine;
use bytes::{Buf, Bytes};
use http::{
header::{AUTHORIZATION, HOST},
Request, Response, Uri,
};
use http_body_util::{BodyExt, Empty};
use hyper::body::Incoming;
use hyper_util::rt::TokioIo;
use tokio::net::{TcpSocket, UnixStream};
pub use types::*;
pub mod types;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("connection failed")]
IoError(#[from] io::Error),
#[error("request failed")]
HyperError(#[from] hyper::Error),
#[error("http error")]
HttpError(#[from] http::Error),
#[error("unprocessible entity")]
UnprocessableEntity,
#[error("unable to parse json")]
ParsingError(#[from] serde_json::Error),
#[error("unable to parse certificate or key")]
UnknownCertificateOrKey,
}
pub type Result<T> = std::result::Result<T, Error>;
pub trait LocalApiClient: Clone {
fn get(&self, uri: Uri) -> impl Future<Output = Result<Response<Incoming>>> + Send;
}
#[derive(Clone)]
pub struct LocalApi<T: LocalApiClient> {
client: T,
}
impl LocalApi<UnixStreamClient> {
pub fn new_with_socket_path<P: AsRef<Path>>(socket_path: P) -> Self {
let socket_path = socket_path.as_ref().to_path_buf();
let client = UnixStreamClient { socket_path };
Self { client }
}
}
impl LocalApi<TcpWithPasswordClient> {
pub fn new_with_port_and_password<S: Into<String>>(port: u16, password: S) -> Self {
let password = password.into();
let client = TcpWithPasswordClient { port, password };
Self { client }
}
}
impl<T: LocalApiClient> LocalApi<T> {
pub async fn certificate_pair(&self, domain: &str) -> Result<(PrivateKey, Vec<Certificate>)> {
let response = self
.client
.get(
format!("/localapi/v0/cert/{domain}?type=pair")
.parse()
.unwrap(),
)
.await?;
let body = response.into_body().collect().await?.aggregate();
let items = rustls_pemfile::read_all(&mut body.reader())
.collect::<std::result::Result<Vec<_>, _>>()?;
let (certificates, mut private_keys) = items
.into_iter()
.map(|item| match item {
rustls_pemfile::Item::Sec1Key(data) => Ok((false, data.secret_sec1_der().to_vec())),
rustls_pemfile::Item::Pkcs8Key(data) => {
Ok((false, data.secret_pkcs8_der().to_vec()))
}
rustls_pemfile::Item::Pkcs1Key(data) => {
Ok((false, data.secret_pkcs1_der().to_vec()))
}
rustls_pemfile::Item::X509Certificate(data) => Ok((true, data.to_vec())),
_ => Err(Error::UnknownCertificateOrKey),
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.partition::<Vec<(bool, Vec<u8>)>, _>(|&(cert, _)| cert);
let certificates = certificates
.into_iter()
.map(|(_, data)| Certificate(data))
.collect();
let (_, private_key_data) = private_keys.pop().ok_or(Error::UnknownCertificateOrKey)?;
let private_key = PrivateKey(private_key_data);
Ok((private_key, certificates))
}
pub async fn status(&self) -> Result<Status> {
let response = self
.client
.get(Uri::from_static("/localapi/v0/status"))
.await?;
let body = response.into_body().collect().await?.aggregate();
let status = serde_json::de::from_reader(body.reader())?;
Ok(status)
}
pub async fn whois(&self, address: SocketAddr) -> Result<Whois> {
let response = self
.client
.get(
format!("/localapi/v0/whois?addr={address}")
.parse()
.unwrap(),
)
.await?;
let body = response.into_body().collect().await?.aggregate();
let whois = serde_json::de::from_reader(body.reader())?;
Ok(whois)
}
}
#[derive(Clone)]
pub struct UnixStreamClient {
socket_path: PathBuf,
}
impl LocalApiClient for UnixStreamClient {
async fn get(&self, uri: Uri) -> Result<Response<Incoming>> {
let request = Request::builder()
.method("GET")
.header(HOST, "local-tailscaled.sock")
.uri(uri)
.body(Empty::<Bytes>::new())?;
let response = self.request(request).await?;
Ok(response)
}
}
impl UnixStreamClient {
async fn request(&self, request: Request<Empty<Bytes>>) -> Result<Response<Incoming>> {
let stream = TokioIo::new(UnixStream::connect(&self.socket_path).await?);
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(stream).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Error in connection: {}", e);
}
});
let response = request_sender.send_request(request).await?;
if response.status() == 200 {
Ok(response)
} else {
Err(Error::UnprocessableEntity)
}
}
}
#[derive(Clone)]
pub struct TcpWithPasswordClient {
port: u16,
password: String,
}
impl LocalApiClient for TcpWithPasswordClient {
async fn get(&self, uri: Uri) -> Result<Response<Incoming>> {
let request = Request::builder()
.method("GET")
.header(HOST, "local-tailscaled.sock")
.header(
AUTHORIZATION,
format!(
"Basic {}",
base64::engine::general_purpose::STANDARD_NO_PAD
.encode(format!(":{}", self.password))
),
)
.header("Sec-Tailscale", "localapi")
.uri(uri)
.body(Empty::<Bytes>::new())?;
let response = self.request(request).await?;
Ok(response)
}
}
impl TcpWithPasswordClient {
async fn request(&self, request: Request<Empty<Bytes>>) -> Result<Response<Incoming>> {
let stream = TcpSocket::new_v4()?
.connect((Ipv4Addr::LOCALHOST, self.port).into())
.await?;
let stream = TokioIo::new(stream);
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(stream).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Error in connection: {}", e);
}
});
let response = request_sender.send_request(request).await?;
if response.status() == 200 {
Ok(response)
} else {
Err(Error::UnprocessableEntity)
}
}
}