pocket_relay_client_shared/
api.rs1use crate::{servers::HTTP_PORT, MIN_SERVER_VERSION};
4use hyper::{
5 header::{self, HeaderName, HeaderValue},
6 Body, HeaderMap, Response,
7};
8use log::error;
9use reqwest::{Client, Identity, Upgraded};
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use std::{path::Path, str::FromStr};
13use thiserror::Error;
14use url::Url;
15
16pub const DETAILS_ENDPOINT: &str = "api/server";
18pub const TELEMETRY_ENDPOINT: &str = "api/server/telemetry";
20pub const UPGRADE_ENDPOINT: &str = "api/server/upgrade";
22pub const TUNNEL_ENDPOINT: &str = "api/server/tunnel";
24
25pub const SERVER_IDENT: &str = "POCKET_RELAY_SERVER";
27
28pub const USER_AGENT: &str = concat!("PocketRelayClient/v", env!("CARGO_PKG_VERSION"));
30
31mod headers {
33 pub const ASSOCIATION: &str = "x-association";
35
36 pub const LEGACY_SCHEME: &str = "x-pocket-relay-scheme";
38 pub const LEGACY_HOST: &str = "x-pocket-relay-host";
40 pub const LEGACY_PORT: &str = "x-pocket-relay-port";
42 pub const LEGACY_LOCAL_HTTP: &str = "x-pocket-relay-local-http";
45}
46
47pub fn create_http_client(identity: Option<Identity>) -> Result<Client, reqwest::Error> {
53 let mut builder = Client::builder().user_agent(USER_AGENT);
54
55 if let Some(identity) = identity {
56 builder = builder.identity(identity);
57 }
58
59 builder.build()
60}
61
62#[derive(Debug, Error)]
64pub enum ClientIdentityError {
65 #[error("Failed to read identity: {0}")]
67 Read(#[from] std::io::Error),
68 #[error("Failed to create identity: {0}")]
70 Create(#[from] reqwest::Error),
71}
72
73pub fn read_client_identity(path: &Path) -> Result<Identity, ClientIdentityError> {
80 let bytes = std::fs::read(path).map_err(ClientIdentityError::Read)?;
82
83 Identity::from_pkcs12_der(&bytes, "").map_err(ClientIdentityError::Create)
85}
86
87#[derive(Deserialize)]
90struct ServerDetails {
91 version: Version,
93 #[serde(default)]
95 ident: Option<String>,
96 association: Option<String>,
98 tunnel_port: Option<u16>,
100}
101
102#[derive(Debug, Clone)]
106pub struct LookupData {
107 pub url: Url,
109 pub version: Version,
111 pub association: Option<String>,
113 pub tunnel_port: Option<u16>,
115}
116
117#[derive(Debug, Error)]
119pub enum LookupError {
120 #[error("Invalid Connection URL: {0}")]
122 InvalidHostTarget(#[from] url::ParseError),
123 #[error("Failed to connect to server: {0}")]
125 ConnectionFailed(reqwest::Error),
126 #[error("Server replied with error response: {0}")]
128 ErrorResponse(reqwest::Error),
129 #[error("Invalid server response: {0}")]
131 InvalidResponse(reqwest::Error),
132 #[error("Server identifier was incorrect (Not a PocketRelay server?)")]
134 NotPocketRelay,
135 #[error("Server version is too outdated ({0}) this client requires servers of version {1} or greater")]
137 ServerOutdated(Version, Version),
138}
139
140pub async fn lookup_server(
147 http_client: reqwest::Client,
148 host: String,
149) -> Result<LookupData, LookupError> {
150 let mut url = String::new();
151
152 let mut inferred_scheme = false;
154
155 if !host.starts_with("http://") && !host.starts_with("https://") {
157 url.push_str("http://");
158
159 inferred_scheme = true;
160 }
161
162 url.push_str(&host);
163
164 if !url.ends_with('/') {
166 url.push('/');
167 }
168
169 let mut url = Url::from_str(&url)?;
170
171 if url.port().is_some_and(|port| port == 443) && inferred_scheme {
173 let _ = url.set_scheme("https");
174 }
175
176 let info_url = url
177 .join(DETAILS_ENDPOINT)
178 .expect("Failed to create server details URL");
179
180 let response = http_client
182 .get(info_url)
183 .header(header::ACCEPT, "application/json")
184 .send()
185 .await
186 .map_err(LookupError::ConnectionFailed)?;
187
188 #[cfg(debug_assertions)]
190 {
191 use log::debug;
192
193 debug!("Response Status: {}", response.status());
194 debug!("HTTP Version: {:?}", response.version());
195 debug!("Content Length: {:?}", response.content_length());
196 debug!("HTTP Headers: {:?}", response.headers());
197 }
198
199 let response = response
201 .error_for_status()
202 .map_err(LookupError::ErrorResponse)?;
203
204 let details = response
206 .json::<ServerDetails>()
207 .await
208 .map_err(LookupError::InvalidResponse)?;
209
210 if details.ident.is_none() || details.ident.is_some_and(|value| value != SERVER_IDENT) {
212 return Err(LookupError::NotPocketRelay);
213 }
214
215 if details.version < MIN_SERVER_VERSION {
217 return Err(LookupError::ServerOutdated(
218 details.version,
219 MIN_SERVER_VERSION,
220 ));
221 }
222
223 #[cfg(debug_assertions)]
225 {
226 use log::debug;
227 if let Some(association) = &details.association {
228 debug!("Acquired association token: {}", association);
229 }
230 }
231
232 Ok(LookupData {
233 url,
234 version: details.version,
235 association: details.association,
236 tunnel_port: details.tunnel_port,
237 })
238}
239
240#[derive(Debug, Error)]
242pub enum ServerStreamError {
243 #[error("Request failed: {0}")]
245 RequestFailed(reqwest::Error),
246 #[error("Server error response: {0}")]
248 ServerError(reqwest::Error),
249 #[error("Upgrade failed: {0}")]
251 UpgradeFailure(reqwest::Error),
252}
253
254pub async fn create_server_stream(
262 http_client: &reqwest::Client,
263 base_url: &Url,
264 association: Option<&String>,
265) -> Result<Upgraded, ServerStreamError> {
266 let endpoint_url: Url = base_url
268 .join(UPGRADE_ENDPOINT)
269 .expect("Failed to create upgrade endpoint");
270
271 let mut headers: HeaderMap<HeaderValue> = [
273 (header::CONNECTION, HeaderValue::from_static("Upgrade")),
274 (header::UPGRADE, HeaderValue::from_static("blaze")),
275 (
277 HeaderName::from_static(headers::LEGACY_SCHEME),
278 HeaderValue::from_static("http"),
279 ),
280 (
281 HeaderName::from_static(headers::LEGACY_HOST),
282 HeaderValue::from_static("127.0.0.1"),
283 ),
284 (
285 HeaderName::from_static(headers::LEGACY_PORT),
286 HeaderValue::from(HTTP_PORT),
287 ),
288 (
289 HeaderName::from_static(headers::LEGACY_LOCAL_HTTP),
290 HeaderValue::from_static("true"),
291 ),
292 ]
293 .into_iter()
294 .collect();
295
296 if let Some(association) = association {
298 headers.insert(
299 HeaderName::from_static(headers::ASSOCIATION),
300 HeaderValue::from_str(association).expect("Invalid association token"),
301 );
302 }
303
304 let response = http_client
306 .get(endpoint_url)
307 .headers(headers)
308 .send()
309 .await
310 .map_err(ServerStreamError::RequestFailed)?;
311
312 let response = response
314 .error_for_status()
315 .map_err(ServerStreamError::ServerError)?;
316
317 response
319 .upgrade()
320 .await
321 .map_err(ServerStreamError::UpgradeFailure)
322}
323
324#[derive(Serialize)]
326pub struct TelemetryEvent {
327 pub values: Vec<(String, String)>,
329}
330
331pub async fn publish_telemetry_event(
338 http_client: &reqwest::Client,
339 base_url: &Url,
340 event: TelemetryEvent,
341) -> Result<(), reqwest::Error> {
342 let endpoint_url: Url = base_url
344 .join(TELEMETRY_ENDPOINT)
345 .expect("Failed to create telemetry endpoint");
346
347 let response = http_client.post(endpoint_url).json(&event).send().await?;
349
350 let _ = response.error_for_status()?;
352
353 Ok(())
354}
355
356#[derive(Debug, Error)]
358pub enum ProxyError {
359 #[error("Request failed: {0}")]
361 RequestFailed(reqwest::Error),
362 #[error("Request failed: {0}")]
364 BodyFailed(reqwest::Error),
365}
366
367pub async fn proxy_http_request(
374 http_client: &reqwest::Client,
375 url: Url,
376) -> Result<Response<Body>, ProxyError> {
377 let response = http_client
379 .get(url)
380 .send()
381 .await
382 .map_err(ProxyError::RequestFailed)?;
383
384 let status = response.status();
386 let headers = response.headers().clone();
387
388 let body: bytes::Bytes = response.bytes().await.map_err(ProxyError::BodyFailed)?;
390
391 let mut response = Response::new(Body::from(body));
393 *response.status_mut() = status;
394 *response.headers_mut() = headers;
395
396 Ok(response)
397}
398
399pub async fn create_server_tunnel(
406 http_client: &reqwest::Client,
407 base_url: &Url,
408 association: &str,
409) -> Result<Upgraded, ServerStreamError> {
410 let endpoint_url: Url = base_url
412 .join(TUNNEL_ENDPOINT)
413 .expect("Failed to create tunnel endpoint");
414
415 let mut headers: HeaderMap<HeaderValue> = [
417 (header::CONNECTION, HeaderValue::from_static("Upgrade")),
418 (header::UPGRADE, HeaderValue::from_static("tunnel")),
419 ]
420 .into_iter()
421 .collect();
422
423 headers.insert(
425 HeaderName::from_static(headers::ASSOCIATION),
426 HeaderValue::from_str(association).expect("Invalid association token"),
427 );
428
429 let response = http_client
431 .get(endpoint_url)
432 .headers(headers)
433 .send()
434 .await
435 .map_err(ServerStreamError::RequestFailed)?;
436
437 let response = response
439 .error_for_status()
440 .map_err(ServerStreamError::ServerError)?;
441
442 response
444 .upgrade()
445 .await
446 .map_err(ServerStreamError::UpgradeFailure)
447}