mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
A design that describes what WeKan already DOES belongs with the feature it describes, not in the folder for things being proposed. Fifteen docs move: Design/Accessibility.md -> Features/Accessibility/Accessibility-Improvements.md Design/Original-Positions.md -> Features/Board/Original-Positions.md Design/Page/* -> Features/Page/* (thirteen pages that exist) Design/AdminPanelProblems.md -> Features/Admin-Panel/Problems/Design.md What stays in docs/Design is what the folder is for: the principles (Design-Principles, Monkey-Proof-Software), the comparisons, the roadmap (Multiverse), and the proposals not yet built (Multitenancy, Autoupdate). The Admin Panel Problems design is new and covers six changes - summaries, the per-actor tally, IPv4/IPv6 columns, an API report, a per-address login tally, and blocking with increasing delays. Three are shipped and three are planned; it is filed under the feature because the pane exists, with each planned item marked so it can become plain description as it lands. Every reference in the repository is rewritten - 152 files, including the CSS comments that point at the table-page design, which a first pass over .md and .js alone missed. Two suites asserted the old paths and now assert the new ones. A moved doc whose links nobody fixed is worse than one nobody moved. Thanks to xet7 !
72 lines
3.1 KiB
JavaScript
72 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
// Which address family an attempt came from, so every Admin Panel → Problems
|
|
// report can show IPv4 and IPv6 in their own columns.
|
|
//
|
|
// One request arrives from ONE address, so a row has one or the other - never
|
|
// both. Two columns rather than one is still the right shape: an admin scanning
|
|
// for "the 10.0.0.0/8 range" and one scanning for "everything from that /64" are
|
|
// looking for different-shaped things, and a single mixed column makes both
|
|
// harder. A row simply leaves the other column empty.
|
|
//
|
|
// THE TRAP THIS EXISTS FOR is the IPv4-mapped IPv6 address. A dual-stack socket
|
|
// reports an IPv4 client as `::ffff:203.0.113.9`, so without unwrapping:
|
|
//
|
|
// * the same client counts as two different actors depending on whether it
|
|
// reached a v4 or a dual-stack listener;
|
|
// * it lands in the IPv6 column, where somebody looking for IPv4 will not
|
|
// find it;
|
|
// * and an admin blocking `203.0.113.9` does not match what is recorded.
|
|
//
|
|
// So a mapped address is unwrapped to the IPv4 it is, and only a genuine IPv6
|
|
// address is recorded as one.
|
|
|
|
// ::ffff:203.0.113.9 and the deprecated ::203.0.113.9 form.
|
|
const MAPPED = /^::(?:ffff:)?((?:\d{1,3}\.){3}\d{1,3})$/i;
|
|
const IPV4 = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
|
|
// Everything an IPv4 octet may be. `010` is not 10 here - a leading zero is how
|
|
// an address is disguised, and Node does not produce one, so it is not accepted.
|
|
function isIpv4(value) {
|
|
if (!IPV4.test(value)) return false;
|
|
return value.split('.').every(part => {
|
|
if (part.length > 1 && part[0] === '0') return false;
|
|
const n = Number(part);
|
|
return n >= 0 && n <= 255;
|
|
});
|
|
}
|
|
|
|
// An IPv6 address as Node writes one: hex groups and colons, optionally with a
|
|
// zone (`fe80::1%eth0`) and optionally a trailing IPv4 part. Deliberately not a
|
|
// full validator - this classifies what the socket layer produced, and anything
|
|
// unrecognised is returned untouched rather than guessed at.
|
|
function isIpv6(value) {
|
|
const bare = String(value).split('%')[0];
|
|
return bare.includes(':') && /^[0-9a-f:.]+$/i.test(bare);
|
|
}
|
|
|
|
// Split one address into the two columns.
|
|
// '203.0.113.9' -> { ipv4: '203.0.113.9', ipv6: '' }
|
|
// '::ffff:203.0.113.9' -> { ipv4: '203.0.113.9', ipv6: '' } (unwrapped)
|
|
// '2001:db8::1' -> { ipv4: '', ipv6: '2001:db8::1' }
|
|
// 'unknown' / '' -> { ipv4: '', ipv6: '' }
|
|
function classifyAddress(address) {
|
|
const value = String(address == null ? '' : address).trim();
|
|
if (!value || value === 'unknown') return { ipv4: '', ipv6: '' };
|
|
|
|
const mapped = MAPPED.exec(value);
|
|
if (mapped && isIpv4(mapped[1])) return { ipv4: mapped[1], ipv6: '' };
|
|
if (isIpv4(value)) return { ipv4: value, ipv6: '' };
|
|
if (isIpv6(value)) return { ipv4: '', ipv6: value };
|
|
return { ipv4: '', ipv6: '' };
|
|
}
|
|
|
|
// The address as it should be STORED and counted: a mapped IPv4 becomes the
|
|
// IPv4 it is, so one client is one actor however it reached the server.
|
|
function normaliseAddress(address) {
|
|
const { ipv4, ipv6 } = classifyAddress(address);
|
|
return ipv4 || ipv6 || (address == null ? '' : String(address));
|
|
}
|
|
|
|
module.exports = { classifyAddress, normaliseAddress, isIpv4, isIpv6 };
|