Implemented proxying of GSD requests

This commit is contained in:
Dennis Nemec
2025-09-30 21:55:53 +02:00
parent b95454458c
commit e8954ba5c1
14 changed files with 2589 additions and 2 deletions

10
.gitignore vendored
View File

@ -16,3 +16,13 @@ target/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Added by cargo
/target
config.toml
.idea/

2061
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "delivery-backend"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.6"
chrono = "0.4.42"
http = "1.3.1"
log = "0.4.28"
redis = { version = "0.32.6", features = ["connection-manager", "tokio-comp"] }
reqwest = { version = "0.12.23", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
simplelog = "0.12.2"
tokio = { version = "1.47.1", features = ["full"] }
toml = "0.9.7"

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
# Build stage
FROM rust:1.90.0-slim-trixie as builder
# Create app directory
WORKDIR /app
# Copy manifests
COPY Cargo.toml Cargo.lock ./
# Copy source code
COPY src ./src
# Build for release
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
# Install runtime dependencies (if needed, e.g., for SSL)
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 appuser
# Set working directory
WORKDIR /app
# Copy only the binary from builder
COPY --from=builder /app/target/release/delivery-backend /app/service
# Change ownership to non-root user
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Expose port (adjust as needed)
EXPOSE 8080
# Run the binary
CMD ["/app/service"]

View File

@ -1,2 +1 @@
# Holzleitner-Lieferservice-Backend
# Lieferservice Rust Backend

9
config.toml.example Normal file
View File

@ -0,0 +1,9 @@
log_file_prefix = "delivery_backend"
host_ip = "127.0.0.1"
host_port = 3000
redis_url = "redis://127.0.0.1:6379"
gsd_app_key = "GSD-RestApi"
gsd_rest_url = "http://192.168.1.9:8334"
gsd_user = "GSDWebServiceTmp"
gsd_password = "<PASSWORD>"
gsd_app_names = ["GSD-RestApi"]

43
docker-compose.yaml Normal file
View File

@ -0,0 +1,43 @@
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: redis-server
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
microservice:
build:
context: .
dockerfile: Dockerfile
container_name: rust-microservice
ports:
- "8080:8080"
environment:
- REDIS_URL=redis://redis:6379
- RUST_LOG=info
depends_on:
redis:
condition: service_healthy
networks:
- app-network
restart: unless-stopped
networks:
app-network:
driver: bridge
volumes:
redis-data:
driver: local

26
src/api.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::middleware::AppState;
use axum::Extension;
use axum::extract::Request;
use axum::response::IntoResponse;
use http::StatusCode;
use log::error;
use std::sync::Arc;
use axum::body::Body;
pub async fn handle_post(
Extension(state): Extension<Arc<AppState>>,
request: Request<Body>,
) -> impl IntoResponse {
match state.clone().gsd_service.forward_post_request(request).await {
Ok(e) => e.text().await.unwrap().into_response(),
Err(e) => {
error!("Failed to forward post: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
pub async fn handle_login() -> impl IntoResponse {
}

65
src/config.rs Normal file
View File

@ -0,0 +1,65 @@
use std::fs;
use std::path::PathBuf;
const CONFIG_FILE: &str = "config.toml";
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Config {
// Backend server configuration
pub log_file_prefix: String,
pub host_ip: String,
pub host_port: u16,
pub redis_url: String,
// GSD RestAPI configuration
pub gsd_app_key: String,
pub gsd_rest_url: String,
pub gsd_user: String,
pub gsd_password: String,
pub gsd_app_names: Vec<String>,
}
impl Config {
pub fn get_host_url(&self) -> String {
format!("{}:{}", self.host_ip, self.host_port)
}
}
fn get_config_absolute_path() -> PathBuf {
PathBuf::from(CONFIG_FILE)
}
pub fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
if fs::exists(get_config_absolute_path())? {
Ok(toml::from_str(&fs::read_to_string(CONFIG_FILE)?)?)
} else {
let config = create_standard_config();
save_config(&config)?;
Ok(config)
}
}
pub fn generate_log_file_name(prefix: String) -> String {
format!("{}_{}.log", prefix, chrono::Local::now().to_rfc3339())
}
pub fn save_config(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
fs::write(get_config_absolute_path(), toml::to_string(config)?)?;
Ok(())
}
pub fn create_standard_config() -> Config {
Config {
log_file_prefix: String::from("delivery_backend"),
host_ip: String::from("127.0.0.1"),
host_port: 3000,
redis_url: String::from("redis://127.0.0.1:6379"),
gsd_rest_url: String::from("http://127.0.0.1:8334"),
gsd_app_key: String::from("GSD-RestApi"),
gsd_app_names: vec![String::from("GSD-RestApi")],
gsd_user: String::from("<GSD-USER>"),
gsd_password: String::from("<GSD-Password>"),
}
}

54
src/main.rs Normal file
View File

@ -0,0 +1,54 @@
use crate::api::{handle_login, handle_post};
use crate::config::load_config;
use crate::middleware::AppState;
use crate::repository::RedisRepository;
use crate::util::initialize_logging;
use axum::routing::post;
use axum::{Extension, Router};
use log::info;
use std::sync::Arc;
mod api;
mod config;
mod middleware;
mod repository;
mod service_gsd;
mod util;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config()?;
initialize_logging(&config);
info!("Logging initialized");
info!("Starting Holzleitner Delivery Backend");
let redis_url = config.redis_url.clone();
let host_url = config.get_host_url().clone();
let state = Arc::new(AppState {
config: config.clone(),
repository: RedisRepository::try_new(redis_url).await?,
gsd_service: (&config).into(),
});
let app = Router::new()
.route("/login", post(handle_login))
.route("/{*wildcard}", post(handle_post))
.layer(Extension(state.clone()))
.route_layer(axum::middleware::from_fn_with_state(
state.clone(),
middleware::gsd_add_header,
))
.route_layer(axum::middleware::from_fn_with_state(
state.clone(),
middleware::auth_middleware,
))
.with_state(state);
let listener = tokio::net::TcpListener::bind(host_url).await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}

82
src/middleware.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::config::Config;
use crate::repository::RedisRepository;
use crate::service_gsd::GSDService;
use axum::extract::{Request, State};
use axum::http::{HeaderValue, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use log::{error, info};
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub config: Config,
pub repository: RedisRepository,
pub gsd_service: GSDService,
}
pub async fn auth_middleware(
State(_state): State<Arc<AppState>>,
request: Request,
next: Next,
) -> Response {
next.run(request).await
}
pub async fn gsd_add_header(
State(state): State<Arc<AppState>>,
mut request: Request,
next: Next,
) -> Response {
let state_cloned = state.clone();
let session = state_cloned.repository.get_session().await;
match session {
Ok(session) => {
let session_value;
if session.is_none() {
match state_cloned.gsd_service.get_session().await {
Ok(session) => {
session_value = session.clone();
match state_cloned.repository.set_session(session.clone()).await {
Ok(_) => {
info!("Redis: saved session {}", &session);
}
Err(err) => {
error!("Redis: failed to save session: {}", err);
}
}
}
Err(error) => {
error!("Error getting session: {:?}", error);
return StatusCode::UNAUTHORIZED.into_response();
}
}
} else {
session_value = session.unwrap();
}
request.headers_mut().insert(
"sessionId",
HeaderValue::from_str(session_value.as_str()).unwrap(),
);
}
Err(error) => {
error!(
"Redis error occured during fetching current session id. Error: {}",
error
);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
}
request.headers_mut().insert(
"appkey",
HeaderValue::from_str(state_cloned.config.gsd_app_key.as_str()).unwrap(),
);
next.run(request).await
}

37
src/repository.rs Normal file
View File

@ -0,0 +1,37 @@
use redis::aio::ConnectionManager;
use redis::{AsyncTypedCommands, Connection, RedisError, RedisResult};
pub fn get_redis_connection(redis_url: String) -> RedisResult<Connection> {
redis::Client::open(redis_url)?.get_connection()
}
#[derive(Clone)]
pub struct RedisRepository {
connection_manager: ConnectionManager,
}
impl RedisRepository {
pub async fn try_new(redis_url: String) -> Result<RedisRepository, RedisError> {
Ok(RedisRepository {
connection_manager: redis::Client::open(redis_url)?
.get_connection_manager()
.await?,
})
}
pub async fn get_session(&self) -> RedisResult<Option<String>> {
self.connection_manager
.clone()
.get::<String>("current_session".to_string())
.await
}
pub async fn set_session(&self, session: String) -> RedisResult<()> {
self.connection_manager
.clone()
.set("current_session", session)
.await?;
Ok(())
}
}

120
src/service_gsd.rs Normal file
View File

@ -0,0 +1,120 @@
use axum::body::Body;
use axum::extract::Request;
use log::{error, info};
use crate::config::Config;
use reqwest::Response;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GSDLoginRequestDTO {
user: String,
pass: String,
app_names: Vec<String>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GSDLoginResponseDTO {
status: GSDLoginResponseStatusDTO,
data: Option<GSDLoginResponseDataDTO>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GSDLoginResponseDataDTO {
session_id: String,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GSDLoginResponseStatusDTO {
internal_status: String,
status_message: String,
}
#[derive(Clone)]
pub struct GSDService {
host_url: String,
app_names: Vec<String>,
app_key: String,
username: String,
password: String,
}
#[derive(Debug)]
pub enum GSDServiceError {
LoginFailed,
LoginResponseParsingFailed,
RequestError(String),
}
impl GSDService {
pub async fn get_session(&self) -> Result<String, GSDServiceError> {
info!("Session: No session found. Generate session from GSD server {}", self.host_url);
let dto = GSDLoginRequestDTO {
user: self.username.clone(),
pass: self.password.clone(),
app_names: self.app_names.clone(),
};
let response = reqwest::Client::new()
.post(format!("{}/v1/login", self.host_url.clone()))
.header("appKey", self.app_key.as_str())
.header("Content-Type", "application/json")
.json(&dto)
.send()
.await
.map_err(|e| {
error!("Session: error request to GSD: {}", e);
GSDServiceError::LoginFailed
})?;
let response_dto: GSDLoginResponseDTO = response
.json()
.await
.map_err(|e| {
error!("Session: error request to GSD: {}", e);
GSDServiceError::LoginResponseParsingFailed
})?;
if response_dto.status.internal_status != "0" {
error!("Session: error message from GSD: {}", response_dto.status.status_message);
Err(GSDServiceError::LoginFailed)
} else {
match response_dto.data {
Some(data) => {
info!("Session: successfully obtained session with session id {}", &data.session_id);
Ok(data.session_id.clone())
},
None => {
error!("Session: failed to obtain session id. No session id in request found.");
Err(GSDServiceError::LoginResponseParsingFailed)
},
}
}
}
pub async fn forward_post_request(&self, request: Request<Body>) -> Result<Response, GSDServiceError> {
let (parts, body) = request.into_parts();
reqwest::Client::new()
.post(format!("{}{}", self.host_url.clone(), parts.uri))
.headers(parts.headers)
.body(axum::body::to_bytes(body, usize::MAX).await.unwrap())
.send()
.await
.map_err(|e| GSDServiceError::RequestError(e.to_string()))
}
}
impl From<&Config> for GSDService {
fn from(config: &Config) -> Self {
Self {
host_url: config.gsd_rest_url.clone(),
app_names: config.gsd_app_names.clone(),
app_key: config.gsd_app_key.clone(),
username: config.gsd_user.clone(),
password: config.gsd_password.clone(),
}
}
}

21
src/util.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::config::{Config, generate_log_file_name};
use log::LevelFilter;
use simplelog::{ColorChoice, CombinedLogger, TermLogger, TerminalMode, WriteLogger};
use std::fs::File;
pub fn initialize_logging(config: &Config) {
CombinedLogger::init(vec![
TermLogger::new(
LevelFilter::Info,
simplelog::Config::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
),
WriteLogger::new(
LevelFilter::Info,
simplelog::Config::default(),
File::create(generate_log_file_name(config.log_file_prefix.clone())).unwrap(),
),
])
.unwrap();
}