hfs 3.4.0-alpha1 → 3.4.0-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/admin/assets/AccountsPage-CoiPofEv.js +4 -0
  2. package/admin/assets/{ConfigFilePage-D5TZZyMR.js → ConfigFilePage-jUH2feqi.js} +1 -1
  3. package/admin/assets/{CustomHtmlPage-DV2-ufWZ.js → CustomHtmlPage-Pl_AJZrl.js} +1 -1
  4. package/admin/assets/{InternetPage-DT3wSQYQ.js → InternetPage-DIcAwhFk.js} +1 -1
  5. package/admin/assets/{LangPage-DbXCv7wR.js → LangPage-BwVY8QRD.js} +1 -1
  6. package/admin/assets/{LogsPage-DmAxUZpu.js → LogsPage-CcjS12cP.js} +1 -1
  7. package/admin/assets/{MonitorPage-D7ydOeq_.js → MonitorPage-DMVFXyMe.js} +1 -1
  8. package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
  9. package/admin/assets/{PluginsPage-BYzfwuvg.js → PluginsPage-CC2dINcb.js} +1 -1
  10. package/admin/assets/{icons-CfBG9vvB.js → icons-C_ux1zLA.js} +1 -1
  11. package/admin/assets/{index-CVCnYMoB.js → index-BSSQOMG_.js} +4 -4
  12. package/admin/assets/{routing-C4l8rnk_.js → routing-Be72yFmd.js} +1 -1
  13. package/admin/index.html +1 -1
  14. package/frontend/assets/{dist-legacy-CKczq_fx.js → dist-legacy-D7Dg5w3L.js} +1 -1
  15. package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
  16. package/frontend/assets/{lib-legacy-D2tyILlj.js → lib-legacy-BxFHpdER.js} +1 -1
  17. package/frontend/assets/{sha512-legacy-CFcDeiiP.js → sha512-legacy-C8xoQBnP.js} +1 -1
  18. package/frontend/index.html +1 -1
  19. package/npm-shrinkwrap.json +66 -66
  20. package/package.json +1 -1
  21. package/plugins/antibrute/plugin.js +72 -18
  22. package/src/admin-langs/embedded.js +2 -2
  23. package/src/admin-langs/hfs-admin-lang-ar.json +34 -26
  24. package/src/admin-langs/hfs-admin-lang-bg.json +35 -27
  25. package/src/admin-langs/hfs-admin-lang-de.json +37 -29
  26. package/src/admin-langs/hfs-admin-lang-el.json +38 -30
  27. package/src/admin-langs/hfs-admin-lang-en.json +8 -0
  28. package/src/admin-langs/hfs-admin-lang-es.json +37 -29
  29. package/src/admin-langs/hfs-admin-lang-fi.json +100 -92
  30. package/src/admin-langs/hfs-admin-lang-fr.json +39 -31
  31. package/src/admin-langs/hfs-admin-lang-hu.json +37 -29
  32. package/src/admin-langs/hfs-admin-lang-it.json +35 -27
  33. package/src/admin-langs/hfs-admin-lang-ja.json +36 -28
  34. package/src/admin-langs/hfs-admin-lang-ko.json +39 -31
  35. package/src/admin-langs/hfs-admin-lang-lt.json +35 -27
  36. package/src/admin-langs/hfs-admin-lang-ms.json +36 -28
  37. package/src/admin-langs/hfs-admin-lang-nl.json +36 -28
  38. package/src/admin-langs/hfs-admin-lang-pl.json +36 -28
  39. package/src/admin-langs/hfs-admin-lang-pt-br.json +36 -28
  40. package/src/admin-langs/hfs-admin-lang-ro.json +37 -29
  41. package/src/admin-langs/hfs-admin-lang-ru.json +35 -27
  42. package/src/admin-langs/hfs-admin-lang-sr-latn.json +35 -27
  43. package/src/admin-langs/hfs-admin-lang-sr.json +39 -31
  44. package/src/admin-langs/hfs-admin-lang-th.json +35 -27
  45. package/src/admin-langs/hfs-admin-lang-tr.json +38 -30
  46. package/src/admin-langs/hfs-admin-lang-uk.json +35 -27
  47. package/src/admin-langs/hfs-admin-lang-vi.json +35 -27
  48. package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
  49. package/src/admin-langs/hfs-admin-lang-zh.json +728 -720
  50. package/src/adminApis.js +2 -1
  51. package/src/adminLang.js +8 -7
  52. package/src/api.accounts.js +9 -4
  53. package/src/api.auth.js +11 -3
  54. package/src/api.vfs.js +3 -2
  55. package/src/auth.js +25 -14
  56. package/src/consoleLog.js +4 -0
  57. package/src/const.js +1 -1
  58. package/src/cross-const.js +1 -1
  59. package/src/cross.js +1 -1
  60. package/src/customHtml.js +3 -2
  61. package/src/deletePermission.js +120 -0
  62. package/src/events.js +6 -0
  63. package/src/index.js +3 -2
  64. package/src/lang.js +6 -1
  65. package/src/langs/embedded.js +2 -2
  66. package/src/langs/hfs-lang-ar.json +2 -1
  67. package/src/langs/hfs-lang-bg.json +2 -1
  68. package/src/langs/hfs-lang-de.json +2 -1
  69. package/src/langs/hfs-lang-el.json +2 -1
  70. package/src/langs/hfs-lang-en.json +2 -1
  71. package/src/langs/hfs-lang-es.json +2 -1
  72. package/src/langs/hfs-lang-fi.json +2 -1
  73. package/src/langs/hfs-lang-fr.json +2 -1
  74. package/src/langs/hfs-lang-hu.json +2 -1
  75. package/src/langs/hfs-lang-it.json +2 -1
  76. package/src/langs/hfs-lang-ja.json +2 -1
  77. package/src/langs/hfs-lang-ko.json +2 -1
  78. package/src/langs/hfs-lang-lt.json +2 -1
  79. package/src/langs/hfs-lang-ms.json +2 -1
  80. package/src/langs/hfs-lang-nl.json +2 -1
  81. package/src/langs/hfs-lang-pl.json +2 -1
  82. package/src/langs/hfs-lang-pt-br.json +2 -1
  83. package/src/langs/hfs-lang-ro.json +2 -1
  84. package/src/langs/hfs-lang-ru.json +2 -1
  85. package/src/langs/hfs-lang-sr-latn.json +2 -1
  86. package/src/langs/hfs-lang-sr.json +2 -1
  87. package/src/langs/hfs-lang-th.json +2 -1
  88. package/src/langs/hfs-lang-tr.json +2 -1
  89. package/src/langs/hfs-lang-uk.json +2 -1
  90. package/src/langs/hfs-lang-vi.json +2 -1
  91. package/src/langs/hfs-lang-zh-hant.json +189 -0
  92. package/src/langs/hfs-lang-zh.json +96 -98
  93. package/src/listen.js +8 -4
  94. package/src/middlewares.js +31 -28
  95. package/src/perm.js +26 -4
  96. package/src/plugins.js +2 -2
  97. package/src/serveFile.js +5 -9
  98. package/src/serveGuiAndSharedFiles.js +9 -8
  99. package/src/serveGuiFiles.js +2 -2
  100. package/src/sessionStamp.js +53 -0
  101. package/src/upload.js +1 -1
  102. package/src/uploadOwners.js +4 -2
  103. package/src/vfs.js +80 -33
  104. package/src/webdav.js +2 -2
  105. package/admin/assets/AccountsPage-C6J2hRcY.js +0 -4
  106. package/admin/assets/OptionsPage-DpN3ajRm.js +0 -3
  107. package/frontend/assets/index-legacy-ZYae30AY.js +0 -1
  108. package/src/admin-langs/hfs-admin-lang-zh-tw.json +0 -1141
  109. package/src/langs/hfs-lang-zh-tw.json +0 -187
