main
ts 132 lines 4.89 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 _ from 'lodash'
4 import { Connection, disconnect, getConnections } from './connections'
5 import { apiAssertTypes, isLocalHost, safeDecodeURIComponent, shortenAgent, wait, wantArray } from './misc'
6 import { ApiHandlers } from './apiMiddleware'
7 import Koa from 'koa'
8 import { totalGot, totalInSpeedKb, totalOutSpeedKb, totalSent } from './throttler'
9 import { getCurrentUsername } from './auth'
10 import { SendListReadable } from './SendList'
11 import { storedMap } from './persistence'
12 import { countUniqueBy } from './cross'
13
14 export default {
15
16 async disconnect({ ip, port, allButLocalhost }) {
17 apiAssertTypes({
18 string_undefined: { ip },
19 number_undefined: { port },
20 boolean_undefined: { allButLocalhost },
21 })
22 const match = allButLocalhost ? ((x: any) => !isLocalHost(x.ip))
23 : _.matches({ ip, port })
24 const found = getConnections().filter(c => match(getConnAddress(c)))
25 for (const c of found)
26 disconnect(c.socket, "manual disconnection")
27 return { result: found.length }
28 },
29
30 get_connections({}, ctx) {
31 const list = new SendListReadable({
32 diff: true,
33 addAtStart: getConnections().map(serializeConnection),
34 })
35 type Change = Partial<Omit<Connection,'ip'>>
36 list.props({ you: ctx.ip })
37 return list.events(ctx, {
38 connection(conn: Connection) {
39 list.add(serializeConnection(conn))
40 },
41 connectionClosed(conn: Connection) {
42 list.remove(getConnAddress(conn))
43 },
44 connectionNewIp(conn: Connection, oldIp: string, newIp: string) {
45 list.update(getConnAddress(conn, oldIp), { ip: newIp })
46 },
47 connectionUpdated(conn: Connection, change: Change) {
48 if (conn.socket.closed || _.isEmpty(change)) return
49 if (change.ctx) {
50 Object.assign(change, fromCtx(change.ctx))
51 change.ctx = undefined
52 }
53 list.update(getConnAddress(conn), change)
54 },
55 })
56 },
57
58 async *get_connection_stats() {
59 while (1) {
60 const connections = getConnections()
61 yield {
62 outSpeedKb: totalOutSpeedKb,
63 inSpeedKb: totalInSpeedKb,
64 sent_got: [totalSent.get(), totalGot.get(), totalGotSentResetTime.get()] as const,
65 connections: connections.length,
66 ips: countUniqueBy(connections, conn => conn.ip),
67 }
68 await wait(1000)
69 }
70 },
71
72 async clear_persistent({ k }) {
73 apiAssertTypes({ string_array: { k } })
74 totalGotSentResetTime.set(new Date)
75 for (const x of wantArray(k))
76 void storedMap.del(x)
77 },
78
79 } satisfies ApiHandlers
80
81 export function serializeConnection(conn: Connection) {
82 const { socket, started, secure } = conn
83 return {
84 ...getConnAddress(conn),
85 v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
86 // connection fields are request-scoped once transfer tracking starts; socket counters cover earlier snapshots
87 got: conn.got || socket.bytesRead,
88 sent: conn.sent || socket.bytesWritten,
89 outSpeedKb: conn.outSpeedKb,
90 inSpeedKb: conn.inSpeedKb,
91 country: conn.country,
92 started,
93 secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
94 ...fromCtx(conn.ctx),
95 }
96 }
97
98 function fromCtx(ctx?: Koa.Context) {
99 if (!ctx) return
100 return {
101 user: getCurrentUsername(ctx),
102 agent: shortenAgent(ctx.get('user-agent')),
103 ...inferOperation(ctx)
104 }
105 }
106
107 export function inferOperation(ctx: Koa.Context) {
108 const s = ctx.state // short alias
109 return {
110 archive: s.archive,
111 ...s.browsing ? { op: 'browsing', path: safeDecodeURIComponent(s.browsing) }
112 : s.uploadPath ? { op: 'upload', path: safeDecodeURIComponent(s.uploadPath) }
113 : {
114 op: !s.considerAsGui && (ctx.state.archive || ctx.state.vfsNode) ? 'download' : undefined,
115 path: safeDecodeURIComponent(ctx.originalUrl),
116 },
117 opProgress: _.isNumber(s.opProgress) ? _.round(s.opProgress, 3) : undefined,
118 opTotal: s.opTotal,
119 opOffset: s.opOffset,
120 }
121 }
122
123 function getConnAddress(conn: Connection, overrideIp?: string) {
124 return {
125 ip: overrideIp ?? conn.ip,
126 port: conn.socket.remotePort,
127 }
128 }
129
130 const totalGotSentResetTime = storedMap.singleSync('totalGotSentResetTime', new Date(0))
131 totalGotSentResetTime.ready().then(() => // because default value is not stored, and we need to init this value
132 totalGotSentResetTime.set(was => was.getTime() ? was : new Date) )