auto-detect proxy on Windows #812

Massimo Melina committed Jan 13, 2025 at 00:03 UTC a7416d357a5a765ddaf4699be124ba9fff19e79f
7 files changed +42 -17
src/adminApis.ts
+1 -1
@@ -181,7 +181,7 @@ const frpDebounced = debounceAsync(async () => {
181 catch {
182 return false
183 }
184 -})
184 +}, { retain: 10_000 })
185
186 export function anyAccountCanLoginAdmin() {
187 return Boolean(_.find(accountsConfig.get(), accountCanLoginAdmin))
src/api.vfs.ts
+2 -8
@@ -14,10 +14,8 @@ import {
14 IS_WINDOWS, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE,
15 IS_BINARY, APP_PATH
16 } from './const'
17 -import { getDiskSpace, getDiskSpaces, getDrives } from './util-os'
17 +import { getDiskSpace, getDiskSpaces, getDrives, reg } from './util-os'
18 import { getBaseUrlOrDefault, getServerStatus } from './listen'
19 -import { promisify } from 'util'
20 -import { execFile } from 'child_process'
19 import { SendListReadable } from './SendList'
20
21 // to manipulate the tree we need the original node
@@ -253,7 +251,7 @@ const apis: ApiHandlers = {
251 async windows_integrated() {
252 return {
253 is: await reg('query', WINDOWS_REG_KEY)
256 - .then(x => x.stdout.includes('REG_SZ'), () => false)
254 + .then(x => x.includes('REG_SZ'), () => false)
255 }
256 },
257
@@ -284,7 +282,3 @@ function simplifyName(node: VfsNode) {
282 }
283
284 const WINDOWS_REG_KEY = 'HKCU\\Software\\Classes\\*\\shell\\AddToHFS3'
287 -
288 -function reg(...pars: string[]) {
289 - return promisify(execFile)('reg', pars)
290 -}
src/config.ts
+4 -1
@@ -141,7 +141,7 @@ export function setConfig(newCfg: Record<string,unknown>, save?: boolean) {
141 if (!newCfg.hasOwnProperty(k))
142 apply(k, newCfg[k], true)
143 started = true
144 - events.emit('configReady')
144 + events.emit('configReady', startedWithoutConfig)
145 if (version !== VERSION) // be sure to save version
146 saveConfigAsap()
147
@@ -186,12 +186,15 @@ function stringify(obj: any) {
186 return yaml.stringify(obj, { lineWidth:1000 })
187 }
188
189 +let startedWithoutConfig = false
190 console.log("config", filePath)
191 export const configFile = watchLoad(filePath, text => {
192 + startedWithoutConfig = !text
193 try { setConfig(yaml.parse(text, { uniqueKeys: false }) || {}, false) }
194 catch(e: any) { console.error("Error in", filePath, ':', e.message || String(e)) }
195 }, {
196 failedOnFirstAttempt(){
197 + startedWithoutConfig = true
198 console.log("No config file, using defaults")
199 setTimeout(() => // this is called synchronously, but we need to call setConfig after first tick, when all configs are defined
200 setConfig({}, false))
src/debounceAsync.ts
+7 -4
@@ -7,21 +7,23 @@ export function debounceAsync<Cancelable extends boolean = false, A extends unkn
7 options: {
8 // time to wait after invocation of the debounced function. If you call again while waiting, the timer starts again.
9 wait?: number,
10 - // in a train of invocations, should we execute also the first one, or just the last one?
11 - leading?: boolean,
10 // since the wait-ing is renewed at each invocation, indefinitely, do you want to put a cap to it?
11 maxWait?: number,
12 + // in a train of invocations, should we execute also the first one, or just the last one?
13 + leading?: boolean,
14 // for how long do you want to cache last success value, and return that at next invocation?
15 retain?: number,
16 // for how long do you want to cache last failure value, and return that at next invocation?
17 retainFailure?: number,
18 + // if a call is overlapping another, return the same promise, instead of queuing
19 + reuseRunning?: boolean,
20 // should we offer a cancel method to the returned function? if we do, the awaited-type will include undefined
21 cancelable?: Cancelable
22 } = {}
23 ) {
24 type MaybeUndefined<T> = Cancelable extends true ? undefined | T : T
25 type MaybeR = MaybeUndefined<R>
24 - const { wait=0, leading=false, maxWait=Infinity, cancelable=false, retain=0, retainFailure } = options
26 + const { wait=0, leading=false, maxWait=Infinity, cancelable=false, retain=0, retainFailure, reuseRunning } = options
27 let started = 0 // latest callback invocation
28 let runningCallback: Promise<R> | undefined // latest callback invocation result
29 let latestDebouncer: Promise<MaybeR | R> // latest wrapper invocation
@@ -44,7 +46,7 @@ export function debounceAsync<Cancelable extends boolean = false, A extends unkn
46 })
47
48 async function debouncer(...args: A) {
47 - if (runningCallback)
49 + if (reuseRunning && runningCallback)
50 return runningCallback as MaybeR
51 const now = Date.now()
52 if (latestCallback && now - latestTimestamp < (latestHasFailed ? retainFailure ?? retain : retain))
@@ -61,6 +63,7 @@ export function debounceAsync<Cancelable extends boolean = false, A extends unkn
63 }
64 if (whoIsWaiting !== args) // another fresher call is waiting
65 return latestDebouncer
66 + await runningCallback // in case we don't reuseRunning
67 return exec()
68 }
69
src/nat.ts
+1 -1
@@ -88,7 +88,7 @@ export const getNatInfo = debounceAsync(async () => {
88 externalPort,
89 proto: status?.https?.listening ? 'https' : status?.http?.listening ? 'http' : '',
90 }
91 -})
91 +}, { reuseRunning: true })
92 getNatInfo()
93
94 function findGateway(): Promise<string | undefined> {
src/outboundProxy.ts
+22 -1
@@ -1,9 +1,13 @@
1 import { defineConfig } from './config'
2 import { parse } from 'node:url'
3 import { httpStream } from './util-http'
4 +import { reg } from './util-os'
5 +import events from './events'
6 +import { IS_WINDOWS } from './const'
7 +import { prefix } from './cross'
8
9 // 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 => {
10 +const outboundProxy = defineConfig('outbound_proxy', '', v => {
11 try {
12 parse(v)
13 httpStream.defaultProxy = v
@@ -13,3 +17,20 @@ defineConfig('outbound_proxy', '', v => {
17 return ''
18 }
19 })
20 +
21 +
22 +events.once('configReady', async startedWithoutConfig => {
23 + if (!IS_WINDOWS || !startedWithoutConfig) return
24 + // try to read Windows system setting for proxy
25 + const out = await reg('query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings')
26 + if (!/ProxyEnable.+(\d)/.exec(out)?.[1]) return
27 + const read = /ProxyServer.+?([\d:.]+)/.exec(out)?.[1]
28 + if (!read) return
29 + // it can be like "IP:PORT" or "http=IP:PORT;https=IP:PORT;ftp=IP:PORT"
30 + const url = prefix('https://', /https=([\d:.]+)/.exec(out)?.[1]) // prefer https
31 + || prefix('http://', /http=([\d:.]+)/.exec(out)?.[1])
32 + || !read.includes('=') && 'http://' + read // simpler form
33 + if (!url) return
34 + outboundProxy.set(url)
35 + console.log("detected proxy", read)
36 +})
\ No newline at end of file
src/util-os.ts
+5 -1
@@ -1,6 +1,6 @@
1 import { dirname } from 'path'
2 import { existsSync, statfsSync } from 'fs'
3 -import { exec, ExecOptions } from 'child_process'
3 +import { exec, execFile, ExecOptions } from 'child_process'
4 import { isWindowsDrive, onlyTruthy, promiseBestEffort } from './misc'
5 import Parser from '@gregoranders/csv';
6 import { pid, ppid } from 'node:process'
@@ -87,3 +87,7 @@ export const RUNNING_AS_SERVICE = IS_WINDOWS && getWindowsServicePids().then(x =
87 console.log("couldn't determine if we are running as a service")
88 console.debug(e)
89 })
90 +
91 +export function reg(...pars: string[]) {
92 + return promisify(execFile)('reg', pars).then(x => x.stdout)
93 +}