@samitouri / QOSami-HFS / commits / 988e40a6

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