1#![doc = include_str!("../README.md")]
2#![doc = "\n## Example\n\n```rust"]
3#![doc = include_str!("../examples/main.rs")]
4#![doc = "```"]
5
6use std::{path::PathBuf, sync::Arc, time::Duration};
7
8use api::ApiRoutes;
9use axum::{
10 Json, Router,
11 body::{Body, Bytes},
12 http::{Request, Response, StatusCode, Uri},
13 middleware::Next,
14 response::Redirect,
15 routing::get,
16 serve,
17};
18use brk_error::Result;
19use brk_interface::Interface;
20use brk_logger::OwoColorize;
21use brk_mcp::route::MCPRoutes;
22use files::FilesRoutes;
23use log::{error, info};
24use quick_cache::sync::Cache;
25use tokio::net::TcpListener;
26use tower_http::{compression::CompressionLayer, trace::TraceLayer};
27use tracing::Span;
28
29mod api;
30mod extended;
31mod files;
32
33use extended::*;
34
35#[derive(Clone)]
36pub struct AppState {
37 interface: &'static Interface<'static>,
38 path: Option<PathBuf>,
39 cache: Arc<Cache<String, Bytes>>,
40}
41
42pub const VERSION: &str = env!("CARGO_PKG_VERSION");
43
44pub struct Server(AppState);
45
46impl Server {
47 pub fn new(interface: Interface<'static>, files_path: Option<PathBuf>) -> Self {
48 Self(AppState {
49 interface: Box::leak(Box::new(interface)),
50 path: files_path,
51 cache: Arc::new(Cache::new(5_000)),
52 })
53 }
54
55 pub async fn serve(self, mcp: bool) -> Result<()> {
56 let state = self.0;
57
58 let compression_layer = CompressionLayer::new()
59 .br(true)
60 .deflate(true)
61 .gzip(true)
62 .zstd(true);
63
64 let response_uri_layer = axum::middleware::from_fn(
65 async |request: Request<Body>, next: Next| -> Response<Body> {
66 let uri = request.uri().clone();
67 let mut response = next.run(request).await;
68 response.extensions_mut().insert(uri);
69 response
70 },
71 );
72
73 let trace_layer = TraceLayer::new_for_http()
74 .on_request(())
75 .on_response(
76 |response: &Response<Body>, latency: Duration, _span: &Span| {
77 let latency = latency.bright_black();
78 let status = response.status();
79 let uri = response.extensions().get::<Uri>().unwrap();
80 match status {
81 StatusCode::OK => {
82 info!("{} {} {:?}", status.as_u16().green(), uri, latency)
83 }
84 StatusCode::NOT_MODIFIED
85 | StatusCode::TEMPORARY_REDIRECT
86 | StatusCode::PERMANENT_REDIRECT => {
87 info!("{} {} {:?}", status.as_u16().bright_black(), uri, latency)
88 }
89 _ => error!("{} {} {:?}", status.as_u16().red(), uri, latency),
90 }
91 },
92 )
93 .on_body_chunk(())
94 .on_failure(())
95 .on_eos(());
96
97 let router = Router::new()
98 .add_api_routes()
99 .add_files_routes(state.path.as_ref())
100 .add_mcp_routes(state.interface, mcp)
101 .route("/version", get(Json(VERSION)))
102 .route(
103 "/health",
104 get(Json(sonic_rs::json!({
105 "status": "healthy",
106 "service": "brk-server",
107 "timestamp": jiff::Timestamp::now().to_string()
108 }))),
109 )
110 .route(
111 "/discord",
112 get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
113 )
114 .route("/crates", get(Redirect::temporary("https://crates.io/crates/brk")))
115 .route(
116 "/status",
117 get(Redirect::temporary("https://status.bitview.space")),
118 )
119 .route("/github", get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk")))
120 .route(
121 "/cli",
122 get(Redirect::temporary("https://crates.io/crates/brk_cli")),
123 )
124 .route(
125 "/hosting",
126 get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk?tab=readme-ov-file#hosting-as-a-service")),
127 )
128 .route("/nostr", get(Redirect::temporary("https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44")))
129 .with_state(state)
130 .layer(compression_layer)
131 .layer(response_uri_layer)
132 .layer(trace_layer);
133
134 let mut port = 3110;
135
136 let mut listener;
137 loop {
138 listener = TcpListener::bind(format!("0.0.0.0:{port}")).await;
139 if listener.is_ok() {
140 break;
141 }
142 port += 1;
143 }
144
145 info!("Starting server on port {port}...");
146
147 let listener = listener.unwrap();
148
149 serve(listener, router).await?;
150
151 Ok(())
152 }
153}