@@ -1,191 +1,189 @@
1
1
  {
2
- "author": "yx3016 & MircoH0 & CMJNB & Cheuksing",
3
- "version": 1.5,
4
- "hfs_version": "0.57.5",
2
+ "author": "Hug3O",
3
+ "version": 1.2,
5
4
  "translate": {
6
5
  "Select": "选择",
7
- "n_files": "{n} 个文件",
8
- "n_folders": "{n} 个文件夹",
9
- "filter_count": "{n} 符合筛选条件",
10
- "select_count": "{n} 已选中",
11
- "filter_placeholder": "输入筛选内容,对以下列表进行筛选",
6
+ "n_files": "{n,plural,one{# 文件} other{# 文件}}",
7
+ "n_folders": "{n,plural,one{# 文件夹} other{# 文件夹}}",
8
+ "filter_count": "{n,plural, one{# 个已筛选} other{# 个已筛选}}",
9
+ "select_count": "{n,plural, one{# 个已选择} other{# 个已选择}}",
10
+ "filter_placeholder": "在此输入以筛选下方列表",
12
11
  "Select some files": "选择一些文件",
13
- "zip_checkboxes": "点击复选框选择文件,然后再次使用Zip进行打包!",
14
- "zip_tooltip_selected": "将所选元素下载为单个ZIP文件",
15
- "zip_tooltip_whole": "将整个列表(未过滤)下载为单个ZIP文件。若已选择部分元素,则仅下载这些元素。",
16
- "zip_confirm_search": "是否将此次搜索的所有结果下载为ZIP压缩包?",
17
- "zip_confirm_folder": "确认将整个文件夹下载为ZIP压缩包?",
18
- "select_tooltip": "选择功能适用于“Zip”和“删除”操作,也可用于筛选列表。",
19
- "delete_hint": "请先单击“选择”某个项目,方可执行删除操作。",
20
- "delete_confirm": "确认删除 {n} 项?",
21
- "delete_completed": "删除 {n} 项成功!",
22
- "delete_failed": "删除失败,{n,plural, other{# 项失败}}",
12
+ "zip_checkboxes": "使用复选框选择文件,然后可以再次使用 Zip",
13
+ "zip_tooltip_selected": "将选中的元素作为单个 zip 文件下载",
14
+ "zip_tooltip_whole": "将整个列表(未筛选)作为单个 zip 文件下载。如果你选择了一些元素,则只下载那些元素。",
15
+ "zip_confirm_search": "将此搜索的所有结果下载为 ZIP 压缩包?",
16
+ "zip_confirm_folder": "将整个文件夹打包为ZIP再进行下载?",
17
+ "select_tooltip": "选择适用于“Zip”和“删除”(如果可用),但你也可以筛选列表",
18
+ "delete_hint": "要删除,请先点击“选择”",
19
+ "delete_confirm": "删除 {n,plural, one{# 个项目} other{# 个项目}}?",
20
+ "delete_completed": "删除:已完成 {n} 个",
21
+ "delete_failed": ",{n,plural, one{# 个失败} other{# 个失败}}",
23
22
  "delete_select": "选择要删除的内容",
24
23
  "Delete": "删除",
25
24
  "Options": "选项",
26
25
  "Search": "搜索",
27
- "Zip": "压缩",
28
- "search_msg": "搜索当前文件夹及其子文件夹",
29
- "Searching": "搜索中...",
30
- "Searched": "搜索完成!",
31
- "Clear search": "清除搜索条件",
32
- "Interrupted": "中断",
33
- "stopped_before": "已停止",
34
- "empty_list": "列表为空",
35
- "filter_none": "无过滤条件",
36
- "Admin-panel": "管理面板",
26
+ "Zip": "Zip",
27
+ "search_msg": "搜索此文件夹及子文件夹",
28
+ "Searching": "正在搜索",
29
+ "Searched": "已搜索",
30
+ "Clear search": "清除搜索",
31
+ "Interrupted": "已中断",
32
+ "stopped_before": "在找到任何内容之前已停止",
33
+ "empty_list": "这里什么都没有",
34
+ "filter_none": "没有与此筛选条件匹配的内容",
35
+ "alpha_idx": "字母索引",
36
+ "alpha_idx_button": "AZ",
37
+ "Admin-panel": "后台管理",
37
38
  "Login": "登录",
38
39
  "Username": "用户名",
39
40
  "Password": "密码",
40
- "login_untrusted": "登录中止:服务器标识不可信!",
41
- "login_bad_credentials": "凭证信息有误",
42
- "confirm_url_login": "此链接正尝试以“{username}”身份登录",
43
- "login_bad_cookies": "Cookie错误 - 登录失败",
41
+ "login_untrusted": "登录已中止:无法信任服务器身份",
42
+ "login_bad_credentials": "凭据无效",
43
+ "confirm_url_login": "此链接正尝试以“{username}”身份登录你",
44
+ "login_bad_cookies": "Cookie 无法正常工作 - 登录失败",
44
45
  "User panel": "用户面板",
45
46
  "Change password": "更改密码",
46
47
  "enter_pass": "输入新密码",
47
- "enter_pass2": "再次输入密码",
48
- "pass2_mismatch": "两次输入的密码不一致,修改失败!",
49
- "password_changed": "密码修改成功",
50
- "Logout": "注销",
48
+ "enter_pass2": "再次输入相同的新密码",
49
+ "pass2_mismatch": "你输入的第二个密码与第一个不匹配。过程已中止。",
50
+ "password_changed": "密码已更改",
51
+ "Logout": "退出登录",
51
52
  "connection error": "连接错误",
52
53
  "Full timestamp:": "完整时间戳:",
53
54
  "Search was interrupted": "搜索已中断",
54
55
  "Stop list": "停止列表",
55
- "download_starting": "开始下载",
56
- "wrong_account": "用户 {u} 无访问权限,请更换用户",
57
- "no_upload_here": "无上传权限",
56
+ "download_starting": "你的下载现在应开始",
57
+ "wrong_account": "账户 {u} 没有访问权限,请尝试其他账户",
58
+ "no_upload_here": "当前文件夹没有上传权限",
58
59
  "Create folder": "创建文件夹",
59
60
  "Pick files": "选择文件",
60
61
  "Pick folder": "选择文件夹",
61
- "ready_to_upload": "{n,plural,other{# 个文件}},{size},准备上传",
62
+ "ready_to_upload": "{n,plural,one{# 个文件} other{# 个文件}},{size},准备上传",
62
63
  "Send": "发送",
63
64
  "Clear": "清除",
64
65
  "failed_upload": "无法上传 {name}",
65
- "confirm_resume": "是否重命名?",
66
66
  "file too large": "文件过大",
67
67
  "Enter folder name": "输入文件夹名称",
68
68
  "Successfully created": "创建成功",
69
- "enter_folder": "进入文件夹",
70
- "folder_exists": "已存在同名文件夹",
71
- "Sort by": "排序依据",
72
- "name": "文件名",
73
- "extension": "扩展名",
69
+ "enter_folder": "输入文件夹",
70
+ "folder_exists": "同名文件夹已存在",
71
+ "Sort by:": "排序:{by}",
72
+ "name": "文件名称",
73
+ "extension": "文件后缀",
74
74
  "size": "文件大小",
75
- "time": "文件时间",
76
- "Invert order": "倒序",
77
- "Folders first": "文件夹优先",
75
+ "time": "修改时间",
76
+ "Invert order": "倒序排列",
77
+ "Folders first": "文件夹先",
78
78
  "Numeric names": "数字名称",
79
79
  "theme:": "主题:",
80
80
  "auto": "自动",
81
- "light": "亮色",
82
- "dark": "暗色",
83
- "parent folder": "上级文件夹",
81
+ "light": "浅色",
82
+ "dark": "深色",
83
+ "parent folder": "父文件夹",
84
84
  "home": "主页",
85
85
  "Continue": "继续",
86
86
  "Confirm": "确认",
87
- "Don't": "否",
87
+ "Don't": "不要",
88
88
  "Warning": "警告",
89
89
  "Error": "错误",
90
90
  "Info": "信息",
91
91
  "Unauthorized": "未授权",
92
- "Forbidden": "禁止访问",
92
+ "Forbidden": "禁止",
93
93
  "Not found": "未找到",
94
94
  "Server error": "服务器错误",
95
95
  "Upload": "上传",
96
96
  "upload_concluded": "上传已结束:",
97
- "upload_finished": "上传{n} 完成 ({size})",
98
- "upload_errors": "上传失败{n} 次",
99
- "upload_file_rejected": "部分文件被服务器拒绝接收",
100
- "delete_unfinished_upload": "要移除未完成的上传吗?",
97
+ "upload_finished": "{n} 个已完成({size})",
98
+ "upload_errors": "{n} 个失败",
99
+ "upload_file_rejected": "某些文件未被接受",
100
+ "delete_unfinished_upload": "删除未完成的上传?",
101
101
  "File menu": "文件菜单",
102
102
  "Folder menu": "文件夹菜单",
103
103
  "Name": "名称",
104
104
  "file_open": "打开",
105
105
  "Download": "下载",
106
- "Missing permission": "权限不足",
106
+ "Missing permission": "缺少权限",
107
107
  "Reload": "重新加载",
108
108
  "Get list": "获取列表",
109
- "Skip existing files": "跳过已存在的文件",
109
+ "Skip existing files": "跳过现有文件",
110
110
  "Size": "大小",
111
- "Timestamp": "时间戳",
112
- "Show": "显示/播放",
111
+ "Timestamp": "修改时间",
112
+ "Show": "显示",
113
113
  "Loading failed": "加载失败",
114
+ "Download limit reached": "已达到下载数量上限",
115
+ "Retry": "重试",
114
116
  "Rename": "重命名",
115
117
  "Tiles mode:": "平铺模式:",
116
118
  "off": "关闭",
117
119
  "Operation successful": "操作成功",
118
120
  "Uploader": "上传者",
119
- "Download counter": "下载次数",
121
+ "Download counter": "下载计数器",
120
122
  "Switch zoom mode": "切换缩放模式",
121
123
  "Full screen": "全屏",
122
124
  "File Show help": "文件显示帮助",
123
- "showHelpMain": "您可以使用键盘执行以下操作:",
125
+ "showHelpMain": "你可以使用键盘执行一些操作:",
124
126
  "showHelp_←/→": "←/→",
125
127
  "showHelp_↑/↓": "↑/↓",
126
128
  "showHelp_space": "空格",
127
129
  "showHelp_←/→_body": "转到上一个/下一个文件",
128
- "showHelp_↑/↓_body": "滚动长图",
129
- "showHelp_space_body": "选择",
130
- "showHelp_D_body": "下载",
131
- "showHelp_Z_body": "缩放模式",
132
- "showHelp_F_body": "全屏",
133
- "Destination": "目标位置",
134
- "in_queue": "{n} 个在队列中",
135
- "enter_comment": "对此文件的评论",
136
- "Comment": "评论",
137
- "upload_dd_hint": "您可以将文件拖放到文件列表中进行上传",
130
+ "showHelp_↑/↓_body": "滚动长图片",
131
+ "Destination": "目标",
132
+ "in_queue": "队列中有 {n} 个",
133
+ "enter_comment": "为 {name} 添加注释",
134
+ "Comment": "注释",
135
+ "upload_dd_hint": "你可以通过将文件拖放到文件列表上来上传文件",
138
136
  "Upload not available": "上传不可用",
139
137
  "Cut": "剪切",
140
- "n_items": "{n,plural, other{# 个项目}}",
141
- "move_results": "{good} 个已移动,{bad} 个失败",
142
- "after_cut": "您的选择已成功添加到剪贴板,请前往目标文件夹粘贴。",
138
+ "n_items": "{n,plural, one{# 个项目} other{# 个项目}}",
139
+ "move_results": "{good} 个已移动{bad,plural, =0{} other{,# 个失败}}",
140
+ "after_cut": "你的选择现在已在剪贴板中。\n转到目标文件夹进行粘贴。",
143
141
  "Cancel clipboard": "取消剪贴板",
144
142
  "to_clipboard_source": "返回源文件夹",
145
143
  "Paste": "粘贴",
146
- "clipboard_list": "剪贴板文件:",
144
+ "clipboard_list": "剪贴板中的项目:",
147
145
  "Close": "关闭",
148
146
  "Folder": "文件夹",
149
147
  "Web page": "网页",
150
148
  "Link": "链接",
151
149
  "Auto-play": "自动播放",
152
- "autoplay_seconds": "图片间隔秒数",
150
+ "autoplay_seconds": "图片等待秒数",
153
151
  "Select all": "全选",
154
- "go_first": "跳转到第一个项目",
155
- "go_last": "跳转到最后一个项目",
152
+ "go_first": "转到第一项",
153
+ "go_last": "转到最后一项",
156
154
  "Shuffle": "随机播放",
157
155
  "Repeat": "重复",
158
- "showHelpListShortcut": "在文件列表中,按住 {key} 点击可快速显示",
156
+ "showHelpListShortcut": "在文件列表中,按住 {key} 点击可显示",
159
157
  "Invalid value": "无效值",
160
- "upload_interrupted": "{n} 中断",
158
+ "upload_interrupted": "{n} 个已中断",
161
159
  "Overwrite policy": "覆盖策略",
162
160
  "Rename to avoid overwriting": "重命名以避免覆盖",
163
- "Overwrite existing files": "覆盖已存在的文件",
161
+ "Overwrite existing files": "覆盖现有文件",
164
162
  "Menu": "菜单",
165
- "clipboard": "剪贴板 ({content})",
166
- "to_clipboard_source_tooltip": "前往剪贴板内容所在的文件夹",
163
+ "clipboard": "剪贴板({content})",
164
+ "to_clipboard_source_tooltip": "转到剪贴板内容所在的文件夹",
167
165
  "more_items": "还有 {n} 个项目",
168
- "Show details": "显示详细信息",
166
+ "Show details": "显示详情",
169
167
  "upload_conflict": "已存在",
170
168
  "Logged in": "已登录",
171
- "Logged out": "已注销",
169
+ "Logged out": "已退出登录",
172
170
  "Cancel": "取消",
173
- "allow_session_ip_change": "允许会话期间IP变更",
174
- "focus_hint": "您可以通过键盘输入快速搜索并选择列表中的项目。",
171
+ "allow_session_ip_change": "允许此会话期间 IP 变更",
172
+ "focus_hint": "通过在键盘上输入,你可以搜索并聚焦列表中的项目。",
175
173
  "copy_links": "复制链接",
176
- "Calculate": "计算",
174
+ "Calculate": "统计",
177
175
  "Creation": "创建时间",
178
- "creation": "创建",
179
- "required_change_password": "您需要更改密码",
176
+ "creation": "创建时间",
177
+ "required_change_password": "你需要更改密码",
180
178
  "Wildcards": "通配符",
181
- "Considering resume": "正在尝试恢复上传",
179
+ "Considering resume": "考虑恢复",
182
180
  "Yes": "是",
183
181
  "No": "否",
184
- "esc_again": "按 ESC 两次关闭",
182
+ "esc_again": "按两次 ESC 关闭",
185
183
  "insufficient storage": "存储空间不足",
186
- "Account disabled": "账户已禁用",
184
+ "Account disabled": "账户已停用",
187
185
  "upload_name": "使用新名称上传",
188
- "Uploading": "上传中",
189
- "proxy_413": "检查代理服务器上的此限制"
186
+ "Uploading": "正在上传",
187
+ "proxy_413": "请检查代理服务器上的此限制"
190
188
  }
191
189
  }
