tailscale_localapi/
lib.rs1use std::{
2 io,
3 net::{Ipv4Addr, SocketAddr},
4 path::{Path, PathBuf},
5};
6
7use async_trait::async_trait;
8use base64::Engine;
9use http::{
10 header::{AUTHORIZATION, HOST},
11 Request, Response, Uri,
12};
13use hyper::{body::Buf, Body};
14use tokio::net::{TcpSocket, UnixStream};
15pub use types::*;
16
17pub mod types;
19
20#[derive(thiserror::Error, Debug)]
22pub enum Error {
23 #[error("connection failed")]
24 IoError(#[from] io::Error),
25 #[error("request failed")]
26 HyperError(#[from] hyper::Error),
27 #[error("http error")]
28 HttpError(#[from] hyper::http::Error),
29 #[error("unprocessible entity")]
30 UnprocessableEntity,
31 #[error("unable to parse json")]
32 ParsingError(#[from] serde_json::Error),
33 #[error("unable to parse certificate or key")]
34 UnknownCertificateOrKey,
35}
36
37pub type Result<T> = std::result::Result<T, Error>;
39
40#[async_trait]
42pub trait LocalApiClient: Clone {
43 async fn get(&self, uri: Uri) -> Result<Response<Body>>;
44}
45
46#[derive(Clone)]
48pub struct LocalApi<T: LocalApiClient> {
49 client: T,
51}
52
53impl LocalApi<UnixStreamClient> {
54 pub fn new_with_socket_path<P: AsRef<Path>>(socket_path: P) -> Self {
57 let socket_path = socket_path.as_ref().to_path_buf();
58 let client = UnixStreamClient { socket_path };
59 Self { client }
60 }
61}
62
63impl LocalApi<TcpWithPasswordClient> {
64 pub fn new_with_port_and_password<S: Into<String>>(port: u16, password: S) -> Self {
67 let password = password.into();
68 let client = TcpWithPasswordClient { port, password };
69 Self { client }
70 }
71}
72
73impl<T: LocalApiClient> LocalApi<T> {
74 pub async fn certificate_pair(&self, domain: &str) -> Result<(PrivateKey, Vec<Certificate>)> {
77 let response = self
78 .client
79 .get(
80 format!("/localapi/v0/cert/{domain}?type=pair")
81 .parse()
82 .unwrap(),
83 )
84 .await?;
85
86 let body = hyper::body::aggregate(response.into_body()).await?;
87 let items = rustls_pemfile::read_all(&mut body.reader())?;
88 let (certificates, mut private_keys) = items
89 .into_iter()
90 .map(|item| match item {
91 rustls_pemfile::Item::ECKey(data)
92 | rustls_pemfile::Item::PKCS8Key(data)
93 | rustls_pemfile::Item::RSAKey(data) => Ok((false, data)),
94 rustls_pemfile::Item::X509Certificate(data) => Ok((true, data)),
95 _ => Err(Error::UnknownCertificateOrKey),
96 })
97 .collect::<Result<Vec<_>>>()?
98 .into_iter()
99 .partition::<Vec<(bool, Vec<u8>)>, _>(|&(cert, _)| cert);
100
101 let certificates = certificates
102 .into_iter()
103 .map(|(_, data)| Certificate(data))
104 .collect();
105 let (_, private_key_data) = private_keys.pop().ok_or(Error::UnknownCertificateOrKey)?;
106 let private_key = PrivateKey(private_key_data);
107
108 Ok((private_key, certificates))
109 }
110
111 pub async fn status(&self) -> Result<Status> {
113 let response = self
114 .client
115 .get(Uri::from_static("/localapi/v0/status"))
116 .await?;
117 let body = hyper::body::aggregate(response.into_body()).await?;
118 let status = serde_json::de::from_reader(body.reader())?;
119
120 Ok(status)
121 }
122
123 pub async fn whois(&self, address: SocketAddr) -> Result<Whois> {
125 let response = self
126 .client
127 .get(
128 format!("/localapi/v0/whois?addr={address}")
129 .parse()
130 .unwrap(),
131 )
132 .await?;
133 let body = hyper::body::aggregate(response.into_body()).await?;
134 let whois = serde_json::de::from_reader(body.reader())?;
135
136 Ok(whois)
137 }
138}
139
140#[derive(Clone)]
143pub struct UnixStreamClient {
144 socket_path: PathBuf,
145}
146
147#[async_trait]
148impl LocalApiClient for UnixStreamClient {
149 async fn get(&self, uri: Uri) -> Result<Response<Body>> {
150 let request = Request::builder()
151 .method("GET")
152 .header(HOST, "local-tailscaled.sock")
153 .uri(uri)
154 .body(Body::empty())?;
155
156 let response = self.request(request).await?;
157 Ok(response)
158 }
159}
160
161impl UnixStreamClient {
162 async fn request(&self, request: Request<Body>) -> Result<Response<Body>> {
163 let stream = UnixStream::connect(&self.socket_path).await?;
164 let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?;
165
166 tokio::spawn(async move {
167 if let Err(e) = connection.await {
168 eprintln!("Error in connection: {}", e);
169 }
170 });
171
172 let response = request_sender.send_request(request).await?;
173 if response.status() == 200 {
174 Ok(response)
175 } else {
176 Err(Error::UnprocessableEntity)
177 }
178 }
179}
180
181#[derive(Clone)]
184pub struct TcpWithPasswordClient {
185 port: u16,
186 password: String,
187}
188
189#[async_trait]
190impl LocalApiClient for TcpWithPasswordClient {
191 async fn get(&self, uri: Uri) -> Result<Response<Body>> {
192 let request = Request::builder()
193 .method("GET")
194 .header(HOST, "local-tailscaled.sock")
195 .header(
196 AUTHORIZATION,
197 format!(
198 "Basic {}",
199 base64::engine::general_purpose::STANDARD_NO_PAD
200 .encode(format!(":{}", self.password))
201 ),
202 )
203 .header("Sec-Tailscale", "localapi")
204 .uri(uri)
205 .body(Body::empty())?;
206
207 let response = self.request(request).await?;
208 Ok(response)
209 }
210}
211
212impl TcpWithPasswordClient {
213 async fn request(&self, request: Request<Body>) -> Result<Response<Body>> {
214 let stream = TcpSocket::new_v4()?
215 .connect((Ipv4Addr::LOCALHOST, self.port).into())
216 .await?;
217 let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?;
218
219 tokio::spawn(async move {
220 if let Err(e) = connection.await {
221 eprintln!("Error in connection: {}", e);
222 }
223 });
224
225 let response = request_sender.send_request(request).await?;
226 if response.status() == 200 {
227 Ok(response)
228 } else {
229 Err(Error::UnprocessableEntity)
230 }
231 }
232}