commit b7055a61a1b2014113baf22a4a263d3d72331183
parent 81051135e22e2ae489da68862e5ac81276f4f77b
Author: mtmn <miro@haravara.org>
Date: Fri, 8 May 2026 11:26:23 +0200
shirts: add validation for url schema
Diffstat:
8 files changed, 111 insertions(+), 22 deletions(-)
diff --git a/bandeno/README.md b/bandeno/README.md
@@ -0,0 +1,34 @@
+# bandeno
+
+A Bandcamp JSON Feed proxy. Returns artist discographies and fan collections as [JSON Feed](https://jsonfeed.org/) for use in feed readers.
+
+## Running
+
+```sh
+deno task dev
+```
+
+## Usage
+
+### Artist feed
+
+Returns a JSON Feed of all releases on an artist's Bandcamp page.
+
+```
+GET /?artist=<artist>
+```
+
+### Fan collection feed
+
+Returns a JSON Feed of all albums in a fan's Bandcamp collection.
+
+```
+GET /?fan=<fan>
+```
+
+## Configuration
+
+| Variable | Default | Description |
+|----------|-------------|--------------------|
+| `PORT` | `8000` | Port to listen on |
+| `HOST` | `127.0.0.1` | Host to bind to |
diff --git a/bandeno/flake.nix b/bandeno/flake.nix
@@ -11,7 +11,7 @@
src = ./.;
installPhase = ''
mkdir -p $out
- cp -r . $out/
+ cp deno.json deno.lock main.ts $out/
'';
};
});
diff --git a/shirts/README.md b/shirts/README.md
@@ -0,0 +1,46 @@
+# shirts
+
+A URL shortener. Links are stored in a SQLite database and served under a configurable base URL.
+
+## Building
+
+```sh
+mix deps.get
+mix release
+```
+
+## Usage
+
+### Create a short link
+
+```sh
+curl -X POST http://localhost:8721/curtail \
+ -H "Authorization: Bearer <token>" \
+ -H "Content-Type: application/json" \
+ -d '{"url": "https://example.com/very/long/path"}'
+```
+
+Optionally provide a custom code:
+
+```sh
+curl -X POST http://localhost:8721/curtail \
+ -H "Authorization: Bearer <token>" \
+ -H "Content-Type: application/json" \
+ -d '{"url": "https://example.com/very/long/path", "code": "my-code"}'
+```
+
+### Follow a short link
+
+```
+GET /s/<code>
+```
+
+## Configuration
+
+| Variable | Description |
+|------------|--------------------------------------|
+| `PORT` | Port to listen on (default: `8721`) |
+| `HOST` | Host to bind to (default: `127.0.0.1`) |
+| `BASE_URL` | Base URL used when generating links |
+| `AUTH_TOKEN` | Bearer token for the create endpoint |
+| `DB_PATH` | Path to the SQLite database file |
diff --git a/shirts/config/runtime.exs b/shirts/config/runtime.exs
@@ -4,7 +4,7 @@ if config_env() == :prod do
config :shortener,
port: System.get_env("PORT") |> then(&(if &1, do: String.to_integer(&1), else: 4000)),
host: System.get_env("HOST") || "127.0.0.1",
- auth_token: System.get_env("AUTH_TOKEN") || "changeme",
+ auth_token: System.fetch_env!("AUTH_TOKEN"),
db_path: System.get_env("DB_PATH") || "data.db",
base_url: System.get_env("BASE_URL") || "http://localhost"
end
diff --git a/shirts/deploy b/shirts/deploy
@@ -1,2 +0,0 @@
-#!/bin/bash
-podman build -t shirt_linkener . && podman save shirt_linkener | ssh clam "sudo podman load && sudo systemctl restart shirt-linkener"
diff --git a/shirts/lib/shortener/application.ex b/shirts/lib/shortener/application.ex
@@ -23,12 +23,6 @@ defmodule Shortener.Application do
end
@impl true
- def prep_stop(state) do
- Logger.info("Shortener application is preparing to stop")
- state
- end
-
- @impl true
def stop(_state) do
Logger.info("Shortener application stopped")
:ok
diff --git a/shirts/lib/shortener/db.ex b/shirts/lib/shortener/db.ex
@@ -47,8 +47,8 @@ defmodule Shortener.DB do
with {:ok, stmt} <-
Exqlite.Sqlite3.prepare(conn, "INSERT INTO shirts (code, url) VALUES (?1, ?2)"),
:ok <- Exqlite.Sqlite3.bind(stmt, [code, url]),
- :done <- Exqlite.Sqlite3.step(conn, stmt),
- :ok <- Exqlite.Sqlite3.release(conn, stmt) do
+ :done <- Exqlite.Sqlite3.step(conn, stmt) do
+ _ = Exqlite.Sqlite3.release(conn, stmt)
:ok
else
{:error, reason} -> {:error, reason}
@@ -62,12 +62,14 @@ defmodule Shortener.DB do
with {:ok, stmt} <-
Exqlite.Sqlite3.prepare(conn, "SELECT url FROM shirts WHERE code = ?1"),
:ok <- Exqlite.Sqlite3.bind(stmt, [code]),
- {:row, [url]} <- Exqlite.Sqlite3.step(conn, stmt),
- :ok <- Exqlite.Sqlite3.release(conn, stmt) do
- {:ok, url}
- else
- :done -> :not_found
- {:error, reason} -> {:error, reason}
+ step_result <- Exqlite.Sqlite3.step(conn, stmt) do
+ _ = Exqlite.Sqlite3.release(conn, stmt)
+
+ case step_result do
+ {:row, [url]} -> {:ok, url}
+ :done -> :not_found
+ {:error, reason} -> {:error, reason}
+ end
end
{:reply, result, conn}
diff --git a/shirts/lib/shortener/router.ex b/shirts/lib/shortener/router.ex
@@ -36,8 +36,11 @@ defmodule Shortener.Router do
{:error, :missing_url} ->
json(conn, 422, %{error: "missing required field: url"})
- {:error, reason} ->
- json(conn, 409, %{error: "could not save link: #{inspect(reason)}"})
+ {:error, :invalid_url} ->
+ json(conn, 422, %{error: "url must be a valid http or https URL"})
+
+ {:error, _reason} ->
+ json(conn, 409, %{error: "could not save link"})
end
end
@@ -53,6 +56,9 @@ defmodule Shortener.Router do
:not_found ->
json(conn, 404, %{error: "not found"})
+
+ {:error, _} ->
+ json(conn, 500, %{error: "internal error"})
end
end
@@ -78,8 +84,17 @@ defmodule Shortener.Router do
defp fetch_url(conn) do
case conn.body_params do
- %{"url" => url} when is_binary(url) and url != "" -> {:ok, url}
- _ -> {:error, :missing_url}
+ %{"url" => url} when is_binary(url) and url != "" ->
+ uri = URI.parse(url)
+
+ if uri.scheme in ["http", "https"] and is_binary(uri.host) and uri.host != "" do
+ {:ok, url}
+ else
+ {:error, :invalid_url}
+ end
+
+ _ ->
+ {:error, :missing_url}
end
end