package/src/listen.js CHANGED
@@ -70,10 +70,16 @@ const consoleLog_1 = require("./consoleLog");
70
70
  const first_1 = require("./first");
71
71
  const fileAttr_1 = require("./fileAttr");
72
72
  const uploadOwners_1 = require("./uploadOwners");
73
+ const sessionStamp_1 = require("./sessionStamp");
73
74
  let httpSrv;
74
75
  let httpsSrv;
75
76
  // the update relaunch can keep a bridge process alive, so we proactively close listeners here to release ports before the next binary binds; do it before (5) the storage file is closed, because sockets write there
76
- (0, first_1.onProcessExit)(() => Promise.all([stopServer(httpSrv), stopServer(httpsSrv)]), 5);
77
+ (0, first_1.onProcessExit)(() => {
78
+ const stopped = Promise.all([stopServer(httpSrv), stopServer(httpsSrv)]);
79
+ for (const { socket } of (0, connections_1.getConnections)()) // closeAllConnections misses TCP sockets awaiting TLS and connections upgraded by plugins
80
+ socket.destroy();
81
+ return stopped;
82
+ }, 5);
77
83
  const openBrowserAtStart = (0, config_1.defineConfig)(misc_1.CFG.open_browser_at_start, true);
78
84
  exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x => /(?<=\/\/)[^\/]+/.exec(x)?.[0]); // compiled is host only
79
85
  async function getBaseUrlOrDefault() {
@@ -88,7 +94,7 @@ const commonServerOptions = {
88
94
  };
89
95
  // these are properties that can be assigned to the server object
90
96
  const commonServerAssign = { headersTimeout: 30_000, timeout: misc_1.MINUTE }; // 'headersTimeout' is not recognized by type lib, and 'timeout' is not effective when passed in parameters
91
- const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), fileAttr_1.fileAttrDb.isOpening(), uploadOwners_1.uploadOwners.isOpening(), events_1.default.once('app')]);
97
+ const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), sessionStamp_1.sessionStampsReady, fileAttr_1.fileAttrDb.isOpening(), uploadOwners_1.uploadOwners.isOpening(), events_1.default.once('app')]);
92
98
  const considerHttp = (0, misc_1.debounceAsync)(async () => {
93
99
  await readyToListen;
94
100
  void stopServer(httpSrv);
@@ -302,8 +308,6 @@ function stopServer(srv) {
302
308
  console.debug("Failed to stop server", String(err));
303
309
  resolve(err);
304
310
  });
305
- if (first_1.quitting)
306
- srv.closeAllConnections();
307
311
  });
308
312
  }
309
313
  function getCurrentPort(srv) {
@@ -4,26 +4,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.someSecurity = exports.cloudflareDetected = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
7
+ exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.blockFilter = exports.prepareConnection = exports.someSecurity = exports.cloudflareDetected = exports.gzipper = exports.sessionDuration = void 0;
8
8
  exports.getProxyDetected = getProxyDetected;
9
9
  exports.failAllowNet = failAllowNet;
10
10
  const koa_compress_1 = __importDefault(require("koa-compress"));
11
11
  const const_1 = require("./const");
12
12
  const misc_1 = require("./misc");
13
- const node_crypto_1 = require("node:crypto");
14
- const lang_1 = require("./lang");
15
- const i18n_1 = require("./i18n");
16
- const stream_1 = require("stream");
17
13
  const block_1 = require("./block");
18
14
  const perm_1 = require("./perm");
19
15
  const connections_1 = require("./connections");
20
16
  const auth_1 = require("./auth");
17
+ const sessionStamp_1 = require("./sessionStamp");
21
18
  const zlib_1 = require("zlib");
22
19
  const listen_1 = require("./listen");
20
+ const roots_1 = require("./roots");
21
+ const node_net_1 = require("node:net");
23
22
  const config_1 = require("./config");
24
23
  const koa_session_1 = __importDefault(require("koa-session"));
25
24
  const index_1 = require("./index");
26
25
  const events_1 = __importDefault(require("./events"));
26
+ const node_crypto_1 = require("node:crypto");
27
+ const lang_1 = require("./lang");
28
+ const i18n_1 = require("./i18n");
27
29
  const forceHttps = (0, config_1.defineConfig)(misc_1.CFG.force_https, true);
28
30
  (0, config_1.defineConfig)(misc_1.CFG.ignore_proxies, false);
29
31
  const allowAuthorizationHeader = (0, config_1.defineConfig)(misc_1.CFG.authorization_header, true);
@@ -38,26 +40,9 @@ exports.gzipper = (0, koa_compress_1.default)({
38
40
  return /text|javascript|style/i.test(type);
39
41
  },
40
42
  });
