diff --git a/Dockerfile b/Dockerfile index 2b3448ec..da330849 100644 --- a/Dockerfile +++ b/Dockerfile @@ -93,12 +93,12 @@ ENV VPNSP=pia \ FIREWALL_INPUT_PORTS= \ FIREWALL_OUTBOUND_SUBNETS= \ FIREWALL_DEBUG=off \ - # Tinyproxy - TINYPROXY=off \ - TINYPROXY_LOG=Info \ - TINYPROXY_PORT=8888 \ - TINYPROXY_USER= \ - TINYPROXY_PASSWORD= \ + # HTTP proxy + HTTPPROXY= \ + HTTPPROXY_LOG=off \ + HTTPPROXY_PORT=8888 \ + HTTPPROXY_USER= \ + HTTPPROXY_PASSWORD= \ # Shadowsocks SHADOWSOCKS=off \ SHADOWSOCKS_LOG=off \ @@ -109,10 +109,9 @@ ENV VPNSP=pia \ ENTRYPOINT ["/entrypoint"] EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp HEALTHCHECK --interval=10m --timeout=10s --start-period=30s --retries=2 CMD /entrypoint healthcheck -RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tinyproxy tzdata && \ - rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \ +RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \ + rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* && \ deluser openvpn && \ - deluser tinyproxy && \ deluser unbound && \ mkdir /gluetun # TODO remove once SAN is added to PIA servers certificates, see https://github.com/pia-foss/manual-connections/issues/10 diff --git a/README.md b/README.md index b9168390..4c78a382 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Gluetun VPN client *Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access, -Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN and PureVPN VPN servers, using Go, OpenVPN, -iptables, DNS over TLS, ShadowSocks and Tinyproxy* +Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN and PureVPN VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* **ANNOUNCEMENT**: *Github Wiki reworked* @@ -36,7 +35,7 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy* - Choose the vpn network protocol, `udp` or `tcp` - Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices - Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP) -- Built in HTTP proxy (Tinyproxy, tunnels TCP) +- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP) - [Connect other containers to it](https://github.com/qdm12/gluetun#connect-to-it) - [Connect LAN devices to it](https://github.com/qdm12/gluetun#connect-to-it) - Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆 @@ -243,15 +242,16 @@ None of the following values are required. | `SHADOWSOCKS_PASSWORD` | | | Password to use to connect to Shadowsocks | | `SHADOWSOCKS_METHOD` | `chacha20-ietf-poly1305` | `chacha20-ietf-poly1305`, `aes-128-gcm`, `aes-256-gcm` | Method to use for Shadowsocks | -### Tinyproxy +### HTTP proxy | Variable | Default | Choices | Description | | --- | --- | --- | --- | -| `TINYPROXY` | `off` | `on`, `off` | Enable the internal HTTP proxy tinyproxy | -| `TINYPROXY_LOG` | `Info` | `Info`, `Connect`, `Notice`, `Warning`, `Error`, `Critical` | Tinyproxy log level | -| `TINYPROXY_PORT` | `8888` | `1024` to `65535` | Internal port number for Tinyproxy to listen on | -| `TINYPROXY_USER` | | | Username to use to connect to Tinyproxy | -| `TINYPROXY_PASSWORD` | | | Password to use to connect to Tinyproxy | +| `HTTPPROXY` | `off` | `on`, `off` | Enable the internal HTTP proxy | +| `HTTPPROXY_LOG` | `off` | `on` or `off` | Logs every tunnel requests | +| `HTTPPROXY_PORT` | `8888` | `1024` to `65535` | Internal port number for the HTTP proxy to listen on | +| `HTTPPROXY_USER` | | | Username to use to connect to the HTTP proxy | +| `HTTPPROXY_PASSWORD` | | | Password to use to connect to the HTTP proxy | +| `HTTPPROXY_STEALTH` | `off` | `on` or `off` | Stealth mode means HTTP proxy headers are not added to your requests | ### System @@ -295,15 +295,16 @@ There are various ways to achieve this, depending on your use case. Add `network_mode: "container:gluetun"` to your *docker-compose.yml*, provided Gluetun is already running

--
Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)

+-

Connect LAN devices through the built-in HTTP proxy (i.e. with Chrome, Kodi, etc.)

