Update botui
This commit is contained in:
parent
1a50680712
commit
9a2596ed4a
7 changed files with 251 additions and 85 deletions
22
Cargo.toml
22
Cargo.toml
|
|
@ -4,6 +4,9 @@ version = "6.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "General Bots UI - Pure web interface"
|
description = "General Bots UI - Pure web interface"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
repository = "https://github.com/GeneralBots/BotServer"
|
||||||
|
keywords = ["chatbot", "ui", "web-interface", "general-bots"]
|
||||||
|
categories = ["web-programming", "gui"]
|
||||||
|
|
||||||
[dependencies.botlib]
|
[dependencies.botlib]
|
||||||
path = "../botlib"
|
path = "../botlib"
|
||||||
|
|
@ -49,3 +52,22 @@ tracing = "0.1"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||||
webbrowser = "0.8"
|
webbrowser = "0.8"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unused_imports = "warn"
|
||||||
|
unused_variables = "warn"
|
||||||
|
unused_mut = "warn"
|
||||||
|
unsafe_code = "deny"
|
||||||
|
missing_debug_implementations = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
all = "warn"
|
||||||
|
pedantic = "warn"
|
||||||
|
nursery = "warn"
|
||||||
|
cargo = "warn"
|
||||||
|
unwrap_used = "warn"
|
||||||
|
expect_used = "warn"
|
||||||
|
panic = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
# Disabled: transitive dependencies we cannot control
|
||||||
|
multiple_crate_versions = "allow"
|
||||||
|
|
|
||||||
129
PROMPT.md
129
PROMPT.md
|
|
@ -5,6 +5,118 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ZERO TOLERANCE POLICY
|
||||||
|
|
||||||
|
**This project has the strictest code quality requirements possible.**
|
||||||
|
|
||||||
|
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ABSOLUTE PROHIBITIONS
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings
|
||||||
|
❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it
|
||||||
|
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||||
|
❌ NEVER use .expect() - use ? or proper error handling
|
||||||
|
❌ NEVER use panic!() or unreachable!() - handle all cases
|
||||||
|
❌ NEVER use todo!() or unimplemented!() - write real code
|
||||||
|
❌ NEVER leave unused imports - DELETE them
|
||||||
|
❌ NEVER leave dead code - DELETE it or IMPLEMENT it
|
||||||
|
❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI
|
||||||
|
❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml
|
||||||
|
❌ NEVER add comments explaining what code does - code must be self-documenting
|
||||||
|
❌ NEVER use CDN links - all assets must be local
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CARGO.TOML LINT EXCEPTIONS
|
||||||
|
|
||||||
|
When a clippy lint has **technical false positives** that cannot be fixed in code,
|
||||||
|
disable it in `Cargo.toml` with a comment explaining why:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[lints.clippy]
|
||||||
|
# Disabled: has false positives for functions with mut self, heap types (Vec, String)
|
||||||
|
missing_const_for_fn = "allow"
|
||||||
|
# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference
|
||||||
|
needless_pass_by_value = "allow"
|
||||||
|
# Disabled: transitive dependencies we cannot control
|
||||||
|
multiple_crate_versions = "allow"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Approved exceptions:**
|
||||||
|
- `missing_const_for_fn` - false positives for `mut self`, heap types
|
||||||
|
- `needless_pass_by_value` - Tauri/framework requirements
|
||||||
|
- `multiple_crate_versions` - transitive dependencies
|
||||||
|
- `future_not_send` - when async traits require non-Send futures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY CODE PATTERNS
|
||||||
|
|
||||||
|
### Error Handling - Use `?` Operator
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
let value = something.unwrap();
|
||||||
|
let value = something.expect("msg");
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
let value = something?;
|
||||||
|
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self Usage in Impl Blocks
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
impl MyStruct {
|
||||||
|
fn new() -> MyStruct { MyStruct { } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
impl MyStruct {
|
||||||
|
fn new() -> Self { Self { } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format Strings - Inline Variables
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
format!("Hello {}", name)
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
format!("Hello {name}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Display vs ToString
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
impl ToString for MyType { }
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
impl std::fmt::Display for MyType { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Derive Eq with PartialEq
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct MyStruct { }
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
struct MyStruct { }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Weekly Maintenance - EVERY MONDAY
|
## Weekly Maintenance - EVERY MONDAY
|
||||||
|
|
||||||
### Package Review Checklist
|
### Package Review Checklist
|
||||||
|
|
@ -435,13 +547,20 @@ ui/minimal/index.html # Minimal chat UI
|
||||||
|
|
||||||
## Remember
|
## Remember
|
||||||
|
|
||||||
- **Two LLM modes**: Execution (fazer) vs Review (conferir)
|
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||||
|
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||||
|
- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment
|
||||||
|
- **NO DEAD CODE** - Delete unused code, never prefix with _
|
||||||
|
- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling
|
||||||
|
- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name)
|
||||||
|
- **USE SELF** - In impl blocks, use Self not the type name
|
||||||
|
- **DERIVE EQ** - Always derive Eq with PartialEq
|
||||||
|
- **DISPLAY NOT TOSTRING** - Implement Display, not ToString
|
||||||
|
- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly
|
||||||
- **HTMX first**: Minimize JS, delegate to server
|
- **HTMX first**: Minimize JS, delegate to server
|
||||||
- **Local assets**: No CDN, all vendor files local
|
- **Local assets**: No CDN, all vendor files local
|
||||||
- **Dead code**: Never use _ prefix, implement real code
|
|
||||||
- **cargo audit**: Must pass with 0 warnings
|
|
||||||
- **No business logic**: All logic in botserver
|
- **No business logic**: All logic in botserver
|
||||||
- **Feature gates**: Unused code never compiles
|
|
||||||
- **HTML responses**: Server returns fragments, not JSON
|
- **HTML responses**: Server returns fragments, not JSON
|
||||||
- **Version**: Always 6.1.0 - do not change without approval
|
- **Version**: Always 6.1.0 - do not change without approval
|
||||||
- **Theme system**: Use data-theme attribute on body, 6 themes available
|
- **Theme system**: Use data-theme attribute on body, 6 themes available
|
||||||
|
- **Session Continuation**: When running out of context, create detailed summary: (1) what was done, (2) what remains, (3) specific files and line numbers, (4) exact next steps.
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
//! BotUI - General Bots Pure Web UI
|
//! `BotUI` - General Bots Pure Web UI
|
||||||
//!
|
//!
|
||||||
//! This crate provides the web UI layer for General Bots:
|
//! This crate provides the web UI layer for General Bots:
|
||||||
//! - Serves static HTMX UI files (suite, minimal)
|
//! - Serves static HTMX UI files (suite, minimal)
|
||||||
//! - Proxies API requests to botserver
|
//! - Proxies API requests to botserver
|
||||||
//! - WebSocket support for real-time communication
|
//! - `WebSocket` support for real-time communication
|
||||||
//!
|
//!
|
||||||
//! For desktop/mobile native features, see the `botapp` crate which
|
//! For desktop/mobile native features, see the `botapp` crate which
|
||||||
//! wraps this pure web UI with Tauri.
|
//! wraps this pure web UI with Tauri.
|
||||||
|
|
|
||||||
31
src/main.rs
31
src/main.rs
|
|
@ -1,3 +1,7 @@
|
||||||
|
//! `BotUI` main entry point
|
||||||
|
//!
|
||||||
|
//! Starts the web UI server for General Bots.
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
|
@ -21,14 +25,15 @@ fn get_port() -> u16 {
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
init_logging();
|
init_logging();
|
||||||
|
|
||||||
info!("BotUI {} starting...", env!("CARGO_PKG_VERSION"));
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
info!("BotUI {version} starting...");
|
||||||
|
|
||||||
let app = ui_server::configure_router();
|
let app = ui_server::configure_router();
|
||||||
let port = get_port();
|
let port = get_port();
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
info!("UI server listening on http://{}", addr);
|
info!("UI server listening on http://{addr}");
|
||||||
|
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
.with_graceful_shutdown(shutdown_signal())
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
|
@ -40,24 +45,28 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
tokio::signal::ctrl_c()
|
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||||
.await
|
log::error!("Failed to install Ctrl+C handler: {e}");
|
||||||
.expect("Failed to install Ctrl+C handler");
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let terminate = async {
|
let terminate = async {
|
||||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
|
||||||
.expect("Failed to install SIGTERM handler")
|
Ok(mut signal) => {
|
||||||
.recv()
|
signal.recv().await;
|
||||||
.await;
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to install SIGTERM handler: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
let terminate = std::future::pending::<()>();
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ctrl_c => info!("Received Ctrl+C, shutting down..."),
|
() = ctrl_c => info!("Received Ctrl+C, shutting down..."),
|
||||||
_ = terminate => info!("Received SIGTERM, shutting down..."),
|
() = terminate => info!("Received SIGTERM, shutting down..."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Shared types and state management for BotUI
|
//! Shared types and state management for `BotUI`
|
||||||
//!
|
//!
|
||||||
//! This module provides shared application state and utilities
|
//! This module provides shared application state and utilities
|
||||||
//! used across the UI server.
|
//! used across the UI server.
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
//! Application state management
|
//! Application state management
|
||||||
//!
|
//!
|
||||||
//! This module contains the shared application state that is passed to all
|
//! This module contains the shared application state that is passed to all
|
||||||
//! route handlers and provides access to the BotServer client.
|
//! route handlers and provides access to the `BotServer` client.
|
||||||
|
|
||||||
use botlib::http_client::BotServerClient;
|
use botlib::http_client::BotServerClient;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Application state shared across all handlers
|
/// Application state shared across all handlers
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
/// HTTP client for communicating with BotServer
|
/// HTTP client for communicating with `BotServer`
|
||||||
pub client: Arc<BotServerClient>,
|
pub client: Arc<BotServerClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
/// Create a new application state
|
/// Create a new application state
|
||||||
///
|
///
|
||||||
/// Uses BOTSERVER_URL environment variable if set, otherwise defaults to localhost:8080
|
/// Uses `BOTSERVER_URL` environment variable if set, otherwise defaults to localhost:8080
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let url = std::env::var("BOTSERVER_URL").ok();
|
let url = std::env::var("BOTSERVER_URL").ok();
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -24,7 +25,7 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the BotServer is healthy
|
/// Check if the `BotServer` is healthy
|
||||||
pub async fn health_check(&self) -> bool {
|
pub async fn health_check(&self) -> bool {
|
||||||
self.client.health_check().await
|
self.client.health_check().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
//! UI Server module for `BotUI`
|
||||||
|
//!
|
||||||
|
//! Handles HTTP routing, WebSocket proxying, and static file serving.
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::{
|
extract::{
|
||||||
|
|
@ -12,7 +16,7 @@ use axum::{
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::Path, path::PathBuf};
|
||||||
use tokio_tungstenite::{
|
use tokio_tungstenite::{
|
||||||
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
|
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
|
||||||
};
|
};
|
||||||
|
|
@ -52,7 +56,7 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
||||||
match fs::read_to_string("ui/minimal/index.html") {
|
match fs::read_to_string("ui/minimal/index.html") {
|
||||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load minimal UI: {}", e);
|
error!("Failed to load minimal UI: {e}");
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
[("content-type", "text/plain")],
|
[("content-type", "text/plain")],
|
||||||
|
|
@ -66,7 +70,7 @@ pub async fn serve_suite() -> impl IntoResponse {
|
||||||
match fs::read_to_string("ui/suite/index.html") {
|
match fs::read_to_string("ui/suite/index.html") {
|
||||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load suite UI: {}", e);
|
error!("Failed to load suite UI: {e}");
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
[("content-type", "text/plain")],
|
[("content-type", "text/plain")],
|
||||||
|
|
@ -77,23 +81,24 @@ pub async fn serve_suite() -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||||
match state.health_check().await {
|
if state.health_check().await {
|
||||||
true => (
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"service": "botui",
|
"service": "botui",
|
||||||
"mode": "web"
|
"mode": "web"
|
||||||
})),
|
})),
|
||||||
),
|
)
|
||||||
false => (
|
} else {
|
||||||
|
(
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "unhealthy",
|
"status": "unhealthy",
|
||||||
"service": "botui",
|
"service": "botui",
|
||||||
"error": "botserver unreachable"
|
"error": "botserver unreachable"
|
||||||
})),
|
})),
|
||||||
),
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,8 +126,7 @@ fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<St
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.starts_with("/apps/") {
|
if let Some(after_apps) = path.strip_prefix("/apps/") {
|
||||||
let after_apps = &path[6..];
|
|
||||||
if let Some(end) = after_apps.find('/') {
|
if let Some(end) = after_apps.find('/') {
|
||||||
return Some(after_apps[..end].to_string());
|
return Some(after_apps[..end].to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -139,18 +143,14 @@ async fn proxy_api(
|
||||||
let path = original_uri.path();
|
let path = original_uri.path();
|
||||||
let query = original_uri
|
let query = original_uri
|
||||||
.query()
|
.query()
|
||||||
.map(|q| format!("?{}", q))
|
.map_or_else(String::new, |q| format!("?{q}"));
|
||||||
.unwrap_or_default();
|
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
let headers = req.headers().clone();
|
let headers = req.headers().clone();
|
||||||
|
|
||||||
let app_context = extract_app_context(&headers, path);
|
let app_context = extract_app_context(&headers, path);
|
||||||
|
|
||||||
let target_url = format!("{}{}{}", state.client.base_url(), path, query);
|
let target_url = format!("{}{path}{query}", state.client.base_url());
|
||||||
debug!(
|
debug!("Proxying {method} {path} to {target_url} (app: {app_context:?})");
|
||||||
"Proxying {} {} to {} (app: {:?})",
|
|
||||||
method, path, target_url, app_context
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
|
|
@ -158,7 +158,7 @@ async fn proxy_api(
|
||||||
.unwrap_or_else(|_| reqwest::Client::new());
|
.unwrap_or_else(|_| reqwest::Client::new());
|
||||||
let mut proxy_req = client.request(method.clone(), &target_url);
|
let mut proxy_req = client.request(method.clone(), &target_url);
|
||||||
|
|
||||||
for (name, value) in headers.iter() {
|
for (name, value) in &headers {
|
||||||
if name != "host" {
|
if name != "host" {
|
||||||
if let Ok(v) = value.to_str() {
|
if let Ok(v) = value.to_str() {
|
||||||
proxy_req = proxy_req.header(name.as_str(), v);
|
proxy_req = proxy_req.header(name.as_str(), v);
|
||||||
|
|
@ -173,11 +173,11 @@ async fn proxy_api(
|
||||||
let body_bytes = match axum::body::to_bytes(req.into_body(), usize::MAX).await {
|
let body_bytes = match axum::body::to_bytes(req.into_body(), usize::MAX).await {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to read request body: {}", e);
|
error!("Failed to read request body: {e}");
|
||||||
return Response::builder()
|
return build_error_response(
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
.body(Body::from("Failed to read request body"))
|
"Failed to read request body",
|
||||||
.unwrap();
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -186,40 +186,51 @@ async fn proxy_api(
|
||||||
}
|
}
|
||||||
|
|
||||||
match proxy_req.send().await {
|
match proxy_req.send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => build_proxy_response(resp).await,
|
||||||
let status = resp.status();
|
Err(e) => {
|
||||||
let headers = resp.headers().clone();
|
error!("Proxy request failed: {e}");
|
||||||
|
build_error_response(StatusCode::BAD_GATEWAY, &format!("Proxy error: {e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match resp.bytes().await {
|
fn build_error_response(status: StatusCode, message: &str) -> Response<Body> {
|
||||||
Ok(body) => {
|
Response::builder()
|
||||||
let mut response = Response::builder().status(status);
|
.status(status)
|
||||||
|
.body(Body::from(message.to_string()))
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Body::from("Failed to build error response"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (name, value) in headers.iter() {
|
async fn build_proxy_response(resp: reqwest::Response) -> Response<Body> {
|
||||||
response = response.header(name, value);
|
let status = resp.status();
|
||||||
}
|
let headers = resp.headers().clone();
|
||||||
|
|
||||||
response.body(Body::from(body)).unwrap_or_else(|_| {
|
match resp.bytes().await {
|
||||||
Response::builder()
|
Ok(body) => {
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
let mut response = Response::builder().status(status);
|
||||||
.body(Body::from("Failed to build response"))
|
|
||||||
.unwrap()
|
for (name, value) in &headers {
|
||||||
})
|
response = response.header(name, value);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to read response body: {}", e);
|
|
||||||
Response::builder()
|
|
||||||
.status(StatusCode::BAD_GATEWAY)
|
|
||||||
.body(Body::from(format!("Failed to read response: {}", e)))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.body(Body::from(body)).unwrap_or_else(|_| {
|
||||||
|
build_error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to build response",
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Proxy request failed: {}", e);
|
error!("Failed to read response body: {e}");
|
||||||
Response::builder()
|
build_error_response(
|
||||||
.status(StatusCode::BAD_GATEWAY)
|
StatusCode::BAD_GATEWAY,
|
||||||
.body(Body::from(format!("Proxy error: {}", e)))
|
&format!("Failed to read response: {e}"),
|
||||||
.unwrap()
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +255,7 @@ async fn ws_proxy(
|
||||||
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
|
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
||||||
let backend_url = format!(
|
let backend_url = format!(
|
||||||
"{}/ws?session_id={}&user_id={}",
|
"{}/ws?session_id={}&user_id={}",
|
||||||
|
|
@ -256,13 +268,16 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
||||||
params.user_id
|
params.user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("Proxying WebSocket to: {}", backend_url);
|
info!("Proxying WebSocket to: {backend_url}");
|
||||||
|
|
||||||
let tls_connector = native_tls::TlsConnector::builder()
|
let Ok(tls_connector) = native_tls::TlsConnector::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.danger_accept_invalid_hostnames(true)
|
.danger_accept_invalid_hostnames(true)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build TLS connector");
|
else {
|
||||||
|
error!("Failed to build TLS connector");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
|
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
|
||||||
|
|
||||||
|
|
@ -272,7 +287,7 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
||||||
let backend_socket = match backend_result {
|
let backend_socket = match backend_result {
|
||||||
Ok((socket, _)) => socket,
|
Ok((socket, _)) => socket,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to connect to backend WebSocket: {}", e);
|
error!("Failed to connect to backend WebSocket: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -350,14 +365,14 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(TungsteniteMessage::Close(_)) | Err(_) => break,
|
Ok(TungsteniteMessage::Close(_)) | Err(_) => break,
|
||||||
_ => {}
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = client_to_backend => info!("Client connection closed"),
|
() = client_to_backend => info!("Client connection closed"),
|
||||||
_ = backend_to_client => info!("Backend connection closed"),
|
() = backend_to_client => info!("Backend connection closed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,14 +388,14 @@ fn create_ui_router() -> Router<AppState> {
|
||||||
Router::new().fallback(any(proxy_api))
|
Router::new().fallback(any(proxy_api))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_static_routes(router: Router<AppState>, suite_path: &PathBuf) -> Router<AppState> {
|
fn add_static_routes(router: Router<AppState>, suite_path: &Path) -> Router<AppState> {
|
||||||
let mut r = router;
|
let mut r = router;
|
||||||
|
|
||||||
for dir in SUITE_DIRS {
|
for dir in SUITE_DIRS {
|
||||||
let path = suite_path.join(dir);
|
let path = suite_path.join(dir);
|
||||||
r = r
|
r = r
|
||||||
.nest_service(&format!("/suite/{}", dir), ServeDir::new(path.clone()))
|
.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()))
|
||||||
.nest_service(&format!("/{}", dir), ServeDir::new(path));
|
.nest_service(&format!("/{dir}"), ServeDir::new(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
r
|
r
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue