Compare commits

...

5 Commits

Author SHA1 Message Date
deepend-tildeclub fef86b5a4f
Remove empty line from ttrv.md header 2025-09-29 16:14:21 -06:00
deepend-tildeclub 5f58e63935
Add setup guide for ttrv Reddit client
Added documentation for setting up the ttrv Reddit client, including app creation, configuration, and common issues.
2025-09-29 16:12:53 -06:00
deepend 2bca54f976 dont need to keep a copy of the dynamic robots.txt 2025-09-29 20:14:06 +00:00
deepend f8d4440255 forgot a few files in last push. 2025-09-29 20:11:26 +00:00
deepend 7ba6e46df1 signup improvements, user firewall block check. 2025-09-29 20:08:34 +00:00
13 changed files with 470 additions and 167 deletions

6
.gitignore vendored
View File

@ -13,3 +13,9 @@ icons/
stats/
cache/
polls/polls.db
robots.txt
share/
polls/
botanygarden/
robots.txt
signup/vendor/

21
blocks/.csfdeny Normal file
View File

@ -0,0 +1,21 @@
###############################################################################
# Copyright 2006-2018, Way to the Web Limited
# URL: http://www.configserver.com
# Email: sales@waytotheweb.com
###############################################################################
# The following IP addresses will be blocked in iptables
# One IP address per line
# CIDR addressing allowed with a quaded IP (e.g. 192.168.254.0/24)
# Only list IP addresses, not domain names (they will be ignored)
#
# Note: If you add the text "do not delete" to the comments of an entry then
# DENY_IP_LIMIT will ignore those entries and not remove them
#
# Advanced port+ip filtering allowed with the following format
# tcp/udp|in/out|s/d=port,port,...|s/d=ip
#
# See readme.txt for more information regarding advanced port filtering
#
94.134.107.75 # lfd: (sshd-session) sshd-session bruteforce 94.134.107.75 (DE/Germany/i5E866B4B.versanet.de): 1 in the last 3600 secs - Fri Sep 26 08:28:02 2025
2001:9e8:65ff:f600:bcfe:932d:c0a0:6735 # lfd: (sshd-session) sshd-session bruteforce 2001:9e8:65ff:f600:bcfe:932d:c0a0:6735 (Unknown): 1 in the last 3600 secs - Fri Sep 26 08:52:42 2025
2001:9e8:65d8:1900:8cbd:20f5:640a:c57b # lfd: (sshd-session) sshd-session bruteforce 2001:9e8:65d8:1900:8cbd:20f5:640a:c57b (Unknown): 1 in the last 3600 secs - Sun Sep 28 17:45:13 2025

6
blocks/.env Normal file
View File

@ -0,0 +1,6 @@
APP_ENV=prod
DENY_FILE=.csfdeny
ALLOW_IPS=149.56.184.115
TRUST_PROXY=0
TRUSTED_PROXIES=
API_TOKEN=

