add: check for ip bans

This commit is contained in:
trisua 2025-07-25 18:33:16 -04:00
parent 06b0aa0b4c
commit 70adae5b66
9 changed files with 84 additions and 47 deletions

48
Cargo.lock generated
View file

@ -82,30 +82,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "attobin"
version = "0.2.1"
dependencies = [
"axum",
"axum-extra",
"dotenv",
"glob",
"nanoneo",
"pathbufd",
"regex",
"serde",
"serde_json",
"serde_valid",
"tera",
"tetratto-core",
"tetratto-shared",
"tokio",
"toml 0.9.2",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@ -629,6 +605,30 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fluffle"
version = "0.2.1"
dependencies = [
"axum",
"axum-extra",
"dotenv",
"glob",
"nanoneo",
"pathbufd",
"regex",
"serde",
"serde_json",
"serde_valid",
"tera",
"tetratto-core",
"tetratto-shared",
"tokio",
"toml 0.9.2",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "fnv"
version = "1.0.7"

View file

@ -1,11 +1,11 @@
[package]
name = "attobin"
name = "fluffle"
version = "0.2.1"
edition = "2024"
authors = ["trisuaso"]
repository = "https://trisua.com/t/tetratto"
repository = "https://trisua.com/t/fluffle"
license = "AGPL-3.0-or-later"
homepage = "https://tetratto.com"
homepage = "https://fluffle.cc"
[dependencies]
tetratto-core = "12.0.2"

View file

@ -1,6 +1,6 @@
# 🦌 attobin
# 🐰 fluffle
Attobin is a simple Rentry (~December 2024) clone that uses a Tetratto app as a backend.
Fluffle is a familiar Markdown pastebin-y site :)
Since Tetratto is used as a backend, you'll obviously need to create an app at <https://tetratto.com/developer>. Once you've created the app, scroll down to "Secret key" and roll the key. Copy the key since you'll need it for later.
@ -34,10 +34,10 @@ You can build (without running) for release using the following command:
cargo build -r
```
Once you've built the binary, it'll be located at (from the root `attobin/` directory) `target/release/attobin`.
Once you've built the binary, it'll be located at (from the root `fluffle/` directory) `target/release/fluffle`.
All templates are compiled with [nanoneo](https://trisua.com/t/nanoneo), so it's recommended that you familiarize yourself with that syntax.
## Attribution
Attobin is licensed under the AGPL-3.0 license. Tetratto is also licensed under the AGPL-3.0 license. Attobin is not affiliated with [Tetratto](https://tetratto.com) or [Rentry](https://rentry.co).
Fluffle is licensed under the AGPL-3.0 license. Tetratto is also licensed under the AGPL-3.0 license.

View file

@ -8,4 +8,4 @@ All option names can either be in all lowercase, or all uppercase. While option
Metadata options go in the "Metadata" tab in the entry editor page. Each option should be on a new line, and should be formatted as `NAME = value`. If you're familiar with TOML, you should be comfortable with metadata formatting.
You can view a list of all options and what they do [here](/public/reference/attobin/model/struct.EntryMetadata.html#fields).
You can view a list of all options and what they do [here](/public/reference/fluffle/model/struct.EntryMetadata.html#fields).

View file

@ -4,8 +4,8 @@ function media_theme_pref() {
if (
window.matchMedia("(prefers-color-scheme: dark)").matches &&
(!window.localStorage.getItem("attobin:theme") ||
window.localStorage.getItem("attobin:theme") === "Auto")
(!window.localStorage.getItem("fluffle:theme") ||
window.localStorage.getItem("fluffle:theme") === "Auto")
) {
document.documentElement.classList.add("dark");
@ -13,16 +13,16 @@ function media_theme_pref() {
document.getElementById("switch_dark").classList.remove("hidden");
} else if (
window.matchMedia("(prefers-color-scheme: light)").matches &&
(!window.localStorage.getItem("attobin:theme") ||
window.localStorage.getItem("attobin:theme") === "Auto")
(!window.localStorage.getItem("fluffle:theme") ||
window.localStorage.getItem("fluffle:theme") === "Auto")
) {
document.documentElement.classList.remove("dark");
document.getElementById("switch_light").classList.remove("hidden");
document.getElementById("switch_dark").classList.add("hidden");
} else if (window.localStorage.getItem("attobin:theme")) {
} else if (window.localStorage.getItem("fluffle:theme")) {
/* restore theme */
const current = window.localStorage.getItem("attobin:theme");
const current = window.localStorage.getItem("fluffle:theme");
document.documentElement.className = current.toLowerCase();
if (current === "Light") {
@ -48,7 +48,7 @@ globalThis.temporary_set_theme = (theme) => {
};
globalThis.set_theme = (theme) => {
window.localStorage.setItem("attobin:theme", theme);
window.localStorage.setItem("fluffle:theme", theme);
document.documentElement.className = theme;
media_theme_pref();
};

View file

@ -44,7 +44,7 @@
(text "what"))
(a
("class" "button")
("href" "https://trisua.com/t/attobin")
("href" "https://trisua.com/t/fluffle")
(text "source"))))
(a

View file

@ -121,7 +121,7 @@ async fn main() {
.await
.unwrap();
info!("🦌 attobin.");
info!("🐰 fluffle.");
info!("listening on http://0.0.0.0:{}", port);
axum::serve(
listener,

View file

@ -13,6 +13,9 @@ pub struct Entry {
pub content: String,
#[serde(default)]
pub metadata: String,
/// The IP address of the last editor of the entry.
#[serde(default)]
pub last_edit_from: String,
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]

View file

@ -1,3 +1,5 @@
use std::env::var;
use crate::{
State,
model::{Entry, EntryMetadata},
@ -5,6 +7,7 @@ use crate::{
use axum::{
Extension, Json, Router,
extract::Path,
http::{HeaderMap, HeaderValue},
response::{Html, IntoResponse},
routing::{get, get_service, post},
};
@ -48,18 +51,15 @@ pub fn routes() -> Router {
fn default_context(data: &DataClient, build_code: &str) -> Context {
let mut ctx = Context::new();
ctx.insert(
"name",
&std::env::var("NAME").unwrap_or("Attobin".to_string()),
);
ctx.insert("name", &var("NAME").unwrap_or("Fluffle".to_string()));
ctx.insert(
"theme_color",
&std::env::var("THEME_COLOR").unwrap_or("#fbc27f".to_string()),
&var("THEME_COLOR").unwrap_or("#a3b3ff".to_string()),
);
ctx.insert("tetratto", &data.host);
ctx.insert(
"what_page_slug",
&std::env::var("WHAT_SLUG").unwrap_or("what".to_string()),
&var("WHAT_SLUG").unwrap_or("what".to_string()),
);
ctx.insert("build_code", &build_code);
ctx
@ -306,11 +306,27 @@ const CREATE_WAIT_TIME: usize = 5000;
async fn create_request(
jar: CookieJar,
headers: HeaderMap,
Extension(data): Extension<State>,
Json(req): Json<CreateEntry>,
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
let (ref data, _, _) = *data.read().await;
// get real ip
let real_ip = headers
.get(var("REAL_IP_HEADER").unwrap_or("CF-Connecting-IP".to_string()))
.unwrap_or(&HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_string();
// check for ip ban
if !real_ip.is_empty() {
if data.check_ip(&real_ip.as_str()).await.is_ok() {
return Err(Json(Error::NotAllowed.into()));
}
}
// check wait time
if let Some(cookie) = jar.get("__Secure-Claim-Next") {
if unix_epoch_timestamp()
@ -394,6 +410,7 @@ async fn create_request(
edited: created,
content: req.content,
metadata: req.metadata,
last_edit_from: real_ip,
})
.unwrap(),
)
@ -441,12 +458,28 @@ struct EditEntry {
}
async fn edit_request(
headers: HeaderMap,
Extension(data): Extension<State>,
Path(slug): Path<String>,
Json(req): Json<EditEntry>,
) -> impl IntoResponse {
let (ref data, _, _) = *data.read().await;
// get real ip
let real_ip = headers
.get(var("REAL_IP_HEADER").unwrap_or("CF-Connecting-IP".to_string()))
.unwrap_or(&HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_string();
// check for ip ban
if !real_ip.is_empty() {
if data.check_ip(&real_ip.as_str()).await.is_ok() {
return Json(Error::NotAllowed.into());
}
}
// check content length
if req.content.len() < 2 {
return Json(Error::DataTooShort("content".to_string()).into());
@ -586,6 +619,7 @@ async fn edit_request(
// update
entry.content = req.content;
entry.metadata = req.metadata;
entry.last_edit_from = real_ip;
entry.edited = unix_epoch_timestamp();
if let Err(e) = data