HTTP proxy written in Go to replace Tinyproxy (#269)
This commit is contained in:
17
Dockerfile
17
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
|
||||
|
||||
29
README.md
29
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
|
||||
|
||||
</p></details>
|
||||
- <details><summary>Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)</summary><p>
|
||||
- <details><summary>Connect LAN devices through the built-in HTTP proxy (i.e. with Chrome, Kodi, etc.)</summary><p>
|
||||
|
||||
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
|
||||
|
||||
</p></details>
|
||||
- <details><summary>Connect LAN devices through the built-in *Shadowsocks* proxy (per app, system wide, etc.)</summary><p>
|
||||
|
||||
@@ -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,7 +83,6 @@ 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)
|
||||
@@ -93,7 +92,6 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
|
||||
"OpenVPN": ovpnConf.Version,
|
||||
"Unbound": dnsConf.Version,
|
||||
"IPtables": firewallConf.Version,
|
||||
"TinyProxy": tinyProxyConf.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)
|
||||
|
||||
@@ -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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
33
internal/httpproxy/auth.go
Normal file
33
internal/httpproxy/auth.go
Normal file
@@ -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
|
||||
}
|
||||
62
internal/httpproxy/handler.go
Normal file
62
internal/httpproxy/handler.go
Normal file
@@ -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",
|
||||
}
|
||||
74
internal/httpproxy/http.go
Normal file
74
internal/httpproxy/http.go
Normal file
@@ -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)
|
||||
}
|
||||
64
internal/httpproxy/https.go
Normal file
64
internal/httpproxy/https.go
Normal file
@@ -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()
|
||||
}
|
||||
139
internal/httpproxy/loop.go
Normal file
139
internal/httpproxy/loop.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
55
internal/httpproxy/server.go
Normal file
55
internal/httpproxy/server.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -13,12 +13,8 @@ import (
|
||||
//nolint:lll
|
||||
var regularExpressions = struct { //nolint:gochecknoglobals
|
||||
unboundPrefix *regexp.Regexp
|
||||
tinyproxyLoglevel *regexp.Regexp
|
||||
tinyproxyPrefix *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]+\]: `),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ============",
|
||||
|
||||
@@ -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.
|
||||
|
||||
78
internal/params/httpproxy.go
Normal file
78
internal/params/httpproxy.go
Normal file
@@ -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"))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
71
internal/settings/httpproxy.go
Normal file
71
internal/settings/httpproxy.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()}
|
||||
}
|
||||
Reference in New Issue
Block a user