266
blocks/index.php Normal file
View File

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
/**
* CSF deny list JSON API
* - Outputs only JSON
* - Locked down by IP allowlist (and optional token)
* - Parses:
* - Single IP or CIDR (v4/v6), with optional trailing comment
* - Advanced rules: proto(s)|in/out|s/d=port(s)|s/d=ip
*/
////////////////////////////
// Bootstrap & headers
////////////////////////////
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
header('Cache-Control: no-store');
$env = loadEnv(__DIR__.'/.env');
$env += [
'APP_ENV' => 'prod',
'DENY_FILE' => '/etc/csf/csf.deny', // or full path to ".csfdeny"
'ALLOW_IPS' => '', // comma-separated allowlist
'TRUST_PROXY' => '0', // "1" to respect X-Forwarded-For from trusted proxies
'TRUSTED_PROXIES'=> '', // comma-separated IPs/CIDRs of proxies
'API_TOKEN' => '', // optional shared token
];
if (($env['APP_ENV'] ?? 'prod') !== 'dev') {
ini_set('display_errors', '0');
ini_set('log_errors', '1');
}
////////////////////////////
// Access control
////////////////////////////
$clientIp = resolveClientIp((bool)intval($env['TRUST_PROXY']), $env['TRUSTED_PROXIES']);
if (!ipAllowed($clientIp, $env['ALLOW_IPS'])) {
http_response_code(403);
echo json_encode(['error' => 'forbidden', 'reason' => 'ip_not_allowed', 'client_ip' => $clientIp], JSON_UNESCAPED_SLASHES);
exit;
}
if (($env['API_TOKEN'] ?? '') !== '' && !hash_equals($env['API_TOKEN'], $_SERVER['HTTP_X_API_TOKEN'] ?? '')) {
http_response_code(403);
echo json_encode(['error' => 'forbidden', 'reason' => 'bad_token'], JSON_UNESCAPED_SLASHES);
exit;
}
////////////////////////////
// Read & parse deny file
////////////////////////////
$file = $env['DENY_FILE'];
if (!is_readable($file)) {
http_response_code(500);
echo json_encode(['error' => 'unreadable_file', 'path' => $file], JSON_UNESCAPED_SLASHES);
exit;
}
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
$entries = [];
foreach ($lines as $rawLine) {
$line = trim($rawLine);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
// Split off trailing comment after a space + '#'
$comment = null;
$hashPos = strpos($line, ' #');
if ($hashPos !== false) {
$comment = trim(substr($line, $hashPos + 2));
$line = trim(substr($line, 0, $hashPos));
}
$parsed = parseCsfLine($line);
if ($parsed !== null) {
$parsed['raw'] = $rawLine;
if ($comment !== null) {
$parsed['comment'] = $comment;
}
$entries[] = $parsed;
}
}
////////////////////////////
// Output
////////////////////////////
echo json_encode([
'generated_at' => gmdate('c'),
'source' => realpath($file) ?: $file,
'count' => count($entries),
'entries' => $entries,
], JSON_UNESCAPED_SLASHES);
/* -------------------- helpers -------------------- */
function loadEnv(string $path): array
{
$out = [];
if (!is_file($path) || !is_readable($path)) {
return $out;
}
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $l) {
$l = trim($l);
if ($l === '' || str_starts_with($l, '#')) { continue; }
if (!str_contains($l, '=')) { continue; }
[$k, $v] = array_map('trim', explode('=', $l, 2));
$v = trim($v, " \t\n\r\0\x0B\"'");
$out[$k] = $v;
}
return $out;
}
function resolveClientIp(bool $trustProxy, string $trustedProxiesCsv): string
{
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
if (!$trustProxy) {
return $remote;
}
$proxyList = array_filter(array_map('trim', explode(',', $trustedProxiesCsv ?: '')));
// If the REMOTE_ADDR is not a trusted proxy, ignore forwarded headers
if (!ipAllowed($remote, implode(',', $proxyList))) {
return $remote;
}
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff === '') {
return $remote;
}
// pick the first IP in XFF chain
$first = trim(explode(',', $xff)[0]);
return filter_var($first, FILTER_VALIDATE_IP) ? $first : $remote;
}
function ipAllowed(string $ip, string $allowCsv): bool
{
if ($allowCsv === '') {
return false;
}
$allow = array_filter(array_map('trim', explode(',', $allowCsv)));
foreach ($allow as $cidrOrIp) {
if ($cidrOrIp === '') { continue; }
if (str_contains($cidrOrIp, '/')) {
if (ipInCidr($ip, $cidrOrIp)) { return true; }
} else {
if (hash_equals($cidrOrIp, $ip)) { return true; }
}
}
return false;
}
function parseCsfLine(string $line): ?array
{
// If it contains pipes, treat as advanced rule
if (str_contains($line, '|')) {
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 2) {
return null;
}
// proto(s)
$protoRaw = strtolower($parts[0]);
$protocols = array_filter(array_map('trim', explode('/', $protoRaw)));
// direction
$direction = strtolower($parts[1] ?? '');
$srcIp = $dstIp = null;
$srcPorts = $dstPorts = [];
for ($i = 2; $i < count($parts); $i++) {
$kv = explode('=', $parts[$i], 2);
if (count($kv) !== 2) { continue; }
[$key, $val] = [strtolower(trim($kv[0])), trim($kv[1])];
if ($key === 's' || $key === 'src') {
// src may be IP/CIDR or port list
if (looksLikeIpOrCidr($val)) {
$srcIp = $val;
} else {
$srcPorts = parsePortList($val);
}
} elseif ($key === 'd' || $key === 'dst') {
if (looksLikeIpOrCidr($val)) {
$dstIp = $val;
} else {
$dstPorts = parsePortList($val);
}
}
}
return [
'type' => 'rule',
'protocols' => $protocols ?: ['tcp'],
'direction' => in_array($direction, ['in','out'], true) ? $direction : 'in',
'ports' => ['source' => $srcPorts, 'dest' => $dstPorts],
'source_ip' => $srcIp,
'dest_ip' => $dstIp,
];
}
// Plain IP or CIDR
if (looksLikeIpOrCidr($line)) {
// split ip/cidr
$ip = $line;
$cidr = null;
if (str_contains($line, '/')) {
[$ip, $mask] = explode('/', $line, 2);
$ip = trim($ip);
$cidr = (string)intval($mask);
}
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return null;
}
return [
'type' => ($cidr !== null ? 'cidr' : 'ip'),
'ip' => $ip,
'cidr' => $cidr,
];
}
return null;
}
function looksLikeIpOrCidr(string $val): bool
{
if (str_contains($val, '/')) {
[$ip, $mask] = explode('/', $val, 2);
return (bool)filter_var($ip, FILTER_VALIDATE_IP) && ctype_digit($mask);
}
return (bool)filter_var($val, FILTER_VALIDATE_IP);
}
function parsePortList(string $val): array
{
$out = [];
foreach (explode(',', $val) as $p) {
$p = trim($p);
if ($p === '') { continue; }
// keep ranges as raw strings (e.g., "1000:2000"), normalize digits
if (preg_match('/^\d+(:\d+)?$/', $p)) {
$out[] = $p;
}
}
return $out;
}
function ipInCidr(string $ip, string $cidr): bool
{
[$subnet, $maskBits] = explode('/', $cidr, 2);
$maskBits = (int)$maskBits;
$ipBin = @inet_pton($ip);
$subnetBin = @inet_pton($subnet);
if ($ipBin === false || $subnetBin === false) {
return false;
}
$len = strlen($ipBin);
if ($len !== strlen($subnetBin)) {
return false; // v4 vs v6 mismatch
}
$bytes = intdiv($maskBits, 8);
$remainder = $maskBits % 8;
if ($bytes > 0 && substr($ipBin, 0, $bytes) !== substr($subnetBin, 0, $bytes)) {
return false;
}
if ($remainder === 0) {
return true;
}
$mask = chr(0xFF << (8 - $remainder) & 0xFF);
return (ord($ipBin[$bytes]) & ord($mask)) === (ord($subnetBin[$bytes]) & ord($mask));
}

View File

@ -65,17 +65,17 @@ try {
// Create a default admin user with a hashed password
// NOTE: In production, you should not hardcode these credentials.
// Instead, store them outside of your code or set them up once.
$adminUsername = 'admin';
$adminPlainPassword = 'password'; // Change this in production
$adminHashedPassword = password_hash($adminPlainPassword, PASSWORD_DEFAULT);
// $adminUsername = 'admin';
// $adminPlainPassword = 'password'; // Change this in production
// $adminHashedPassword = password_hash($adminPlainPassword, PASSWORD_DEFAULT);
$insertUser = $db->prepare("
INSERT INTO users (username, password)
VALUES (:username, :password)
");
$insertUser->bindValue(':username', $adminUsername, PDO::PARAM_STR);
$insertUser->bindValue(':password', $adminHashedPassword, PDO::PARAM_STR);
$insertUser->execute();
// $insertUser = $db->prepare("
// INSERT INTO users (username, password)
// VALUES (:username, :password)
// ");
// $insertUser->bindValue(':username', $adminUsername, PDO::PARAM_STR);
// $insertUser->bindValue(':password', $adminHashedPassword, PDO::PARAM_STR);
// $insertUser->execute();
}
// Optionally, you can return $db or leave it globally accessible

View File

@ -1,91 +0,0 @@
User-Agent: MojeekBot
Allow: /~xwindows/
User-Agent: Qwantify
Allow: /~xwindows/
User-Agent: Wibybot
Allow: /~xwindows/
User-Agent: search.marginalia.nu
Allow: /~xwindows/
User-Agent: SearchMySiteBot
Allow: /~xwindows/
User-Agent: Duckduckbot
Allow: /~xwindows/
User-Agent: ia_archiver
Allow: /~xwindows/
User-Agent: Googlebot
Allow: /~xwindows/$
Allow: /~xwindows/index.html$
Disallow: /~xwindows/
User-Agent: bingbot
Allow: /~xwindows/$
Allow: /~xwindows/index.html$
Disallow: /~xwindows/
User-Agent: YandexBot
Allow: /~xwindows/$
Allow: /~xwindows/index.html$
Disallow: /~xwindows/
User-Agent: YandexFavicons
Disallow: /~xwindows/
User-Agent: MegaIndex.ru
Disallow: /~xwindows/
User-Agent: Amazonbot
Disallow: /~xwindows/
User-Agent: Linespider
Disallow: /~xwindows/
User-Agent: Bytespider
Disallow: /~xwindows/
User-Agent: CCBot
Disallow: /~xwindows/
User-Agent: Neevabot
Disallow: /~xwindows/
User-Agent: PetalBot
Disallow: /~xwindows/
User-Agent: SemrushBot
Disallow: /~xwindows/
User-Agent: AhrefsBot
Disallow: /~xwindows/
User-Agent: DataForSeoBot
Disallow: /~xwindows/
User-Agent: dotbot
Disallow: /~xwindows/
User-Agent: Barkrowler
Disallow: /~xwindows/
User-Agent: MJ12bot
Disallow: /~xwindows/
User-Agent: BuiltWith
Disallow: /~xwindows/
User-Agent: webprosbot
Disallow: /~xwindows/
User-Agent: Dataprovider
Disallow: /~xwindows/
User-Agent: *
Allow: /~xwindows/$
Allow: /~xwindows/index.html$
Disallow: /~xwindows/

9
signup/_probe.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$cmd='/usr/bin/sendmail.postfix -t -i -f signup@tilde.club';
$ds=[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']];
$p=proc_open($cmd,$ds,$pipes);
fwrite($pipes[0],"To: root@tilde.club\r\nSubject: proc_open test\r\n\r\nhi\r\n"); fclose($pipes[0]);
$stdout=stream_get_contents($pipes[1]); fclose($pipes[1]);
$stderr=stream_get_contents($pipes[2]); fclose($pipes[2]);
$rc=proc_close($p);
var_dump(['rc'=>$rc,'stderr'=>$stderr,'sendmail_path'=>ini_get('sendmail_path')]);

5
signup/composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"pear/net_dns2": "^1.5"
}
}

70
signup/composer.lock generated Normal file
View File

@ -0,0 +1,70 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "013e57ab3ff38d936fd23c522b9d5268",
"packages": [
{
"name": "pear/net_dns2",
"version": "v1.5.5",
"source": {
"type": "git",
"url": "https://github.com/mikepultz/netdns2.git",
"reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mikepultz/netdns2/zipball/ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9",
"reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^9"
},
"type": "library",
"autoload": {
"psr-0": {
"Net_DNS2": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Mike Pultz",
"email": "mike@mikepultz.com",
"homepage": "https://mikepultz.com/",
"role": "lead"
}
],
"description": "Native PHP DNS Resolver and Updater Library",
"homepage": "https://netdns2.com/",
"keywords": [
"PEAR",
"dns",
"network"
],
"support": {
"issues": "https://github.com/mikepultz/netdns2/issues",
"source": "https://github.com/mikepultz/netdns2"
},
"time": "2025-05-17T20:56:28+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@ -424,8 +424,11 @@
public static function GetDNSRecord($domain, $types = array("MX", "A"), $nameservers = array("8.8.8.8", "8.8.4.4"), $cache = true)
{
// Check for a mail server based on a DNS lookup.
if (!class_exists("Net_DNS2_Resolver")) require_once str_replace("\\", "/", dirname(__FILE__)) . "/Net/DNS2.php";
if (!class_exists('Net_DNS2_Resolver')) {
// Composer autoloader should already be loaded above; this is a last-ditch attempt.
$autoload = dirname(__DIR__, 2) . '/vendor/autoload.php';
if (is_file($autoload)) { require $autoload; }
}
$resolver = new Net_DNS2_Resolver(array("nameservers" => $nameservers));
try
{

View File

@ -1,4 +1,8 @@
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$title = "sign up for the tilde.club!";
include __DIR__."/../header.php";

View File

@ -1,6 +1,5 @@
<?php
$filepath = __FILE__;
# require __DIR__.'/../vendor/autoload.php';
require_once "email/smtp.php";
function getUserIpAddr() {
@ -43,71 +42,22 @@ function is_ssh_pubkey($string): bool
return false;
}
function forbidden_name($name): bool
function forbidden_name(string $name): bool
{
$badnames = [
'0x0',
'abuse',
'admin',
'administrator',
'auth',
'autoconfig',
'bbj',
'broadcasthost',
'cloud',
'forum',
'ftp',
'git',
'gopher',
'hostmaster',
'imap',
'info',
'irc',
'is',
'isatap',
'it',
'localdomain',
'localhost',
'lounge',
'mail',
'mailer-daemon',
'marketing',
'marketting',
'mis',
'news',
'nobody',
'noc',
'noreply',
'pop',
'pop3',
'postmaster',
'retro',
'root',
'sales',
'security',
'smtp',
'ssladmin',
'ssladministrator',
'sslwebmaster',
'support',
'sysadmin',
'team',
'usenet',
'uucp',
'webmaster',
'wpad',
'www',
'znc',
$bad = [
'0x0','abuse','admin','administrator','auth','autoconfig','bbj','broadcasthost','cloud','forum','ftp',
'git','gopher','hostmaster','imap','info','irc','is','isatap','it','localdomain','localhost','lounge',
'mail','mailer-daemon','marketing','marketting','mis','news','nobody','noc','noreply','pop','pop3',
'postmaster','retro','root','sales','security','smtp','ssladmin','ssladministrator','sslwebmaster',
'support','sysadmin','team','usenet','uucp','webmaster','wpad','www','znc',
];
return in_array(
$name,
array_merge(
$badnames,
file("/var/signups_current", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES),
file("/var/banned_names.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)
)
);
$lists = [$bad];
foreach (['/var/signups_current','/var/banned_names.txt'] as $p) {
$t = @file($p, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (is_array($t)) { $lists[] = $t; } // ignore missing/unreadable files
}
return in_array($name, array_merge(...$lists), true);
}
function forbidden_email($email): bool
@ -198,7 +148,9 @@ reason: {$_REQUEST["interest"]}
$makeuser
";
if (mail('root', 'new tilde.club signup', $msgbody)) {
$to = 'root@tilde.club';
$headers = "To: {$to}\r\nFrom: signup <signup@tilde.club>\r\n";
if (mail($to, 'new tilde.club signup', $msgbody, $headers)) {
echo '<div class="alert alert-success" role="alert">
email sent! we\'ll get back to you soon with login instructions! (timeframe for processing signups varies greatly) <a href="/">back to tilde.club home</a>
</div>';

52
wiki/source/ttrv.md Normal file
View File

@ -0,0 +1,52 @@
---
title: ttrv (Reddit in your terminal)
author: deepend
category: software
---
`ttrv` is a TUI Reddit client.
## Setup
### 1) Create the right Reddit app
* Go to [https://www.reddit.com/prefs/apps](https://www.reddit.com/prefs/apps) → **create another app…**
* **Type:** “installed app” (not “script”, not “web app”)
* **Redirect URI (must match exactly):** `http://127.0.0.1:65000/` **← note trailing slash**
* Copy the **client ID** (14-char string under the app name). *Installed app has no secret.*
### 2) Put creds in the right file
Create or edit `~/.config/ttrv/ttrv.cfg`:
```ini
[ttrv]
oauth_client_id = YOUR_CLIENT_ID
oauth_client_secret =
oauth_redirect_uri = http://127.0.0.1:65000/
oauth_redirect_port = 65000
autologin = True
persistent = True
```
* Make a starter config: `ttrv --copy-config`
* Refresh token is stored at: `~/.local/share/ttrv/refresh-token`
### 3) First login
Run `ttrv`, press **u**, authorize in the browser; it will callback to `http://127.0.0.1:65000/`.
## Clear any bad cached token
```bash
ttrv --clear-auth
# or
rm -f ~/.local/share/ttrv/refresh-token
```
## Common “invalid client id” causes
* Wrong app type (must be **installed app**)
* Redirect mismatch (anything other than **[http://127.0.0.1:65000/](http://127.0.0.1:65000/)**, missing slash, different port)
**GitHub:** [https://github.com/tildeclub/ttrv](https://github.com/tildeclub/ttrv)