admin/options: "Outbound proxy" #812
Massimo Melina committed
Dec 25, 2024 at 23:53 UTC
988e40a613b4a44ad6c14d18ec34db24951f931a
7 files changed
+41
-12
admin/src/OptionsPage.ts
+5
-3
@@ -154,11 +154,13 @@ export default function OptionsPage() {
154
helperText: "Access Admin-panel without entering credentials"
155
},
156
157
- { k: 'proxies', comp: NumberField, max: 9, label: "Number of HTTP proxies", placeholder: "none",
157
+ { k: 'proxies', comp: NumberField, sm: 4, md: 4, max: 9, label: "Number of HTTP proxies", placeholder: "none",
158
error: proxyWarning(values, status),
159
- helperText: "Wrong number will prevent detection of users' IP address"
159
+ helperText: "Wrong number will prevent detection of users' IP"
160
},
161
- { k: 'allowed_referer', placeholder: "any", label: "Links from other websites", comp: AllowedReferer,
161
+ { k: 'outbound_proxy', sm: 5, md: 4, placeholder: "none", helperText: "URL form",
162
+ getError: x => try_(() => x && new URL(x) && '', () => "Invalid URL") },
163
+ { k: 'allowed_referer', comp: AllowedReferer, sm: 3, md: 4, placeholder: "any", label: "Links from other websites",
164
helperText: "In case another website is linking your files" },
165
166
{ k: 'block', label: false, comp: ArrayField, prepend: true, sm: true, autoRowHeight: true,
config.md
+2
-1
@@ -118,7 +118,8 @@ Configuration can be done in several ways
118
- `geo_allow_unknown` set false to disconnect connections for which country cannot be determined. Works only if `geo_allow` is set. Default is true.
119
- `dynamic_dns_url` URL to be requested to keep a domain updated with your latest IP address.
120
Optionally, you can append “>” followed by a regular expression to determine a successful answer, otherwise status code will be used.
121
- Multiple URLs are supported and you can specify one for each line.
121
+ Multiple URLs are supported and you can specify one for each line.
122
+- `outbound_proxy` if you need outgoing http(s) requests to pass through an http proxy. Default is none.
123
- `auto_basic` automatically detect (based on user-agent) when the basic web inteface should be served, to support legacy browsers. Default is true. No UI.
124
- `allow_session_ip_change` should requests of the same login session be allowed from different IP addresses. Default is false, to prevent cookie stealing. You can set it `true` to always allow it, or `https` to allow only on https, where stealing the cookie is harder. No UI.
125
- `authorization_header` support Authentication HTTP header. Default is true. No UI.
src/config.ts
+2
-1
@@ -4,7 +4,8 @@ import { ORIGINAL_CWD, VERSION, CONFIG_FILE } from './const'
4
import { watchLoad } from './watchLoad'
5
import yaml from 'yaml'
6
import _ from 'lodash'
7
-import { DAY, debounceAsync, newObj, throw_, tryJson, wait, with_ } from './misc'
7
+import { DAY, newObj, throw_, tryJson, wait, with_ } from './cross'
8
+import { debounceAsync } from './debounceAsync'
9
import { statSync } from 'fs'
10
import { join, resolve } from 'path'
11
import events from './events'
src/misc.ts
+1
-1
@@ -161,4 +161,4 @@ export async function deleteNode(ctx: Koa.Context, node: VfsNode, uri: string) {
161
} catch (e: any) {
162
return e
163
}
164
-}
\ No newline at end of file
164
+}
src/outboundProxy.ts
new
+15
@@ -0,0 +1,15 @@
1
+import { defineConfig } from './config'
2
+import { parse } from 'node:url'
3
+import { httpStream } from './util-http'
4
+
5
+// don't move this in util-http, where it would mostly belong, as a require to config.ts would prevent tests using util-http
6
+defineConfig('outbound_proxy', '', v => {
7
+ try {
8
+ parse(v)
9
+ httpStream.defaultProxy = v
10
+ }
11
+ catch {
12
+ console.warn("invalid URL", v)
13
+ return ''
14
+ }
15
+})
src/util-http.ts
+14
-5
@@ -1,6 +1,7 @@
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 https, { RequestOptions } from 'node:https'
3
+import { parse } from 'node:url'
4
+import https from 'node:https'
5
import http, { IncomingMessage } from 'node:http'
6
import { Readable } from 'node:stream'
7
import _ from 'lodash'
@@ -19,8 +20,9 @@ export async function httpWithBody(url: string, options?: XRequestOptions): Prom
20
})
21
}
22
22
-export interface XRequestOptions extends RequestOptions {
23
+export interface XRequestOptions extends https.RequestOptions {
24
body?: string | Buffer | Readable
25
+ proxy?: string // url format
26
// basic cookie store
27
jar?: Record<string, string>
28
noRedirect?: boolean
@@ -28,8 +30,10 @@ export interface XRequestOptions extends RequestOptions {
30
httpThrow?: boolean
31
}
32
31
-export function httpStream(url: string, { body, jar, noRedirect, httpThrow, ...options }: XRequestOptions ={}): Promise<IncomingMessage> {
33
+export declare namespace httpStream { let defaultProxy: string | undefined }
34
+export function httpStream(url: string, { body, jar, noRedirect, httpThrow, proxy, ...options }: XRequestOptions ={}): Promise<IncomingMessage> {
35
return new Promise((resolve, reject) => {
36
+ proxy ??= httpStream.defaultProxy
37
options.headers ??= {}
38
if (body) {
39
options.method ||= 'POST'
@@ -43,8 +47,13 @@ export function httpStream(url: string, { body, jar, noRedirect, httpThrow, ...o
47
if (jar)
48
options.headers.cookie = _.map(jar, (v,k) => `${k}=${v}; `).join('')
49
+ (options.headers.cookie || '') // preserve parameter
46
- const proto = url.startsWith('https:') ? https : http
47
- const req = proto.request(url, options, res => {
50
+ Object.assign(options, _.pick(parse(proxy || url), ['hostname', 'port', 'path', 'protocol']))
51
+ if (proxy) {
52
+ options.path = url
53
+ options.headers.host ??= parse(url).host || undefined
54
+ }
55
+ const proto = options.protocol === 'https:' ? https : http
56
+ const req = proto.request(options, res => {
57
console.debug("http responded", res.statusCode, "to", url)
58
if (jar) for (const entry of res.headers['set-cookie'] || []) {
59
const [, k, v] = /(.+?)=([^;]+)/.exec(entry) || []
src/watchLoad.ts
+2
-1
@@ -2,7 +2,8 @@
2
3
import { FSWatcher, watch } from 'fs'
4
import fs from 'fs/promises'
5
-import { debounceAsync, readFileBusy } from './misc'
5
+import { readFileBusy } from './misc'
6
+import { debounceAsync } from './debounceAsync'
7
import { BetterEventEmitter } from './events'
8
9
export type WatchLoadCanceller = () => void