Skip to content

Commit

Permalink
Merge branch 'refs/heads/54'
Browse files Browse the repository at this point in the history
# Conflicts:
#	admin/src/HomePage.ts
#	admin/src/InstalledPlugins.ts
#	admin/src/VfsMenuBar.ts
#	central.json
#	dev-plugins.md
#	frontend/src/FilterBar.ts
#	frontend/src/fileMenu.ts
#	frontend/src/icons.ts
#	frontend/src/show.ts
#	package-lock.json
#	package.json
#	shared/dialogs.ts
#	shared/index.ts
#	src/api.vfs.ts
#	src/const.ts
#	src/cross.ts
#	src/github.ts
#	src/langs/hfs-lang-fi.json
#	src/nat.ts
#	src/perm.ts
#	src/upload.ts
#	tests/config.yaml
  • Loading branch information
rejetto committed Nov 9, 2024
2 parents 5e10ae8 + a1881aa commit efe838f
Show file tree
Hide file tree
Showing 123 changed files with 2,356 additions and 1,390 deletions.
73 changes: 7 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).

## Installation

For service installation instructions, [see our wiki](https://github.com/rejetto/hfs/wiki/Service-installation).

For Docker installation, [see dedicated repo](https://github.com/damienzonly/hfs-docker).

NB: minimum Windows version required is 8.1 , Windows Server 2012 R2 (because of Node.js 18)

1. go to https://github.com/rejetto/hfs/releases
Expand Down Expand Up @@ -86,41 +90,6 @@ If this procedure fails, it may be that you are missing one of [these requiremen

Configuration and other files will be stored in `%HOME%/.vfs`

### Service

If you want to run HFS at boot (as a service), we suggest the following methods

#### On Linux
1. [install node.js](https://nodejs.org)
2. create a file `/etc/systemd/system/hfs.service` with this content
```
[Unit]
Description=HFS
After=network.target
[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/npx -y hfs@latest
[Install]
WantedBy=multi-user.target
```
3. run `sudo systemctl daemon-reload && sudo systemctl enable hfs && sudo systemctl start hfs && sudo systemctl status hfs`

NB: update will be attempted at each restart

#### On Windows

1. [install node.js](https://nodejs.org)
2. run `npm -g i hfs`
3. run `npx qckwinsvc2 install name="HFS" description="HFS" path="%APPDATA%\npm\node_modules\hfs\src\index.js" args="--cwd %HOMEPATH%\.hfs" now`

To update
- run `npx qckwinsvc2 uninstall name="HFS"`
- run `npm -g update hfs`
- run `npx qckwinsvc2 install name="HFS" description="HFS" path="%APPDATA%\npm\node_modules\hfs\src\index.js" args="--cwd %HOMEPATH%\.hfs" now`

## Console commands

If you have full access to HFS' console, you can enter commands. Start with `help` to have a full list.
Expand Down Expand Up @@ -150,43 +119,17 @@ In the Languages section of the Admin-panel you can install additional language

If your language is missing, please consider [translating yourself](https://github.com/rejetto/hfs/wiki/Translation).

## Why you should upgrade from HFS 2.x

HFS 2.x is vulnerable to important attacks, and there is no known solution at the moment.

As you can see from the list of features, we already have some goods that you cannot find in HFS 2.
Other than that, you can also consider:

- it's more robust: it was designed to be an always-running server, while HFS 1-2 was designed for occasional usage (transfer and quit)
- passwords are never really stored, just a non-reversible hash is
- faster search (up to 12x)
- more flexible permissions

## Security

While this project focuses on ease of use, we care about security.
- HTTPS support
- Passwords are not saved, and not disclosed even without https thanks to [SRP](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)
- Automated tests ran on every release, including libraries audit
- No default admin password

Some actions you can take for improved security:
- use https, better if using a proper certificate, even free with [Letsencrypt](https://letsencrypt.org/).
- have a domain (ddns is ok too), configure it in "Internet" page, and enable "Accept requests only using domain"
- install "antidos" plugin
- ensure "antibrute" plugin is running
- disable "unprotected admin on localhost"

## Hidden features

- Appending `#LOGIN` to address will bring up the login dialog
- Appending ?lang=CODE to address will force a specific language
- right/ctrl/command click on toggle-all checkbox will invert each checkbox state
- Right-click on toggle-all checkbox will invert each checkbox state
- Appending `?login=USER:PASSWORD` will automatically log in the browser
- Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
- Appending `?search=PATTERN` will trigger search at start
- Right-click on "check for updates" will let you input a URL of a version to install
- shift+click on a file will show & play
- Shift+click on a file will show & play
- Type the name of a file/folder to focus it, and ctrl+backspace to go to parent folder

## Contribute

Expand Down Expand Up @@ -227,6 +170,4 @@ There are several ways to contribute

- [License](https://github.com/rejetto/hfs/blob/master/LICENSE.txt)

- [To-do list](todo.md)

- Flag images are public-domain, downloaded from https://flagpedia.net
3 changes: 3 additions & 0 deletions admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
<title>HFS Admin-panel</title>
<script type="module" src="/src/index.ts"></script>
<link rel="icon" type="image/svg+xml" href="/hfs-logo-icon.svg" />
<link href="../frontend/fontello.css" rel="stylesheet" />
<link href="../frontend/fontello.woff2" rel="prefetch" />
</head>
<body>
<style class="removeAtBoot">@media (prefers-color-scheme: dark) { html { background-color: #000; color: #888; } }</style>
<div id="root"></div>
<script nomodule>document.getElementById('root').innerText = "Please use a newer browser"</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@types/react-dom": "^18.2.18",
"@types/react-window": "^1.8.8",
"@types/react-virtualized-auto-sizer": "^1.0.4",
"vite": "^5.0.12"
"vite": "^5.4.8"
},
"eslintConfig": {
"extends": [
Expand Down
10 changes: 7 additions & 3 deletions admin/src/AccountForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Alert } from '@mui/material'
import { apiCall } from './api'
import { alertDialog, useDialogBarColors } from './dialog'
import { formatTimestamp, isEqualLax, prefix, useIsMobile, wantArray } from './misc'
import { IconBtn, modifiedProps } from './mui'
import { IconBtn, propsForModifiedValues } from './mui'
import { Account } from './AccountsPage'
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
import { AutoDelete, Delete } from '@mui/icons-material'
Expand Down Expand Up @@ -70,7 +70,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
{ k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, xl: 8,
helperText: "To access THIS interface you are using right now",
...!account.admin && account.adminActualAccess && { value: true, helperText: "This permission is inherited" },
...!account.admin && account.adminActualAccess && { value: true, disabled: true, helperText: "This permission is inherited. To disable it, act on the groups." },
},
{ k: 'disable_password_change', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Allow password change", xs: 'auto' },
group && h(Alert, { severity: 'info' }, `To add users to this group, select the user and then click "Inherit"`),
Expand All @@ -88,7 +88,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
],
onError: alertDialog,
save: {
...modifiedProps( !isEqualLax(values, account)),
...propsForModifiedValues(isModifiedConfig(values, account)),
async onClick() {
const { password='', password2, adminActualAccess, hasPassword, invalidated, ...withoutPassword } = values
if (add) {
Expand Down Expand Up @@ -116,6 +116,10 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
})
}

export function isModifiedConfig(a: any, b: any) {
return !isEqualLax(a, b, (a,b) => !a && !b || undefined)
}

// you can set password directly in add/set_account, but using this api instead will add extra security because it is not sent as clear-text, so it's especially good if you are not in localhost and not using https
export async function apiNewPassword(username: string, password: string) {
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
Expand Down
22 changes: 11 additions & 11 deletions admin/src/AccountsPage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <[email protected]> - License https://www.gnu.org/licenses/gpl-3.0.txt

import { createElement as h, useState, useEffect, Fragment } from "react"
import { createElement as h, useState, useEffect, Fragment, useMemo } from "react"
import { apiCall, useApiEx } from './api'
import { Alert, Box, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
import { Close, Delete, DoNotDisturb, Group, MilitaryTech, Person, PersonAdd, Schedule } from '@mui/icons-material'
Expand All @@ -13,20 +13,20 @@ import _ from 'lodash'
import { alertDialog, confirmDialog, toast } from './dialog'
import { useSnapState } from './state'
import { importAccountsCsv } from './importAccountsCsv'
import { AccountAdminSend } from '../../src/api.accounts'
import apiAccounts from '../../src/api.accounts'

export type Account = AccountAdminSend
export type Account = ReturnType<typeof apiAccounts.get_accounts>['list'][0]

export default function AccountsPage() {
const { username } = useSnapState()
const { data, reload, element } = useApiEx('get_accounts')
const { data, reload, element } = useApiEx<typeof apiAccounts.get_accounts>('get_accounts')
const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
const selectionMode = Array.isArray(sel)
useEffect(() => { // if accounts are reloaded, review the selection to remove elements that don't exist anymore
if (Array.isArray(data?.list) && selectionMode)
setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
setSel( sel.filter(u => data!.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
}, [data]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
const list: Account[] | undefined = data?.list
const list = useMemo(() => data && _.sortBy(data.list, [x => !x.adminActualAccess, 'username']), [data])
const selectedAccount = selectionMode && _.find(list, { username: sel[0] })
const sideBreakpoint = 'md'
const isSideBreakpoint = useBreakpoint(sideBreakpoint)
Expand Down Expand Up @@ -109,7 +109,7 @@ export default function AccountsPage() {
setSel(ids)
}
},
list?.map((ac: Account) =>
list?.map(ac =>
h(TreeItem, {
key: ac.username,
nodeId: ac.username,
Expand Down Expand Up @@ -155,8 +155,8 @@ export default function AccountsPage() {
if (errors.length)
return alertDialog("Following elements couldn't be deleted: " + errors.join(', '), 'error')
}
}

export function account2icon(ac: Account, props={}) {
return h(ac.hasPassword ? Person : Group, props)
}
function account2icon(ac: Account, props={}) {
return h(ac.hasPassword ? Person : Group, props)
}
}
4 changes: 3 additions & 1 deletion admin/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ function App() {
function ApplyTheme(props:any) {
return h(Box, {
sx: { bgcolor: 'background.default', color: 'text.primary', flex: 1,
maxWidth: '100%' /*avoid horizontal overflow (eg: customHtml with long line) */ },
transition: 'background-color .4s',
maxWidth: '100%' /*avoid horizontal overflow (eg: customHtml with long line) */
},
...props
})
}
Expand Down
47 changes: 33 additions & 14 deletions admin/src/ArrayField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,38 @@ import { createElement as h, Fragment, useMemo, useState } from 'react'
import { Dict, isOrderedEqual, setHidden, swap } from './misc'
import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
import { formDialog } from './dialog'
import { DataGrid, GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
import { BoolField, FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
import { Box, FormHelperText, FormLabel } from '@mui/material'
import { DateTimeField } from './DateTimeField'
import _ from 'lodash'
import { Center, IconBtn } from './mui'
import { DataTable } from './DataTable'

type ArrayFieldProps<T> = FieldProps<T[]> & { fields: FieldDescriptor[], height?: number, reorder?: boolean, prepend?: boolean }
export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, ...rest }: ArrayFieldProps<T>) {
const rows = useMemo(() => (value||[]).map((x,$idx) =>
type ArrayFieldProps<T> = FieldProps<T[]> & { fields: FieldDescriptor[], height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean }
export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, ...rest }: ArrayFieldProps<T>) {
if (!Array.isArray(value)) // avoid crash if non-array values are passed, especially developing plugins
value = []
const rows = useMemo(() => value!.map((x,$idx) =>
setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
[JSON.stringify(value)]) //eslint-disable-line
const form = {
fields: fields.map(({ $width, $column, $type, ...rest }) => _.defaults(rest, byType[$type]?.field))
fields: fields.map(({ $width, $column, $type, $hideUnder, ...rest }) => _.defaults(rest, byType[$type]?.field))
}
setApi?.({ isEqual: isOrderedEqual }) // don't rely on stringify, as it wouldn't work with non-json values
const [undo, setUndo] = useState<typeof value>()
return h(Fragment, {},
label && h(FormLabel, { sx: { ml: 1 } }, label),
helperText && h(FormHelperText, {}, helperText),
h(Box, { ...rest },
h(DataGrid, {
h(DataTable, {
rows,
sx: { '.MuiDataGrid-virtualScroller': { minHeight: '3em' } },
...autoRowHeight && { getRowHeight: () => 'auto' as const },
sx: {
'.MuiDataGrid-virtualScroller': { minHeight: '3em' },
...autoRowHeight && { '.MuiDataGrid-cell': { minHeight: '52px !important' } }
},
style: undefined, // override style making it fill the flex
hideFooterSelectedRowCount: true,
hideFooter: true,
slots: {
Expand All @@ -42,7 +50,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
columns: [
...fields.map(f => {
const def = byType[f.$type]?.column
return ({
return {
field: f.k,
headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
disableColumnMenu: true,
Expand All @@ -51,8 +59,9 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
},
...def,
...f.$width ? { [f.$width >= 8 ? 'width' : 'flex']: f.$width } : (!def?.width && !def?.flex && { flex: 1 }),
hideUnder: f.$hideUnder,
...f.$column,
})
}
}),
{
field: '',
Expand Down Expand Up @@ -91,30 +100,40 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
icon: h(Edit),
label: title,
title,
onClick(event: MouseEvent) {
onClick(ev: MouseEvent) {
ev.stopPropagation()
formDialog<T>({ values: row as any, form, title }).then(x => {
if (x)
set(value!.map((oldRec, i) => i === $idx ? x : oldRec), event)
set(value!.map((oldRec, i) => i === $idx ? x : oldRec), ev)
})
}
}),
h(GridActionsCellItem as any, {
icon: h(Delete),
label: "Delete",
showInMenu: reorder,
onClick: ev => set(value!.filter((rec, i) => i !== $idx), ev),
onClick: ev => {
ev.stopPropagation()
set(value!.filter((rec, i) => i !== $idx), ev)
},
}),
reorder && $idx && h(GridActionsCellItem as any, {
icon: h(ArrowUpward),
label: "Move up",
showInMenu: true,
onClick: ev => set(swap(value!.slice(), $idx, $idx - 1), ev),
onClick: ev => {
ev.stopPropagation()
set(swap(value!.slice(), $idx, $idx - 1), ev)
},
}),
reorder && $idx < rows.length - 1 && h(GridActionsCellItem as any, {
icon: h(ArrowDownward),
label: "Move down",
showInMenu: true,
onClick: ev => set(swap(value!.slice(), $idx, $idx + 1), ev),
onClick: ev => {
ev.stopPropagation()
set(swap(value!.slice(), $idx, $idx + 1), ev)
},
}),
].filter(Boolean)
}
Expand Down
2 changes: 1 addition & 1 deletion admin/src/ConfigFilePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function ConfigFilePage() {

function copy() {
if (!text) return
navigator.clipboard.writeText(text.replace(/^\s*(\w*password|srp):.+\n/gm, ''))
navigator.clipboard.writeText(text.replace(/^(\s*(\w*password\w*|srp):\s*).+\n/gm, '$1: removed\n'))
toast("copied")
}
}
4 changes: 2 additions & 2 deletions admin/src/ConfigForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Form, FormProps } from '@hfs/mui-grid-form'
import { apiCall, useApiEx } from './api'
import { createElement as h, useEffect, useState, Dispatch } from 'react'
import _ from 'lodash'
import { IconBtn, modifiedProps } from './mui'
import { IconBtn, propsForModifiedValues } from './mui'
import { RestartAlt } from '@mui/icons-material'
import { Callback, onlyTruthy } from '../../src/cross'

Expand Down Expand Up @@ -35,7 +35,7 @@ export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }:
},
save: saveOnChange ? false : {
onClick: save,
...modifiedProps(modified),
...propsForModifiedValues(modified),
},
...formProps,
...rest,
Expand Down
Loading

0 comments on commit efe838f

Please sign in to comment.