Implement HTTP proxy option (#515).

This commit adds a "Proxy" field to the network settings screen, which
can be used to specify a HTTP proxy for any outgoing requests from the
device.
This commit is contained in:
Daniel Collins 2025-05-24 19:53:09 +01:00
parent 7e64a529f8
commit 002c2f4937
6 changed files with 66 additions and 4 deletions

View File

@ -3,6 +3,7 @@ package confparser
import (
"fmt"
"net"
"net/url"
"reflect"
"slices"
"strconv"
@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error {
if _, err := idna.Lookup.ToASCII(val); err != nil {
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
}
case "proxy":
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
}
default:
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
}

View File

@ -3,6 +3,8 @@ package network
import (
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/guregu/null/v6"
@ -33,6 +35,7 @@ type IPv6StaticConfig struct {
}
type NetworkConfig struct {
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
@ -69,6 +72,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
return listenOptions
}
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
return func(*http.Request) (*url.URL, error) {
if s.HTTPProxy.String == "" {
return nil, nil
} else {
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
return proxyUrl, nil
}
}
}
func (s *NetworkInterfaceState) GetHostname() string {
hostname := ToValidHostname(s.config.Hostname.String)

View File

@ -5,6 +5,7 @@ import (
"errors"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
)
@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
ctx,
url,
timeout,
t.networkConfig.GetTransportProxyFunc(),
)
duration := time.Since(startTime)
@ -111,10 +113,16 @@ func queryHttpTime(
ctx context.Context,
url string,
timeout time.Duration,
proxyFunc func(*http.Request) (*url.URL, error),
) (now *time.Time, response *http.Response, err error) {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = proxyFunc
client := http.Client{
Transport: transport,
Timeout: timeout,
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err

10
ota.go
View File

@ -89,7 +89,14 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
return nil, fmt.Errorf("error creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = config.NetworkConfig.GetTransportProxyFunc()
client := &http.Client{
Transport: transport,
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request: %w", err)
}
@ -135,6 +142,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
client := http.Client{
Timeout: 10 * time.Minute,
Transport: &http.Transport{
Proxy: config.NetworkConfig.GetTransportProxyFunc(),
TLSHandshakeTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
RootCAs: rootcerts.ServerCertPool(),

View File

@ -730,6 +730,7 @@ export type TimeSyncMode =
export interface NetworkSettings {
hostname: string;
domain: string;
http_proxy: string;
ipv4_mode: IPv4Mode;
ipv6_mode: IPv6Mode;
lldp_mode: LLDPMode;

View File

@ -34,6 +34,7 @@ dayjs.extend(relativeTime);
const defaultNetworkSettings: NetworkSettings = {
hostname: "",
http_proxy: "",
domain: "",
ipv4_mode: "unknown",
ipv6_mode: "unknown",
@ -185,6 +186,10 @@ export default function SettingsNetworkRoute() {
setNetworkSettings({ ...networkSettings, hostname: value });
};
const handleProxyChange = (value: string) => {
setNetworkSettings({ ...networkSettings, http_proxy: value });
};
const handleDomainChange = (value: string) => {
setNetworkSettings({ ...networkSettings, domain: value });
};
@ -253,6 +258,26 @@ export default function SettingsNetworkRoute() {
</div>
</SettingsItem>
</div>
<div className="space-y-4">
<SettingsItem
title="HTTP Proxy"
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
>
<div className="relative">
<div>
<InputField
size="SM"
type="text"
placeholder="http://proxy.example.com:8080/"
defaultValue={networkSettings.http_proxy}
onChange={e => {
handleProxyChange(e.target.value);
}}
/>
</div>
</div>
</SettingsItem>
</div>
<div className="space-y-4">
<div className="space-y-1">