brk_server/
lib.rs

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}