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)