41
- const headRequests = async (ctx, next) => {
42
- const head = ctx.method === 'HEAD';
43
- if (head)
44
- ctx.method = 'GET'; // let other middlewares work, so we can collect the size at the end
45
- await next();
46
- if (!head || ctx.body === undefined)
47
- return;
48
- const { length, status } = ctx.response;
49
- if (ctx.body)
50
- ctx.body = stream_1.Readable.from(''); // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
51
- ctx.status = status;
52
- if (length)
53
- ctx.response.length = length;
54
- };
55
- exports.headRequests = headRequests;
56
43
  let proxyDetected;
57
44
  const someSecurity = (ctx, next) => {
58
45
  enforceSessionIp(ctx);
59
- if (!ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip))
60
- return;
61
46
  const decodedPath = (0, misc_1.try_)(() => decodeURI(ctx.path));
62
47
  if (!decodedPath || (0, misc_1.hasDirTraversal)(decodedPath))
63
48
  return;
@@ -111,6 +96,16 @@ async function sendPage(ctx, titleKey, content) {
111
96
  body{margin-top:30vh;text-align:center;font-family:sans-serif}a{margin:1em}
112
97
  </style></head><body><h1>${(0, misc_1.escapeHTML)(title)}</h1>${content(t)}</body></html>`;
113
98
  }
99
+ const prepareConnection = (ctx, next) => {
100
+ // normalize once so auth, filters and logging agree on the same client address
101
+ ctx.request.ip = (0, misc_1.normalizeIp)(ctx.ip);
102
+ ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
103
+ (0, connections_1.updateConnectionForCtx)(ctx);
104
+ return next();
105
+ };
106
+ exports.prepareConnection = prepareConnection;
107
+ const blockFilter = (ctx, next) => !ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip) || next();
108
+ exports.blockFilter = blockFilter;
114
109
  function enforceSessionIp(ctx) {
115
110
  const s = ctx.session;
116
111
  if (!s?.username || s[misc_1.ALLOW_SESSION_IP_CHANGE])
@@ -129,8 +124,7 @@ function getProxyDetected() {
129
124
  return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
130
125
  }
131
126
  const prepareState = async (ctx, next) => {
132
- // normalize once so auth, filters and logging agree on the same client address
133
- ctx.request.ip = (0, misc_1.normalizeIp)(ctx.ip);
127
+ ctx.state.getOrHead = ctx.method === 'GET' || ctx.method === 'HEAD';
134
128
  // invalidate before account resolution; someSecurity calls again to bind logins made below
135
129
  enforceSessionIp(ctx);
136
130
  // rootsMiddleware consults proxy-aware admin access before someSecurity runs
@@ -148,12 +142,13 @@ const prepareState = async (ctx, next) => {
148
142
  }
149
143
  const s = ctx.session;
150
144
  if (s?.username) {
151
- if (s.ts < auth_1.invalidateSessionBefore.get(s?.username))
145
+ const account = (0, perm_1.getAccount)(s.username, false);
146
+ if (!account || !s.stamp || s.stamp !== (0, sessionStamp_1.getSessionStamp)(s.username)) {
152
147
  delete s.username;
148
+ delete s.stamp;
149
+ }
153
150
  s.maxAge = exports.sessionDuration.compiled();
154
151
  }
155
- // calculate these once and for all
156
- ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
157
152
  // explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
158
153
  let via;
159
154
  const urlAccount = await urlLogin();
@@ -178,7 +173,7 @@ const prepareState = async (ctx, next) => {
178
173
  ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
179
174
  }
180
175
  ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
181
- (0, connections_1.updateConnectionForCtx)(ctx);
176
+ (0, connections_1.updateConnectionForCtx)(ctx); // publish the resolved account after the initial pre-auth connection update
182
177
  await next();
183
178
  async function urlLogin() {
184
179
  via = 'url';
@@ -232,6 +227,14 @@ const prepareState = async (ctx, next) => {
232
227
  catch { }
233
228
  }
234
229
  function autoLogin() {
230
+ const host = ctx.host.toLowerCase();
231
+ const hostname = (0, misc_1.normalizeHost)(host);
232
+ // a rebinding page uses the victim's real IP, but retains the attacker's hostname
233
+ if (hostname !== 'localhost' && !(0, node_net_1.isIP)(hostname)
234
+ // URL normalization matches browsers' default-port removal and internationalized domain names
235
+ && host !== (listen_1.baseUrl.get() && (0, misc_1.try_)(() => new URL(listen_1.baseUrl.get()).host.toLowerCase()))
236
+ && roots_1.roots.compiled()(host) === undefined)
237
+ return;
235
238
  via = 'net';
236
239
  // keep the mask direct so group inheritance cannot make identity depend on account order
237
240
  return Object.values(perm_1.accounts.get()).find(a => (0, perm_1.accountCanLogin)(a) && a.auto_login_net && (0, misc_1.netMatches)(ctx.ip, a.auto_login_net, true));
package/src/perm.js CHANGED
@@ -12,6 +12,7 @@ exports.getAccount = getAccount;
12
12
  exports.saveSrpInfo = saveSrpInfo;
13
13
  exports.createAdmin = createAdmin;
14
14
  exports.updateAccount = updateAccount;
15
+ exports.invalidateAccountSessions = invalidateAccountSessions;
15
16
  exports.normalizeUsername = normalizeUsername;
16
17
  exports.renameAccount = renameAccount;
17
18
  exports.addAccount = addAccount;
@@ -29,6 +30,7 @@ const config_1 = require("./config");
29
30
  const tssrp6a_1 = require("tssrp6a");
30
31
  const events_1 = __importDefault(require("./events"));
31
32
  const auth_1 = require("./auth");
33
+ const sessionStamp_1 = require("./sessionStamp");
32
34
  // provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
33
35
  function expandUsername(who) {
34
36
  return new Set(Array.from(walkActiveAccounts([who]), a => a.username));
@@ -69,6 +71,7 @@ async function createAdmin(password, username = 'admin') {
69
71
  }
70
72
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
71
73
  async function updateAccount(account, change) {
74
+ await sessionStamp_1.sessionStampsReady;
72
75
  const jsonWas = JSON.stringify(account);
73
76
  const { username: usernameWas } = account;
74
77
  if (typeof change === 'function')
@@ -111,8 +114,27 @@ async function updateAccount(account, change) {
111
114
  if (jsonWas !== JSON.stringify(account)) // this test will miss the 'username' field, because hidden, but renameAccount is already calling saveAccountsASAP
112
115
  saveAccountsAsap();
113
116
  }
114
- const saveAccountsAsap = config_1.saveConfigAsap;
117
+ function reconcileAccountSessions() {
118
+ (0, sessionStamp_1.syncSessionStamps)(lodash_1.default.mapKeys(exports.accounts.get(), (_account, username) => normalizeUsername(username)));
119
+ }
120
+ function saveAccountsAsap() {
121
+ // API callers need the updated stamp before preserving their own session in the response
122
+ reconcileAccountSessions();
123
+ (0, config_1.saveConfigAsap)();
124
+ }
125
+ function invalidateAccountSessions(account) {
126
+ const found = typeof account === 'string' ? getAccount(account) : account;
127
+ if (!found)
128
+ return false;
129
+ (0, sessionStamp_1.renewSessionStamp)(found.username);
130
+ return true;
131
+ }
115
132
  exports.accounts = (0, config_1.defineConfig)(misc_1.CFG.accounts, {});
133
+ exports.accounts.sub(async () => {
134
+ await sessionStamp_1.sessionStampsReady;
135
+ // do not debounce: removing and recreating a username must not be coalesced into one change
136
+ reconcileAccountSessions();
137
+ });
116
138
  exports.accounts.sub(lodash_1.default.debounce(obj => {
117
139
  // consider some validation here, in case of manual edit of the config
118
140
  lodash_1.default.each(obj, (rec, k) => {
@@ -190,14 +212,14 @@ function addAccount(username, props, updateExisting = false) {
190
212
  if (account && !updateExisting)
191
213
  return;
192
214
  account = (0, misc_1.setHidden)(account || {}, { username }); // hidden so that stringification won't include it
193
- Object.assign(account, lodash_1.default.pickBy(props, Boolean));
194
215
  exports.accounts.set(was => Object.assign(was, { [username]: account }));
195
- return updateAccount(account, account).then(() => account);
216
+ return updateAccount(account, lodash_1.default.pickBy(props, Boolean)).then(() => account);
196
217
  }
197
218
  function delAccount(username) {
198
219
  if (!getAccount(username))
199
220
  return false;
200
- exports.accounts.set(was => lodash_1.default.omit(was, normalizeUsername(username)));
221
+ username = normalizeUsername(username);
222
+ exports.accounts.set(was => lodash_1.default.omit(was, username));
201
223
  saveAccountsAsap();
202
224
  return true;
203
225
  }
package/src/plugins.js CHANGED
@@ -298,8 +298,8 @@ const pluginsMiddleware = async (ctx, next) => {
298
298
  const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
299
299
  const name = a.shift();
300
300
  if (plugins.has(name)) { // do it only if the plugin is loaded
301
- if (ctx.get('referer')?.endsWith('/'))
302
- ctx.state.considerAsGui = true;
301
+ // this server-owned route is GUI content; client headers must not grant the exemption
302
+ ctx.state.considerAsGui = true;
303
303
  await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
304
304
  }
305
305
  return;
package/src/serveFile.js CHANGED
@@ -31,10 +31,10 @@ const crypto_1 = require("crypto");
31
31
  const iconv_lite_1 = __importDefault(require("iconv-lite"));
32
32
  const upload_1 = require("./upload");
33
33
  const allowedReferer = (0, config_1.defineConfig)(misc_1.CFG.allowed_referer, '');
34
+ const requireUploadApprovalForActiveContent = (0, config_1.defineConfig)(misc_1.CFG.require_upload_approval_for_active_content, true);
34
35
  const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
35
36
  const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
36
37
  const maxDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
37
- const GUI_ASSET_MIME = /^(image\/|audio\/|video\/|font\/|text\/css$|(?:text|application)\/(?:javascript|ecmascript)$|application\/x-javascript$)/i;
38
38
  function toAsciiEquivalent(s) {
39
39
  return iconv_lite_1.default.encode(iconv_lite_1.default.decode(Buffer.from(s), 'utf-8'), 'ascii').toString().replaceAll('?', '');
40
40
  }
@@ -64,15 +64,11 @@ async function serveFileNode(ctx, node) {
64
64
  const download = 'dl' in ctx.query;
65
65
  disposition(ctx, name, download);
66
66
  if (!download && source && isActiveContentMime(effectiveMime)) {
67
- const upload = await (0, upload_1.getUploadMeta)(source);
68
- if ((0, upload_1.isUploading)(source) || upload && upload.approved !== true)
67
+ if ((0, upload_1.isUploading)(source)
68
+ || requireUploadApprovalForActiveContent.get() && (await (0, upload_1.getUploadMeta)(source))?.approved === false)
69
69
  return (0, errorPages_1.sendErrorPage)(ctx, const_1.HTTP_FORBIDDEN);
70
70
  }
71
- const fetchDest = ctx.get('sec-fetch-dest');
72
- ctx.state.considerAsGui ??= !download && ctx.get('referer')?.endsWith('/')
73
- && (fetchDest ? fetchDest !== 'document' && fetchDest !== 'empty' // modern clients
74
- // legacy clients often send Accept: */* for archive downloads, so the served mime is a safer signal than request headers here
75
- : GUI_ASSET_MIME.test(effectiveMime));
71
+ // shared files count even when embedded: client headers cannot grant exemptions from limits or logging
76
72
  await serveFile(ctx, source || '', effectiveMime);
77
73
  await enforceDownloadLimits(ctx);
78
74
  }
@@ -111,7 +107,7 @@ async function serveFile(ctx, filePath, mime, cached) {
111
107
  ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
112
108
  return;
113
109
  }
114
- if (ctx.method !== 'GET')
110
+ if (!ctx.state.getOrHead)
115
111
  return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
116
112
  try {
117
113
  const stats = cached?.stats || await (0, util_1.promisify)(fs_1.stat)(filePath); // using fs's function instead of fs/promises, because only the former is supported by pkg
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.serveSharedFiles = exports.guiFilesMiddleware = void 0;
7
+ const deletePermission_1 = require("./deletePermission");
7
8
  const path_1 = require("path");
8
9
  const vfs_1 = require("./vfs");
9
10
  const errorPages_1 = require("./errorPages");
@@ -95,7 +96,7 @@ const serveSharedFiles = async (ctx, next) => {
95
96
  }
96
97
  return;
97
98
  }
98
- if (/^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get() && ctx.method === 'GET') // originalUrl to not be subject to changes (vhosting plugin)
99
+ if (ctx.state.getOrHead && /^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
99
100
  return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
100
101
  let node = await (0, vfs_1.urlToNode)(path, ctx);
101
102
  if (!node)
@@ -106,9 +107,9 @@ const serveSharedFiles = async (ctx, next) => {
106
107
  const { source } = node;
107
108
  if (!source)
108
109
  return ctx.status = cross_const_1.HTTP_METHOD_NOT_ALLOWED;
109
- if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
110
- return;
111
110
  try {
111
+ if (!await (0, deletePermission_1.checkDeletePermission)(node, ctx))
112
+ return;
112
113
  if ((await events_1.default.emitAsync('deleting', { node, ctx }))?.isDefaultPrevented())
113
114
  return ctx.status = cross_const_1.HTTP_FAILED_DEPENDENCY;
114
115
  await (0, promises_2.rm)(source, { recursive: true });
@@ -122,11 +123,11 @@ const serveSharedFiles = async (ctx, next) => {
122
123
  return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
123
124
  }
124
125
  }
125
- if (path.endsWith('/') && !get) { // final slash needed on browsers to make resource urls working with html pages
126
- const found = await (0, vfs_1.getDefaultFile)(node, ctx);
127
- if (found && /\.html?/i.test((0, vfs_1.getNodeName)(node = found)))
128
- ctx.state.considerAsGui = true;
129
- }
126
+ if (path.endsWith('/') && !get) // final slash needed on browsers to make resource urls working with html pages
127
+ node = await (0, vfs_1.getDefaultFile)(node, ctx) || node;
128
+ // classify the resolved default so a file can override its folder's GUI status
129
+ if (node.gui_asset)
130
+ ctx.state.considerAsGui = true;
130
131
  if (get === 'icon')
131
132
  return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
132
133
  if (!(0, vfs_1.nodeIsFolder)(node))