admin/options: log user-agent
Massimo Melina committed
Dec 4, 2023 at 10:33 UTC
428dfbe729afb67f8364d0c16b130855b3254b03
6 files changed
+112
-64
admin/src/LogsPage.ts
+43
-3
@@ -1,10 +1,10 @@
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 { createElement as h, Fragment, useMemo, useState } from 'react';
4
-import { Box, Tab, Tabs } from '@mui/material'
4
+import { Box, Tab, Tabs, Tooltip } from '@mui/material'
5
import { API_URL, useApiList } from './api'
6
import { DataTable } from './DataTable'
7
-import { formatBytes, HTTP_UNAUTHORIZED, prefix, tryJson } from '@hfs/shared'
7
+import { Dict, formatBytes, HTTP_UNAUTHORIZED, prefix, shortenAgent, tryJson } from '@hfs/shared'
8
import { logLabels } from './OptionsPage'
9
import { Flex, typedKeys, useBreakpoint, usePauseButton, useToggleButton } from './misc';
10
import { GridColDef } from '@mui/x-data-grid'
@@ -43,7 +43,7 @@ function LogFile({ file, pause, showApi }: { file: string, pause?: boolean, show
43
const { extra } = x
44
x.notes = extra?.dl ? "fully downloaded"
45
: extra?.ul ? "uploaded " + formatBytes(extra.size)
46
- : x.status === HTTP_UNAUTHORIZED && x.uri.startsWith(API_URL + 'loginSrp') ? "login failed" + prefix(':\n', extra?.u)
46
+ : x.status === HTTP_UNAUTHORIZED && x.uri?.startsWith(API_URL + 'loginSrp') ? "login failed" + prefix(':\n', extra?.u)
47
: x.notes
48
return x
49
}
@@ -117,6 +117,14 @@ function LogFile({ file, pause, showApi }: { file: string, pause?: boolean, show
117
hideUnder: 'md',
118
valueFormatter: ({ value }) => formatBytes(value as number)
119
},
120
+ {
121
+ headerName: "Agent",
122
+ hideUnder: 'xl',
123
+ field: 'extra',
124
+ valueGetter: ({ value }) => value?.ua,
125
+ renderCell: ({ value }) =>
126
+ value && agentIcons(value),
127
+ },
128
{
129
field: 'notes',
130
headerName: "Notes",
@@ -148,3 +156,35 @@ function LogFile({ file, pause, showApi }: { file: string, pause?: boolean, show
156
]
157
})
158
}
159
+
160
+export function agentIcons(agent: string) {
161
+ const short = shortenAgent(agent)
162
+ const browserIcon = icon(short, {
163
+ Chrome: 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
164
+ Chromium: 'https://upload.wikimedia.org/wikipedia/commons/f/fe/Chromium_Material_Icon.svg',
165
+ Firefox: 'https://upload.wikimedia.org/wikipedia/commons/a/a0/Firefox_logo%2C_2019.svg',
166
+ Safari: 'https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg',
167
+ Edge: 'https://upload.wikimedia.org/wikipedia/commons/f/f6/Edge_Logo_2019.svg',
168
+ Opera: 'https://upload.wikimedia.org/wikipedia/commons/4/49/Opera_2015_icon.svg',
169
+ })
170
+ const os = _.findKey(OSS, re => re.test(agent))
171
+ const osIcon = os && {
172
+ linux: '🐧',
173
+ win: '⊞',
174
+ mac: '',
175
+ ios: '',
176
+ }[os]
177
+ return h(Tooltip, { title: agent, children: h('span', {}, browserIcon || short, ' ', osIcon) })
178
+
179
+ function icon(k: string, map: Dict<string>) {
180
+ const src = map[k]
181
+ return src && h('img', { src, style: { height: '1em', verticalAlign: 'text-bottom', marginRight: '.2em' } })
182
+ }
183
+}
184
+
185
+const OSS = {
186
+ mac: /Mac OS/,
187
+ ios: /iPhone OS/,
188
+ win: /Windows NT/,
189
+ linux: /Linux/,
190
+}
admin/src/MonitorPage.ts
+2
@@ -12,6 +12,7 @@ import { Field, SelectField } from '@hfs/mui-grid-form'
12
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
13
import { toast } from "./dialog"
14
import { ALL as COUNTRIES } from './countries'
15
+import { agentIcons } from './LogsPage'
16
17
export default function MonitorPage() {
18
return h(Fragment, {},
@@ -207,6 +208,7 @@ function Connections() {
208
field: 'agent',
209
headerName: "Agent",
210
hideUnder: 'lg',
211
+ renderCell: ({ value }) => agentIcons(value)
212
},
213
],
214
actionsProps: { hideUnder: 'sm' },
admin/src/OptionsPage.ts
+28
-24
@@ -95,16 +95,16 @@ export default function OptionsPage() {
95
return { sm: 6 }
96
},
97
fields: [
98
- { k: 'port', comp: ServerPort, sm: 4, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
99
- { k: 'https_port', comp: ServerPort, sm: 4, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
98
+ { k: 'port', comp: ServerPort, md: 4, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
99
+ { k: 'https_port', comp: ServerPort, md: 4, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
100
onChange(v: number) {
101
if (v >= 0 && !httpsEnabled && !values.cert)
102
suggestMakingCert().then()
103
return v
104
}
105
},
106
- status && { k: 'listen_interface', comp: SelectField, sm: 4, options: [{ label: "any", value: '' }, '127.0.0.1', '::1', ...status?.ips] },
107
- httpsEnabled && values.port >= 0 && { k: 'force_https', comp: BoolField, sm: 12, md: 4, label: "Force HTTPS",
106
+ status && { k: 'listen_interface', comp: SelectField, md: 4, options: [{ label: "any", value: '' }, '127.0.0.1', '::1', ...status?.ips] },
107
+ httpsEnabled && values.port >= 0 && { k: 'force_https', comp: BoolField, md: 4, label: "Force HTTPS",
108
helperText: "Not applied to localhost"
109
},
110
httpsEnabled && { k: 'cert', comp: FileField, md: 4, label: "HTTPS certificate file",
@@ -128,23 +128,7 @@ export default function OptionsPage() {
128
},
129
{ k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output", helperText: "Doesn't apply to localhost" },
130
{ k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output per-ip" },
131
- { k: 'log', label: logLabels.log, md: 3, helperText: "Requests are logged here" },
132
- { k: 'error_log', label: logLabels.error_log, md: 3, placeholder: "errors go to main log",
133
- helperText: "If you want errors in a different log"
134
- },
135
- { k: 'log_rotation', comp: SelectField, md: 3, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
136
- helperText: "To keep log-files smaller"
137
- },
138
- { k: 'dont_log_net', comp: NetmaskField, label: "Don't log address", md: 3, placeholder: "no exception",
139
- helperText: h(WildcardsSupported)
140
- },
141
- { k: 'log_gui', sm: 3, comp: BoolField, label: "Log interface loading" },
142
- { k: 'log_api', sm: 3, comp: BoolField, label: "Log API requests" },
143
- { k: 'proxies', comp: NumberField, min: 0, max: 9, sm: 6, label: "How many HTTP proxies between this server and users?",
144
- error: proxyWarning(values, status),
145
- helperText: "Wrong number will prevent detection of users' IP address"
146
- },
147
- { k: 'dont_overwrite_uploading', comp: BoolField, label: "Don't overwrite uploading",
131
+ { k: 'dont_overwrite_uploading', comp: BoolField, sm: 12, md: 6, label: "Don't overwrite uploading",
132
helperText: "Files will be numbered to avoid overwriting" },
133
{ k: 'delete_unfinished_uploads_after', comp: NumberField, md: 3, min : 0, unit: "seconds", placeholder: "Never",
134
helperText: "Leave empty to never delete" },
@@ -161,7 +145,11 @@ export default function OptionsPage() {
145
{ k: 'descript_ion', comp: BoolField, label: "Support file DESCRIPT.ION", helperText: "Old file format, used for comments" },
146
{ k: 'descript_ion_encoding', label: "Encoding of file DESCRIPT.ION", comp: SelectField, disabled: !values.descript_ion,
147
options: ['utf8',720,775,819,850,852,862,869,874,808, ..._.range(1250,1257),10029,20866,21866] },
164
- { k: 'mime', comp: ArrayField, label: false, reorder: true, prepend: true, sm: 12,
148
+ { k: 'proxies', comp: NumberField, min: 0, max: 9, label: "How many HTTP proxies between this server and users?",
149
+ error: proxyWarning(values, status),
150
+ helperText: "Wrong number will prevent detection of users' IP address"
151
+ },
152
+ { k: 'mime', comp: ArrayField, label: false, reorder: true, prepend: true, md: 6,
153
fields: [
154
{ k: 'k', label: "File mask", $width: 1, $column: {
155
renderCell: ({ value, id }: any) => h('code', {},
@@ -186,8 +174,24 @@ export default function OptionsPage() {
174
{ k: 'server_code', comp: TextEditorField, sm: 12, getError: v => try_(() => new Function(v) && null, e => e.message),
175
helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
176
},
189
- h(Divider, {}, "Front-end options", h(Box, { fontSize: 'small' }, "Following options affect only the front-end")),
190
- { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", sm: 6, md: 4,
177
+
178
+ h(Divider, {}, "Log", h(Box, { fontSize: 'small' })),
179
+ { k: 'log', label: logLabels.log, md: 3, helperText: "Requests are logged here" },
180
+ { k: 'error_log', label: logLabels.error_log, md: 3, placeholder: "errors go to main log",
181
+ helperText: "If you want errors in a different log"
182
+ },
183
+ { k: 'log_rotation', comp: SelectField, md: 3, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
184
+ helperText: "To keep log-files smaller"
185
+ },
186
+ { k: 'dont_log_net', comp: NetmaskField, label: "Don't log address", md: 3, placeholder: "no exception",
187
+ helperText: h(WildcardsSupported)
188
+ },
189
+ { k: 'log_gui', sm: 4, comp: BoolField, label: "Log interface loading", helperText: "Some requests are necessary to load the interface" },
190
+ { k: 'log_api', sm: 4, comp: BoolField, label: "Log API requests", helperText: "Requests for commands" },
191
+ { k: 'log_ua', sm: 4, comp: BoolField, label: "Log User-Agent", helperText: "Contains browser and possibly OS information" },
192
+
193
+ h(Divider, {}, "Front-end", h(Box, { fontSize: 'small' }, "Following options affect only the front-end")),
194
+ { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", md: 4,
195
options: { "by clicking on file name": true, "by dedicated button": false }
196
},
197
{ k: 'title', md: 8, helperText: "You can see this in the tab of your browser" },
src/api.monitor.ts
+2
-36
@@ -2,7 +2,7 @@
2
3
import _ from 'lodash'
4
import { Connection, getConnections } from './connections'
5
-import { pendingPromise, typedEntries, wait } from './misc'
5
+import { pendingPromise, shortenAgent, typedEntries, wait } from './misc'
6
import { ApiHandlers, SendListReadable } from './apiMiddleware'
7
import Koa from 'koa'
8
import { totalGot, totalInSpeed, totalOutSpeed, totalSent } from './throttler'
@@ -86,7 +86,7 @@ const apis: ApiHandlers = {
86
|| (ctx.fileSource || ctx.state.archive) && decodeURIComponent(ctx.path) // downloads
87
return {
88
user: getCurrentUsername(ctx),
89
- agent: getBrowser(ctx.get('user-agent')),
89
+ agent: shortenAgent(ctx.get('user-agent')),
90
archive: ctx.state.archive,
91
upload: ctx.state.uploadProgress,
92
...path && { path },
@@ -120,37 +120,3 @@ function getConnAddress(conn: Connection) {
120
port: conn.socket.remotePort,
121
}
122
}
123
-
124
-function getBrowser(agent: string) {
125
- for (const [name,re] of Object.entries(BROWSERS))
126
- if (re.test(agent))
127
- return name
128
- return ''
129
-}
130
-const BROWSERS = {
131
- YaBrowser: /yabrowser/i,
132
- AlamoFire: /alamofire/i,
133
- Edge: /edge|edga|edgios|edg/i,
134
- PhantomJS: /phantomjs/i,
135
- Konqueror: /konqueror/i,
136
- Amaya: /amaya/i,
137
- Epiphany: /epiphany/i,
138
- SeaMonkey: /seamonkey/i,
139
- Flock: /flock/i,
140
- OmniWeb: /omniweb/i,
141
- Opera: /opera|OPR\//i,
142
- Chromium: /chromium/i,
143
- Facebook: /FBA[NV]/,
144
- Chrome: /chrome|crios/i,
145
- WinJs: /msapphost/i,
146
- IE: /msie|trident/i,
147
- Firefox: /firefox|fxios/i,
148
- Safari: /safari/i,
149
- PS5: /playstation 5/i,
150
- PS4: /playstation 4/i,
151
- PS3: /playstation 3/i,
152
- PSP: /playstation portable/i,
153
- PS: /playstation/i,
154
- Xbox: /xbox/i,
155
- UC: /UCBrowser/i,
156
-}
\ No newline at end of file
src/cross.ts
+33
@@ -393,4 +393,37 @@ export function runAt(ts: number, cb: Callback) {
393
cancel = true
394
clearTimeout(t)
395
}
396
+}
397
+
398
+export function shortenAgent(agent: string) {
399
+ return _.findKey(BROWSERS, re => re.test(agent))
400
+ || /^[^/(]+ ?/.exec(agent)?.[0]
401
+ || agent
402
+}
403
+const BROWSERS = {
404
+ YaBrowser: /yabrowser/i,
405
+ AlamoFire: /alamofire/i,
406
+ Edge: /edge|edga|edgios|edg/i,
407
+ PhantomJS: /phantomjs/i,
408
+ Konqueror: /konqueror/i,
409
+ Amaya: /amaya/i,
410
+ Epiphany: /epiphany/i,
411
+ SeaMonkey: /seamonkey/i,
412
+ Flock: /flock/i,
413
+ OmniWeb: /omniweb/i,
414
+ Opera: /opera|OPR\//i,
415
+ Chromium: /chromium/i,
416
+ Facebook: /FBA[NV]/,
417
+ Chrome: /chrome|crios/i,
418
+ WinJs: /msapphost/i,
419
+ IE: /msie|trident/i,
420
+ Firefox: /firefox|fxios/i,
421
+ Safari: /safari/i,
422
+ PS5: /playstation 5/i,
423
+ PS4: /playstation 4/i,
424
+ PS3: /playstation 3/i,
425
+ PSP: /playstation portable/i,
426
+ PS: /playstation/i,
427
+ Xbox: /xbox/i,
428
+ UC: /UCBrowser/i,
429
}
\ No newline at end of file
src/log.ts
+4
-1
@@ -61,6 +61,7 @@ errorLogFile.sub(path => {
61
62
const logRotation = defineConfig('log_rotation', 'weekly')
63
const dontLogNet = defineConfig('dont_log_net', '127.0.0.1|::1', v => makeNetMatcher(v))
64
+const logUA = defineConfig('log_ua', false)
65
66
const debounce = _.debounce(cb => cb(), 1000)
67
@@ -103,9 +104,11 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
104
const user = getCurrentUsername(ctx)
105
const length = ctx.state.length ?? ctx.length
106
const uri = ctx.originalUrl
106
- const extra = ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
107
+ let extra = ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
108
|| ctx.state.uploadPath && { ul: ctx.state.uploadPath, size: ctx.state.uploadSize }
109
|| ctx.state.logExtra
110
+ if (logUA.get())
111
+ extra = Object.assign({ ua: ctx.get('user-agent') }, extra)
112
events.emit(logger.name, Object.assign(_.pick(ctx, ['ip', 'method','status']), { length, user, ts: now, uri, extra }))
113
debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
114
stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries