Skip to content

Commit

Permalink
Support hosting service on non-root path prefix (#160)
Browse files Browse the repository at this point in the history
fixes #159.


The path prefix is inferred by the following order:

1. The environment `RSS_FUNNEL_PATH_PREFIX` (Example value: `"app/"`),
or if unspecified
2. The path of environment `RSS_FUNNEL_APP_BASE`, or if unspecified
3. `"/"`.
  • Loading branch information
shouya authored Nov 11, 2024
1 parent 95b3bf0 commit 6ee058e
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 156 deletions.
4 changes: 3 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use url::Url;

use crate::{
server::{self, EndpointConfig, ServerConfig},
util::relative_path,
ConfigError, Result,
};

Expand Down Expand Up @@ -167,7 +168,8 @@ async fn health_check(health_check_config: HealthCheckConfig) -> Result<()> {
server = format!("http://{server}");
}

let url = Url::parse(&server)?.join("/_health")?;
let path = relative_path("_health");
let url = Url::parse(&server)?.join(&path)?;
match reqwest::get(url.clone()).await {
Ok(res) if res.status().is_success() => {
eprintln!("{server} is healthy");
Expand Down
2 changes: 1 addition & 1 deletion src/filter/image_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use url::Url;
use super::{FeedFilter, FeedFilterConfig, FilterContext};
use crate::{feed::Feed, ConfigError, Error, Result};

const IMAGE_PROXY_ROUTE: &str = "/_image";
const IMAGE_PROXY_ROUTE: &str = "_image";

#[derive(
JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash,
Expand Down
1 change: 1 addition & 0 deletions src/filter/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use super::{FeedFilter, FeedFilterConfig, FilterContext};
JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash,
)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum MergeConfig {
/// Simple merge with default client and no filters
Simple(MergeSimpleConfig),
Expand Down
61 changes: 45 additions & 16 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ mod web;

use std::{path::Path, sync::Arc};

use axum::{routing::get, Extension, Router};
use axum::{
response::{IntoResponse, Redirect},
routing::get,
Extension, Router,
};
use clap::Parser;
use http::StatusCode;
use tower_http::compression::CompressionLayer;
use tracing::{info, warn};

use crate::Result;
use crate::{
util::{self, relative_path},
Result,
};

pub use endpoint::{EndpointConfig, EndpointParam};

use self::{feed_service::FeedService, watcher::Watcher};
Expand Down Expand Up @@ -62,7 +70,10 @@ impl ServerConfig {

pub async fn run_without_config(self) -> Result<()> {
let feed_service = FeedService::new_otf().await?;
info!("serving on-the-fly endpoint on /otf");
let rel_path = relative_path("otf");
info!(
"No config detected. Serving automatic on-the-fly endpoint on {rel_path}"
);
self.serve(feed_service).await
}

Expand Down Expand Up @@ -108,27 +119,23 @@ impl ServerConfig {
self.serve(feed_service).await
}

pub async fn serve(self, feed_service: FeedService) -> Result<()> {
info!("listening on {}", &self.bind);
let listener = tokio::net::TcpListener::bind(&*self.bind).await?;

let mut app = Router::new();
pub fn router(&self, feed_service: FeedService) -> Router {
let mut routes = Router::new();

#[cfg(feature = "inspector-ui")]
if self.inspector_ui {
app = app
routes = routes
.nest("/", inspector::router())
.nest("/_/", web::router())
.route(
"/",
get(|| async { axum::response::Redirect::temporary("/_/") }),
);
.route("/", get(redirect_to_home));
} else {
app = app.route("/", get(|| async { "rss-funnel is up and running!" }));
routes =
routes.route("/", get(|| async { "rss-funnel is up and running!" }));
}

if !cfg!(feature = "inspector-ui") {
app = app.route("/", get(|| async { "rss-funnel is up and running!" }));
routes =
routes.route("/", get(|| async { "rss-funnel is up and running!" }));
}

let feed_service_router = Router::new()
Expand All @@ -138,17 +145,39 @@ impl ServerConfig {
}))
.layer(CompressionLayer::new().gzip(true));

app = app
routes = routes
// deprecated, will be removed on 0.2
.route("/health", get(|| async { "ok" }))
.route("/_health", get(|| async { "ok" }))
.merge(image_proxy::router())
.merge(feed_service_router)
.layer(Extension(feed_service));

routes
}

pub async fn serve(self, feed_service: FeedService) -> Result<()> {
info!("listening on {}", &self.bind);
let listener = tokio::net::TcpListener::bind(&*self.bind).await?;

let mut app = Router::new();

let prefix = util::relative_path("");
if prefix == "/" {
app = self.router(feed_service);
} else {
info!("Path prefix set to {prefix}");
app = app.nest(&prefix, self.router(feed_service))
};

info!("starting server");
let server = axum::serve(listener, app);

Ok(server.await?)
}
}

async fn redirect_to_home() -> impl IntoResponse {
let home_path = relative_path("_/");
Redirect::temporary(&home_path)
}
20 changes: 12 additions & 8 deletions src/server/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use axum::{
use axum_extra::extract::CookieJar;
use http::request::Parts;

use crate::util::relative_path;

use super::feed_service::FeedService;

pub struct Auth;
Expand Down Expand Up @@ -42,7 +44,8 @@ impl<S: Send + Sync> FromRequestParts<S> for Auth {
}

fn login() -> Response {
Redirect::to("/_inspector/login.html?login_required=1").into_response()
let login_path = relative_path("_inspector/login.html?login_required=1");
Redirect::to(&login_path).into_response()
}

#[derive(serde::Deserialize)]
Expand All @@ -59,17 +62,18 @@ pub async fn handle_login(
match feed_service.login(&params.username, &params.password).await {
Some(session_id) => {
let cookie_jar = cookie_jar.add(("session_id", session_id));
(cookie_jar, Redirect::to("/_inspector/index.html")).into_response()
let home_path = relative_path("_inspector/index.html");
(cookie_jar, Redirect::to(&home_path)).into_response()
}
_ => {
let login_path = relative_path("_inspector/login.html?bad_auth=1");
Redirect::to(&login_path).into_response()
}
_ => Redirect::to("/_inspector/login.html?bad_auth=1").into_response(),
}
}

pub async fn handle_logout(cookie_jar: CookieJar) -> Response {
let cookie_jar = cookie_jar.remove("session_id");
(
cookie_jar,
Redirect::to("/_inspector/login.html?logged_out=1"),
)
.into_response()
let logout_path = relative_path("_inspector/login.html?logged_out=1");
(cookie_jar, Redirect::to(&logout_path)).into_response()
}
16 changes: 1 addition & 15 deletions src/server/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ impl EndpointParam {
}

fn get_base(req: &Parts) -> Option<Url> {
if let Some(url) = Self::base_from_env().as_ref() {
if let Some(url) = crate::util::app_base_from_env().as_ref() {
return Some(url.clone());
}

Expand Down Expand Up @@ -222,20 +222,6 @@ impl EndpointParam {
let base = base.parse().ok()?;
Some(base)
}

fn base_from_env() -> &'static Option<Url> {
use std::env;
use std::sync::OnceLock;

static APP_BASE_URL: OnceLock<Option<Url>> = OnceLock::new();
APP_BASE_URL.get_or_init(|| {
let var = env::var("RSS_FUNNEL_APP_BASE").ok();
var.map(|v| {
v.parse()
.expect("Invalid base url specified in RSS_FUNNEL_APP_BASE")
})
})
}
}

impl Service<Request> for EndpointService {
Expand Down
1 change: 1 addition & 0 deletions src/server/feed_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{cli::RootConfig, ConfigError};

use super::{endpoint::EndpointService, EndpointConfig};

// can be cheaply cloned.
#[derive(Clone)]
pub struct FeedService {
inner: Arc<RwLock<Inner>>,
Expand Down
5 changes: 4 additions & 1 deletion src/server/image_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ use tower_http::cors::CorsLayer;
use tracing::{info, warn};
use url::Url;

use crate::util::relative_path;

lazy_static::lazy_static! {
static ref SIGN_KEY: Box<[u8]> = init_sign_key();
}

pub fn router() -> Router {
info!("handling image proxy: /_image");
let image_proxy_path = relative_path("_image");
info!("handling image proxy: {image_proxy_path}");

use tower_http::cors::AllowOrigin;
let cors = CorsLayer::new()
Expand Down
11 changes: 8 additions & 3 deletions src/server/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use http::StatusCode;
use login::Auth;
use maud::{html, Markup};

use crate::util::relative_path;

use super::{feed_service::FeedService, EndpointParam};

#[derive(rust_embed::RustEmbed)]
Expand Down Expand Up @@ -93,9 +95,11 @@ impl<S> axum::extract::FromRequestParts<S> for AutoReload {

async fn handle_home(auth: Option<Auth>) -> impl IntoResponse {
if auth.is_some() {
Redirect::temporary("/_/endpoints")
let endpoints_path = relative_path("_/endpoints");
Redirect::temporary(&endpoints_path)
} else {
Redirect::temporary("/_/login")
let login_path = relative_path("_/login");
Redirect::temporary(&login_path)
}
}

Expand Down Expand Up @@ -147,9 +151,10 @@ fn favicon() -> Markup {
}

fn sprite(icon: &str) -> Markup {
let sprite_path = relative_path("_/sprite.svg");
html! {
svg class="icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" {
use xlink:href=(format!("/_/sprite.svg#{icon}"));
use xlink:href=(format!("{sprite_path}#{icon}"));
}
}
}
Expand Down
13 changes: 9 additions & 4 deletions src/server/web/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
filter::FilterContext,
server::{endpoint::EndpointService, web::sprite, EndpointParam},
source::{FromScratch, Source},
util::relative_path,
};

pub async fn render_endpoint_page(
Expand Down Expand Up @@ -51,7 +52,8 @@ pub async fn render_endpoint_page(
body {
header .header-bar {
button .left-button {
a href="/_/" { "Back" }
@let home_path = relative_path("_/");
a href=(home_path) { "Back" }
}
h2 { (path) }
button .button title="Copy Endpoint URL" onclick="copyToClipboard()" {
Expand Down Expand Up @@ -108,13 +110,14 @@ fn source_control_fragment(
) -> Markup {
match source {
Source::Dynamic => html! {
@let endpoint_path = relative_path(&format!("_/endpoint/{path}"));
input
.hx-included.grow
type="text"
name="source"
placeholder="Source URL"
value=[param.source().map(|url| url.as_str())]
hx-get=(format!("/_/endpoint/{path}"))
hx-get=(endpoint_path)
hx-trigger="keyup changed delay:500ms"
hx-push-url="true"
hx-indicator=".loading"
Expand Down Expand Up @@ -174,6 +177,7 @@ fn source_template_fragment(
@match fragment {
Either::Left(plain) => span style="white-space: nowrap" { (plain) },
Either::Right((name, Some(placeholder))) => {
@let endpoint_path = relative_path(&format!("_/endpoint/{path}"));
@let value=queries.get(name);
@let default_value=placeholder.default.as_ref();
@let value=value.or(default_value);
Expand All @@ -184,7 +188,7 @@ fn source_template_fragment(
placeholder=(name)
pattern=[validation]
value=[value]
hx-get=(format!("/_/endpoint/{path}"))
hx-get=(endpoint_path)
hx-trigger="keyup changed delay:500ms"
hx-push-url="true"
hx-include=".hx-included"
Expand Down Expand Up @@ -246,9 +250,10 @@ fn render_config_fragment(
} @else {
div {
header { b { "Filters:" } }
@let endpoint_path = relative_path(&format!("_/endpoint/{path}"));
ul #filter-list .hx-included
hx-vals="js:{...gatherFilterSkip()}"
hx-get=(format!("/_/endpoint/{path}"))
hx-get=(endpoint_path)
hx-trigger="click from:.filter-name"
hx-push-url="true"
hx-include=".hx-included"
Expand Down
8 changes: 6 additions & 2 deletions src/server/web/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
cli::RootConfig,
server::{web::sprite, EndpointConfig},
source::SourceConfig,
util::relative_path,
};

pub fn render_endpoint_list_page(
Expand Down Expand Up @@ -59,7 +60,10 @@ fn endpoint_list_entry_fragment(endpoint: &EndpointConfig) -> Markup {
html! {
li {
p {
a href={"/_/endpoint/" (endpoint.path.trim_start_matches('/'))} {
@let normalized_path = endpoint.path.trim_start_matches('/');
@let endpoint_path = format!("/_/endpoint/{}", normalized_path);
@let endpoint_path = relative_path(&endpoint_path);
a href=(endpoint_path) {
(endpoint.path)
}

Expand Down Expand Up @@ -104,7 +108,7 @@ fn short_source_repr(source: &SourceConfig) -> Markup {
},
SourceConfig::Simple(url) if url.starts_with("/") => {
let path = url_path(url.as_str());
let path = path.map(|p| format!("/_/{p}"));
let path = path.map(|p| relative_path(&format!("_/{p})")));
html! {
@if let Some(path) = path {
span .tag.local {
Expand Down
Loading

0 comments on commit 6ee058e

Please sign in to comment.