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