that is a lot of changes wow
Some checks failed
/ build (push) Failing after 47s

This commit is contained in:
Dustin Thomas 2025-08-22 12:55:55 -05:00
parent 2970fa1a15
commit 37e69a7b17
Signed by: cptlobster
GPG key ID: 33D607425C830B4C
14 changed files with 1123 additions and 20 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
/target
/.idea/
/.vscode/
sg1.toml
*.md

View file

@ -0,0 +1,41 @@
on:
push:
branches:
- 'main'
paths:
- ".forgejo/workflows/*"
- "src/**"
- "templates/**"
- "Dockerfile*"
- ".dockerignore"
- "Cargo.*"
- "overseer.sh"
jobs:
build:
runs-on: docker
container:
image: docker:cli
steps:
- name: "Install Git and NodeJS"
run: "apk add git nodejs"
- name: "Checkout"
uses: "actions/checkout@v4"
- name: "Login to Forge Packages"
uses: "docker/login-action@v3"
with:
registry: "forge.cptlobster.dev"
username: "cptlobster"
password: ${{ secrets.PACKAGE_TOKEN }}
- name: "Build and Push (Minimal)"
uses: "docker/build-push-action@v6"
with:
push: true
tags: "forge.cptlobster.dev/cptlobster/stargate:dev"
- name: "Build and Push (Rich Output)"
uses: "docker/build-push-action@v6"
with:
push: true
file: "Dockerfile.rich"
tags: "forge.cptlobster.dev/cptlobster/stargate:dev-rich"

View file

@ -0,0 +1,33 @@
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: docker
container:
image: docker:cli
steps:
- name: "Install Git and NodeJS"
run: "apk add git nodejs"
- name: "Checkout"
uses: "actions/checkout@v4"
- name: "Login to Forge Packages"
uses: "docker/login-action@v3"
with:
registry: "forge.cptlobster.dev"
username: "cptlobster"
password: ${{ secrets.PACKAGE_TOKEN }}
- name: "Build and Push"
uses: "docker/build-push-action@v6"
with:
push: true
tags: "forge.cptlobster.dev/cptlobster/stargate:${{ forge.ref_name }},forge.cptlobster.dev/cptlobster/stargate:latest"
- name: "Build and Push (Rich Output)"
uses: "docker/build-push-action@v6"
with:
push: true
file: "Dockerfile.rich"
tags: "forge.cptlobster.dev/cptlobster/stargate:${{ forge.ref_name }}-rich,forge.cptlobster.dev/cptlobster/stargate:rich"

856
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,14 @@ name = "stargate"
version = "0.1.0"
edition = "2024"
[features]
rich = [ "dep:tera", "dep:rocket_dyn_templates" ]
[dependencies]
rocket = "0.5.1"
regex = "1.11.1"
watchfile = { version = "0.1.1", default-features = false, features = ["toml"] }
tokio = { version = "1.45.1", features = ["sync"] }
log = "0.4.27"
log = "0.4.27"
tera = { version = "1.20.0", optional = true, default-features = false }
rocket_dyn_templates = { version = "0.2.0", optional = true, features = ["tera"] }

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM rust:alpine AS builder
WORKDIR /usr/src/stargate
COPY . .
RUN cargo install --path .
FROM alpine:3.22
COPY --from-builder /usr/local/cargo/bin/stargate /usr/local/bin/stargate
WORKDIR /etc/stargate
CMD ["stargate"]

13
Dockerfile.rich Normal file
View file

@ -0,0 +1,13 @@
FROM rust:alpine AS builder
WORKDIR /usr/src/stargate
COPY . .
RUN cargo install --features rich --path .
FROM alpine:3.22
COPY --from-builder /usr/local/cargo/bin/stargate /usr/local/bin/stargate
WORKDIR /etc/stargate
CMD ["stargate"]

View file

