forked from rejetto/hfs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmisc.ts
141 lines (129 loc) · 4.79 KB
/
misc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
import { EventEmitter } from 'events'
import { basename } from 'path'
import _ from 'lodash'
import Koa from 'koa'
import { Connection } from './connections'
import assert from 'assert'
export * from './util-http'
export * from './util-files'
export * from './cross'
export * from './debounceAsync'
import { Readable } from 'stream'
import { SocketAddress, BlockList } from 'node:net'
import { ApiError } from './apiMiddleware'
import { HTTP_BAD_REQUEST } from './const'
import { isIpLocalHost, makeMatcher } from './cross'
import { isIPv6 } from 'net'
type ProcessExitHandler = (signal:string) => any
const cbs = new Set<ProcessExitHandler>()
export function onProcessExit(cb: ProcessExitHandler) {
cbs.add(cb)
return () => cbs.delete(cb)
}
onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal =>
Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() =>
process.exit(0)))
export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[])=> void) {
let already = false
for (const e of events)
emitter.once(e, (...args) => {
if (already) return
already = true
cb(...args)
})
}
export function pattern2filter(pattern: string){
const matcher = makeMatcher(pattern.includes('*') ? pattern // if you specify *, we'll respect its position
: pattern.split('|').map(x => `*${x}*`).join('|'))
return (s?:string) =>
!s || !pattern || matcher(basename(s))
}
// install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
export function onOff(em: EventEmitter, events: { [eventName:string]: (...args: any[]) => void }) {
events = { ...events } // avoid later modifications, as we need this later for uninstallation
for (const [k,cb] of Object.entries(events))
for (const e of k.split(' '))
em.on(e, cb)
return () => {
for (const [k,cb] of Object.entries(events))
for (const e of k.split(' '))
em.off(e, cb)
}
}
export function isLocalHost(c: Connection | Koa.Context | string) {
const ip = typeof c === 'string' ? c : c.socket.remoteAddress // don't use Context.ip as it is subject to proxied ips, and that's no use for localhost detection
return ip && isIpLocalHost(ip)
}
export function makeNetMatcher(mask: string, emptyMaskReturns=false) {
if (!mask)
return () => emptyMaskReturns
if (!mask.includes('/'))
return makeMatcher(mask)
const all = mask.split('|')
const neg = all[0]?.[0] === '!'
if (neg)
all[0] = all[0]!.slice(1)
const bl = new BlockList()
for (const x of all) {
const m = /^([.:\da-f]+)(?:\/(\d+)|-(.+)|)$/i.exec(x)
if (!m) {
console.warn("error in network mask", x)
continue
}
const address = parseAddress(m[1]!)
if (m[2])
bl.addSubnet(address, Number(m[2]))
else if (m[3])
bl.addRange(address, parseAddress(m[2]!))
else
bl.addAddress(address)
}
return (ip: string) =>
neg !== bl.check(parseAddress(ip))
}
function parseAddress(s: string) {
return new SocketAddress({ address: s, family: isIPv6(s) ? 'ipv6' : 'ipv4' })
}
export function same(a: any, b: any) {
try {
assert.deepStrictEqual(a, b)
return true
}
catch { return false }
}
export function asyncGeneratorToReadable<T>(generator: AsyncIterable<T>) {
const iterator = generator[Symbol.asyncIterator]()
return new Readable({
objectMode: true,
destroy() {
iterator.return?.()
},
read() {
iterator.next().then(it =>
this.push(it.done ? null : it.value))
}
})
}
// produces as promises resolve, not sequentially
export class AsapStream<T> extends Readable {
finished = false
constructor(private promises: Promise<T>[]) {
super({ objectMode: true })
}
_read() {
if (this.finished) return
this.finished = true
for (const p of this.promises)
p.then(x => x !== undefined && this.push(x),
e => this.emit('error', e) )
Promise.allSettled(this.promises).then(() => this.push(null))
}
}
export function apiAssertTypes(paramsByType: { [type:string]: { [name:string]: any } }) {
for (const [types,params] of Object.entries(paramsByType))
for (const type of types.split('_'))
for (const [name,val] of Object.entries(params))
if (type === 'array' ? !Array.isArray(val) : typeof val !== type)
throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
}