self-check will bypass filters
Massimo Melina committed
Jan 2, 2024 at 12:46 UTC
548158164706a6db25a20accc60dfb7d793c274c
5 files changed
+44
-27
src/geo.ts
+2
-2
@@ -17,7 +17,7 @@ setInterval(checkFiles, DAY) // keep updated at run-time
17
export const ip2country = _.memoize((ip: string) => ip2location.getCountryShortAsync(ip).then(v => v === '-' ? '' : v, () => ''))
18
19
export const geoFilter: Middleware = async (ctx, next) => {
20
- if (enabled.get() && allow.get() !== null && !isLocalHost(ctx)) {
20
+ if (!ctx.state.skipFilters && enabled.get() && allow.get() !== null && !isLocalHost(ctx)) {
21
const { connection } = ctx.state
22
const country = connection.country ??= await ip2country(ctx.ip)
23
if (country)
@@ -26,7 +26,7 @@ export const geoFilter: Middleware = async (ctx, next) => {
26
if (country ? list.get().includes(country) !== allow.get() : !allowUnknown.get())
27
return disconnect(ctx)
28
}
29
- return next()
29
+ await next()
30
}
31
32
function isOpen() {
src/index.ts
+1
-1
@@ -33,11 +33,11 @@ const keys = process.env.COOKIE_SIGN_KEYS?.split(',')
33
|| [randomId(30)] // randomness at start gives some extra security, btu also invalidates existing sessions
34
export const app = new Koa({ keys })
35
app.use(sessionMiddleware)
36
+ .use(selfCheckMiddleware)
37
.use(someSecurity)
38
.use(acmeMiddleware)
39
.use(prepareState)
40
.use(geoFilter)
40
- .use(selfCheckMiddleware)
41
.use(gzipper)
42
.use(paramsDecoder) // must be done before plugins, so they can manipulate params
43
.use(pluginsMiddleware)
src/middlewares.ts
+2
-2
@@ -191,7 +191,7 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
191
try {
192
if (dirTraversal(decodeURI(ctx.path)))
193
return ctx.status = HTTP_FOOL
194
- if (applyBlock(ctx.socket, ctx.ip))
194
+ if (!ctx.state.skipFilters && applyBlock(ctx.socket, ctx.ip))
195
return
196
197
if (!ctx.ips.length && ctx.get('X-Forwarded-For') // empty ctx.ips implies we didn't configure for proxies
@@ -204,7 +204,7 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
204
catch {
205
return ctx.status = HTTP_FOOL
206
}
207
- if (forceBaseUrl.get() && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled())
207
+ if (!ctx.state.skipFilters && forceBaseUrl.get() && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled())
208
return disconnect(ctx)
209
return next()
210
}
src/roots.ts
+1
-1
@@ -32,7 +32,7 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
32
const root = host2root(ctx.host)
33
if (root === '' || root === '/') return
34
if (root === undefined) {
35
- if (!rootsMandatory.get() || isLocalHost(ctx)) return
35
+ if (ctx.state.skipFilters || !rootsMandatory.get() || isLocalHost(ctx)) return
36
disconnect(ctx)
37
return true // true will avoid calling next
38
}
src/selfCheck.ts
+38
-21
@@ -6,13 +6,24 @@ import _ from 'lodash'
6
import { haveTimeout } from './cross'
7
import { httpString } from './util-http'
8
9
+let selfChecking = false
10
+
11
const CHECK_URL = SPECIAL_URI + 'self-check'
10
-export const selfCheckMiddleware: Middleware = async (ctx, next) => { // koa format
11
- if (ctx.url.startsWith(CHECK_URL))
12
+export const selfCheckMiddleware: Middleware = async (ctx, next) => {
13
+ if (selfChecking && ctx.url.startsWith(CHECK_URL)) {
14
ctx.body = 'HFS'
15
+ ctx.state.skipFilters = true
16
+ }
17
await next()
18
}
19
20
+
21
+declare module "koa" {
22
+ interface DefaultState {
23
+ skipFilters?: boolean
24
+ }
25
+}
26
+
27
export async function selfCheck(url: string) {
28
interface PortScannerService {
29
type?: string
@@ -27,27 +38,33 @@ export async function selfCheck(url: string) {
38
console.log(`checking server ${url}`)
39
const parsed = new URL(url)
40
const family = !isIP(parsed.hostname) ? undefined : isIPv6(parsed.hostname) ? 6 : 4
30
- for (const services of _.chunk(_.shuffle<PortScannerService>(prjInfo.selfCheckServices), 2)) {
31
- try {
32
- return await Promise.any(services.map(async (svc) => {
33
- if (!svc.url || svc.type) throw 'unsupported ' + svc.type // only default type supported for now
34
- let { url: serviceUrl, body, regexpSuccess, regexpFailure, ...rest } = svc
35
- const service = new URL(serviceUrl).hostname
36
- console.log('trying external service', service)
37
- body = applySymbols(body)
38
- serviceUrl = applySymbols(serviceUrl)!
39
- const res = await haveTimeout(6_000, httpString(serviceUrl, { family, ...rest, body }))
40
- const success = new RegExp(regexpSuccess).test(res)
41
- const failure = new RegExp(regexpFailure).test(res)
42
- if (success === failure) throw 'inconsistent: ' + service + ': ' + res // this result cannot be trusted
43
- console.debug(service, 'responded', success)
44
- return { success, service, url }
45
- }))
46
- }
47
- catch (e: any) {
48
- console.debug(e?.errors?.map(String) || e?.cause || String(e))
41
+ try {
42
+ selfChecking = true
43
+ for (const services of _.chunk(_.shuffle<PortScannerService>(prjInfo.selfCheckServices), 2)) {
44
+ try {
45
+ return await Promise.any(services.map(async (svc) => {
46
+ if (!svc.url || svc.type) throw 'unsupported ' + svc.type // only default type supported for now
47
+ let { url: serviceUrl, body, regexpSuccess, regexpFailure, ...rest } = svc
48
+ const service = new URL(serviceUrl).hostname
49
+ console.log('trying external service', service)
50
+ body = applySymbols(body)
51
+ serviceUrl = applySymbols(serviceUrl)!
52
+ const res = await haveTimeout(6_000, httpString(serviceUrl, { family, ...rest, body }))
53
+ const success = new RegExp(regexpSuccess).test(res)
54
+ const failure = new RegExp(regexpFailure).test(res)
55
+ if (success === failure) throw 'inconsistent: ' + service + ': ' + res // this result cannot be trusted
56
+ console.debug(service, 'responded', success)
57
+ return { success, service, url }
58
+ }))
59
+ }
60
+ catch (e: any) {
61
+ console.debug(e?.errors?.map(String) || e?.cause || String(e))
62
+ }
63
}
64
}
65
+ finally {
66
+ selfChecking = false
67
+ }
68
69
function applySymbols(s?: string) {
70
return s?.replace('$IP', parsed.hostname)