Skip to content

Commit 1b0f73c

Browse files
Merge branch 'redlib-org:main' into main
2 parents 874c266 + ba98178 commit 1b0f73c

26 files changed

Lines changed: 1055 additions & 520 deletions

Cargo.lock

Lines changed: 306 additions & 262 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ name = "redlib"
33
description = " Alternative private front-end to Reddit"
44
license = "AGPL-3.0-only"
55
repository = "https://github.com/redlib-org/redlib"
6-
version = "0.35.1"
6+
version = "0.36.0"
77
authors = [
88
"Matthew Esposito <matt+cargo@matthew.science>",
99
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
1010
]
1111
edition = "2021"
12+
rust-version = "1.81"
1213
default-run = "redlib"
1314

1415
[dependencies]
15-
rinja = { version = "0.3.4", default-features = false }
16+
askama = { version = "0.14.0", default-features = false, features = [
17+
"std",
18+
"derive",
19+
] }
1620
cached = { version = "0.54.0", features = ["async"] }
1721
clap = { version = "4.4.11", default-features = false, features = [
1822
"std",
@@ -27,14 +31,13 @@ hyper = { version = "0.14.31", features = ["full"] }
2731
percent-encoding = "2.3.1"
2832
route-recognizer = "0.3.1"
2933
serde_json = "1.0.133"
30-
tokio = { version = "1.35.1", features = ["full"] }
34+
tokio = { version = "1.44.2", features = ["full"] }
3135
time = { version = "0.3.31", features = ["local-offset"] }
3236
url = "2.5.0"
3337
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
3438
libflate = "2.0.0"
3539
brotli = { version = "7.0.0", features = ["std"] }
3640
toml = "0.8.8"
37-
once_cell = "1.19.0"
3841
serde_yaml = "0.9.29"
3942
build_html = "2.4.0"
4043
uuid = { version = "1.6.1", features = ["v4"] }
@@ -56,7 +59,8 @@ htmlescape = "0.3.1"
5659
bincode = "1.3.3"
5760
base2048 = "2.0.2"
5861
revision = "0.10.0"
59-
62+
fake_user_agent = "0.2.2"
63+
rustls = "0.21.12"
6064

6165
[dev-dependencies]
6266
lipsum = "0.9.0"

Dockerfile.alpine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ USER redlib
3737
EXPOSE 8080
3838

3939
# Run a healthcheck every minute to make sure redlib is functional
40-
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
40+
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
4141

4242
# Add container metadata
4343
LABEL org.opencontainers.image.authors="sigaloid"

Dockerfile.ubuntu

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ USER redlib
4343
EXPOSE 8080
4444

4545
# Run a healthcheck every minute to make sure redlib is functional
46-
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
46+
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
4747

4848
# Add container metadata
4949
LABEL org.opencontainers.image.authors="sigaloid"

README.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
---
88

9-
**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://redlib.matthew.science/r/unpopularopinion) without being [tracked](#reddit).
9+
**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://farside.link/redlib/r/unpopularopinion) without being [tracked](#reddit).
1010

1111
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
1212
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
@@ -30,7 +30,6 @@
3030
- [Reddit](#reddit)
3131
- [Redlib](#redlib-1)
3232
- [Server](#server)
33-
- [Official instance (redlib.matthew.science)](#official-instance-redlibmatthewscience)
3433
5. [Deployment](#deployment)
3534
- [Docker](#docker)
3635
- [Docker Compose](#docker-compose)
@@ -75,7 +74,7 @@ Redlib currently implements most of Reddit's (signed-out) functionalities but st
7574

7675
- [Rust](https://www.rust-lang.org/) - Programming language
7776
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
78-
- [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
77+
- [Askama](https://github.com/askama-rs/askama) - Templating engine
7978
- [Rustls](https://github.com/rustls/rustls) - TLS library
8079

8180
## How is it different from other Reddit front ends?
@@ -159,17 +158,7 @@ For transparency, I hope to describe all the ways Redlib handles user privacy.
159158

160159
- **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
161160

162-
- **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data.
163-
164-
#### Official instance (redlib.matthew.science)
165-
166-
The official instance is hosted at https://redlib.matthew.science.
167-
168-
- **Server:** The official instance runs a production binary, and thus logs nothing.
169-
170-
- **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic.
171-
172-
- **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
161+
- **Cookies:** Redlib uses optional cookies to store any configured settings in the settings menu. These are not cross-site cookies and the cookies hold no personal data.
173162

174163
---
175164

app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
},
7777
"REDLIB_FULL_URL": {
7878
"required": false
79+
},
80+
"REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS": {
81+
"required": false
7982
}
8083
}
8184
}

src/client.rs

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ use futures_lite::{future::Boxed, FutureExt};
55
use hyper::client::HttpConnector;
66
use hyper::header::HeaderValue;
77
use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri};
8-
use hyper_rustls::HttpsConnector;
8+
use hyper_rustls::{ConfigBuilderExt, HttpsConnector};
99
use libflate::gzip;
1010
use log::{error, trace, warn};
11-
use once_cell::sync::Lazy;
1211
use percent_encoding::{percent_encode, CONTROLS};
1312
use serde_json::Value;
1413

1514
use std::sync::atomic::Ordering;
1615
use std::sync::atomic::{AtomicBool, AtomicU16};
16+
use std::sync::LazyLock;
1717
use std::{io, result::Result};
1818

1919
use crate::dbg_msg;
20-
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
20+
use crate::oauth::{force_refresh_token, token_daemon, Oauth, OauthBackendImpl};
2121
use crate::server::RequestExt;
2222
use crate::utils::{format_url, Post};
2323

@@ -30,12 +30,40 @@ const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
3030
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
3131
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
3232

33-
pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
34-
Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
33+
pub static HTTPS_CONNECTOR: LazyLock<HttpsConnector<HttpConnector>> = LazyLock::new(|| {
34+
hyper_rustls::HttpsConnectorBuilder::new()
35+
.with_tls_config(
36+
rustls::ClientConfig::builder()
37+
// These are the Firefox 145.0 cipher suite,
38+
// minus the suites missing forward-secrecy support,
39+
// in the same order.
40+
// https://github.com/redlib-org/redlib/issues/446#issuecomment-3609306592
41+
.with_cipher_suites(&[
42+
rustls::cipher_suite::TLS13_AES_256_GCM_SHA384,
43+
rustls::cipher_suite::TLS13_AES_128_GCM_SHA256,
44+
rustls::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
45+
rustls::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
46+
rustls::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
47+
rustls::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
48+
rustls::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
49+
rustls::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
50+
rustls::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
51+
])
52+
// .with_safe_default_cipher_suites()
53+
.with_safe_default_kx_groups()
54+
.with_safe_default_protocol_versions()
55+
.unwrap()
56+
.with_native_roots()
57+
.with_no_client_auth(),
58+
)
59+
.https_only()
60+
.enable_http2()
61+
.build()
62+
});
3563

36-
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
64+
pub static CLIENT: LazyLock<Client<HttpsConnector<HttpConnector>>> = LazyLock::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
3765

38-
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
66+
pub static OAUTH_CLIENT: LazyLock<ArcSwap<Oauth>> = LazyLock::new(|| {
3967
let client = block_on(Oauth::new());
4068
tokio::spawn(token_daemon());
4169
ArcSwap::new(client.into())
@@ -154,7 +182,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
154182
let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
155183

156184
// Build the hyper client from the HTTPS connector.
157-
let client: &Lazy<Client<_, Body>> = &CLIENT;
185+
let client: &LazyLock<Client<_, Body>> = &CLIENT;
158186

159187
let mut builder = Request::get(parsed_uri);
160188

@@ -165,6 +193,12 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
165193
}
166194
}
167195

196+
// Add User-Agent header of the currently spoofed device
197+
{
198+
let client = OAUTH_CLIENT.load_full();
199+
builder = builder.header("User-Agent", client.user_agent());
200+
}
201+
168202
let stream_request = builder.body(Body::empty()).map_err(|_| "Couldn't build empty body in stream".to_string())?;
169203

170204
client
@@ -216,7 +250,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
216250
let url = format!("{base_path}{path}");
217251

218252
// Construct the hyper client from the HTTPS connector.
219-
let client: &Lazy<Client<_, Body>> = &CLIENT;
253+
let client: &LazyLock<Client<_, Body>> = &CLIENT;
220254

221255
// Build request to Reddit. When making a GET, request gzip compression.
222256
// (Reddit doesn't do brotli yet.)
@@ -356,7 +390,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
356390
.boxed()
357391
}
358392

359-
// Make a request to a Reddit API and parse the JSON response
393+
/// Make a request to a Reddit API and parse the JSON response
360394
#[cached(size = 100, time = 30, result = true)]
361395
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
362396
// Closure to quickly build errors
@@ -488,11 +522,17 @@ async fn self_check(sub: &str) -> Result<(), String> {
488522
}
489523

490524
pub async fn rate_limit_check() -> Result<(), String> {
525+
// First, test the Oauth client: we can perform a rate limit check if the OAuth backend is MobileSpoof; if GenericWeb, we skip the check.
526+
if matches!(OAUTH_CLIENT.load().backend, OauthBackendImpl::GenericWeb(_)) {
527+
warn!("[⚠️] Cannot perform rate limit check, running as GenericWeb. Skipping check.");
528+
return Ok(());
529+
}
530+
491531
// First, check a subreddit.
492532
self_check("reddit").await?;
493533
// This will reduce the rate limit to 99. Assert this check.
494534
if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 {
495-
return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)));
535+
return Err(format!("Rate limit check 1 failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)));
496536
}
497537
// Now, we switch out the OAuth client.
498538
// This checks for the IP rate limit association.
@@ -501,7 +541,7 @@ pub async fn rate_limit_check() -> Result<(), String> {
501541
self_check("rust").await?;
502542
// Again, assert the rate limit check.
503543
if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 {
504-
return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)));
544+
return Err(format!("Rate limit check 2 failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst)));
505545
}
506546

507547
Ok(())

src/config.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
use once_cell::sync::Lazy;
21
use serde::{Deserialize, Serialize};
3-
use std::{env::var, fs::read_to_string};
2+
use std::{env::var, fs::read_to_string, sync::LazyLock};
43

5-
// Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we
6-
// can reduce reliance on once_cell.
7-
//
8-
// This is the local static that is initialized at runtime (technically at
9-
// first request) and contains the instance settings.
10-
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
4+
/// This is the local static that is initialized at runtime (technically at
5+
/// first request) and contains the instance settings.
6+
pub static CONFIG: LazyLock<Config> = LazyLock::new(Config::load);
117

12-
// This serves as the frontend for an archival API - on removed comments, this URL
13-
// will be the base of a link, to display removed content (on another site).
8+
/// This serves as the frontend for an archival API - on removed comments, this URL
9+
/// will be the base of a link, to display removed content (on another site).
1410
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "undelete.pullpush.io";
1511

1612
/// Stores the configuration parsed from the environment variables and the
@@ -109,6 +105,9 @@ pub struct Config {
109105

110106
#[serde(rename = "REDLIB_FULL_URL")]
111107
pub(crate) full_url: Option<String>,
108+
109+
#[serde(rename = "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS")]
110+
pub(crate) default_remove_default_feeds: Option<String>,
112111
}
113112

114113
impl Config {
@@ -156,6 +155,7 @@ impl Config {
156155
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
157156
enable_rss: parse("REDLIB_ENABLE_RSS"),
158157
full_url: parse("REDLIB_FULL_URL"),
158+
default_remove_default_feeds: parse("REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS"),
159159
}
160160
}
161161
}
@@ -185,6 +185,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
185185
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
186186
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
187187
"REDLIB_FULL_URL" => config.full_url.clone(),
188+
"REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS" => config.default_remove_default_feeds.clone(),
188189
_ => None,
189190
}
190191
}

src/duplicates.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// Handler for post duplicates.
1+
//! Handler for post duplicates.
22
33
use crate::client::json;
44
use crate::server::RequestExt;
55
use crate::subreddit::{can_access_quarantine, quarantine};
66
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
77

8+
use askama::Template;
89
use hyper::{Body, Request, Response};
9-
use rinja::Template;
1010
use serde_json::Value;
1111
use std::borrow::ToOwned;
1212
use std::collections::HashSet;

src/instance_info.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ use crate::{
33
server::RequestExt,
44
utils::{ErrorTemplate, Preferences},
55
};
6+
use askama::Template;
67
use build_html::{Container, Html, HtmlContainer, Table};
78
use hyper::{http::Error, Body, Request, Response};
8-
use once_cell::sync::Lazy;
9-
use rinja::Template;
109
use serde::{Deserialize, Serialize};
10+
use std::sync::LazyLock;
1111
use time::OffsetDateTime;
1212

13-
// This is the local static that is intialized at runtime (technically at
14-
// the first request to the info endpoint) and contains the data
15-
// retrieved from the info endpoint.
16-
pub static INSTANCE_INFO: Lazy<InstanceInfo> = Lazy::new(InstanceInfo::new);
13+
/// This is the local static that is initialized at runtime (technically at
14+
/// the first request to the info endpoint) and contains the data
15+
/// retrieved from the info endpoint.
16+
pub static INSTANCE_INFO: LazyLock<InstanceInfo> = LazyLock::new(InstanceInfo::new);
1717

1818
/// Handles instance info endpoint
1919
pub async fn instance_info(req: Request<Body>) -> Result<Response<Body>, String> {
@@ -128,6 +128,7 @@ impl InstanceInfo {
128128
["Pushshift frontend", &convert(&self.config.pushshift)],
129129
["RSS enabled", &convert(&self.config.enable_rss)],
130130
["Full URL", &convert(&self.config.full_url)],
131+
["Remove default feeds", &convert(&self.config.default_remove_default_feeds)],
131132
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
132133
])
133134
.with_header_row(["Settings"]),
@@ -169,6 +170,7 @@ impl InstanceInfo {
169170
Pushshift frontend: {:?}\n
170171
RSS enabled: {:?}\n
171172
Full URL: {:?}\n
173+
Remove default feeds: {:?}\n
172174
Config:\n
173175
Banner: {:?}\n
174176
Hide awards: {:?}\n
@@ -195,6 +197,7 @@ impl InstanceInfo {
195197
self.config.sfw_only,
196198
self.config.enable_rss,
197199
self.config.full_url,
200+
self.config.default_remove_default_feeds,
198201
self.config.pushshift,
199202
self.config.banner,
200203
self.config.default_hide_awards,

0 commit comments

Comments
 (0)