diff --git a/Cargo.lock b/Cargo.lock
index b24632f6..8886007a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1392,7 +1392,7 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "pocket-relay"
-version = "0.5.7"
+version = "0.5.8"
dependencies = [
"argon2",
"axum",
diff --git a/Cargo.toml b/Cargo.toml
index 0bdb4766..8db7044b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pocket-relay"
-version = "0.5.7"
+version = "0.5.8"
description = "Pocket Relay Server"
readme = "README.md"
keywords = ["EA", "PocketRelay", "MassEffect"]
diff --git a/Dockerfile b/Dockerfile
index 79110d02..d7d59ce8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ RUN apk add curl
WORKDIR /app
# Download server executable
-RUN curl -LJ -o pocket-relay-linux https://github.com/PocketRelay/Server/releases/download/v0.5.7/pocket-relay-linux
+RUN curl -LJ -o pocket-relay-linux https://github.com/PocketRelay/Server/releases/download/v0.5.8/pocket-relay-linux
# Make the server executable
RUN chmod +x ./pocket-relay-linux
diff --git a/default.json b/default.json
deleted file mode 100644
index beb13361..00000000
--- a/default.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "port": 80,
- "dashboard": {
- "super_email": "example@example.com",
- "super_password": "password"
- },
- "menu_message": "Pocket Relay - Logged as: {n}",
- "galaxy_at_war": {
- "decay": 0.0,
- "promotions": true
- },
- "logging": "info",
- "retriever": {
- "enabled": true,
- "origin_fetch": true,
- "origin_fetch_data": true
- }
-}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 3d6cd0f0..f6411e72 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,13 @@ version: "3"
services:
pocket-relay:
container_name: pocket-relay
+ restart: unless-stopped
ports:
# Server port
- 80:80/tcp
- image: jacobtread/pocket-relay:latest
\ No newline at end of file
+ image: jacobtread/pocket-relay:latest
+ volumes:
+ # Bind the server config to a local config.json file
+ - ./config.json:/app/config.json
+ # Binding the server data to a local data folder
+ - ./data:/app/data
diff --git a/examples/nginx/config.json b/examples/nginx/config.json
new file mode 100644
index 00000000..d6112d35
--- /dev/null
+++ b/examples/nginx/config.json
@@ -0,0 +1,4 @@
+{
+ "port": 80,
+ "reverse_proxy": true
+}
\ No newline at end of file
diff --git a/examples/nginx/docker-compose.yml b/examples/nginx/docker-compose.yml
new file mode 100644
index 00000000..ccc25b6f
--- /dev/null
+++ b/examples/nginx/docker-compose.yml
@@ -0,0 +1,20 @@
+version: "3"
+services:
+ server:
+ restart: unless-stopped
+ container_name: pocket-relay
+ image: jacobtread/pocket-relay:latest
+ volumes:
+ # Bind the server config to a local config.json file
+ - ./config.json:/app/config.json
+ # Binding the server data to a local data folder
+ - ./data:/app/data
+ nginx:
+ restart: unless-stopped
+ image: nginx
+ ports:
+ - "80:80/tcp"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ depends_on:
+ - server
\ No newline at end of file
diff --git a/examples/nginx/nginx.conf b/examples/nginx/nginx.conf
new file mode 100644
index 00000000..df0062d1
--- /dev/null
+++ b/examples/nginx/nginx.conf
@@ -0,0 +1,21 @@
+events {}
+
+http {
+ server {
+ listen 80;
+
+ server_name localhost;
+
+ location / {
+ proxy_pass http://server:80;
+
+ # Provide server with real IP address of clients
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # Upgrade websocket connections
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_http_version 1.1;
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index 77695261..7daa0968 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,6 +4,7 @@ use serde::Deserialize;
use std::{env, fs::read_to_string, path::Path};
pub struct RuntimeConfig {
+ pub reverse_proxy: bool,
pub galaxy_at_war: GalaxyAtWarConfig,
pub menu_message: String,
pub dashboard: DashboardConfig,
@@ -58,6 +59,7 @@ pub struct ServicesConfig {
#[serde(default)]
pub struct Config {
pub port: Port,
+ pub reverse_proxy: bool,
pub dashboard: DashboardConfig,
pub menu_message: String,
pub galaxy_at_war: GalaxyAtWarConfig,
@@ -69,6 +71,7 @@ impl Default for Config {
fn default() -> Self {
Self {
port: 80,
+ reverse_proxy: false,
dashboard: Default::default(),
menu_message: "Pocket Relay - Logged as: {n}".to_string(),
galaxy_at_war: Default::default(),
diff --git a/src/middleware/ip_address.rs b/src/middleware/ip_address.rs
new file mode 100644
index 00000000..62e19449
--- /dev/null
+++ b/src/middleware/ip_address.rs
@@ -0,0 +1,75 @@
+use std::net::SocketAddr;
+
+use axum::{
+ async_trait,
+ body::boxed,
+ extract::{rejection::ExtensionRejection, ConnectInfo, FromRequestParts},
+ http::request::Parts,
+ response::{IntoResponse, Response},
+ Extension,
+};
+use hyper::{HeaderMap, StatusCode};
+use log::warn;
+use thiserror::Error;
+
+use crate::state::App;
+
+/// Middleware for extracting the server public address
+pub struct IpAddress(pub SocketAddr);
+
+const REAL_IP_HEADER: &str = "X-Real-IP";
+
+#[async_trait]
+impl FromRequestParts for IpAddress
+where
+ S: Send + Sync,
+{
+ type Rejection = IpAddressError;
+
+ async fn from_request_parts(parts: &mut Parts, state: &S) -> Result {
+ let reverse_proxy = App::config().reverse_proxy;
+ if reverse_proxy {
+ let ip = match extract_ip_header(&parts.headers) {
+ Some(ip) => ip,
+ None => {
+ warn!("Failed to extract X-Real-IP header from connecting client. If you are NOT using a reverse proxy\n\
+ disable the `reverse_proxy` config property, otherwise check that your reverse proxy is configured\n\
+ correctly according the guide. (Closing connection with error)");
+ return Err(IpAddressError::InvalidOrMissing);
+ }
+ };
+ return Ok(Self(ip));
+ }
+ let value = Extension::>::from_request_parts(parts, state).await?;
+ Ok(Self(value.0 .0))
+ }
+}
+
+fn extract_ip_header(headers: &HeaderMap) -> Option {
+ let header = headers.get(REAL_IP_HEADER)?;
+ let value = header.to_str().ok()?;
+ value.parse().ok()
+}
+
+/// Error type used by the token checking middleware to handle
+/// different errors and create error respones based on them
+#[derive(Debug, Error)]
+pub enum IpAddressError {
+ #[error(transparent)]
+ ConnectInfo(#[from] ExtensionRejection),
+ #[error("X-Real-IP header is invalid or missing")]
+ InvalidOrMissing,
+}
+
+/// IntoResponse implementation for TokenError to allow it to be
+/// used within the result type as a error response
+impl IntoResponse for IpAddressError {
+ #[inline]
+ fn into_response(self) -> Response {
+ let status: StatusCode = match self {
+ IpAddressError::ConnectInfo(err) => return err.into_response(),
+ _ => StatusCode::BAD_REQUEST,
+ };
+ (status, boxed(self.to_string())).into_response()
+ }
+}
diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs
index 586fac87..94a42ff6 100644
--- a/src/middleware/mod.rs
+++ b/src/middleware/mod.rs
@@ -4,5 +4,7 @@ pub mod auth;
pub mod blaze_upgrade;
/// Middleware functions related to CORS implementation
pub mod cors;
+/// IP address extraction middleware
+pub mod ip_address;
/// XML response types
pub mod xml;
diff --git a/src/routes/server.rs b/src/routes/server.rs
index 2f9849fa..7ef020f8 100644
--- a/src/routes/server.rs
+++ b/src/routes/server.rs
@@ -3,14 +3,13 @@
use crate::{
database::entities::players::PlayerRole,
- middleware::{auth::AdminAuth, blaze_upgrade::BlazeUpgrade},
+ middleware::{auth::AdminAuth, blaze_upgrade::BlazeUpgrade, ip_address::IpAddress},
session::Session,
state::{self, App},
utils::logging::LOG_FILE_NAME,
};
use axum::{
body::Empty,
- extract::ConnectInfo,
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Json,
@@ -19,10 +18,7 @@ use blaze_pk::packet::PacketCodec;
use interlink::service::Service;
use log::{debug, error};
use serde::{Deserialize, Serialize};
-use std::{
- net::SocketAddr,
- sync::atomic::{AtomicU32, Ordering},
-};
+use std::sync::atomic::{AtomicU32, Ordering};
use tokio::{fs::read_to_string, io::split};
use tokio_util::codec::{FramedRead, FramedWrite};
@@ -75,10 +71,7 @@ pub async fn dashboard_details() -> Json {
/// Handles upgrading connections from the Pocket Relay Client tool
/// from HTTP over to the Blaze protocol for proxing the game traffic
/// as blaze sessions using HTTP Upgrade
-pub async fn upgrade(
- ConnectInfo(socket_addr): ConnectInfo,
- upgrade: BlazeUpgrade,
-) -> Response {
+pub async fn upgrade(IpAddress(socket_addr): IpAddress, upgrade: BlazeUpgrade) -> Response {
// TODO: Socket address extraction for forwarded reverse proxy
tokio::spawn(async move {
diff --git a/src/state.rs b/src/state.rs
index 1a9e1ea9..f9f19ed9 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -51,6 +51,7 @@ impl App {
// Config data persisted to runtime
let runtime_config = RuntimeConfig {
+ reverse_proxy: config.reverse_proxy,
galaxy_at_war: config.galaxy_at_war,
menu_message,
dashboard: config.dashboard,