main
ts 144 lines 5.08 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { basename, dirname, join } from 'path'
4 import Koa from 'koa'
5 import { Connection } from './connections'
6 export * from './util-http'
7 export * from './util-files'
8 export * from './fileAttr'
9 export * from './cross'
10 export * from './debounceAsync'
11 export * from './AsapStream'
12 import { Readable, Transform } from 'stream'
13 import { SocketAddress, BlockList } from 'node:net'
14 import { ApiError } from './apiMiddleware'
15 import { HTTP_BAD_REQUEST } from './const'
16 import { Callback, isIpLocalHost, makeMatcher, try_ } from './cross'
17 import { isIPv6 } from 'net'
18 import _ from 'lodash'
19
20 export function pattern2filter(pattern: string){
21 const matcher = makeMatcher(pattern.includes('*') ? pattern // if you specify *, we'll respect its position
22 : pattern.split('|').map(x => `*${x}*`).join('|'), false, false)
23 return (s: string) =>
24 !pattern || matcher(basename(s||''))
25 }
26
27 export function isLocalHost(c: Connection | Koa.Context | string) {
28 const ip = typeof c === 'string' ? c : c.ip
29 return ip && isIpLocalHost(ip)
30 }
31
32 // this will memory-leak over mask, so be careful with what you use this. Object is 3x faster than _.memoize
33 export function netMatches(ip: string, mask: string, emptyMaskReturns=false) {
34 const cache = (netMatches as any).cache ||= {}
35 return (cache[mask + (emptyMaskReturns ? '1' : '0')] ||= makeNetMatcher(mask, emptyMaskReturns))(ip) // cache the matcher
36 }
37 export function makeNetMatcher(mask: string, emptyMaskReturns=false) {
38 if (!mask)
39 return () => emptyMaskReturns
40 mask = mask.replaceAll(' ','')
41 mask = mask.replace('localhost', '::1|127.0.0.1')
42 try {
43 if (!/\/|-(?![^\[]*\])/.test(mask)) { // when no CIDR and no ranges are used, then we use standard matcher, otherwise BlockList. For "-" we must skip those inside []
44 if (/[^.:\da-fA-F*?|()!]/.test(mask))
45 throw mask
46 return makeMatcher(mask)
47 }
48 const all = mask.split('|')
49 const neg = all[0]?.[0] === '!'
50 if (neg)
51 all[0] = all[0]!.slice(1)
52 const bl = new BlockList()
53 for (const x of all) {
54 const m = /^([.:\da-f]+)(?:\/(\d+)|-([.:\da-f]+)|)$/i.exec(x) // parse cidr or range
55 if (!m) throw x // we don't support wildcards in this case
56 const address = try_(() => parseAddress(m[1]!),
57 () => { throw m[1] })
58 if (!address) continue
59 if (m[2])
60 try { bl.addSubnet(address, Number(m[2])) }
61 catch { throw x }
62 else if (m[3])
63 try { bl.addRange(address, parseAddress(m[3]!)) }
64 catch { throw m[3] }
65 else
66 bl.addAddress(address)
67 }
68 return (ip: string) => {
69 try { return neg !== bl.check(parseAddress(ip)) }
70 catch {
71 console.error("Invalid address ", ip)
72 return false
73 }
74 }
75 }
76 catch(e: any) {
77 throw "error in net-mask: " + e
78 }
79 }
80
81 // can throw ERR_INVALID_ADDRESS
82 function parseAddress(s: string) {
83 return new SocketAddress({ address: s, family: isIPv6(s) ? 'ipv6' : 'ipv4' })
84 }
85
86 export function same(a: any, b: any) {
87 return _.isEqual(a, b)
88 }
89
90 export function asyncGeneratorToReadable<T>(generator: AsyncIterable<T>) {
91 const iterator = generator[Symbol.asyncIterator]()
92 return new Readable({
93 objectMode: true,
94 destroy() {
95 void iterator.return?.()
96 },
97 read() {
98 iterator.next().then(it => {
99 if (it.done)
100 this.emit('ending')
101 return this.push(it.done ? null : it.value)
102 })
103 }
104 })
105 }
106
107 export function apiAssertTypes(paramsByType: { [type:string]: { [name:string]: any } }) {
108 for (const [types,params] of Object.entries(paramsByType)) {
109 if (!_.isPlainObject(params))
110 throw "invalid apiAssertTypes call"
111 for (const [name, val] of Object.entries(params))
112 if (!types.split('_').some(t => t === 'array' ? Array.isArray(val) : t === 'object' ? _.isPlainObject(val) : typeof val === t))
113 throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
114 }
115 }
116
117 export function createStreamLimiter(limit: number) {
118 let got = 0
119 return new Transform({
120 transform(chunk, enc, done) {
121 const left = limit - got
122 got += chunk.length
123 if (left > 0) {
124 this.push(chunk.length > left ? chunk.slice(0, left) : chunk)
125 if (got >= limit)
126 this.end()
127 }
128 done()
129 }
130 })
131 }
132
133 export function retrySync(cb: Callback, attempts=20, sleep=500) {
134 const sleepSyncBuffer = new Int32Array(new SharedArrayBuffer(4))
135 for (let retry = 0; ; retry++) {
136 try { return cb() }
137 catch (e: any) {
138 if (e?.code !== 'EBUSY' || retry >= attempts)
139 throw e
140 Atomics.wait(sleepSyncBuffer, 0, 0, sleep)
141 }
142 }
143 }
144