use ling_http::axum;
use ling_http::tokio;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Router;
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub content_type: String,
pub body: String,
pub set_cookie: Option<String>,
pub location: Option<String>,
}
impl Default for HttpResponse {
fn default() -> Self {
Self {
status: 200,
content_type: "text/plain; charset=utf-8".to_string(),
body: String::new(),
set_cookie: None,
location: None,
}
}
}
pub struct PendingRequest {
pub method: String,
pub path: String,
pub query: String,
pub body: String,
pub cookie: String,
pub authorization: String,
pub client_ip: String,
pub respond_to: tokio::sync::oneshot::Sender<HttpResponse>,
}
#[derive(Clone)]
struct ServerState {
tx: std::sync::mpsc::Sender<PendingRequest>,
}
async fn catch_all(State(state): State<ServerState>, req: Request) -> Response {
let method = req.method().to_string();
let path = req.uri().path().to_string();
let query = req.uri().query().unwrap_or("").to_string();
let cookie = req
.headers()
.get(axum::http::header::COOKIE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let authorization = req
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let client_ip = req
.headers()
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.or_else(|| {
req.headers()
.get("x-real-ip")
.and_then(|v| v.to_str().ok())
.map(|s| s.trim().to_string())
})
.unwrap_or_default();
let body_bytes = match axum::body::to_bytes(req.into_body(), 32 * 1024 * 1024).await {
Ok(b) => b,
Err(_) => return (StatusCode::BAD_REQUEST, "body too large or unreadable").into_response(),
};
let body = String::from_utf8_lossy(&body_bytes).into_owned();
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
let sent = state.tx.send(PendingRequest {
method,
path,
query,
body,
cookie,
authorization,
client_ip,
respond_to: resp_tx,
});
if sent.is_err() {
return (StatusCode::SERVICE_UNAVAILABLE, "no Ling http_serve loop running").into_response();
}
match resp_rx.await {
Ok(resp) => {
let status = StatusCode::from_u16(resp.status).unwrap_or(StatusCode::OK);
let mut r = (status, resp.body).into_response();
if let Ok(value) = resp.content_type.parse() {
r.headers_mut().insert(axum::http::header::CONTENT_TYPE, value);
}
if let Some(sc) = &resp.set_cookie {
if let Ok(value) = sc.parse() {
r.headers_mut().insert(axum::http::header::SET_COOKIE, value);
}
}
if let Some(loc) = &resp.location {
if let Ok(value) = loc.parse() {
r.headers_mut().insert(axum::http::header::LOCATION, value);
}
}
r.headers_mut().insert(
axum::http::header::X_FRAME_OPTIONS,
axum::http::HeaderValue::from_static("DENY"),
);
r.headers_mut().insert(
axum::http::header::CONTENT_SECURITY_POLICY,
axum::http::HeaderValue::from_static("frame-ancestors 'none'"),
);
r
},
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "handler dropped the response").into_response(),
}
}
pub fn spawn_server(
host: String,
port: u16,
static_dirs: Vec<(String, String)>,
) -> std::sync::mpsc::Receiver<PendingRequest> {
let (tx, rx) = std::sync::mpsc::channel::<PendingRequest>();
let state = ServerState { tx };
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("http_serve: failed to start async runtime: {e}");
return;
},
};
rt.block_on(async move {
let addr = format!("{host}:{port}");
let addr: std::net::SocketAddr = match addr.parse() {
Ok(a) => a,
Err(e) => {
eprintln!("http_serve: bad address '{addr}': {e}");
return;
},
};
let mut router: Router<ServerState> = Router::new();
for (prefix, dir) in &static_dirs {
router = router.nest_service(prefix, tower_http::services::ServeDir::new(dir));
}
let router: Router = router.fallback(catch_all).with_state(state);
if let Err(e) = ling_http::serve_http(router, addr).await {
eprintln!("http_serve: {e}");
}
});
});
rx
}
pub fn value_to_response(v: &crate::runtime::Value) -> HttpResponse {
use crate::runtime::Value;
match v {
Value::Str(s) => HttpResponse {
status: 200,
content_type: "text/html; charset=utf-8".to_string(),
body: s.clone(),
set_cookie: None,
location: None,
},
Value::Struct { fields, .. } => {
let mut resp = HttpResponse::default();
resp.content_type = "text/html; charset=utf-8".to_string();
for (k, val) in fields {
match (k.as_str(), val) {
("status", Value::Number(n)) => resp.status = *n as u16,
("body", Value::Str(s)) => resp.body = s.clone(),
("content_type", Value::Str(s)) => resp.content_type = s.clone(),
("set_cookie", Value::Str(s)) if !s.is_empty() => resp.set_cookie = Some(s.clone()),
("location", Value::Str(s)) if !s.is_empty() => resp.location = Some(s.clone()),
_ => {},
}
}
resp
},
other => HttpResponse {
status: 200,
content_type: "text/plain; charset=utf-8".to_string(),
body: other.to_string(),
set_cookie: None,
location: None,
},
}
}
type JobMap = std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Option<String>>>>;
#[derive(Clone, Default)]
pub struct AsyncJobs(JobMap);
impl AsyncJobs {
pub fn new() -> Self {
Self::default()
}
pub fn start_post(&self, url: String, content_type: String, body: String) -> String {
let id = {
use rand::RngCore;
let mut buf = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut buf);
buf.iter().map(|b| format!("{b:02x}")).collect::<String>()
};
self.0.lock().unwrap().insert(id.clone(), None);
let jobs = self.0.clone();
let job_id = id.clone();
async_runtime_handle().spawn(async move {
let client = reqwest::Client::new();
let result = client
.post(&url)
.header(reqwest::header::CONTENT_TYPE, content_type)
.body(body)
.send()
.await;
let text = match result {
Ok(resp) => resp
.text()
.await
.unwrap_or_else(|e| format!("{{\"error\":\"body read failed: {e}\"}}")),
Err(e) => format!("{{\"error\":\"{}\"}}", e.to_string().replace('"', "'")),
};
jobs.lock().unwrap().insert(job_id, Some(text));
});
id
}
pub fn poll(&self, id: &str) -> Option<String> {
self.0.lock().unwrap().get(id).cloned().flatten()
}
pub fn start_sdai_txt2img(&self, base_url: String, prompt: String, width: u32, height: u32) -> String {
let id = {
use rand::RngCore;
let mut buf = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut buf);
buf.iter().map(|b| format!("{b:02x}")).collect::<String>()
};
self.0.lock().unwrap().insert(id.clone(), None);
let jobs = self.0.clone();
let job_id = id.clone();
async_runtime_handle().spawn(async move {
let url = format!("{}/sdapi/v1/txt2img", base_url.trim_end_matches('/'));
let payload = ling_http::serde_json::json!({
"prompt": prompt,
"negative_prompt": "blurry, lowres, watermark, text, signature",
"steps": 24,
"cfg_scale": 7,
"width": width,
"height": height,
"sampler_name": "Euler a",
});
let client = reqwest::Client::new();
let result = client
.post(&url)
.timeout(std::time::Duration::from_secs(180))
.json(&payload)
.send()
.await;
let outcome = match result {
Ok(resp) => {
if !resp.status().is_success() {
format!("ERROR: SDAI returned HTTP {}", resp.status())
} else {
match resp.json::<ling_http::serde_json::Value>().await {
Ok(v) => match v.get("images").and_then(|im| im.get(0)).and_then(|s| s.as_str()) {
Some(b64) => b64.to_string(),
None => "ERROR: no images[0] in SDAI response".to_string(),
},
Err(e) => format!("ERROR: bad JSON from SDAI: {e}"),
}
}
},
Err(e) => format!("ERROR: {}", e.to_string().replace('"', "'")),
};
jobs.lock().unwrap().insert(job_id, Some(outcome));
});
id
}
}
#[derive(Clone, Default)]
struct OAuthResult {
error: String,
sub: String,
email: String,
name: String,
}
type OAuthMap = std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Option<OAuthResult>>>>;
#[derive(Clone, Default)]
pub struct OAuthJobs(OAuthMap);
impl OAuthJobs {
pub fn new() -> Self {
Self::default()
}
pub fn start_google_login(
&self,
code: String,
client_id: String,
client_secret: String,
redirect_uri: String,
) -> String {
let id = {
use rand::RngCore;
let mut buf = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut buf);
buf.iter().map(|b| format!("{b:02x}")).collect::<String>()
};
self.0.lock().unwrap().insert(id.clone(), None);
let jobs = self.0.clone();
let job_id = id.clone();
async_runtime_handle().spawn(async move {
let client = reqwest::Client::new();
let outcome = async {
let token_resp = client
.post("https://oauth2.googleapis.com/token")
.form(&[
("code", code.as_str()),
("client_id", client_id.as_str()),
("client_secret", client_secret.as_str()),
("redirect_uri", redirect_uri.as_str()),
("grant_type", "authorization_code"),
])
.send()
.await
.map_err(|e| format!("token request failed: {e}"))?;
if !token_resp.status().is_success() {
let status = token_resp.status();
let body = token_resp.text().await.unwrap_or_default();
return Err(format!("Google returned HTTP {status} exchanging code: {body}"));
}
let token_json: ling_http::serde_json::Value = token_resp
.json()
.await
.map_err(|e| format!("bad JSON from Google token endpoint: {e}"))?;
let access_token = token_json
.get("access_token")
.and_then(|v| v.as_str())
.ok_or_else(|| "no access_token in Google's response".to_string())?;
let userinfo_resp = client
.get("https://www.googleapis.com/oauth2/v3/userinfo")
.bearer_auth(access_token)
.send()
.await
.map_err(|e| format!("userinfo request failed: {e}"))?;
if !userinfo_resp.status().is_success() {
return Err(format!("Google returned HTTP {} fetching userinfo", userinfo_resp.status()));
}
let profile: ling_http::serde_json::Value = userinfo_resp
.json()
.await
.map_err(|e| format!("bad JSON from Google userinfo endpoint: {e}"))?;
let sub = profile.get("sub").and_then(|v| v.as_str()).unwrap_or_default();
if sub.is_empty() {
return Err("no sub in Google's userinfo response".to_string());
}
Ok(OAuthResult {
error: String::new(),
sub: sub.to_string(),
email: profile.get("email").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
name: profile.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
})
}
.await
.unwrap_or_else(|e| OAuthResult { error: e, ..Default::default() });
jobs.lock().unwrap().insert(job_id, Some(outcome));
});
id
}
fn get(&self, id: &str) -> Option<OAuthResult> {
self.0.lock().unwrap().get(id).cloned().flatten()
}
pub fn done(&self, id: &str) -> bool {
matches!(self.0.lock().unwrap().get(id), Some(Some(_)))
}
pub fn error(&self, id: &str) -> String {
self.get(id).map(|r| r.error).unwrap_or_default()
}
pub fn sub(&self, id: &str) -> String {
self.get(id).map(|r| r.sub).unwrap_or_default()
}
pub fn email(&self, id: &str) -> String {
self.get(id).map(|r| r.email).unwrap_or_default()
}
pub fn name(&self, id: &str) -> String {
self.get(id).map(|r| r.name).unwrap_or_default()
}
}
fn async_runtime_handle() -> tokio::runtime::Handle {
static HANDLE: std::sync::OnceLock<tokio::runtime::Handle> = std::sync::OnceLock::new();
HANDLE
.get_or_init(|| {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("ling: failed to start async-job tokio runtime");
tx.send(rt.handle().clone()).expect("ling: async-job runtime handle send failed");
rt.block_on(std::future::pending::<()>());
});
rx.recv().expect("ling: async-job runtime handle recv failed")
})
.clone()
}