Skip to content

Commit

Permalink
Merge branch 'refs/heads/53'
Browse files Browse the repository at this point in the history
# Conflicts:
#	admin/src/LogsPage.ts
#	admin/src/OptionsPage.ts
#	frontend/src/icons.ts
#	frontend/src/upload.ts
#	package-lock.json
#	package.json
#	shared/dialogs.ts
#	src/api.get_file_list.ts
#	src/api.monitor.ts
#	src/cross.ts
#	src/upload.ts
#	src/util-os.ts
#	tests/config.yaml
  • Loading branch information
rejetto committed Jul 19, 2024
2 parents 0fc82dc + 96de63b commit 7a34629
Show file tree
Hide file tree
Showing 145 changed files with 12,695 additions and 7,361 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ self.*
config.yaml*
custom.html
accounts.yaml
.DS_Store
.DS_Store
storage
*.kv
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

## Introduction

HFS is the best way via web to access or share files from your disk.
Access via web your files directly from your disk.

- You be the server, share files **fresh from your disk**, with **unlimited** space and bandwidth.
- It's all very **fast**. Try download zipping 100GB, it starts immediately!
- **Easy to use**. HFS tries to detect problems and suggest solutions.
- **Fast!** Try zipping 100GB, download starts immediately!
- **Intelligent**. HFS tries to detect problems and suggest solutions.
- Share **even a single file** with our *virtual file system*, even with a different name, all without touching the real file. Present things the way you want!
- **Watch** all activities in real-time.
- **Control bandwidth**, decide how much to give.
Expand All @@ -34,7 +34,7 @@ This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
- accounts
- resumable downloads & uploads
- download folders as zip archive
- remote delete
- delete, move and rename files
- simple website serving
- plug-ins
- real-time monitoring of connections
Expand All @@ -43,10 +43,13 @@ This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
- geographic firewall
- admin web interface
- multi-language front-end
- virtual hosting (plug-in)
- virtual hosting
- anti-brute-force (plug-in)
- [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
- comments in file descript.ion
- integrated media player
- [customizable with html, css and javascript](https://github.com/rejetto/hfs/wiki/Customization)
- dynamic-dns updater

## Installation

Expand Down Expand Up @@ -132,9 +135,7 @@ Configuration is stored in the file `config.yaml`, exception made for custom HTM

These files are kept in the Current Working Directory (cwd), which is by default the same folder of `hfs.exe`
if you are using this kind of distribution on Windows, or `USER_FOLDER/.hfs` on other systems.
You can decide a different cwd passing `--cwd SOME_FOLDER` parameter at command line.

You can decide also a different file for config by passing `--config SOME_FILE`, or inside an *env* called `HFS_CONFIG`.
You can decide a different folder passing `--cwd SOME_FOLDER` parameter at command line.
Any relative path provided is relative to the *cwd*.

[Check details about config file format](config.md).
Expand All @@ -149,7 +150,9 @@ 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 to 3
## 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:
Expand All @@ -159,24 +162,18 @@ Other than that, you can also consider:
- faster search (up to 12x)
- more flexible permissions

But you may still want to stay with HFS 2.x (so far) for the following reasons

- smaller
- more tested
- classic window interface (can be easier for some people)

## Security

While this project focuses on ease of use, we care about security.
- HTTPS support
- Passwords are not saved, and user password is safe even logging in without https thanks to [SRP](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)
- 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 rejetto/antidos plugin
- install "antidos" plugin
- ensure "antibrute" plugin is running
- disable "unprotected admin on localhost"

Expand Down
15 changes: 7 additions & 8 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
"@hfs/mui-grid-form": "*",
"@hfs/shared": "*",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.5",
"@mui/lab": "^5.0.0-alpha.161",
"@mui/material": "^5.15.10",
"@mui/x-data-grid": "^6.19.1",
"@mui/x-date-pickers": "^6.19.0",
"@mui/icons-material": "^5.15.15",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.15",
"@mui/x-data-grid": "^6.19.11",
"@mui/x-date-pickers": "^6.19.9",
"@mui/x-tree-view": "^6.17.0",
"@gregoranders/csv": "^0.0.13",
"dayjs": "^1.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"react-router-dom": "^6.23.1",
"react-simple-code-editor": "^0.13.1",
"react-window": "^1.8.10",
"tssrp6a": "^3.0.0",
Expand All @@ -37,8 +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-plugin-babel-import": "github:rejetto/vite-plugin-babel-import"
"vite": "^5.0.12"
},
"eslintConfig": {
"extends": [
Expand Down
13 changes: 5 additions & 8 deletions admin/src/AccountForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { createElement as h, ReactNode, useEffect, useRef, useState } from 'reac
import { BoolField, Form, MultiSelectField, NumberField } from '@hfs/mui-grid-form'
import { Alert } from '@mui/material'
import { apiCall } from './api'
import { alertDialog, toast, useDialogBarColors } from './dialog'
import { isEqualLax, useIsMobile, wantArray } from './misc'
import { alertDialog, useDialogBarColors } from './dialog'
import { formatTimestamp, isEqualLax, prefix, useIsMobile, wantArray } from './misc'
import { IconBtn, modifiedProps } from './mui'
import { Account } from './AccountsPage'
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
Expand All @@ -31,7 +31,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
const ref = useRef<HTMLFormElement>()
const expired = Boolean(values.expire)
return h(Form, {
formRef: ref,
formRef: ref,
values,
set(v, k) {
setValues(values => ({ ...values, [k]: v }))
Expand All @@ -42,15 +42,14 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
!add && h(IconBtn, {
icon: Delete,
title: "Delete",
confirm: "Delete?",
confirm: `Delete ${account.username}?`,
...username === account.username && { disabled: true, title: "Cannot delete current account" },
onClick: () => apiCall('del_account', { username: account.username }).then(reload)
}),
h(IconBtn, {
icon: AutoDelete,
title: "Invalidate past sessions",
title: `Invalidate past sessions ${prefix('(', formatTimestamp(account.invalidated || 0), ')')}`,
doneMessage: true,
disabled: account.invalidated,
onClick: () => apiCall('invalidate_sessions', { username: account.username }).then(reload)
}),
...wantArray(addToBar),
Expand Down Expand Up @@ -101,7 +100,6 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
throw e
}
done(got?.username)
toast("Account created", 'success')
return
}
const got = await apiCall('set_account', {
Expand All @@ -112,7 +110,6 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
await apiNewPassword(values.username, password)
if (account.username === username)
state.username = values.username
setTimeout(() => toast("Account modified", 'success'), 1) // workaround: showing a dialog at this point is causing a crash if we are in a dialog
done(got?.username) // username may have been changed, so we pass it back
}
}
Expand Down
9 changes: 5 additions & 4 deletions admin/src/AccountsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { TreeItem, TreeView } from '@mui/x-tree-view'
import MenuButton from './MenuButton'
import AccountForm from './AccountForm'
import _ from 'lodash'
import { alertDialog, confirmDialog } from './dialog'
import { alertDialog, confirmDialog, toast } from './dialog'
import { useSnapState } from './state'
import { importAccountsCsv } from './importAccountsCsv'
import { AccountAdminSend } from '../../src/api.accounts'
Expand Down Expand Up @@ -42,7 +42,7 @@ export default function AccountsPage() {
h(ListItem, { key: username },
h(ListItemText, {}, username))))
)
: with_(selectedAccount || { username: '', hasPassword: sel === 'new-user', adminActualAccess: false, invalidated: true }, a =>
: with_(selectedAccount || { username: '', hasPassword: sel === 'new-user', adminActualAccess: false, invalidated: undefined }, a =>
h(AccountForm, {
account: a,
groups: list.filter(x => !x.hasPassword).map( x => x.username ),
Expand All @@ -54,8 +54,9 @@ export default function AccountsPage() {
],
reload,
done(username) {
setSel([username])
setSel(isSideBreakpoint ? [username] : [])
reload()
toast("Account saved", 'success')
}
}))
useEffect(() => {
Expand All @@ -68,7 +69,7 @@ export default function AccountsPage() {
Content: () => sideContent,
onClose: selectNone,
})
return close
return () => void close()
}, [isSideBreakpoint, sel, selectedAccount])

return element || h(Grid, { container: true, maxWidth: '80em' },
Expand Down
3 changes: 2 additions & 1 deletion admin/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ function App() {

function ApplyTheme(props:any) {
return h(Box, {
sx: { bgcolor: 'background.default', color: 'text.primary', flex: 1, },
sx: { bgcolor: 'background.default', color: 'text.primary', flex: 1,
maxWidth: '100%' /*avoid horizontal overflow (eg: customHtml with long line) */ },
...props
})
}
Expand Down
11 changes: 7 additions & 4 deletions admin/src/ArrayField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { BoolField, FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-g
import { Box, FormHelperText, FormLabel } from '@mui/material'
import { DateTimeField } from './DateTimeField'
import _ from 'lodash'
import { IconBtn } from './mui'
import { Center, IconBtn } from './mui'

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, ...rest }: ArrayFieldProps<T>) {
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) =>
setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
[JSON.stringify(value)]) //eslint-disable-line
Expand All @@ -30,6 +30,9 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
sx: { '.MuiDataGrid-virtualScroller': { minHeight: '3em' } },
hideFooterSelectedRowCount: true,
hideFooter: true,
slots: {
noRowsOverlay: () => h(Center, {}, noRows || "No entries"),
},
slotProps: {
pagination: {
showFirstButton: true,
Expand Down Expand Up @@ -64,7 +67,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
title,
size: 'small',
onClick: ev =>
formDialog<T>({ form, title }).then(x => {
formDialog<T>({ form, title, values: valuesForAdd }).then(x => {
if (!x) return
const newValue = value?.slice() || []
if (prepend) newValue.unshift(x)
Expand Down Expand Up @@ -136,7 +139,7 @@ const byType: Dict<{ field?: Partial<FieldDescriptor>, column?: Partial<GridColD
dateTime: {
field: { comp: DateTimeField },
column: {
type: 'dateTime', minWidth: 90, flex: 0.5,
type: 'dateTime', minWidth: 96, flex: 0.5,
valueGetter: ({ value }) => value && new Date(value),
renderCell: ({ value }) => value && h(Box, {}, value.toLocaleDateString(), h('br'), value.toLocaleTimeString())
}
Expand Down
15 changes: 10 additions & 5 deletions admin/src/ConfigForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@ import { createElement as h, useEffect, useState, Dispatch } from 'react'
import _ from 'lodash'
import { IconBtn, modifiedProps } from './mui'
import { RestartAlt } from '@mui/icons-material'
import { Callback } from '../../src/cross'
import { Callback, onlyTruthy } from '../../src/cross'

type FormRest<T> = Omit<FormProps<T>, 'values' | 'set' | 'save'> & Partial<Pick<FormProps<T>, 'save'>>
export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }: Partial<FormRest<T>> & {
keys: (keyof T)[],
keys?: (keyof T)[],
form: FormRest<T> | ((values: T, optional: { setValues: Dispatch<T> }) => FormRest<T>),
onSave?: Callback,
saveOnChange?: boolean
}) {
const config = useApiEx('get_config', { only: keys })
const [keys_, setKeys_] = useState(keys)
const config = useApiEx(keys_ && 'get_config', { only: keys_ })
const [values, setValues] = useState<any>(config.data)
useEffect(() => setValues((v: any) => config.data || v), [config.data])
const modified = values && !_.isEqual(values, config.data)
useEffect(() => {
if (modified && saveOnChange) save()
}, [modified])
const formProps = _.isFunction(form) ? form(values, { setValues }) : form
useEffect(() => {
if (!keys) // autodetect keys
setKeys_(onlyTruthy(formProps.fields.map(x => (x as any)?.k)))
}, [keys])
if (!values)
return config.element
const formProps = _.isFunction(form) ? form(values, { setValues }) : form
return h(Form, {
values,
set(v, k) {
Expand All @@ -32,7 +37,7 @@ export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }:
onClick: save,
...modifiedProps(modified),
},
...Array.isArray(formProps) ? { fields: formProps } : formProps,
...formProps,
...rest,
barSx: { gap: 1, ...rest.barSx },
addToBar: [
Expand Down
Loading

0 comments on commit 7a34629

Please sign in to comment.