use vfs.forbid to forbid listing
Massimo Melina committed
Jan 22, 2022 at 18:46 UTC
c9db79d6d6f12d1785fb466c70aadf396b48bbe0
9 files changed
+32
-28
README.md
+2
-1
@@ -91,8 +91,9 @@ Valid keys in a node are:
91
- `children`: just for folders, specify its virtual children.
92
Value is a list and its entries are nodes.
93
- `hidden`: this must not be listed, but it's still downloadable.
94
+- `forbid`: set `true` to forbid listing for this folder
95
- `hide`: similar to hidden, but it's from the parent node point of view.
95
- Use this to hide entries that are read from the source, not listed in the VFS.
96
+ Use this to hide children read from the source, not listed in the VFS.
97
Value can be just a file name, a mask, or a list of names/masks.
98
- `rename`: similar to name, but it's from the parent node point.
99
Use this to change the name of entries that are read from the source, not listed in the VFS.
src/api.auth.ts
+4
-4
@@ -106,9 +106,9 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
106
export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
107
if (!newPassword) // clear text version
108
return Error('missing parameters')
109
- if (!ctx.account)
109
+ if (!ctx.state.account)
110
return new ApiError(401)
111
- await updateAccount(ctx.account, account => {
111
+ await updateAccount(ctx.state.account, account => {
112
account.password = newPassword
113
})
114
return {}
@@ -119,9 +119,9 @@ export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
119
return new ApiError(406)
120
if (!salt || !verifier)
121
return Error('missing parameters')
122
- if (!ctx.account)
122
+ if (!ctx.state.account)
123
return new ApiError(401)
124
- await updateAccount(ctx.account, account => {
124
+ await updateAccount(ctx.state.account, account => {
125
saveSrpInfo(account, salt, verifier)
126
delete account.hashed_password // remove leftovers
127
})
src/api.file_list.ts
+3
@@ -5,11 +5,14 @@ import { ApiError, ApiHandler } from './apis'
5
import { stat } from 'fs/promises'
6
import { mapPlugins } from './plugins'
7
import { pattern2filter } from './misc'
8
+import { FORBIDDEN } from './const'
9
10
export const file_list:ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
11
let node = await vfs.urlToNode(path || '/', ctx)
12
if (!node)
13
return
14
+ if (node.forbid)
15
+ return new ApiError(FORBIDDEN)
16
if (search?.includes('..'))
17
return new ApiError(400)
18
if (node.default)
src/const.ts
+1
@@ -14,3 +14,4 @@ export const argv = minimist(process.argv.slice(2))
14
15
export const METHOD_NOT_ALLOWED = 405
16
export const NO_CONTENT = 204
17
+export const FORBIDDEN = 403
src/index.ts
+6
-6
@@ -7,21 +7,21 @@ import { frontEndApis } from './frontEndApis'
7
import { log } from './log'
8
import { pluginsMiddleware } from './plugins'
9
import { throttler } from './throttler'
10
-import { getAccount, getCurrentUsername } from './perm'
10
+import { getAccount, getCurrentUsername, getCurrentUsernameExpanded } from './perm'
11
import { headRequests, gzipper, sessions, frontendAndSharedFiles } from './middlewares'
12
import './listen'
13
14
export const BUILD_TIMESTAMP = "-"
15
export const SESSION_DURATION = 30*60_000
16
17
-console.log('started', new Date().toLocaleString())
18
-console.log('build', BUILD_TIMESTAMP, DEV)
17
+console.log('started', new Date().toLocaleString(), 'build', BUILD_TIMESTAMP, DEV)
18
console.debug('cwd', process.cwd())
20
-export const app = new Koa()
21
-app.keys = ['hfs-keys-test']
19
+export const app = new Koa({ keys: ['hfs-keys-test'] })
20
+
21
app.use(sessions(app))
22
app.use(async (ctx, next) => {
24
- ctx.account = getAccount(getCurrentUsername(ctx))
23
+ ctx.state.usernames = getCurrentUsernameExpanded(ctx) // accounts chained via .belongs for permissions check
24
+ ctx.state.account = getAccount(getCurrentUsername(ctx))
25
await next()
26
})
27
app.use(headRequests)
src/middlewares.ts
+3
-1
@@ -3,7 +3,7 @@ import Koa from 'koa'
3
import session from 'koa-session'
4
import { BUILD_TIMESTAMP, SESSION_DURATION } from './index'
5
import Application from 'koa'
6
-import { FRONTEND_URI } from './const'
6
+import { FORBIDDEN, FRONTEND_URI } from './const'
7
import { vfs } from './vfs'
8
import { isDirectory } from './misc'
9
import { zipStreamFromFolder } from './zip'
@@ -59,6 +59,8 @@ export const frontendAndSharedFiles: Koa.Middleware = async (ctx, next) => {
59
if (!node)
60
return next()
61
const { source } = node
62
+ if (node.forbid)
63
+ return ctx.status = FORBIDDEN
64
if (!source || await isDirectory(source)) {
65
const { get } = ctx.query
66
if (get === 'zip')
src/perm.ts
+2
-2
@@ -10,7 +10,7 @@ import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
10
11
let path = ''
12
13
-interface Account {
13
+export interface Account {
14
username: string, // we'll have username in it, so we don't need to pass it separately
15
password?: string
16
hashed_password?: string
@@ -28,7 +28,7 @@ export function getCurrentUsername(ctx: Koa.Context): string {
28
}
29
30
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
31
-export async function getCurrentUsernameExpanded(ctx: Koa.Context) {
31
+export function getCurrentUsernameExpanded(ctx: Koa.Context) {
32
const who = getCurrentUsername(ctx)
33
if (!who)
34
return []
src/throttler.ts
+1
-1
@@ -16,7 +16,7 @@ export function throttler(): Koa.Middleware {
16
return async (ctx, next) => {
17
await next()
18
const { body } = ctx
19
- if (!body || !(body instanceof Readable) || ctx.account?.ignore_limits)
19
+ if (!body || !(body instanceof Readable) || ctx.state.account?.ignore_limits)
20
return
21
const ipGroup = getOrSet(ip2group, ctx.ip, ()=> {
22
const tg = new ThrottleGroup(Infinity, mainThrottleGroup)
src/vfs.ts
+10
-13
@@ -2,7 +2,6 @@ import fs from 'fs/promises'
2
import { basename } from 'path'
3
import { isMatch } from 'micromatch'
4
import { complySlashes, enforceFinal, isDirectory, prefix, wantArray } from './misc'
5
-import { getCurrentUsernameExpanded } from './perm'
5
import Koa from 'koa'
6
import glob from 'fast-glob'
7
import _ from 'lodash'
@@ -21,6 +20,7 @@ export interface VfsNode {
20
hide?: string | string[],
21
remove?: string | string[],
22
hidden?: boolean,
23
+ forbid?: boolean,
24
rename?: Record<string,string>,
25
perm?: Record<string, SinglePerm>,
26
default?: string,
@@ -41,16 +41,15 @@ export class Vfs {
41
}
42
43
async urlToNode(url: string, ctx: Koa.Context, root?: VfsNode) : Promise<VfsNode | undefined> {
44
- const users = await getCurrentUsernameExpanded(ctx)
44
let run = root || this.root
45
const rest = url.split('/').filter(Boolean).map(decodeURIComponent)
47
- if (forbidden(run, users)) return
46
+ if (!hasPermission(run, ctx)) return
47
while (rest.length) {
48
let piece = rest.shift() as string
49
const child = findChildByName(piece, run)
50
if (child) {
51
run = child
53
- if (forbidden(run, users)) return
52
+ if (!hasPermission(run, ctx)) return
53
continue
54
}
55
if (!run.source)
@@ -93,22 +92,20 @@ function findChildByName(name:string, node:VfsNode) {
92
return node?.children?.find(x => x.name === name)
93
}
94
96
-export function forbidden(node:VfsNode, users:string[]) {
95
+export function hasPermission(node:VfsNode, ctx: Koa.Context) {
96
const { perm } = node
98
- return perm && !users.some(u => perm[u])
97
+ return !perm || ctx.state.usernames.some((u:string) => perm[u])
98
}
99
101
-
100
export async function* walkNode(parent:VfsNode, ctx: Koa.Context, depth:number=0, prefixPath:string=''): AsyncIterableIterator<VfsNode> {
101
const { children, source } = parent
104
- ctx._who = ctx._who || await getCurrentUsernameExpanded(ctx) // cache value
102
if (children)
106
- for (const c of children) {
107
- if (c.hidden || forbidden(c, ctx._who))
103
+ for (const node of children) {
104
+ if (node.hidden || !hasPermission(node, ctx))
105
continue
109
- yield prefixPath ? { ...c, name: prefixPath+c.name } : c
110
- if (depth > 0 && c && (c.children || c.source && await isDirectory(c.source)))
111
- yield* walkNode(c, ctx, depth - 1, prefixPath+c.name+'/')
106
+ yield prefixPath ? { ...node, name: prefixPath+node.name } : node
107
+ if (depth > 0 && node && (node.children || node.source && await isDirectory(node.source)))
108
+ yield* walkNode(node, ctx, depth - 1, prefixPath+node.name+'/')
109
}
110
if (!source)
111
return