@stamhoofd/redirecter 2.39.1 → 2.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +5 -0
- package/index.ts +33 -30
- package/package.json +3 -3
- package/src/classes/Geolocator.ts +30 -30
- package/src/endpoints/RedirectEndpoint.ts +23 -26
- package/stamhoofd.d.ts +7 -8
- package/.eslintrc.js +0 -61
package/index.ts
CHANGED
|
@@ -1,52 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Country } from "@stamhoofd/structures";
|
|
1
|
+
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
+
backendEnv.load({ service: 'redirecter' });
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { Router, RouterServer } from '@simonbackx/simple-endpoints';
|
|
5
|
+
import { Country } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { Geolocator } from './src/classes/Geolocator';
|
|
8
|
+
|
|
9
|
+
process.on('unhandledRejection', (error: Error) => {
|
|
10
|
+
console.error('unhandledRejection');
|
|
9
11
|
console.error(error.message, error.stack);
|
|
10
12
|
process.exit(1);
|
|
11
13
|
});
|
|
12
14
|
|
|
13
15
|
// Set timezone!
|
|
14
|
-
process.env.TZ =
|
|
16
|
+
process.env.TZ = 'UTC';
|
|
15
17
|
|
|
16
18
|
// Quick check
|
|
17
|
-
if (new Date().getTimezoneOffset()
|
|
18
|
-
throw new Error(
|
|
19
|
+
if (new Date().getTimezoneOffset() !== 0) {
|
|
20
|
+
throw new Error('Process should always run in UTC timezone');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
const start = async () => {
|
|
22
|
-
await Geolocator.shared.load(__dirname+
|
|
23
|
+
const start = async () => {
|
|
24
|
+
await Geolocator.shared.load(__dirname + '/src/data/belgium.csv', Country.Belgium);
|
|
23
25
|
|
|
24
26
|
// Netherlands not needed, because it is the current default
|
|
25
|
-
//await Geolocator.shared.load(__dirname+"/src/data/netherlands.csv", Country.Netherlands)
|
|
27
|
+
// await Geolocator.shared.load(__dirname+"/src/data/netherlands.csv", Country.Netherlands)
|
|
26
28
|
|
|
27
|
-
console.log(
|
|
29
|
+
console.log('Initialising server...');
|
|
28
30
|
const router = new Router();
|
|
29
|
-
await router.loadAllEndpoints(__dirname +
|
|
30
|
-
await router.loadAllEndpoints(__dirname +
|
|
31
|
+
await router.loadAllEndpoints(__dirname + '/src/endpoints');
|
|
32
|
+
await router.loadAllEndpoints(__dirname + '/src/endpoints/*');
|
|
31
33
|
|
|
32
34
|
const routerServer = new RouterServer(router);
|
|
33
|
-
routerServer.verbose = false
|
|
35
|
+
routerServer.verbose = false;
|
|
34
36
|
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
35
37
|
|
|
36
38
|
const shutdown = async () => {
|
|
37
|
-
console.log(
|
|
39
|
+
console.log('Shutting down...');
|
|
38
40
|
// Disable keep alive
|
|
39
|
-
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, {
|
|
41
|
+
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
|
|
40
42
|
if (routerServer.server) {
|
|
41
43
|
routerServer.server.headersTimeout = 5000;
|
|
42
44
|
routerServer.server.keepAliveTimeout = 1;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
try {
|
|
46
|
-
await routerServer.close()
|
|
47
|
-
console.log(
|
|
48
|
-
}
|
|
49
|
-
|
|
48
|
+
await routerServer.close();
|
|
49
|
+
console.log('HTTP server stopped');
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error('Failed to stop HTTP server:');
|
|
50
53
|
console.error(err);
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -54,24 +57,24 @@ const start = async () => {
|
|
|
54
57
|
process.exit(0);
|
|
55
58
|
};
|
|
56
59
|
|
|
57
|
-
process.on(
|
|
58
|
-
console.info(
|
|
60
|
+
process.on('SIGTERM', () => {
|
|
61
|
+
console.info('SIGTERM signal received.');
|
|
59
62
|
shutdown().catch((e) => {
|
|
60
|
-
console.error(e)
|
|
63
|
+
console.error(e);
|
|
61
64
|
process.exit(1);
|
|
62
65
|
});
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
process.on(
|
|
66
|
-
console.info(
|
|
68
|
+
process.on('SIGINT', () => {
|
|
69
|
+
console.info('SIGINT signal received.');
|
|
67
70
|
shutdown().catch((e) => {
|
|
68
|
-
console.error(e)
|
|
71
|
+
console.error(e);
|
|
69
72
|
process.exit(1);
|
|
70
73
|
});
|
|
71
74
|
});
|
|
72
75
|
};
|
|
73
76
|
|
|
74
|
-
start().catch(error => {
|
|
75
|
-
console.error(
|
|
77
|
+
start().catch((error) => {
|
|
78
|
+
console.error('unhandledRejection', error);
|
|
76
79
|
process.exit(1);
|
|
77
80
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/redirecter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.1",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"license": "UNLICENCED",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -rf ./dist/src/data && tsc -b && mkdir -p ./dist/src/data && cp ./src/data/* ./dist/src/data",
|
|
8
8
|
"build:full": "rm -rf ./dist && yarn build",
|
|
9
9
|
"start": "yarn build && node ./dist/index.js",
|
|
10
|
-
"lint": "eslint
|
|
10
|
+
"lint": "eslint"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@types/node": "^20.12"
|
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
17
17
|
"@simonbackx/simple-logging": "^1.0.1"
|
|
18
18
|
},
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "31eb933097eb608655f755de9df912c21a420963"
|
|
20
20
|
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import { Country } from
|
|
2
|
-
import fs from
|
|
3
|
-
import readline from
|
|
1
|
+
import { Country } from '@stamhoofd/structures';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import readline from 'readline';
|
|
4
4
|
|
|
5
5
|
function fromIP(ip: string): Buffer {
|
|
6
|
-
const splitted = ip.split(
|
|
7
|
-
if (splitted.length
|
|
8
|
-
throw new Error(
|
|
6
|
+
const splitted = ip.split('.');
|
|
7
|
+
if (splitted.length !== 4) {
|
|
8
|
+
throw new Error('Invalid ipv4 address ' + ip);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
return Buffer.from(splitted.map(s => parseInt(s)))
|
|
11
|
+
return Buffer.from(splitted.map(s => parseInt(s)));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
class IPRange {
|
|
15
|
-
start: Buffer
|
|
16
|
-
end: Buffer // included
|
|
15
|
+
start: Buffer;
|
|
16
|
+
end: Buffer; // included
|
|
17
17
|
|
|
18
|
-
carrier: string
|
|
18
|
+
carrier: string;
|
|
19
19
|
|
|
20
20
|
constructor(start: string, end: string, carrier: string) {
|
|
21
|
-
this.start = fromIP(start)
|
|
22
|
-
this.end = fromIP(end)
|
|
23
|
-
this.carrier = carrier
|
|
21
|
+
this.start = fromIP(start);
|
|
22
|
+
this.end = fromIP(end);
|
|
23
|
+
this.carrier = carrier;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
includes(ip: Buffer): boolean {
|
|
27
|
-
return this.start.compare(ip) <= 0 && this.end.compare(ip) >= 0
|
|
27
|
+
return this.start.compare(ip) <= 0 && this.end.compare(ip) >= 0;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class Geolocator {
|
|
32
|
-
static shared = new Geolocator()
|
|
32
|
+
static shared = new Geolocator();
|
|
33
33
|
|
|
34
|
-
ranges: Map<Country, IPRange[]> = new Map()
|
|
34
|
+
ranges: Map<Country, IPRange[]> = new Map();
|
|
35
35
|
|
|
36
36
|
async load(file: string, country: Country) {
|
|
37
37
|
const lineReader = readline.createInterface({
|
|
38
|
-
input: fs.createReadStream(file, { encoding:
|
|
38
|
+
input: fs.createReadStream(file, { encoding: 'utf-8' }),
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
const range = this.ranges.get(country) ?? []
|
|
42
|
-
this.ranges.set(country, range)
|
|
41
|
+
const range = this.ranges.get(country) ?? [];
|
|
42
|
+
this.ranges.set(country, range);
|
|
43
43
|
|
|
44
44
|
for await (const line of lineReader) {
|
|
45
|
-
const splitted = line.split(
|
|
45
|
+
const splitted = line.split(',');
|
|
46
46
|
if (splitted.length < 2) {
|
|
47
47
|
console.error(`Failed to parse line: ${line}`);
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
|
-
const from = splitted[0]
|
|
51
|
-
const to = splitted[1]
|
|
50
|
+
const from = splitted[0];
|
|
51
|
+
const to = splitted[1];
|
|
52
52
|
|
|
53
|
-
console.log(
|
|
54
|
-
range.push(new IPRange(from, to, splitted[4] ??
|
|
53
|
+
console.log('From ', from, 'to', to);
|
|
54
|
+
range.push(new IPRange(from, to, splitted[4] ?? ''));
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
getCountry(ip: string): Country | undefined {
|
|
59
|
-
const parsed = fromIP(ip)
|
|
59
|
+
const parsed = fromIP(ip);
|
|
60
60
|
|
|
61
61
|
for (const [country, ranges] of this.ranges) {
|
|
62
62
|
for (const range of ranges) {
|
|
63
63
|
if (range.includes(parsed)) {
|
|
64
|
-
return country
|
|
64
|
+
return country;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
getInfo(ip: string): { country: Country
|
|
71
|
-
const parsed = fromIP(ip)
|
|
70
|
+
getInfo(ip: string): { country: Country; carrier: string } | undefined {
|
|
71
|
+
const parsed = fromIP(ip);
|
|
72
72
|
|
|
73
73
|
for (const [country, ranges] of this.ranges) {
|
|
74
74
|
for (const range of ranges) {
|
|
75
75
|
if (range.includes(parsed)) {
|
|
76
|
-
return {country, carrier: range.carrier}
|
|
76
|
+
return { country, carrier: range.carrier };
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|
|
@@ -1,58 +1,55 @@
|
|
|
1
|
-
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints'
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Country } from '@stamhoofd/structures';
|
|
3
|
-
import { URL } from "url";
|
|
4
3
|
|
|
5
4
|
import { Geolocator } from '../classes/Geolocator';
|
|
6
5
|
|
|
7
6
|
type Params = Record<string, never>;
|
|
8
|
-
type Body = undefined
|
|
9
|
-
type Query = undefined
|
|
10
|
-
type ResponseBody = string
|
|
7
|
+
type Body = undefined;
|
|
8
|
+
type Query = undefined;
|
|
9
|
+
type ResponseBody = string;
|
|
11
10
|
|
|
12
11
|
function getRequestIP(request: Request): string {
|
|
13
12
|
let ipAddress = request.request?.socket.remoteAddress;
|
|
14
|
-
if (request.headers[
|
|
15
|
-
ipAddress = request.headers[
|
|
13
|
+
if (request.headers['x-real-ip'] && typeof request.headers['x-real-ip'] === 'string' && (ipAddress === '127.0.0.1' || ipAddress === '0.0.0.0')) {
|
|
14
|
+
ipAddress = request.headers['x-real-ip'];
|
|
16
15
|
}
|
|
17
16
|
if (!ipAddress) {
|
|
18
17
|
ipAddress = '?';
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
return ipAddress.split(
|
|
20
|
+
return ipAddress.split(':', 2)[0];
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return [true, {}]
|
|
23
|
+
export class RedirectEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
24
|
+
protected doesMatch(_request: Request): [true, Params] | [false] {
|
|
25
|
+
return [true, {}];
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
-
const ip = getRequestIP(request.request)
|
|
32
|
-
|
|
33
|
-
const country = Geolocator.shared.getCountry(ip)
|
|
29
|
+
const ip = getRequestIP(request.request);
|
|
34
30
|
|
|
35
|
-
const
|
|
31
|
+
const country = Geolocator.shared.getCountry(ip);
|
|
36
32
|
|
|
33
|
+
const path = request.request.request?.url ?? '';
|
|
37
34
|
|
|
38
35
|
if (country === Country.Belgium) {
|
|
39
|
-
const url =
|
|
40
|
-
const response = new Response(
|
|
41
|
-
response.status = 302
|
|
42
|
-
response.headers[
|
|
36
|
+
const url = 'https://www.stamhoofd.be' + path;
|
|
37
|
+
const response = new Response('Doorverwijzen naar ' + url);
|
|
38
|
+
response.status = 302;
|
|
39
|
+
response.headers['Location'] = url;
|
|
43
40
|
|
|
44
41
|
// Prevent encoding and version requirement
|
|
45
|
-
response.headers[
|
|
42
|
+
response.headers['Content-Type'] = 'text/html';
|
|
46
43
|
return Promise.resolve(response);
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
const url =
|
|
50
|
-
const response = new Response(
|
|
51
|
-
response.status = 302
|
|
52
|
-
response.headers[
|
|
46
|
+
const url = 'https://www.stamhoofd.nl' + path;
|
|
47
|
+
const response = new Response('Doorverwijzen naar ' + url);
|
|
48
|
+
response.status = 302;
|
|
49
|
+
response.headers['Location'] = url;
|
|
53
50
|
|
|
54
51
|
// Prevent encoding and version requirement
|
|
55
|
-
response.headers[
|
|
52
|
+
response.headers['Content-Type'] = 'text/html';
|
|
56
53
|
return Promise.resolve(response);
|
|
57
54
|
}
|
|
58
55
|
}
|
package/stamhoofd.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
2
1
|
export {};
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Stamhoofd uses a global variable to store some configurations. We don't use process.env because we can only store
|
|
6
|
-
* strings into those files. And we need objects for our localized domains (different domains for each locale).
|
|
4
|
+
* Stamhoofd uses a global variable to store some configurations. We don't use process.env because we can only store
|
|
5
|
+
* strings into those files. And we need objects for our localized domains (different domains for each locale).
|
|
7
6
|
* Having to encode and decode those values would be inefficient.
|
|
8
|
-
*
|
|
9
|
-
* So we use our own global configuration variable: STAMHOOFD. Available everywhere and contains
|
|
10
|
-
* other information depending on the environment (frontend/backend/shared). TypeScript will
|
|
7
|
+
*
|
|
8
|
+
* So we use our own global configuration variable: STAMHOOFD. Available everywhere and contains
|
|
9
|
+
* other information depending on the environment (frontend/backend/shared). TypeScript will
|
|
11
10
|
* always suggest the possible keys.
|
|
12
11
|
*/
|
|
13
12
|
declare global {
|
|
14
|
-
const STAMHOOFD: RedirecterEnvironment
|
|
15
|
-
}
|
|
13
|
+
const STAMHOOFD: RedirecterEnvironment;
|
|
14
|
+
}
|
package/.eslintrc.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
ignorePatterns: ["dist/", "node_modules/"],
|
|
4
|
-
parserOptions: {
|
|
5
|
-
"ecmaVersion": 2017
|
|
6
|
-
},
|
|
7
|
-
env: {
|
|
8
|
-
"es6": true,
|
|
9
|
-
"node": true,
|
|
10
|
-
},
|
|
11
|
-
extends: [
|
|
12
|
-
"eslint:recommended",
|
|
13
|
-
],
|
|
14
|
-
plugins: [],
|
|
15
|
-
rules: {
|
|
16
|
-
"no-console": "off",
|
|
17
|
-
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
|
|
18
|
-
"sort-imports": "off",
|
|
19
|
-
"import/order": "off"
|
|
20
|
-
},
|
|
21
|
-
overrides: [
|
|
22
|
-
{
|
|
23
|
-
// Rules for TypeScript and vue
|
|
24
|
-
files: ["*.ts"],
|
|
25
|
-
parser: "@typescript-eslint/parser",
|
|
26
|
-
parserOptions: {
|
|
27
|
-
project: ["./tsconfig.json"]
|
|
28
|
-
},
|
|
29
|
-
plugins: ["@typescript-eslint", "jest"],
|
|
30
|
-
extends: [
|
|
31
|
-
"eslint:recommended",
|
|
32
|
-
"plugin:@typescript-eslint/eslint-recommended",
|
|
33
|
-
"plugin:@typescript-eslint/recommended",
|
|
34
|
-
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
|
35
|
-
"plugin:jest/recommended",
|
|
36
|
-
],
|
|
37
|
-
rules: {
|
|
38
|
-
"no-console": "off",
|
|
39
|
-
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
|
|
40
|
-
"sort-imports": "off",
|
|
41
|
-
"import/order": "off",
|
|
42
|
-
"@typescript-eslint/explicit-function-return-type": "off",
|
|
43
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
44
|
-
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
45
|
-
"@typescript-eslint/no-namespace": "off",
|
|
46
|
-
"@typescript-eslint/no-floating-promises": "error",
|
|
47
|
-
"@typescript-eslint/no-misused-promises": "error",
|
|
48
|
-
"@typescript-eslint/prefer-for-of": "warn",
|
|
49
|
-
"@typescript-eslint/no-empty-interface": "off", // It is convenient to have placeholder interfaces
|
|
50
|
-
"@typescript-eslint/no-this-alias": "off", // No idea why we need this. This breaks code that is just fine. Prohibit the use of function() instead of this rule
|
|
51
|
-
"@typescript-eslint/unbound-method": "off", // Methods are automatically bound in vue, it would break removeEventListeners if we bound it every time unless we save every method in variables again...
|
|
52
|
-
"@typescript-eslint/no-unsafe-assignment": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
53
|
-
"@typescript-eslint/no-unsafe-return": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
54
|
-
"@typescript-eslint/no-unsafe-call": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
55
|
-
"@typescript-eslint/no-unsafe-member-access": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
56
|
-
"@typescript-eslint/restrict-plus-operands": "off", // bullshit one
|
|
57
|
-
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
};
|