@ -1,9 +1,16 @@
# Stargate
Stargate is a URL shortening service (similar to bit.ly).
Stargate is a minimal URL shortening service (similar to bit.ly).
## Usage
You can use the prebuilt Docker image:
```shell
docker run -p 8000:8000 \
-v sg1.toml:/etc/stargate/sg1.toml \
forge.cptlobster.dev/cptlobster/stargate
```
You can compile and run the server by pulling this repository and using cargo:
```shell
@ -14,9 +21,16 @@ You can configure specific routes by editing `sg1.toml`. A sample configuration
configuration format is as follows:
```toml
[default.routes]
"a" = "https://example.com" # simple path match
"b.*" = "https://other.org" # regex match; starts with b
# Simple text matcher
[[routes]]
from = "a"
to = "https://example.com"
# More complex regex matcher
[[routes]]
from = "bean(\\d+)"
to = "https://other.com/beans/$1"
using = "regex"
```
## License

View file

@ -16,3 +16,9 @@ to = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
from = ".*ic"
to = "https://en.wikipedia.org/wiki/Special:Random"
using = "regex"
[[routes]]
from = "dod([fs][0-9]{2})e([0-9]{3})"
# Capture groups can be replaced using numbered capture groups (`$1`, `$2`, ...)
to = "https://dod.cptlobster.dev/episodes/$1/$2"
using = "regex"

View file

@ -33,7 +33,7 @@ impl Default for ConfigManager {
impl ConfigManager {
pub async fn get(&self) -> Config {
log::debug!("File changed: {}", self.watcher.has_changed().unwrap());
log::info!("File changed: {}", self.watcher.has_changed().unwrap());
if self.watcher.has_changed().unwrap() || self.content.read().unwrap().is_none() {
log::info!("Stargate configuration has changed. Updating routes...");
let mut con = self.content.write().unwrap();
@ -49,6 +49,8 @@ impl ConfigManager {
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct Config {
#[serde(default)]
pub hide_routes: bool,
pub routes: Vec<Route>
}
@ -92,19 +94,19 @@ impl Route {
#[serde(crate = "rocket::serde", rename_all = "snake_case")]
pub enum MatcherType {
Regex,
#[serde(other)]
Text
#[serde(other, alias = "text")]
Exact
}
impl Default for MatcherType {
fn default() -> Self {
MatcherType::Text
MatcherType::Exact
}
}
impl Default for Config {
fn default() -> Self {
Config { routes: Vec::new() }
Config { hide_routes: false, routes: Vec::new() }
}
}

View file

@ -14,36 +14,81 @@
mod config;
use rocket::State;
use rocket::{State, Request};
use rocket::response::Redirect;
use rocket::response::status::{NoContent, NotFound};
use crate::config::{ConfigManager, Route};
#[cfg(feature = "rich")]
use rocket_dyn_templates::{Template, tera::Tera, context};
/// List all paths configured for this instance.
#[get("/")]
#[cfg(not(feature = "rich"))]
async fn list_paths(cm: &State<ConfigManager>) -> Result<String, NoContent> {
let config = cm.get().await;
if config.hide_routes { return Err(NoContent) }
let all_routes = config.routes.iter().map(|r: &Route| r.src()).collect::<Vec<String>>();
if all_routes.is_empty() { Err(NoContent) } else {
Ok(all_routes.into_iter().map(|a| format!("/{}", a)).collect::<Vec<String>>().join("\n"))
}
}
/// List all paths configured for this instance.
#[get("/")]
#[cfg(feature = "rich")]
async fn list_paths(cm: &State<ConfigManager>) -> Template {
let config = cm.get().await;
Template::render("main", context! {
config: &config
})
}
/// Try to redirect the specified path; If successful, return a See Other (HTTP 303) to the
/// configured destination. If it fails to match a path, return a Not Found (HTTP 404).
#[get("/<path>")]
#[cfg(not(feature = "rich"))]
async fn redirect_path(cm: &State<ConfigManager>, path: &str) -> Result<Redirect, NotFound<String>> {
let config = cm.get().await;
match config.find(path) {
Some(p) => Ok(Redirect::to(p)),
None => Err(NotFound(format!("Not found: {}", path)))
None => {
Err(NotFound(format!("No matching route found: {}", path)))
}
}
}
/// Try to redirect the specified path; If successful, return a See Other (HTTP 303) to the
/// configured destination. If it fails to match a path, return a Not Found (HTTP 404).
#[get("/<path>")]
#[cfg(feature = "rich")]
async fn redirect_path(cm: &State<ConfigManager>, path: &str) -> Result<Redirect, NotFound<Template>> {
let config = cm.get().await;
match config.find(path) {
Some(p) => Ok(Redirect::to(p)),
None => {
Err(NotFound(Template::render("notfound", context!{
title: "Not Found",
path: path,
config: &config
})))
}
}
}
/// Read the configuration file and start the server.
#[launch]
#[cfg(feature = "rich")]
async fn rocket() -> _ {
rocket::build()
.manage(ConfigManager::default())
.mount("/", routes![list_paths, redirect_path])
.attach(Template::fairing())
}
#[launch]
#[cfg(not(feature = "rich"))]
async fn rocket() -> _ {
rocket::build()
.manage(ConfigManager::default())
.mount("/", routes![list_paths, redirect_path])
}

48
templates/base.html.tera Normal file
View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
{% if title %}
<title>{{ title }} - stargate</title>
{% else %}
<title>stargate</title>
{% endif %}
<style type="text/css">
body {
background-color: hsl(240, 20%, 15%);
color: hsl(240, 40%, 80%);
margin: 5rem;
font-family: sans-serif;
}
a:link, a:visited {
color: hsl(240, 100%, 80%);
}
main {
padding: 0rem 2rem;
}
footer {
border-top: 1px solid hsl(240, 20%, 50%);
margin-top: 4rem;
}
</style>
</head>
<body>
<main>
{% block content %}{% endblock content %}
</main>
<footer>
<h2>stargate</h2>
<p>
stargate is a minimal URL shortener service. For more information and how to deploy this yourself, visit
<a href="https://forge.cptlobster.dev/cptlobster/stargate">the repository</a>.
</p>
<p>
stargate is licensed using <a href="https://www.gnu.org/licenses/gpl-3.0.html">the GNU General Public License,
Version 3</a>.
</p>
</footer>
</body>
</html>

26
templates/main.html.tera Normal file
View file

@ -0,0 +1,26 @@
{% extends "base" %}
{% block content %}
<h1>Routes</h1>
{% if config.hide_routes %}
<p>The administrator has disabled publicly disclosing routes.</p>
{% else %}
<p>
The following routes are configured for this instance. If you are an administrator and wish to add more, please
edit the <code>sg1.toml</code> file.<br />
</p>
<ul>
{%- for route in config.routes %}
<li>
{%- if route.using == "regex" %}
<code>/{{ route.from }}</code>
{%- else %}
<a href="/{{ route.from }}">/{{ route.from }}</a>
{%- endif %}
({{ route.using }})
</li>
{%- else %}
<li>No routes configured.</li>
{%- endfor %}
</ul>
{% endif %}
{% endblock content %}

View file

@ -0,0 +1,12 @@
{% extends "base" %}
{% block content %}
<h1>{{ path }}: Not Found</h1>
<p>
Could not find a matching route. Please check for any spelling mistakes
{%- if config.hide_routes -%}
.
{%- else -%}
, or <a href="/">view the list of available routes</a>.
{%- endif %}
</p>
{% endblock content %}