This commit is contained in:
parent
2970fa1a15
commit
37e69a7b17
14 changed files with 1123 additions and 20 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
/.idea/
|
||||
/.vscode/
|
||||
sg1.toml
|
||||
*.md
|
||||
41
.forgejo/workflows/docker_main.yml
Normal file
41
.forgejo/workflows/docker_main.yml
Normal 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"
|
||||
33
.forgejo/workflows/docker_release.yml
Normal file
33
.forgejo/workflows/docker_release.yml
Normal 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
856
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
13
Dockerfile
Normal 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
13
Dockerfile.rich
Normal 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"]
|
||||
22
README.md
22
README.md
|
|
@ -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
|
||||
|
|
|
|||
6
sg1.toml
6
sg1.toml
|
|
@ -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"
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
49
src/main.rs
49
src/main.rs
|
|
@ -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
48
templates/base.html.tera
Normal 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
26
templates/main.html.tera
Normal 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 %}
|
||||
12
templates/notfound.html.tera
Normal file
12
templates/notfound.html.tera
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue