Browse Source

Merge pull request #399 from vichan-devel/ip-cloaking

ip cloaking
main
Łiźnier Hełam Łabej 3 years ago
committed by GitHub
parent
commit
47df9c6485
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      inc/bans.php
  2. 10
      inc/config.php
  3. 115
      inc/functions.php
  4. 2
      inc/lib/Twig/Extensions/Extension/Tinyboard.php
  5. 37
      inc/mod/pages.php
  6. 2
      templates/mod/ban_appeals.html
  7. 2
      templates/mod/ban_form.html
  8. 2
      templates/mod/log.html
  9. 2
      templates/mod/report.html
  10. 12
      templates/mod/search_results.html
  11. 2
      templates/mod/user.html
  12. 14
      templates/mod/view_ip.html
  13. 2
      templates/post/ip.html

23
inc/bans.php

@ -142,6 +142,7 @@ class Bans {
if ($ban['post'])
$ban['post'] = json_decode($ban['post'], true);
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
$ban['cmask'] = cloak_mask($ban['mask']);
$ban_list[] = $ban;
}
}
@ -161,8 +162,9 @@ class Bans {
$end = end($bans);
foreach ($bans as &$ban) {
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
foreach ($bans as &$ban) {
$uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
$ban['mask'] = cloak_mask($uncloaked_mask);
if ($ban['post']) {
$post = json_decode($ban['post']);
@ -174,11 +176,11 @@ class Bans {
$ban['access'] = true;
}
if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) {
if (filter_var($uncloaked_mask, FILTER_VALIDATE_IP) !== false) {
$ban['single_addr'] = true;
}
if ($filter_staff || ($board_access !== false && !in_array($ban['board'], $board_access))) {
$ban['username'] = '?';
$ban['username'] = '?';
}
if ($filter_ips || ($board_access !== false && !in_array($ban['board'], $board_access))) {
@list($ban['mask'], $subnet) = explode("/", $ban['mask']);
@ -200,7 +202,7 @@ class Bans {
}
}
$out ? fputs($out, "]") : print("]");
$out ? fputs($out, "]") : print("]");
}
@ -230,9 +232,10 @@ class Bans {
error($config['error']['noaccess']);
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
$cloaked_mask = cloak_mask($mask);
modLog("Removed ban #{$ban_id} for " .
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : $mask));
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
}
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
@ -242,7 +245,9 @@ class Bans {
return true;
}
static public function new_ban($mask, $reason, $length = false, $ban_board = false, $mod_id = false, $post = false) {
static public function new_ban($cloaked_mask, $reason, $length = false, $ban_board = false, $mod_id = false, $post = false) {
$mask = uncloak_mask($cloaked_mask);
global $mod, $pdo, $board;
if ($mod_id === false) {
@ -251,6 +256,7 @@ class Bans {
$range = self::parse_range($mask);
$mask = self::range_to_string($range);
$cloaked_mask = cloak_mask($mask);
$query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ipstart, :ipend, :time, :expires, :board, :mod, :reason, 0, :post)");
@ -293,14 +299,13 @@ class Bans {
$query->bindValue(':post', null, PDO::PARAM_NULL);
$query->execute() or error(db_error($query));
if (isset($mod['id']) && $mod['id'] == $mod_id) {
modLog('Created a new ' .
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') .
' ban on ' .
($ban_board ? '/' . $ban_board . '/' : 'all boards') .
' for ' .
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : $mask) .
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask) .
' (<small>#' . $pdo->lastInsertId() . '</small>)' .
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
}

10
inc/config.php

@ -1874,3 +1874,13 @@
// Allowed HTML tags in ?/edit_pages.
$config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr';
// Secret passphrase for IP cloaking
// Disabled if empty.
$config['ipcrypt_key'] = '';
// IP cloak prefix
$config['ipcrypt_prefix'] = 'Cloak';
// Whether to append domain names to IP cloaks
$config['ipcrypt_dns'] = false;

115
inc/functions.php

@ -2860,3 +2860,118 @@ function strategy_first($fun, $array) {
return array('defer');
}
}
function base32_decode($d) {
$charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$d = str_split($d);
$l = array_pop($d);
$b = '';
foreach ($d as $c) {
$b .= sprintf("%05b", strpos($charset, $c));
}
$padding = 8 - strlen($b) % 8;
$b .= str_pad(decbin(strpos($charset, $l)), $padding, '0', STR_PAD_LEFT);
return implode('', array_map(function($c) { return chr(bindec($c)); }, str_split($b, 8)));
}
function base32_encode($d) {
$charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$b = implode('', array_map(function($c) { return sprintf("%08b", ord($c)); }, str_split($d)));
return implode('', array_map(function($c) use ($charset) { return $charset[bindec($c)]; }, str_split($b, 5)));
}
function cloak_ip($ip) {
global $config;
$ipcrypt_key = $config['ipcrypt_key'] ?: null;
if (empty($ipcrypt_key))
return $ip;
$ip_dec = inet_pton($ip);
if ($config['ipcrypt_dns']) {
$host = gethostbyaddr($ip);
if ($host !== $ip) {
$segments = explode('.', $host);
$tld = [];
$tld[] = array_pop($segments);
if (count($segments) >= 2) {
$tld[] = array_pop($segments);
}
$tld = implode('.', array_reverse($tld));
}
}
if (is_numeric($ip))
$ipbytes = pack('N', $ip);
else if ($ip_dec !== false)
$ipbytes = $ip_dec;
else
return "#ERROR";
if (strlen($ipbytes) >= 16)
$ipbytes = substr($ipbytes, 0, 16);
$cyphertext = openssl_encrypt($ipbytes, 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA);
$ret = $config['ipcrypt_prefix'].':' . base32_encode($cyphertext);
if (isset($tld) && !empty($tld)) {
$ret .= '.'.$tld;
}
return $ret;
}
function uncloak_ip($ip) {
global $config;
$ipcrypt_key = ($config['ipcrypt_key']);
if (empty($ipcrypt_key))
return $ip;
$juice = substr($ip, strlen($config['ipcrypt_prefix']) + 1);
if ($delimiter = strpos($juice, '.')) {
$juice = substr($juice, 0, $delimiter);
}
if (substr($ip, 0, strlen($config['ipcrypt_prefix']) + 1) === $config['ipcrypt_prefix'].':') {
$plaintext = openssl_decrypt(base32_decode($juice), 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA);
if ($plaintext === false || strlen($plaintext) == 0)
return '#ERROR';
if (strlen($ip) >= 16)
return inet_ntop($plaintext);
else
return long2ip(unpack('N', $plaintext)[1]);
}
return '#ERROR';
}
function cloak_mask($mask) {
list($net, $block) = explode('/', $mask, 2);
$mask = cloak_ip($net);
if ($block) {
$mask .= '/'.$block;
}
return $mask;
}
function uncloak_mask($mask) {
list($addr, $block) = explode('/', $mask, 2);
$mask = uncloak_ip($addr);
if ($mask === '#ERROR') {
$mask = $addr;
}
if ($block) {
$mask .= '/'.$block;
}
return $mask;
}

2
inc/lib/Twig/Extensions/Extension/Tinyboard.php

@ -28,6 +28,8 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFilter('push', 'twig_push_filter'),
new Twig_SimpleFilter('bidi_cleanup', 'bidi_cleanup'),
new Twig_SimpleFilter('addslashes', 'addslashes'),
new Twig_SimpleFilter('cloak_ip', 'cloak_ip'),
new Twig_SimpleFilter('cloak_mask', 'cloak_mask'),
);
}

37
inc/mod/pages.php

@ -773,7 +773,8 @@ function mod_view_thread50($boardName, $thread) {
echo $page;
}
function mod_ip_remove_note($ip, $id) {
function mod_ip_remove_note($cloaked_ip, $id) {
$ip = uncloak_ip($cloaked_ip);
global $config, $mod;
if (!hasPermission($config['mod']['remove_notes']))
@ -786,13 +787,14 @@ function mod_ip_remove_note($ip, $id) {
$query->bindValue(':ip', $ip);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
modLog("Removed a note for <a href=\"?/IP/{$cloaked_ip}\">{$cloaked_ip}</a>");
modLog("Removed a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
header('Location: ?/IP/' . $cloaked_ip . '#notes', true, $config['redirect_http']);
}
function mod_page_ip($ip) {
function mod_page_ip($cip) {
$ip = uncloak_ip($cip);
global $config, $mod;
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
@ -804,7 +806,7 @@ function mod_page_ip($ip) {
Bans::delete($_POST['ban_id'], true, $mod['boards']);
header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']);
header('Location: ?/IP/' . $cip . '#bans', true, $config['redirect_http']);
return;
}
@ -821,9 +823,9 @@ function mod_page_ip($ip) {
$query->bindValue(':body', $_POST['note']);
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
modLog("Added a note for <a href=\"?/IP/{$cip}\">{$cip}</a>");
header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
header('Location: ?/IP/' . $cip . '#notes', true, $config['redirect_http']);
return;
}
@ -831,7 +833,7 @@ function mod_page_ip($ip) {
$args['ip'] = $ip;
$args['posts'] = array();
if ($config['mod']['dns_lookup'])
if ($config['mod']['dns_lookup'] && empty($config['ipcrypt_key']))
$args['hostname'] = rDNS($ip);
$boards = listBoards();
@ -873,16 +875,16 @@ function mod_page_ip($ip) {
if (hasPermission($config['mod']['modlog_ip'])) {
$query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50");
$query->bindValue(':search', '%' . $ip . '%');
$query->bindValue(':search', '%' . $cip . '%');
$query->execute() or error(db_error($query));
$args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC);
} else {
$args['logs'] = array();
}
$args['security_token'] = make_secure_link_token('IP/' . $ip);
$args['security_token'] = make_secure_link_token('IP/' . $cip);
mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($cip)), 'mod/view_ip.html', $args, $args['hostname'] ?? null);
}
function mod_ban() {
@ -974,7 +976,7 @@ function mod_ban_appeals() {
error(_('Ban appeal not found!'));
}
$ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend']));
$ban['mask'] = cloak_mask(Bans::range_to_string(array($ban['ipstart'], $ban['ipend'])));
if (isset($_POST['unban'])) {
modLog('Accepted ban appeal #' . $ban['id'] . ' for ' . $ban['mask']);
@ -1751,7 +1753,8 @@ function mod_deletebyip($boardName, $post, $global = false) {
}
// Record the action
modLog("Deleted all posts by IP address: <a href=\"?/IP/$ip\">$ip</a>");
$cip = cloak_ip($ip);
modLog("Deleted all posts by IP address: <a href=\"?/IP/$cip\">$cip</a>");
// Redirect
header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
@ -2210,7 +2213,7 @@ function mod_reports() {
$reports = $query->fetchAll(PDO::FETCH_ASSOC);
$report_queries = array();
foreach ($reports as $report) {
foreach ($reports as &$report) {
if (!isset($report_queries[$report['board']]))
$report_queries[$report['board']] = array();
$report_queries[$report['board']][] = $report['post'];
@ -2308,9 +2311,9 @@ function mod_report_dismiss($id, $all = false) {
}
$query->execute() or error(db_error($query));
$cip = cloak_ip($ip);
if ($all)
modLog("Dismissed all reports by <a href=\"?/IP/$ip\">$ip</a>");
modLog("Dismissed all reports by <a href=\"?/IP/$cip\">$cip</a>");
else
modLog("Dismissed a report for post #{$id}", $board);

2
templates/mod/ban_appeals.html

@ -16,7 +16,7 @@
{% if mod|hasPermission(config.mod.show_ip, board.uri) %}
<tr>
<th>{% trans 'IP' %}</th>
<td>{{ ban.mask }}</td>
<td>{{ ban.mask|cloak_mask }}</td>
</tr>
{% endif %}
<tr>

2
templates/mod/ban_form.html

@ -21,7 +21,7 @@
</th>
<td>
{% if not hide_ip %}
<input type="text" name="ip" id="ip" size="30" maxlength="40" value="{{ ip|e }}">
<input type="text" name="ip" id="ip" size="30" maxlength="40" value="{{ ip|cloak_ip|e }}">
{% else %}
<em>{% trans 'hidden' %}</em>
{% endif %}

2
templates/mod/log.html

@ -27,7 +27,7 @@
</td>
<td class="minimal">
{% if mod|hasPermission(config.mod.show_ip_modlog) %}
<a href="?/IP/{{ log.ip }}">{{ log.ip }}</a>
<a href="?/IP/{{ log.ip|cloak_ip }}">{{ log.ip|cloak_ip }}</a>
{% else %}
<em>hidden</em>
{% endif %}

2
templates/mod/report.html

@ -7,7 +7,7 @@
{% trans 'Report date' %}: {{ report.time|date(config.post_date) }}
<br>
{% if mod|hasPermission(config.mod.show_ip, report.board) %}
{% trans 'Reported by' %}: <a href="?/IP/{{ report.ip }}">{{ report.ip }}</a>
{% trans 'Reported by' %}: <a href="?/IP/{{ report.ip|cloak_ip }}">{{ report.ip|cloak_ip }}</a>
<br>
{% endif %}
{% if mod|hasPermission(config.mod.report_dismiss, report.board) or mod|hasPermission(config.mod.report_dismiss_ip, report.board) %}

12
templates/mod/search_results.html

@ -21,7 +21,7 @@
{% for note in results %}
<tr>
<td class="minimal">
<a href="?/IP/{{ note.ip }}#notes">{{ note.ip }}</a>
<a href="?/IP/{{ note.ip|cloak_ip }}#notes">{{ note.ip|cloak_ip }}</a>
</td>
<td class="minimal">
{% if note.username %}
@ -57,9 +57,9 @@
<tr{% if ban.expires != 0 and ban.expires < time() %} style="text-decoration:line-through"{% endif %}>
<td style="white-space: nowrap">
{% if ban.single_addr %}
<a href="?/IP/{{ ban.mask }}#bans">{{ ban.mask }}</a>
<a href="?/IP/{{ ban.mask|cloak_mask }}#bans">{{ ban.mask|cloak_mask }}</a>
{% else %}
{{ ban.mask|e }}
{{ ban.mask|cloak_mask|e }}
{% endif %}
</td>
<td>
@ -148,7 +148,7 @@
{% endif %}
</td>
<td class="minimal">
<a href="?/IP/{{ log.ip }}">{{ log.ip }}</a>
<a href="?/IP/{{ log.ip|cloak_ip }}">{{ log.ip|cloak_ip }}</a>
</td>
<td class="minimal">
<span title="{{ log.time|date(config.post_date) }}">{{ log.time|ago }}</span>
@ -210,8 +210,8 @@
</td>
<td class="minimal">
{% if mod|hasPermission(config.mod.show_ip, post.board) %}
<a href="?/IP/{{ post.ip }}">
{{ post.ip }}
<a href="?/IP/{{ post.ip|cloak_ip }}">
{{ post.ip|cloak_ip }}
</a>
{% else %}
<em>hidden</em>

2
templates/mod/user.html

@ -98,7 +98,7 @@
{% for log in logs %}
<tr>
<td class="minimal">
<a href="?/IP/{{ log.ip }}">{{ log.ip }}</a>
<a href="?/IP/{{ log.ip|cloak_ip }}">{{ log.ip|cloak_ip }}</a>
</td>
<td class="minimal">
<span title="{{ log.time|date(config.post_date) }}">{{ log.time|ago }}</span>

14
templates/mod/view_ip.html

@ -3,7 +3,7 @@
<legend>
<a href="?/{{ config.board_path|sprintf(board_posts.board.uri) }}{{ config.file_index }}">
{{ config.board_abbreviation|sprintf(board_posts.board.uri) }}
-
-
{{ board_posts.board.title|e }}
</a>
</legend>
@ -17,7 +17,7 @@
{% set notes_on_record = 'note' ~ (notes|count != 1 ? 's' : '') ~ ' on record' %}
<legend>{{ notes|count }} {% trans notes_on_record %}</legend>
</legend>
{% if notes|count > 0 %}
<table class="modlog">
<tr>
@ -45,7 +45,7 @@
</td>
{% if mod|hasPermission(config.mod.remove_notes) %}
<td class="minimal">
<a href="?/IP/{{ ip|url_encode(true) }}/remove_note/{{ note.id }}">
<a href="?/IP/{{ ip|cloak_ip|url_encode(true) }}/remove_note/{{ note.id }}">
<small>[{% trans 'remove' %}]</small>
</a>
</td>
@ -54,7 +54,7 @@
{% endfor %}
</table>
{% endif %}
{% if mod|hasPermission(config.mod.create_notes) %}
<form action="" method="post" style="margin:0">
<input type="hidden" name="token" value="{{ security_token }}">
@ -85,7 +85,7 @@
<fieldset id="bans">
{% set bans_on_record = 'ban' ~ (bans|count != 1 ? 's' : '') ~ ' on record' %}
<legend>{{ bans|count }} {% trans bans_on_record %}</legend>
{% for ban in bans %}
<form action="" method="post" style="text-align:center">
<input type="hidden" name="token" value="{{ security_token }}">
@ -102,7 +102,7 @@
</tr>
<tr>
<th>{% trans 'IP' %}</th>
<td>{{ ban.mask }}</td>
<td>{{ ban.cmask }}</td>
</tr>
<tr>
<th>{% trans 'Reason' %}</th>
@ -169,7 +169,7 @@
{% if mod|hasPermission(config.mod.ban) %}
<fieldset>
<legend>{% trans 'New ban' %}</legend>
{% set redirect = '?/IP/' ~ ip ~ '#bans' %}
{% set redirect = '?/IP/' ~ ip|cloak_ip ~ '#bans' %}
{% include 'mod/ban_form.html' %}
</fieldset>
{% endif %}

2
templates/post/ip.html

@ -1,3 +1,3 @@
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
[<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip }}">{{ post.ip }}</a>]
[<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip|cloak_ip }}">{{ post.ip|cloak_ip }}</a>]
{% endif %}

Loading…
Cancel
Save