- You might want to use Shadowsocks instead which tunnels UDP as well as TCP, whereas Tinyproxy only tunnels TCP. + ⚠️ You might want to use Shadowsocks instead which tunnels UDP as well as TCP and does not leak your credentials. + The HTTP proxy will not encrypt your username and password every time you send a request to the HTTP proxy server. - 1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en) + 1. Setup an HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en) 1. Ensure the Gluetun container is launched with: - port `8888` published `-p 8888:8888/tcp` - 1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`. - 1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs + 1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `HTTPPROXY_USER` and `HTTPPROXY_PASSWORD`. Note that Chrome does not support authentication. + 1. If you set `HTTPPROXY_LOG` to `on`, more information will be logged in the Docker logs

-
Connect LAN devices through the built-in *Shadowsocks* proxy (per app, system wide, etc.)

diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index a4bc8920..2cde8f56 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -18,6 +18,7 @@ import ( "github.com/qdm12/gluetun/internal/dns" "github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/healthcheck" + "github.com/qdm12/gluetun/internal/httpproxy" gluetunLogging "github.com/qdm12/gluetun/internal/logging" "github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/params" @@ -27,7 +28,6 @@ import ( "github.com/qdm12/gluetun/internal/settings" "github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/storage" - "github.com/qdm12/gluetun/internal/tinyproxy" "github.com/qdm12/gluetun/internal/updater" versionpkg "github.com/qdm12/gluetun/internal/version" "github.com/qdm12/golibs/command" @@ -83,17 +83,15 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go dnsConf := dns.NewConfigurator(logger, client, fileManager) routingConf := routing.NewRouting(logger) firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager) - tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger) streamMerger := command.NewStreamMerger() paramsReader := params.NewReader(logger, fileManager) fmt.Println(gluetunLogging.Splash(version, commit, buildDate)) printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){ - "OpenVPN": ovpnConf.Version, - "Unbound": dnsConf.Version, - "IPtables": firewallConf.Version, - "TinyProxy": tinyProxyConf.Version, + "OpenVPN": ovpnConf.Version, + "Unbound": dnsConf.Version, + "IPtables": firewallConf.Version, }) allSettings, err := settings.GetAllSettings(paramsReader) @@ -125,11 +123,6 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go logger.Error(err) return 1 } - err = fileManager.SetOwnership("/etc/tinyproxy", uid, gid) - if err != nil { - logger.Error(err) - return 1 - } if allSettings.Firewall.Debug { firewallConf.SetDebug() @@ -161,6 +154,7 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go return 1 } defer func() { + routingConf.SetVerbose(false) if err := routingConf.TearDown(); err != nil { logger.Error(err) } @@ -244,19 +238,17 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go go publicIPLooper.RunRestartTicker(ctx, wg) publicIPLooper.SetPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker - tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, - allSettings.TinyProxy, logger, streamMerger, uid, gid, defaultInterface) - restartTinyproxy := tinyproxyLooper.Restart + httpProxyLooper := httpproxy.NewLooper(httpClient, logger, allSettings.HTTPProxy) wg.Add(1) - go tinyproxyLooper.Run(ctx, wg) + go httpProxyLooper.Run(ctx, wg) shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks, logger, defaultInterface) restartShadowsocks := shadowsocksLooper.Restart wg.Add(1) go shadowsocksLooper.Run(ctx, wg) - if allSettings.TinyProxy.Enabled { - restartTinyproxy() + if allSettings.HTTPProxy.Enabled { + httpProxyLooper.Restart() } if allSettings.ShadowSocks.Enabled { restartShadowsocks() @@ -356,7 +348,7 @@ func printVersions(ctx context.Context, logger logging.Logger, //nolint:lll func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger, logger logging.Logger, signalTunnelReady func()) { - // Blocking line merging paramsReader for all programs: openvpn, tinyproxy, unbound and shadowsocks + // Blocking line merging paramsReader for openvpn and unbound logger.Info("Launching standard output merger") streamMerger.CollectLines(ctx, func(line string) { line, level := gluetunLogging.PostProcessLine(line) diff --git a/docker-compose.yml b/docker-compose.yml index eee84d0f..263039f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - NET_ADMIN network_mode: bridge ports: - - 8888:8888/tcp # Tinyproxy + - 8888:8888/tcp # HTTP proxy - 8388:8388/tcp # Shadowsocks - 8388:8388/udp # Shadowsocks - 8000:8000/tcp # Built-in HTTP control server diff --git a/go.sum b/go.sum index 8aaed7db..811c1ec7 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qdm12/golibs v0.0.0-20201024185935-092412448c2c h1:9EQyDXbeapnPeMeO8Yq7PE6zqYPGkHp/qijNBBTU74c= -github.com/qdm12/golibs v0.0.0-20201024185935-092412448c2c/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc= github.com/qdm12/golibs v0.0.0-20201025221346-fe352060c25a h1:v0zUA1FWeVkTEd9KyxfehbRVJeFGOqyMY6FHO/Q9ITU= github.com/qdm12/golibs v0.0.0-20201025221346-fe352060c25a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc= github.com/qdm12/ss-server v0.0.0-20200819124651-6428e626ee83 h1:b7sNsgsKxH0mbl9L1hdUp5KSDkZ/1kOQ+iHiBVgFElM= diff --git a/internal/constants/colors.go b/internal/constants/colors.go index 8ab1b7fe..de8af916 100644 --- a/internal/constants/colors.go +++ b/internal/constants/colors.go @@ -6,10 +6,6 @@ func ColorUnbound() *color.Color { return color.New(color.FgCyan) } -func ColorTinyproxy() *color.Color { - return color.New(color.FgHiGreen) -} - func ColorOpenvpn() *color.Color { return color.New(color.FgHiMagenta) } diff --git a/internal/constants/paths.go b/internal/constants/paths.go index 67e469b2..49f6153c 100644 --- a/internal/constants/paths.go +++ b/internal/constants/paths.go @@ -21,8 +21,6 @@ const ( TunnelDevice models.Filepath = "/dev/net/tun" // NetRoute is the path to the file containing information on the network route. NetRoute models.Filepath = "/proc/net/route" - // TinyProxyConf is the filepath to the tinyproxy configuration file. - TinyProxyConf models.Filepath = "/etc/tinyproxy/tinyproxy.conf" // RootHints is the filepath to the root.hints file used by Unbound. RootHints models.Filepath = "/etc/unbound/root.hints" // RootKey is the filepath to the root.key file used by Unbound. diff --git a/internal/constants/tinyproxy.go b/internal/constants/tinyproxy.go deleted file mode 100644 index 53cf643f..00000000 --- a/internal/constants/tinyproxy.go +++ /dev/null @@ -1,20 +0,0 @@ -package constants - -import ( - "github.com/qdm12/gluetun/internal/models" -) - -const ( - // TinyProxyInfoLevel is the info log level for TinyProxy. - TinyProxyInfoLevel models.TinyProxyLogLevel = "Info" - // TinyProxyConnectLevel is the info log level for TinyProxy. - TinyProxyConnectLevel models.TinyProxyLogLevel = "Connect" - // TinyProxyNoticeLevel is the info log level for TinyProxy. - TinyProxyNoticeLevel models.TinyProxyLogLevel = "Notice" - // TinyProxyWarnLevel is the warning log level for TinyProxy. - TinyProxyWarnLevel models.TinyProxyLogLevel = "Warning" - // TinyProxyErrorLevel is the error log level for TinyProxy. - TinyProxyErrorLevel models.TinyProxyLogLevel = "Error" - // TinyProxyCriticalLevel is the critical log level for TinyProxy. - TinyProxyCriticalLevel models.TinyProxyLogLevel = "Critical" -) diff --git a/internal/httpproxy/auth.go b/internal/httpproxy/auth.go new file mode 100644 index 00000000..94096637 --- /dev/null +++ b/internal/httpproxy/auth.go @@ -0,0 +1,33 @@ +package httpproxy + +import ( + "encoding/base64" + "net/http" + "strings" +) + +func isAuthorized(responseWriter http.ResponseWriter, request *http.Request, + username, password string) (authorized bool) { + basicAuth := request.Header.Get("Proxy-Authorization") + if len(basicAuth) == 0 { + responseWriter.WriteHeader(http.StatusProxyAuthRequired) + return false + } + b64UsernamePassword := strings.TrimPrefix(basicAuth, "Basic ") + b, err := base64.StdEncoding.DecodeString(b64UsernamePassword) + if err != nil { + responseWriter.WriteHeader(http.StatusUnauthorized) + return false + } + usernamePassword := strings.Split(string(b), ":") + const expectedFields = 2 + if len(usernamePassword) != expectedFields { + responseWriter.WriteHeader(http.StatusBadRequest) + return false + } + if username != usernamePassword[0] && password != usernamePassword[1] { + responseWriter.WriteHeader(http.StatusUnauthorized) + return false + } + return true +} diff --git a/internal/httpproxy/handler.go b/internal/httpproxy/handler.go new file mode 100644 index 00000000..5c206739 --- /dev/null +++ b/internal/httpproxy/handler.go @@ -0,0 +1,62 @@ +package httpproxy + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/qdm12/golibs/logging" +) + +func newHandler(ctx context.Context, wg *sync.WaitGroup, + client *http.Client, logger logging.Logger, + stealth, verbose bool, username, password string) http.Handler { + const relayTimeout = 10 * time.Second + return &handler{ + ctx: ctx, + wg: wg, + client: client, + logger: logger, + relayTimeout: relayTimeout, + verbose: verbose, + stealth: stealth, + username: username, + password: password, + } +} + +type handler struct { + ctx context.Context + wg *sync.WaitGroup + client *http.Client + logger logging.Logger + relayTimeout time.Duration + verbose, stealth bool + username, password string +} + +func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { + if len(h.username) > 0 && !isAuthorized(responseWriter, request, h.username, h.password) { + h.logger.Info("%s unauthorized", request.RemoteAddr) + return + } + switch request.Method { + case http.MethodConnect: + h.handleHTTPS(responseWriter, request) + default: + h.handleHTTP(responseWriter, request) + } +} + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = [...]string{ //nolint:gochecknoglobals + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailers", + "Transfer-Encoding", + "Upgrade", +} diff --git a/internal/httpproxy/http.go b/internal/httpproxy/http.go new file mode 100644 index 00000000..438872dc --- /dev/null +++ b/internal/httpproxy/http.go @@ -0,0 +1,74 @@ +package httpproxy + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "strings" +) + +func (h *handler) handleHTTP(responseWriter http.ResponseWriter, request *http.Request) { + switch request.URL.Scheme { + case "http", "https": + default: + h.logger.Warn("Unsupported scheme %q", request.URL.Scheme) + http.Error(responseWriter, "unsupported scheme", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(h.ctx, h.relayTimeout) + defer cancel() + request = request.WithContext(ctx) + + request.RequestURI = "" + + for _, key := range hopHeaders { + request.Header.Del(key) + } + + if !h.stealth { + setForwardedHeaders(request) + } + + response, err := h.client.Do(request) + if err != nil { + http.Error(responseWriter, "server error", http.StatusInternalServerError) + h.logger.Warn("cannot request %s for client %q: %s", + request.URL, request.RemoteAddr, err) + return + } + defer response.Body.Close() + if h.verbose { + h.logger.Info("%s %s %s %s", request.RemoteAddr, response.Status, request.Method, request.URL) + } + + for _, key := range hopHeaders { + response.Header.Del(key) + } + + targetHeaderPtr := responseWriter.Header() + for key, values := range response.Header { + for _, value := range values { + targetHeaderPtr.Add(key, value) + } + } + + responseWriter.WriteHeader(response.StatusCode) + if _, err := io.Copy(responseWriter, response.Body); err != nil { + h.logger.Error("%s %s: body copy error: %s", request.RemoteAddr, request.URL, err) + } +} + +func setForwardedHeaders(request *http.Request) { + clientIP, _, err := net.SplitHostPort(request.RemoteAddr) + if err != nil { + return + } + // keep existing proxy headers + if prior, ok := request.Header["X-Forwarded-For"]; ok { + clientIP = fmt.Sprintf("%s,%s", strings.Join(prior, ", "), clientIP) + } + request.Header.Set("X-Forwarded-For", clientIP) +} diff --git a/internal/httpproxy/https.go b/internal/httpproxy/https.go new file mode 100644 index 00000000..fa00a34e --- /dev/null +++ b/internal/httpproxy/https.go @@ -0,0 +1,64 @@ +package httpproxy + +import ( + "context" + "io" + "net" + "net/http" + "sync" +) + +func (h *handler) handleHTTPS(responseWriter http.ResponseWriter, request *http.Request) { + dialer := net.Dialer{Timeout: h.relayTimeout} + destinationConn, err := dialer.DialContext(h.ctx, "tcp", request.Host) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusServiceUnavailable) + return + } + + responseWriter.WriteHeader(http.StatusOK) + + hijacker, ok := responseWriter.(http.Hijacker) + if !ok { + http.Error(responseWriter, "Hijacking not supported", http.StatusInternalServerError) + return + } + clientConnection, _, err := hijacker.Hijack() + if err != nil { + h.logger.Warn(err) + http.Error(responseWriter, err.Error(), http.StatusServiceUnavailable) + if err := destinationConn.Close(); err != nil { + h.logger.Error("closing destination connection: %s", err) + } + return + } + + if h.verbose { + h.logger.Info("%s <-> %s", request.RemoteAddr, request.Host) + } + + h.wg.Add(1) + ctx, cancel := context.WithCancel(h.ctx) + const transferGoroutines = 2 + wg := &sync.WaitGroup{} + wg.Add(transferGoroutines) + go func() { // trigger cleanup when done + wg.Wait() + cancel() + }() + go func() { // cleanup + <-ctx.Done() + destinationConn.Close() + clientConnection.Close() + h.wg.Done() + }() + go transfer(destinationConn, clientConnection, wg) + go transfer(clientConnection, destinationConn, wg) +} + +func transfer(destination io.WriteCloser, source io.ReadCloser, wg *sync.WaitGroup) { + _, _ = io.Copy(destination, source) + _ = source.Close() + _ = destination.Close() + wg.Done() +} diff --git a/internal/httpproxy/loop.go b/internal/httpproxy/loop.go new file mode 100644 index 00000000..e36ad109 --- /dev/null +++ b/internal/httpproxy/loop.go @@ -0,0 +1,139 @@ +package httpproxy + +import ( + "context" + "fmt" + "net/http" + "sync" + + "github.com/qdm12/gluetun/internal/settings" + "github.com/qdm12/golibs/logging" +) + +type Looper interface { + Run(ctx context.Context, wg *sync.WaitGroup) + Restart() + Start() + Stop() + GetSettings() (settings settings.HTTPProxy) + SetSettings(settings settings.HTTPProxy) +} + +type looper struct { + client *http.Client + settings settings.HTTPProxy + settingsMutex sync.RWMutex + logger logging.Logger + restart chan struct{} + start chan struct{} + stop chan struct{} +} + +func NewLooper(client *http.Client, logger logging.Logger, + settings settings.HTTPProxy) Looper { + return &looper{ + client: client, + settings: settings, + logger: logger.WithPrefix("http proxy: "), + restart: make(chan struct{}), + start: make(chan struct{}), + stop: make(chan struct{}), + } +} + +func (l *looper) GetSettings() (settings settings.HTTPProxy) { + l.settingsMutex.RLock() + defer l.settingsMutex.RUnlock() + return l.settings +} + +func (l *looper) SetSettings(settings settings.HTTPProxy) { + l.settingsMutex.Lock() + defer l.settingsMutex.Unlock() + l.settings = settings +} + +func (l *looper) isEnabled() bool { + l.settingsMutex.RLock() + defer l.settingsMutex.RUnlock() + return l.settings.Enabled +} + +func (l *looper) setEnabled(enabled bool) { + l.settingsMutex.Lock() + defer l.settingsMutex.Unlock() + l.settings.Enabled = enabled +} + +func (l *looper) Restart() { l.restart <- struct{}{} } +func (l *looper) Start() { l.start <- struct{}{} } +func (l *looper) Stop() { l.stop <- struct{}{} } + +func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + waitForStart := true + for waitForStart { + select { + case <-l.stop: + l.logger.Info("not started yet") + case <-l.start: + waitForStart = false + case <-l.restart: + waitForStart = false + case <-ctx.Done(): + return + } + } + defer l.logger.Warn("loop exited") + + for ctx.Err() == nil { + for !l.isEnabled() { + // wait for a signal to re-enable + select { + case <-l.stop: + l.logger.Info("already disabled") + case <-l.restart: + l.setEnabled(true) + case <-l.start: + l.setEnabled(true) + case <-ctx.Done(): + return + } + } + + settings := l.GetSettings() + address := fmt.Sprintf("0.0.0.0:%d", settings.Port) + + server := New(ctx, address, l.logger, l.client, settings.Stealth, settings.Log, settings.User, settings.Password) + + runCtx, runCancel := context.WithCancel(context.Background()) + runWg := &sync.WaitGroup{} + runWg.Add(1) + go server.Run(runCtx, runWg) + + stayHere := true + for stayHere { + select { + case <-ctx.Done(): + l.logger.Warn("context canceled: exiting loop") + runCancel() + runWg.Wait() + return + case <-l.restart: // triggered restart + l.logger.Info("restarting") + runCancel() + runWg.Wait() + stayHere = false + case <-l.start: + l.logger.Info("already started") + case <-l.stop: + l.logger.Info("stopping") + runCancel() + runWg.Wait() + l.setEnabled(false) + stayHere = false + } + } + runCancel() // repetition for linter only + } +} diff --git a/internal/httpproxy/server.go b/internal/httpproxy/server.go new file mode 100644 index 00000000..3da4bf0a --- /dev/null +++ b/internal/httpproxy/server.go @@ -0,0 +1,55 @@ +package httpproxy + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/qdm12/golibs/logging" +) + +type Server interface { + Run(ctx context.Context, wg *sync.WaitGroup) +} + +type server struct { + address string + handler http.Handler + logger logging.Logger + internalWG *sync.WaitGroup +} + +func New(ctx context.Context, address string, + logger logging.Logger, client *http.Client, + stealth, verbose bool, username, password string) Server { + wg := &sync.WaitGroup{} + return &server{ + address: address, + handler: newHandler(ctx, wg, client, logger, stealth, verbose, username, password), + logger: logger, + internalWG: wg, + } +} + +func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + server := http.Server{Addr: s.address, Handler: s.handler} + go func() { + <-ctx.Done() + s.logger.Warn("context canceled: exiting loop") + defer s.logger.Warn("loop exited") + const shutdownGraceDuration = 2 * time.Second + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + s.logger.Error("failed shutting down: %s", err) + } + }() + s.logger.Info("listening on %s", s.address) + err := server.ListenAndServe() + if err != nil && ctx.Err() != context.Canceled { + s.logger.Error(err) + } + s.internalWG.Wait() +} diff --git a/internal/logging/line.go b/internal/logging/line.go index 031c386a..4820e7e0 100644 --- a/internal/logging/line.go +++ b/internal/logging/line.go @@ -12,13 +12,9 @@ import ( //nolint:lll var regularExpressions = struct { //nolint:gochecknoglobals - unboundPrefix *regexp.Regexp - tinyproxyLoglevel *regexp.Regexp - tinyproxyPrefix *regexp.Regexp + unboundPrefix *regexp.Regexp }{ - unboundPrefix: regexp.MustCompile(`unbound: \[[0-9]{10}\] unbound\[[0-9]+:0\] `), - tinyproxyLoglevel: regexp.MustCompile(`INFO|CONNECT|NOTICE|WARNING|ERROR|CRITICAL`), - tinyproxyPrefix: regexp.MustCompile(`tinyproxy: .+[ ]+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9] \[[0-9]+\]: `), + unboundPrefix: regexp.MustCompile(`unbound: \[[0-9]{10}\] unbound\[[0-9]+:0\] `), } func PostProcessLine(s string) (filtered string, level logging.Level) { @@ -78,21 +74,6 @@ func PostProcessLine(s string) (filtered string, level logging.Level) { filtered = fmt.Sprintf("unbound: %s", filtered) filtered = constants.ColorUnbound().Sprintf(filtered) return filtered, level - case strings.HasPrefix(s, "tinyproxy: "): - logLevel := regularExpressions.tinyproxyLoglevel.FindString(s) - prefix := regularExpressions.tinyproxyPrefix.FindString(s) - filtered = fmt.Sprintf("tinyproxy: %s", s[len(prefix):]) - filtered = constants.ColorTinyproxy().Sprintf(filtered) - switch logLevel { - case "INFO", "CONNECT", "NOTICE": - return filtered, logging.InfoLevel - case "WARNING": - return filtered, logging.WarnLevel - case "ERROR", "CRITICAL": - return filtered, logging.ErrorLevel - default: - return filtered, logging.ErrorLevel - } } return s, logging.InfoLevel } diff --git a/internal/logging/line_test.go b/internal/logging/line_test.go index 6f6fb1cc..c8001a30 100644 --- a/internal/logging/line_test.go +++ b/internal/logging/line_test.go @@ -36,34 +36,6 @@ func Test_PostProcessLine(t *testing.T) { "unbound: [1594595249] unbound[75:0] BLA: init module 0: validator", "unbound: BLA: init module 0: validator", logging.ErrorLevel}, - "tinyproxy info": { - "tinyproxy: INFO Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.InfoLevel}, - "tinyproxy connect": { - "tinyproxy: CONNECT Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.InfoLevel}, - "tinyproxy notice": { - "tinyproxy: NOTICE Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.InfoLevel}, - "tinyproxy warning": { - "tinyproxy: WARNING Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.WarnLevel}, - "tinyproxy error": { - "tinyproxy: ERROR Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.ErrorLevel}, - "tinyproxy critical": { - "tinyproxy: CRITICAL Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.ErrorLevel}, - "tinyproxy unknown": { - "tinyproxy: BLABLA Jul 12 23:07:25 [32]: Reloading config file", - "tinyproxy: Reloading config file", - logging.ErrorLevel}, "openvpn unknown": { "openvpn: message", "openvpn: message", diff --git a/internal/logging/splash.go b/internal/logging/splash.go index 4ebbf0d4..7bf616fe 100644 --- a/internal/logging/splash.go +++ b/internal/logging/splash.go @@ -27,7 +27,7 @@ func title() []string { "================ Gluetun ================", "=========================================", "==== A mix of OpenVPN, DNS over TLS, ====", - "======= Shadowsocks and Tinyproxy =======", + "======= Shadowsocks and HTTP proxy ======", "========= all glued up with Go ==========", "=========================================", "=========== For tunneling to ============", diff --git a/internal/models/alias.go b/internal/models/alias.go index 6dc7b6ae..b789ab7c 100644 --- a/internal/models/alias.go +++ b/internal/models/alias.go @@ -16,8 +16,6 @@ type ( URL string // Filepath is a local filesytem file path. Filepath string - // TinyProxyLogLevel is the log level for TinyProxy. - TinyProxyLogLevel string // VPNProvider is the name of the VPN provider to be used. VPNProvider string // NetworkProtocol contains the network protocol to be used to communicate with the VPN servers. diff --git a/internal/params/httpproxy.go b/internal/params/httpproxy.go new file mode 100644 index 00000000..0b2b6740 --- /dev/null +++ b/internal/params/httpproxy.go @@ -0,0 +1,78 @@ +package params + +import ( + "strings" + + libparams "github.com/qdm12/golibs/params" +) + +// GetHTTPProxy obtains if the HTTP proxy is on from the environment variable +// HTTPPROXY, and using PROXY and TINYPROXY as retro-compatibility names. +func (r *reader) GetHTTPProxy() (enabled bool, err error) { + retroKeysOption := libparams.RetroKeys( + []string{"TINYPROXY", "PROXY"}, + r.onRetroActive, + ) + return r.envParams.GetOnOff("HTTPPROXY", retroKeysOption, libparams.Default("off")) +} + +// GetHTTPProxyLog obtains the if http proxy requests should be logged from +// the environment variable HTTPPROXY_LOG, and using PROXY_LOG_LEVEL and +// TINYPROXY_LOG as retro-compatibility names. +func (r *reader) GetHTTPProxyLog() (log bool, err error) { + s, _ := r.envParams.GetEnv("HTTPPROXY_LOG") + if len(s) == 0 { + s, _ = r.envParams.GetEnv("PROXY_LOG_LEVEL") + if len(s) == 0 { + s, _ = r.envParams.GetEnv("TINYPROXY_LOG") + if len(s) == 0 { + return false, nil // default log disabled + } + } + switch strings.ToLower(s) { + case "info", "connect", "notice": + return true, nil + default: + return false, nil + } + } + return r.envParams.GetOnOff("HTTPPROXY_LOG", libparams.Default("off")) +} + +// GetHTTPProxyPort obtains the HTTP proxy listening port from the environment variable +// HTTPPROXY_PORT, and using PROXY_PORT and TINYPROXY_PORT as retro-compatibility names. +func (r *reader) GetHTTPProxyPort() (port uint16, err error) { + retroKeysOption := libparams.RetroKeys( + []string{"TINYPROXY_PORT", "PROXY_PORT"}, + r.onRetroActive, + ) + return r.envParams.GetPort("HTTPPROXY_PORT", retroKeysOption, libparams.Default("8888")) +} + +// GetHTTPProxyUser obtains the HTTP proxy server user from the environment variable +// HTTPPROXY_USER, and using TINYPROXY_USER and PROXY_USER as retro-compatibility names. +func (r *reader) GetHTTPProxyUser() (user string, err error) { + retroKeysOption := libparams.RetroKeys( + []string{"TINYPROXY_USER", "PROXY_USER"}, + r.onRetroActive, + ) + return r.envParams.GetEnv("HTTPPROXY_USER", + retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset()) +} + +// GetHTTPProxyPassword obtains the HTTP proxy server password from the environment variable +// HTTPPROXY_PASSWORD, and using TINYPROXY_PASSWORD and PROXY_PASSWORD as retro-compatibility names. +func (r *reader) GetHTTPProxyPassword() (password string, err error) { + retroKeysOption := libparams.RetroKeys( + []string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"}, + r.onRetroActive, + ) + return r.envParams.GetEnv("HTTPPROXY_PASSWORD", + retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset()) +} + +// GetHTTPProxyStealth obtains the HTTP proxy server stealth mode +// from the environment variable HTTPPROXY_STEALTH. +func (r *reader) GetHTTPProxyStealth() (stealth bool, err error) { + return r.envParams.GetOnOff("HTTPPROXY_STEALTH", libparams.Default("off")) +} diff --git a/internal/params/params.go b/internal/params/params.go index 345d25a7..0021b67f 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -102,12 +102,13 @@ type Reader interface { GetShadowSocksPassword() (password string, err error) GetShadowSocksMethod() (method string, err error) - // Tinyproxy getters - GetTinyProxy() (activated bool, err error) - GetTinyProxyLog() (models.TinyProxyLogLevel, error) - GetTinyProxyPort() (port uint16, err error) - GetTinyProxyUser() (user string, err error) - GetTinyProxyPassword() (password string, err error) + // HTTP proxy getters + GetHTTPProxy() (activated bool, err error) + GetHTTPProxyLog() (log bool, err error) + GetHTTPProxyPort() (port uint16, err error) + GetHTTPProxyUser() (user string, err error) + GetHTTPProxyPassword() (password string, err error) + GetHTTPProxyStealth() (stealth bool, err error) // Public IP getters GetPublicIPPeriod() (period time.Duration, err error) diff --git a/internal/params/tinyproxy.go b/internal/params/tinyproxy.go deleted file mode 100644 index f61ac183..00000000 --- a/internal/params/tinyproxy.go +++ /dev/null @@ -1,120 +0,0 @@ -package params - -import ( - "strconv" - - "github.com/qdm12/gluetun/internal/models" - libparams "github.com/qdm12/golibs/params" -) - -// GetTinyProxy obtains if TinyProxy is on from the environment variable -// TINYPROXY, and using PROXY as a retro-compatibility name. -func (r *reader) GetTinyProxy() (activated bool, err error) { - // Retro-compatibility - s, err := r.envParams.GetEnv("PROXY") - if err != nil { - return false, err - } else if len(s) != 0 { - r.logger.Warn("You are using the old environment variable PROXY, please consider changing it to TINYPROXY") - return r.envParams.GetOnOff("PROXY", libparams.Compulsory()) - } - return r.envParams.GetOnOff("TINYPROXY", libparams.Default("off")) -} - -// GetTinyProxyLog obtains the TinyProxy log level from the environment variable -// TINYPROXY_LOG, and using PROXY_LOG_LEVEL as a retro-compatibility name. -func (r *reader) GetTinyProxyLog() (models.TinyProxyLogLevel, error) { - // Retro-compatibility - s, err := r.envParams.GetEnv("PROXY_LOG_LEVEL") - if err != nil { - return models.TinyProxyLogLevel(s), err - } else if len(s) != 0 { - r.logger.Warn("You are using the old environment variable PROXY_LOG_LEVEL, please consider changing it to TINYPROXY_LOG") //nolint:lll - s, err = r.envParams.GetValueIfInside("PROXY_LOG_LEVEL", - []string{"Info", "Connect", "Notice", "Warning", "Error", "Critical"}, - libparams.Compulsory()) - return models.TinyProxyLogLevel(s), err - } - s, err = r.envParams.GetValueIfInside("TINYPROXY_LOG", - []string{"Info", "Connect", "Notice", "Warning", "Error", "Critical"}, - libparams.Default("Connect")) - return models.TinyProxyLogLevel(s), err -} - -// GetTinyProxyPort obtains the TinyProxy listening port from the environment variable -// TINYPROXY_PORT, and using PROXY_PORT as a retro-compatibility name. -func (r *reader) GetTinyProxyPort() (port uint16, err error) { - // Retro-compatibility - portStr, err := r.envParams.GetEnv("PROXY_PORT") - switch { - case err != nil: - return 0, err - case len(portStr) != 0: - r.logger.Warn("You are using the old environment variable PROXY_PORT, please consider changing it to TINYPROXY_PORT") - default: - portStr, err = r.envParams.GetEnv("TINYPROXY_PORT", libparams.Default("8888")) - if err != nil { - return 0, err - } - } - if err := r.verifier.VerifyPort(portStr); err != nil { - return 0, err - } - portUint64, err := strconv.ParseUint(portStr, 10, 16) - return uint16(portUint64), err -} - -// GetTinyProxyUser obtains the TinyProxy server user from the environment variable -// TINYPROXY_USER, and using PROXY_USER as a retro-compatibility name. -func (r *reader) GetTinyProxyUser() (user string, err error) { - defer func() { - unsetErr := r.unsetEnv("PROXY_USER") - if err == nil { - err = unsetErr - } - }() - defer func() { - unsetErr := r.unsetEnv("TINYPROXY_USER") - if err == nil { - err = unsetErr - } - }() - // Retro-compatibility - user, err = r.envParams.GetEnv("PROXY_USER", libparams.CaseSensitiveValue()) - if err != nil { - return user, err - } - if len(user) != 0 { - r.logger.Warn("You are using the old environment variable PROXY_USER, please consider changing it to TINYPROXY_USER") - return user, nil - } - return r.envParams.GetEnv("TINYPROXY_USER", libparams.CaseSensitiveValue()) -} - -// GetTinyProxyPassword obtains the TinyProxy server password from the environment variable -// TINYPROXY_PASSWORD, and using PROXY_PASSWORD as a retro-compatibility name. -func (r *reader) GetTinyProxyPassword() (password string, err error) { - defer func() { - unsetErr := r.unsetEnv("PROXY_PASSWORD") - if err == nil { - err = unsetErr - } - }() - defer func() { - unsetErr := r.unsetEnv("TINYPROXY_PASSWORD") - if err == nil { - err = unsetErr - } - }() - - // Retro-compatibility - password, err = r.envParams.GetEnv("PROXY_PASSWORD", libparams.CaseSensitiveValue()) - if err != nil { - return password, err - } - if len(password) != 0 { - r.logger.Warn("You are using the old environment variable PROXY_PASSWORD, please consider changing it to TINYPROXY_PASSWORD") //nolint:lll - return password, nil - } - return r.envParams.GetEnv("TINYPROXY_PASSWORD", libparams.CaseSensitiveValue()) -} diff --git a/internal/settings/httpproxy.go b/internal/settings/httpproxy.go new file mode 100644 index 00000000..de7be358 --- /dev/null +++ b/internal/settings/httpproxy.go @@ -0,0 +1,71 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/qdm12/gluetun/internal/params" +) + +// HTTPProxy contains settings to configure the HTTP proxy. +type HTTPProxy struct { //nolint:maligned + Enabled bool + Port uint16 + User string + Password string + Stealth bool + Log bool +} + +func (h *HTTPProxy) String() string { + if !h.Enabled { + return "HTTP Proxy settings: disabled" + } + auth, log, stealth := disabled, disabled, disabled + if h.User != "" { + auth = enabled + } + if h.Log { + log = enabled + } + if h.Stealth { + stealth = enabled + } + settingsList := []string{ + "HTTP proxy settings:", + fmt.Sprintf("Port: %d", h.Port), + "Authentication: " + auth, + "Stealth: " + stealth, + "Log: " + log, + } + return strings.Join(settingsList, "\n |--") +} + +// GetHTTPProxySettings obtains HTTPProxy settings from environment variables using the params package. +func GetHTTPProxySettings(paramsReader params.Reader) (settings HTTPProxy, err error) { + settings.Enabled, err = paramsReader.GetHTTPProxy() + if err != nil || !settings.Enabled { + return settings, err + } + settings.Port, err = paramsReader.GetHTTPProxyPort() + if err != nil { + return settings, err + } + settings.User, err = paramsReader.GetHTTPProxyUser() + if err != nil { + return settings, err + } + settings.Password, err = paramsReader.GetHTTPProxyPassword() + if err != nil { + return settings, err + } + settings.Stealth, err = paramsReader.GetHTTPProxyStealth() + if err != nil { + return settings, err + } + settings.Log, err = paramsReader.GetHTTPProxyLog() + if err != nil { + return settings, err + } + return settings, nil +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 065459a6..9042062c 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -21,7 +21,7 @@ type Settings struct { System System DNS DNS Firewall Firewall - TinyProxy TinyProxy + HTTPProxy HTTPProxy ShadowSocks ShadowSocks PublicIPPeriod time.Duration UpdaterPeriod time.Duration @@ -44,7 +44,7 @@ func (s *Settings) String() string { s.System.String(), s.DNS.String(), s.Firewall.String(), - s.TinyProxy.String(), + s.HTTPProxy.String(), s.ShadowSocks.String(), s.ControlServer.String(), "Public IP check period: " + s.PublicIPPeriod.String(), // TODO print disabled if 0 @@ -73,7 +73,7 @@ func GetAllSettings(paramsReader params.Reader) (settings Settings, err error) { if err != nil { return settings, err } - settings.TinyProxy, err = GetTinyProxySettings(paramsReader) + settings.HTTPProxy, err = GetHTTPProxySettings(paramsReader) if err != nil { return settings, err } diff --git a/internal/settings/tinyproxy.go b/internal/settings/tinyproxy.go deleted file mode 100644 index d6481df7..00000000 --- a/internal/settings/tinyproxy.go +++ /dev/null @@ -1,59 +0,0 @@ -package settings - -import ( - "fmt" - "strings" - - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/gluetun/internal/params" -) - -// TinyProxy contains settings to configure TinyProxy. -type TinyProxy struct { - User string - Password string - LogLevel models.TinyProxyLogLevel - Port uint16 - Enabled bool -} - -func (t *TinyProxy) String() string { - if !t.Enabled { - return "TinyProxy settings: disabled" - } - auth := disabled - if t.User != "" { - auth = enabled - } - settingsList := []string{ - fmt.Sprintf("Port: %d", t.Port), - "Authentication: " + auth, - "Log level: " + string(t.LogLevel), - } - return "TinyProxy settings:\n" + strings.Join(settingsList, "\n |--") -} - -// GetTinyProxySettings obtains TinyProxy settings from environment variables using the params package. -func GetTinyProxySettings(paramsReader params.Reader) (settings TinyProxy, err error) { - settings.Enabled, err = paramsReader.GetTinyProxy() - if err != nil || !settings.Enabled { - return settings, err - } - settings.User, err = paramsReader.GetTinyProxyUser() - if err != nil { - return settings, err - } - settings.Password, err = paramsReader.GetTinyProxyPassword() - if err != nil { - return settings, err - } - settings.Port, err = paramsReader.GetTinyProxyPort() - if err != nil { - return settings, err - } - settings.LogLevel, err = paramsReader.GetTinyProxyLog() - if err != nil { - return settings, err - } - return settings, nil -} diff --git a/internal/tinyproxy/command.go b/internal/tinyproxy/command.go deleted file mode 100644 index c850250b..00000000 --- a/internal/tinyproxy/command.go +++ /dev/null @@ -1,28 +0,0 @@ -package tinyproxy - -import ( - "context" - "fmt" - "io" - "strings" -) - -func (c *configurator) Start(ctx context.Context) (stdout io.ReadCloser, waitFn func() error, err error) { - c.logger.Info("starting tinyproxy server") - stdout, _, waitFn, err = c.commander.Start(ctx, "tinyproxy", "-d") - return stdout, waitFn, err -} - -// Version obtains the version of the installed Tinyproxy server. -func (c *configurator) Version(ctx context.Context) (string, error) { - output, err := c.commander.Run(ctx, "tinyproxy", "-v") - if err != nil { - return "", err - } - words := strings.Fields(output) - const minWords = 2 - if len(words) < minWords { - return "", fmt.Errorf("tinyproxy -v: output is too short: %q", output) - } - return words[1], nil -} diff --git a/internal/tinyproxy/conf.go b/internal/tinyproxy/conf.go deleted file mode 100644 index bf4efe57..00000000 --- a/internal/tinyproxy/conf.go +++ /dev/null @@ -1,50 +0,0 @@ -package tinyproxy - -import ( - "fmt" - "sort" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/files" -) - -func (c *configurator) MakeConf(logLevel models.TinyProxyLogLevel, - port uint16, user, password string, uid, gid int) error { - c.logger.Info("generating tinyproxy configuration file") - lines := generateConf(logLevel, port, user, password, uid, gid) - return c.fileManager.WriteLinesToFile(string(constants.TinyProxyConf), - lines, - files.Ownership(uid, gid), - files.Permissions(constants.UserReadPermission)) -} - -func generateConf(logLevel models.TinyProxyLogLevel, port uint16, user, password string, uid, gid int) ( - lines []string) { - confMapping := map[string]string{ - "User": fmt.Sprintf("%d", uid), - "Group": fmt.Sprintf("%d", gid), - "Port": fmt.Sprintf("%d", port), - "Timeout": "600", - "DefaultErrorFile": "\"/usr/share/tinyproxy/default.html\"", - "MaxClients": "100", - "MinSpareServers": "5", - "MaxSpareServers": "20", - "StartServers": "10", - "MaxRequestsPerChild": "0", - "DisableViaHeader": "Yes", - "LogLevel": string(logLevel), - // "StatFile": "\"/usr/share/tinyproxy/stats.html\"", - } - if len(user) > 0 { - confMapping["BasicAuth"] = fmt.Sprintf("%s %s", user, password) - } - for k, v := range confMapping { - line := fmt.Sprintf("%s %s", k, v) - lines = append(lines, line) - } - sort.Slice(lines, func(i, j int) bool { - return lines[i] < lines[j] - }) - return lines -} diff --git a/internal/tinyproxy/conf_test.go b/internal/tinyproxy/conf_test.go deleted file mode 100644 index 56ebb07e..00000000 --- a/internal/tinyproxy/conf_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package tinyproxy - -import ( - "testing" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/stretchr/testify/assert" -) - -func Test_generateConf(t *testing.T) { - t.Parallel() - tests := map[string]struct { - logLevel models.TinyProxyLogLevel - port uint16 - user string - password string - lines []string - }{ - "No credentials": { - logLevel: constants.TinyProxyInfoLevel, - port: 2000, - lines: []string{ - "DefaultErrorFile \"/usr/share/tinyproxy/default.html\"", - "DisableViaHeader Yes", - "Group 1001", - "LogLevel Info", - "MaxClients 100", - "MaxRequestsPerChild 0", - "MaxSpareServers 20", - "MinSpareServers 5", - "Port 2000", - "StartServers 10", - "Timeout 600", - "User 1000", - }, - }, - "With credentials": { - logLevel: constants.TinyProxyErrorLevel, - port: 2000, - user: "abc", - password: "def", - lines: []string{ - "BasicAuth abc def", - "DefaultErrorFile \"/usr/share/tinyproxy/default.html\"", - "DisableViaHeader Yes", - "Group 1001", - "LogLevel Error", - "MaxClients 100", - "MaxRequestsPerChild 0", - "MaxSpareServers 20", - "MinSpareServers 5", - "Port 2000", - "StartServers 10", - "Timeout 600", - "User 1000", - }, - }, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - lines := generateConf(tc.logLevel, tc.port, tc.user, tc.password, 1000, 1001) - assert.Equal(t, tc.lines, lines) - }) - } -} diff --git a/internal/tinyproxy/loop.go b/internal/tinyproxy/loop.go deleted file mode 100644 index 7267072d..00000000 --- a/internal/tinyproxy/loop.go +++ /dev/null @@ -1,194 +0,0 @@ -package tinyproxy - -import ( - "context" - "sync" - "time" - - "github.com/qdm12/gluetun/internal/firewall" - "github.com/qdm12/gluetun/internal/settings" - "github.com/qdm12/golibs/command" - "github.com/qdm12/golibs/logging" -) - -type Looper interface { - Run(ctx context.Context, wg *sync.WaitGroup) - Restart() - Start() - Stop() - GetSettings() (settings settings.TinyProxy) - SetSettings(settings settings.TinyProxy) -} - -type looper struct { - conf Configurator - firewallConf firewall.Configurator - settings settings.TinyProxy - settingsMutex sync.RWMutex - logger logging.Logger - streamMerger command.StreamMerger - uid int - gid int - defaultInterface string - restart chan struct{} - start chan struct{} - stop chan struct{} -} - -func (l *looper) logAndWait(ctx context.Context, err error) { - l.logger.Error(err) - const waitTime = time.Minute - l.logger.Info("retrying in %s", waitTime) - timer := time.NewTimer(waitTime) - select { - case <-timer.C: - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - } -} - -func NewLooper(conf Configurator, firewallConf firewall.Configurator, settings settings.TinyProxy, - logger logging.Logger, streamMerger command.StreamMerger, uid, gid int, defaultInterface string) Looper { - return &looper{ - conf: conf, - firewallConf: firewallConf, - settings: settings, - logger: logger.WithPrefix("tinyproxy: "), - streamMerger: streamMerger, - uid: uid, - gid: gid, - defaultInterface: defaultInterface, - restart: make(chan struct{}), - start: make(chan struct{}), - stop: make(chan struct{}), - } -} - -func (l *looper) GetSettings() (settings settings.TinyProxy) { - l.settingsMutex.RLock() - defer l.settingsMutex.RUnlock() - return l.settings -} - -func (l *looper) SetSettings(settings settings.TinyProxy) { - l.settingsMutex.Lock() - defer l.settingsMutex.Unlock() - l.settings = settings -} - -func (l *looper) isEnabled() bool { - l.settingsMutex.RLock() - defer l.settingsMutex.RUnlock() - return l.settings.Enabled -} - -func (l *looper) setEnabled(enabled bool) { - l.settingsMutex.Lock() - defer l.settingsMutex.Unlock() - l.settings.Enabled = enabled -} - -func (l *looper) Restart() { l.restart <- struct{}{} } -func (l *looper) Start() { l.start <- struct{}{} } -func (l *looper) Stop() { l.stop <- struct{}{} } - -func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) { - defer wg.Done() - waitForStart := true - for waitForStart { - select { - case <-l.stop: - l.logger.Info("not started yet") - case <-l.start: - waitForStart = false - case <-l.restart: - waitForStart = false - case <-ctx.Done(): - return - } - } - defer l.logger.Warn("loop exited") - - var previousPort uint16 - for ctx.Err() == nil { - for !l.isEnabled() { - // wait for a signal to re-enable - select { - case <-l.stop: - l.logger.Info("already disabled") - case <-l.restart: - l.setEnabled(true) - case <-l.start: - l.setEnabled(true) - case <-ctx.Done(): - return - } - } - - settings := l.GetSettings() - err := l.conf.MakeConf(settings.LogLevel, settings.Port, settings.User, settings.Password, l.uid, l.gid) - if err != nil { - l.logAndWait(ctx, err) - continue - } - - if previousPort > 0 { - if err := l.firewallConf.RemoveAllowedPort(ctx, previousPort); err != nil { - l.logger.Error(err) - continue - } - } - if err := l.firewallConf.SetAllowedPort(ctx, settings.Port, l.defaultInterface); err != nil { - l.logger.Error(err) - continue - } - previousPort = settings.Port - - tinyproxyCtx, tinyproxyCancel := context.WithCancel(context.Background()) - stream, waitFn, err := l.conf.Start(tinyproxyCtx) - if err != nil { - tinyproxyCancel() - l.logAndWait(ctx, err) - continue - } - go l.streamMerger.Merge(tinyproxyCtx, stream, command.MergeName("tinyproxy")) - waitError := make(chan error) - go func() { - err := waitFn() // blocking - waitError <- err - }() - stayHere := true - for stayHere { - select { - case <-ctx.Done(): - l.logger.Warn("context canceled: exiting loop") - tinyproxyCancel() - <-waitError - close(waitError) - return - case <-l.restart: // triggered restart - l.logger.Info("restarting") - tinyproxyCancel() - <-waitError - close(waitError) - stayHere = false - case <-l.start: - l.logger.Info("already started") - case <-l.stop: - l.logger.Info("stopping") - tinyproxyCancel() - <-waitError - close(waitError) - l.setEnabled(false) - stayHere = false - case err := <-waitError: // unexpected error - tinyproxyCancel() - close(waitError) - l.logAndWait(ctx, err) - } - } - tinyproxyCancel() // repetition for linter only - } -} diff --git a/internal/tinyproxy/tinyproxy.go b/internal/tinyproxy/tinyproxy.go deleted file mode 100644 index c5f1c1a6..00000000 --- a/internal/tinyproxy/tinyproxy.go +++ /dev/null @@ -1,30 +0,0 @@ -package tinyproxy - -import ( - "context" - "io" - - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/command" - "github.com/qdm12/golibs/files" - "github.com/qdm12/golibs/logging" -) - -type Configurator interface { - Version(ctx context.Context) (string, error) - MakeConf(logLevel models.TinyProxyLogLevel, port uint16, user, password string, uid, gid int) error - Start(ctx context.Context) (stdout io.ReadCloser, waitFn func() error, err error) -} - -type configurator struct { - fileManager files.FileManager - logger logging.Logger - commander command.Commander -} - -func NewConfigurator(fileManager files.FileManager, logger logging.Logger) Configurator { - return &configurator{ - fileManager: fileManager, - logger: logger.WithPrefix("tinyproxy configurator: "), - commander: command.NewCommander()} -}