Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa53436e56 | ||
|
|
8dfaebc737 | ||
|
|
062b6a276c | ||
|
|
647cd07de7 | ||
|
|
a530c84c5f | ||
|
|
0bb320065e | ||
|
|
d685d78e74 | ||
|
|
48896176e5 | ||
|
|
54dcf28b31 | ||
|
|
f8bf32bb34 | ||
|
|
748923021c | ||
|
|
a182e3503b | ||
|
|
991cfb8659 | ||
|
|
d0dfc21e2b | ||
|
|
617bd0c600 | ||
|
|
349b5429ba | ||
|
|
8db2944749 | ||
|
|
5986432a22 | ||
|
|
652daec509 | ||
|
|
f94d4b761a | ||
|
|
1ab74e6bb3 | ||
|
|
8e101d49a1 | ||
|
|
7c08e8f607 | ||
|
|
a4caa61c47 | ||
|
|
ebae167815 | ||
|
|
a6f00f2fb2 | ||
|
|
877617cc53 | ||
|
|
2800588ef7 | ||
|
|
f5efa42aaf | ||
|
|
10bd0e1505 | ||
|
|
a4c80b3045 | ||
|
|
dbb71bd695 | ||
|
|
a544f6e604 | ||
|
|
a18e026b70 |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -54,8 +54,10 @@ body:
|
||||
- PrivateVPN
|
||||
- ProtonVPN
|
||||
- PureVPN
|
||||
- SlickVPN
|
||||
- Surfshark
|
||||
- TorGuard
|
||||
- VPNSecure.me
|
||||
- VPNUnlimited
|
||||
- VyprVPN
|
||||
- WeVPN
|
||||
|
||||
5
.github/labels.yml
vendored
5
.github/labels.yml
vendored
@@ -64,12 +64,17 @@
|
||||
- name: ":cloud: PureVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: SlickVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Surfshark"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Torguard"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: VPNSecure.me"
|
||||
color: "cfe8d4"
|
||||
- name: ":cloud: VPNUnlimited"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
|
||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -49,6 +49,9 @@ jobs:
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Mocks check
|
||||
run: docker build --target mocks .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
@@ -123,7 +126,7 @@ jobs:
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
2
.github/workflows/labels.yml
vendored
2
.github/workflows/labels.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -19,7 +19,6 @@ issues:
|
||||
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
- gomnd
|
||||
|
||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
||||
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
@@ -35,8 +34,7 @@ linters:
|
||||
enable:
|
||||
# - cyclop
|
||||
# - errorlint
|
||||
# - varnamelen
|
||||
# - wrapcheck
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
@@ -67,8 +65,8 @@ linters:
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- grouper
|
||||
- ifshort
|
||||
- importas
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
@@ -83,8 +81,8 @@ linters:
|
||||
- nosprintfhostport
|
||||
- prealloc
|
||||
- predeclared
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
@@ -93,6 +91,7 @@ linters:
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -2,17 +2,21 @@ ARG ALPINE_VERSION=3.16
|
||||
ARG GO_ALPINE_VERSION=3.16
|
||||
ARG GO_VERSION=1.17
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.46.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.49.0
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
RUN apk --update add git g++
|
||||
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
|
||||
RUN apk --update add git g++ findutils
|
||||
ENV CGO_ENABLED=0
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
COPY --from=mockgen /bin /go/bin/mockgen
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
@@ -30,6 +34,18 @@ FROM --platform=${BUILDPLATFORM} base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||
RUN git init && \
|
||||
git config user.email ci@localhost && \
|
||||
git config user.name ci && \
|
||||
git config core.fileMode false && \
|
||||
git add -A && \
|
||||
git commit -m "snapshot" && \
|
||||
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
|
||||
go generate -run "mockgen" ./... && \
|
||||
git diff --exit-code && \
|
||||
rm -rf .git/
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG VERSION=unknown
|
||||
@@ -95,8 +111,15 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \
|
||||
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
# # Cyberghost only:
|
||||
OPENVPN_CERT= \
|
||||
OPENVPN_KEY= \
|
||||
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
||||
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
||||
# # VPNSecure only:
|
||||
OPENVPN_ENCRYPTED_KEY= \
|
||||
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
|
||||
OPENVPN_KEY_PASSPHRASE= \
|
||||
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
|
||||
# # Nordvpn only:
|
||||
SERVER_NUMBER= \
|
||||
# # PIA only:
|
||||
@@ -105,6 +128,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
FREE_ONLY= \
|
||||
# # Surfshark only:
|
||||
MULTIHOP_ONLY= \
|
||||
# # VPN Secure only:
|
||||
PREMIUM_ONLY= \
|
||||
# Firewall
|
||||
FIREWALL=on \
|
||||
FIREWALL_VPN_INPUT_PORTS= \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Gluetun VPN client
|
||||
|
||||
Lightweight swiss-knife-like VPN client to multiple VPN sercice providers
|
||||
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
|
||||

|
||||
|
||||
@@ -58,7 +58,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN sercice providers
|
||||
## Features
|
||||
|
||||
- Based on Alpine 3.16 for a small Docker image of 29MB
|
||||
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **Mullvad**, **Ivpn** and **Windscribe**
|
||||
|
||||
@@ -431,7 +431,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
|
||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper, storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot setup control server: %w", err)
|
||||
}
|
||||
@@ -489,18 +489,34 @@ func printVersions(ctx context.Context, logger infoer,
|
||||
}
|
||||
|
||||
type netLinker interface {
|
||||
Addresser
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Addresser interface {
|
||||
AddrList(link netlink.Link, family int) (
|
||||
addresses []netlink.Addr, err error)
|
||||
AddrAdd(link netlink.Link, addr *netlink.Addr) error
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(link netlink.Link, family int) (
|
||||
routes []netlink.Route, err error)
|
||||
RouteAdd(route *netlink.Route) error
|
||||
RouteDel(route *netlink.Route) error
|
||||
RouteReplace(route *netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleList(family int) (rules []netlink.Rule, err error)
|
||||
RuleAdd(rule *netlink.Rule) error
|
||||
RuleDel(rule *netlink.Rule) error
|
||||
}
|
||||
|
||||
type Linker interface {
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkByIndex(index int) (link netlink.Link, err error)
|
||||
|
||||
6
go.mod
6
go.mod
@@ -3,7 +3,7 @@ module github.com/qdm12/gluetun
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.3
|
||||
github.com/breml/rootcerts v0.2.6
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/qdm12/dns v1.11.0
|
||||
@@ -15,8 +15,9 @@ require (
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.4.0
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/text v0.3.7
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
|
||||
@@ -40,6 +41,5 @@ require (
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@@ -4,8 +4,8 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/g
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/breml/rootcerts v0.2.3 h1:1vkYjKOiHVSyuz9Ue4AOrViEvUm8gk8phTg0vbcuU0A=
|
||||
github.com/breml/rootcerts v0.2.3/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
|
||||
github.com/breml/rootcerts v0.2.6 h1:CdPczjzItec+wopLoDsBAFcLEai2q7Yayfg/94/q/2E=
|
||||
github.com/breml/rootcerts v0.2.6/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -127,13 +127,14 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
@@ -178,8 +179,9 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
|
||||
@@ -65,7 +65,7 @@ func (d *DoT) copy() (copied DoT) {
|
||||
// unset field of the receiver settings object.
|
||||
func (d *DoT) mergeWith(other DoT) {
|
||||
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.UpdatePeriod = helpers.MergeWithDurationPtr(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Unbound.mergeWith(other.Unbound)
|
||||
d.Blacklist.mergeWith(other.Blacklist)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func (d *DoT) mergeWith(other DoT) {
|
||||
// settings.
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.UpdatePeriod = helpers.OverrideWithDurationPtr(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Unbound.overrideWith(other.Unbound)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (d *DoT) overrideWith(other DoT) {
|
||||
func (d *DoT) setDefaults() {
|
||||
d.Enabled = helpers.DefaultBool(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.UpdatePeriod = helpers.DefaultDurationPtr(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Unbound.setDefaults()
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ var (
|
||||
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
||||
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
||||
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
||||
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
|
||||
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
||||
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
||||
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
||||
|
||||
@@ -3,6 +3,7 @@ package settings
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -15,6 +16,12 @@ type Health struct {
|
||||
// for the health check server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ServerAddress string
|
||||
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration of the
|
||||
// HTTP server. It defaults to 500 milliseconds.
|
||||
ReadTimeout time.Duration
|
||||
// TargetAddress is the address (host or host:port)
|
||||
// to TCP dial to periodically for the health check.
|
||||
// It cannot be the empty string in the internal state.
|
||||
@@ -40,9 +47,11 @@ func (h Health) Validate() (err error) {
|
||||
|
||||
func (h *Health) copy() (copied Health) {
|
||||
return Health{
|
||||
ServerAddress: h.ServerAddress,
|
||||
TargetAddress: h.TargetAddress,
|
||||
VPN: h.VPN.copy(),
|
||||
ServerAddress: h.ServerAddress,
|
||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||
ReadTimeout: h.ReadTimeout,
|
||||
TargetAddress: h.TargetAddress,
|
||||
VPN: h.VPN.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +59,8 @@ func (h *Health) copy() (copied Health) {
|
||||
// unset field of the receiver settings object.
|
||||
func (h *Health) MergeWith(other Health) {
|
||||
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
|
||||
h.ReadHeaderTimeout = helpers.MergeWithDuration(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = helpers.MergeWithDuration(h.ReadTimeout, other.ReadTimeout)
|
||||
h.TargetAddress = helpers.MergeWithString(h.TargetAddress, other.TargetAddress)
|
||||
h.VPN.mergeWith(other.VPN)
|
||||
}
|
||||
@@ -59,12 +70,18 @@ func (h *Health) MergeWith(other Health) {
|
||||
// settings.
|
||||
func (h *Health) OverrideWith(other Health) {
|
||||
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
|
||||
h.ReadHeaderTimeout = helpers.OverrideWithDuration(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = helpers.OverrideWithDuration(h.ReadTimeout, other.ReadTimeout)
|
||||
h.TargetAddress = helpers.OverrideWithString(h.TargetAddress, other.TargetAddress)
|
||||
h.VPN.overrideWith(other.VPN)
|
||||
}
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
|
||||
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||
h.ReadHeaderTimeout = helpers.DefaultDuration(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
const defaultReadTimeout = 500 * time.Millisecond
|
||||
h.ReadTimeout = helpers.DefaultDuration(h.ReadTimeout, defaultReadTimeout)
|
||||
h.TargetAddress = helpers.DefaultString(h.TargetAddress, "cloudflare.com:443")
|
||||
h.VPN.setDefaults()
|
||||
}
|
||||
@@ -77,6 +94,8 @@ func (h Health) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Health settings:")
|
||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||
node.Appendf("Target address: %s", h.TargetAddress)
|
||||
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -35,23 +35,23 @@ func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (h *HealthyWait) mergeWith(other HealthyWait) {
|
||||
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
|
||||
h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
|
||||
h.Initial = helpers.MergeWithDurationPtr(h.Initial, other.Initial)
|
||||
h.Addition = helpers.MergeWithDurationPtr(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
||||
h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
|
||||
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
|
||||
h.Initial = helpers.OverrideWithDurationPtr(h.Initial, other.Initial)
|
||||
h.Addition = helpers.OverrideWithDurationPtr(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
func (h *HealthyWait) setDefaults() {
|
||||
const initialDurationDefault = 6 * time.Second
|
||||
const additionDurationDefault = 5 * time.Second
|
||||
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
|
||||
h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
|
||||
h.Initial = helpers.DefaultDurationPtr(h.Initial, initialDurationDefault)
|
||||
h.Addition = helpers.DefaultDurationPtr(h.Addition, additionDurationDefault)
|
||||
}
|
||||
|
||||
func (h HealthyWait) String() string {
|
||||
|
||||
@@ -73,7 +73,15 @@ func DefaultStringPtr(existing *string, defaultValue string) (result *string) {
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultDuration(existing *time.Duration,
|
||||
func DefaultDuration(existing time.Duration,
|
||||
defaultValue time.Duration) (result time.Duration) {
|
||||
if existing != 0 {
|
||||
return existing
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func DefaultDurationPtr(existing *time.Duration,
|
||||
defaultValue time.Duration) (result *time.Duration) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
|
||||
@@ -107,7 +107,14 @@ func MergeWithIP(existing, other net.IP) (result net.IP) {
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
||||
func MergeWithDuration(existing, other time.Duration) (result time.Duration) {
|
||||
if existing != 0 {
|
||||
return existing
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
func MergeWithDurationPtr(existing, other *time.Duration) (result *time.Duration) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
|
||||
@@ -93,7 +93,16 @@ func OverrideWithIP(existing, other net.IP) (result net.IP) {
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
||||
func OverrideWithDuration(existing, other time.Duration) (
|
||||
result time.Duration) {
|
||||
if other == 0 {
|
||||
return existing
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
func OverrideWithDurationPtr(existing, other *time.Duration) (
|
||||
result *time.Duration) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package settings
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -33,6 +34,12 @@ type HTTPProxy struct {
|
||||
// each request/response. It cannot be nil in the
|
||||
// internal state.
|
||||
Log *bool
|
||||
// ReadHeaderTimeout is the HTTP header read timeout duration
|
||||
// of the HTTP server. It defaults to 1 second if left unset.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration
|
||||
// of the HTTP server. It defaults to 3 seconds if left unset.
|
||||
ReadTimeout time.Duration
|
||||
}
|
||||
|
||||
func (h HTTPProxy) validate() (err error) {
|
||||
@@ -49,12 +56,14 @@ func (h HTTPProxy) validate() (err error) {
|
||||
|
||||
func (h *HTTPProxy) copy() (copied HTTPProxy) {
|
||||
return HTTPProxy{
|
||||
User: helpers.CopyStringPtr(h.User),
|
||||
Password: helpers.CopyStringPtr(h.Password),
|
||||
ListeningAddress: h.ListeningAddress,
|
||||
Enabled: helpers.CopyBoolPtr(h.Enabled),
|
||||
Stealth: helpers.CopyBoolPtr(h.Stealth),
|
||||
Log: helpers.CopyBoolPtr(h.Log),
|
||||
User: helpers.CopyStringPtr(h.User),
|
||||
Password: helpers.CopyStringPtr(h.Password),
|
||||
ListeningAddress: h.ListeningAddress,
|
||||
Enabled: helpers.CopyBoolPtr(h.Enabled),
|
||||
Stealth: helpers.CopyBoolPtr(h.Stealth),
|
||||
Log: helpers.CopyBoolPtr(h.Log),
|
||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||
ReadTimeout: h.ReadTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +76,8 @@ func (h *HTTPProxy) mergeWith(other HTTPProxy) {
|
||||
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
|
||||
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
|
||||
h.Log = helpers.MergeWithBool(h.Log, other.Log)
|
||||
h.ReadHeaderTimeout = helpers.MergeWithDuration(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = helpers.MergeWithDuration(h.ReadTimeout, other.ReadTimeout)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
@@ -79,6 +90,8 @@ func (h *HTTPProxy) overrideWith(other HTTPProxy) {
|
||||
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
|
||||
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
|
||||
h.Log = helpers.OverrideWithBool(h.Log, other.Log)
|
||||
h.ReadHeaderTimeout = helpers.OverrideWithDuration(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = helpers.OverrideWithDuration(h.ReadTimeout, other.ReadTimeout)
|
||||
}
|
||||
|
||||
func (h *HTTPProxy) setDefaults() {
|
||||
@@ -88,6 +101,10 @@ func (h *HTTPProxy) setDefaults() {
|
||||
h.Enabled = helpers.DefaultBool(h.Enabled, false)
|
||||
h.Stealth = helpers.DefaultBool(h.Stealth, false)
|
||||
h.Log = helpers.DefaultBool(h.Log, false)
|
||||
const defaultReadHeaderTimeout = time.Second
|
||||
h.ReadHeaderTimeout = helpers.DefaultDuration(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
const defaultReadTimeout = 3 * time.Second
|
||||
h.ReadTimeout = helpers.DefaultDuration(h.ReadTimeout, defaultReadTimeout)
|
||||
}
|
||||
|
||||
func (h HTTPProxy) String() string {
|
||||
@@ -106,6 +123,8 @@ func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
|
||||
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
|
||||
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
|
||||
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -20,13 +20,15 @@ type OpenVPN struct {
|
||||
// It can only be "2.4" or "2.5".
|
||||
Version string
|
||||
// User is the OpenVPN authentication username.
|
||||
// It cannot be an empty string in the internal state
|
||||
// if OpenVPN is used.
|
||||
User string
|
||||
// It cannot be nil in the internal state if OpenVPN is used.
|
||||
// It is usually required but in some cases can be the empty string
|
||||
// to indicate no user+password authentication is needed.
|
||||
User *string
|
||||
// Password is the OpenVPN authentication password.
|
||||
// It cannot be an empty string in the internal state
|
||||
// if OpenVPN is used.
|
||||
Password string
|
||||
// It cannot be nil in the internal state if OpenVPN is used.
|
||||
// It is usually required but in some cases can be the empty string
|
||||
// to indicate no user+password authentication is needed.
|
||||
Password *string
|
||||
// ConfFile is a custom OpenVPN configuration file path.
|
||||
// It can be set to the empty string for it to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
@@ -40,16 +42,25 @@ type OpenVPN struct {
|
||||
// It cannot be nil in the internal state.
|
||||
// It is ignored if it is set to the empty string.
|
||||
Auth *string
|
||||
// ClientCrt is the OpenVPN client certificate.
|
||||
// This is notably used by Cyberghost.
|
||||
// Cert is the OpenVPN certificate for the <cert> block.
|
||||
// This is notably used by Cyberghost and VPN secure.
|
||||
// It can be set to the empty string to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
ClientCrt *string
|
||||
// ClientKey is the OpenVPN client key.
|
||||
Cert *string
|
||||
// Key is the OpenVPN key.
|
||||
// This is used by Cyberghost and VPN Unlimited.
|
||||
// It can be set to the empty string to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
ClientKey *string
|
||||
Key *string
|
||||
// EncryptedKey is the content of an encrypted
|
||||
// key for OpenVPN. It is used by VPN secure.
|
||||
// It defaults to the empty string meaning it is not
|
||||
// to be used. KeyPassphrase must be set if this one is set.
|
||||
EncryptedKey *string
|
||||
// KeyPassphrase is the key passphrase to be used by OpenVPN
|
||||
// to decrypt the EncryptedPrivateKey. It defaults to the
|
||||
// empty string and must be set if EncryptedPrivateKey is set.
|
||||
KeyPassphrase *string
|
||||
// PIAEncPreset is the encryption preset for
|
||||
// Private Internet Access. It can be set to an
|
||||
// empty string for other providers.
|
||||
@@ -89,14 +100,14 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||
|
||||
isCustom := vpnProvider == providers.Custom
|
||||
|
||||
if !isCustom && o.User == "" {
|
||||
if !isCustom && *o.User == "" {
|
||||
return ErrOpenVPNUserIsEmpty
|
||||
}
|
||||
|
||||
passwordRequired := !isCustom &&
|
||||
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(o.User))
|
||||
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
|
||||
|
||||
if passwordRequired && o.Password == "" {
|
||||
if passwordRequired && *o.Password == "" {
|
||||
return ErrOpenVPNPasswordIsEmpty
|
||||
}
|
||||
|
||||
@@ -105,16 +116,25 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||
return fmt.Errorf("custom configuration file: %w", err)
|
||||
}
|
||||
|
||||
err = validateOpenVPNClientCertificate(vpnProvider, *o.ClientCrt)
|
||||
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client certificate: %w", err)
|
||||
}
|
||||
|
||||
err = validateOpenVPNClientKey(vpnProvider, *o.ClientKey)
|
||||
err = validateOpenVPNClientKey(vpnProvider, *o.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client key: %w", err)
|
||||
}
|
||||
|
||||
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypted key: %w", err)
|
||||
}
|
||||
|
||||
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
|
||||
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
|
||||
}
|
||||
|
||||
const maxMSSFix = 10000
|
||||
if *o.MSSFix > maxMSSFix {
|
||||
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
||||
@@ -163,6 +183,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
|
||||
switch vpnProvider {
|
||||
case
|
||||
providers.Cyberghost,
|
||||
providers.VPNSecure,
|
||||
providers.VPNUnlimited:
|
||||
if clientCert == "" {
|
||||
return ErrMissingValue
|
||||
@@ -202,23 +223,42 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||
encryptedPrivateKey string) (err error) {
|
||||
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||
return ErrMissingValue
|
||||
}
|
||||
|
||||
if encryptedPrivateKey == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = base64.StdEncoding.DecodeString(encryptedPrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenVPN) copy() (copied OpenVPN) {
|
||||
return OpenVPN{
|
||||
Version: o.Version,
|
||||
User: o.User,
|
||||
Password: o.Password,
|
||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
||||
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
||||
Auth: helpers.CopyStringPtr(o.Auth),
|
||||
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
|
||||
ClientKey: helpers.CopyStringPtr(o.ClientKey),
|
||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
||||
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
||||
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
||||
Interface: o.Interface,
|
||||
ProcessUser: o.ProcessUser,
|
||||
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
||||
Flags: helpers.CopyStringSlice(o.Flags),
|
||||
Version: o.Version,
|
||||
User: helpers.CopyStringPtr(o.User),
|
||||
Password: helpers.CopyStringPtr(o.Password),
|
||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
||||
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
||||
Auth: helpers.CopyStringPtr(o.Auth),
|
||||
Cert: helpers.CopyStringPtr(o.Cert),
|
||||
Key: helpers.CopyStringPtr(o.Key),
|
||||
EncryptedKey: helpers.CopyStringPtr(o.EncryptedKey),
|
||||
KeyPassphrase: helpers.CopyStringPtr(o.KeyPassphrase),
|
||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
||||
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
||||
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
||||
Interface: o.Interface,
|
||||
ProcessUser: o.ProcessUser,
|
||||
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
||||
Flags: helpers.CopyStringSlice(o.Flags),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,13 +266,15 @@ func (o *OpenVPN) copy() (copied OpenVPN) {
|
||||
// unset field of the receiver settings object.
|
||||
func (o *OpenVPN) mergeWith(other OpenVPN) {
|
||||
o.Version = helpers.MergeWithString(o.Version, other.Version)
|
||||
o.User = helpers.MergeWithString(o.User, other.User)
|
||||
o.Password = helpers.MergeWithString(o.Password, other.Password)
|
||||
o.User = helpers.MergeWithStringPtr(o.User, other.User)
|
||||
o.Password = helpers.MergeWithStringPtr(o.Password, other.Password)
|
||||
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
|
||||
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
|
||||
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
|
||||
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
|
||||
o.Cert = helpers.MergeWithStringPtr(o.Cert, other.Cert)
|
||||
o.Key = helpers.MergeWithStringPtr(o.Key, other.Key)
|
||||
o.EncryptedKey = helpers.MergeWithStringPtr(o.EncryptedKey, other.EncryptedKey)
|
||||
o.KeyPassphrase = helpers.MergeWithStringPtr(o.KeyPassphrase, other.KeyPassphrase)
|
||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
|
||||
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
|
||||
@@ -247,13 +289,15 @@ func (o *OpenVPN) mergeWith(other OpenVPN) {
|
||||
// settings.
|
||||
func (o *OpenVPN) overrideWith(other OpenVPN) {
|
||||
o.Version = helpers.OverrideWithString(o.Version, other.Version)
|
||||
o.User = helpers.OverrideWithString(o.User, other.User)
|
||||
o.Password = helpers.OverrideWithString(o.Password, other.Password)
|
||||
o.User = helpers.OverrideWithStringPtr(o.User, other.User)
|
||||
o.Password = helpers.OverrideWithStringPtr(o.Password, other.Password)
|
||||
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
|
||||
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
|
||||
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
|
||||
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
|
||||
o.Cert = helpers.OverrideWithStringPtr(o.Cert, other.Cert)
|
||||
o.Key = helpers.OverrideWithStringPtr(o.Key, other.Key)
|
||||
o.EncryptedKey = helpers.OverrideWithStringPtr(o.EncryptedKey, other.EncryptedKey)
|
||||
o.KeyPassphrase = helpers.OverrideWithStringPtr(o.KeyPassphrase, other.KeyPassphrase)
|
||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
|
||||
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
|
||||
@@ -265,14 +309,19 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
|
||||
|
||||
func (o *OpenVPN) setDefaults(vpnProvider string) {
|
||||
o.Version = helpers.DefaultString(o.Version, openvpn.Openvpn25)
|
||||
o.User = helpers.DefaultStringPtr(o.User, "")
|
||||
if vpnProvider == providers.Mullvad {
|
||||
o.Password = "m"
|
||||
o.Password = helpers.DefaultStringPtr(o.Password, "m")
|
||||
} else {
|
||||
o.Password = helpers.DefaultStringPtr(o.Password, "")
|
||||
}
|
||||
|
||||
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
|
||||
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
|
||||
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
|
||||
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
|
||||
o.Cert = helpers.DefaultStringPtr(o.Cert, "")
|
||||
o.Key = helpers.DefaultStringPtr(o.Key, "")
|
||||
o.EncryptedKey = helpers.DefaultStringPtr(o.EncryptedKey, "")
|
||||
o.KeyPassphrase = helpers.DefaultStringPtr(o.KeyPassphrase, "")
|
||||
|
||||
var defaultEncPreset string
|
||||
if vpnProvider == providers.PrivateInternetAccess {
|
||||
@@ -294,8 +343,8 @@ func (o OpenVPN) String() string {
|
||||
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OpenVPN settings:")
|
||||
node.Appendf("OpenVPN version: %s", o.Version)
|
||||
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
|
||||
node.Appendf("User: %s", helpers.ObfuscatePassword(*o.User))
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(*o.Password))
|
||||
|
||||
if *o.ConfFile != "" {
|
||||
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||
@@ -309,12 +358,17 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Auth: %s", *o.Auth)
|
||||
}
|
||||
|
||||
if *o.ClientCrt != "" {
|
||||
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
|
||||
if *o.Cert != "" {
|
||||
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.Cert))
|
||||
}
|
||||
|
||||
if *o.ClientKey != "" {
|
||||
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
|
||||
if *o.Key != "" {
|
||||
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.Key))
|
||||
}
|
||||
|
||||
if *o.EncryptedKey != "" {
|
||||
node.Appendf("Encrypted key: %s (key passhrapse %s)",
|
||||
helpers.ObfuscateData(*o.EncryptedKey), helpers.ObfuscatePassword(*o.KeyPassphrase))
|
||||
}
|
||||
|
||||
if *o.PIAEncPreset != "" {
|
||||
|
||||
@@ -60,8 +60,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Privado, providers.Purevpn,
|
||||
providers.Surfshark, providers.VPNUnlimited,
|
||||
providers.Vyprvpn:
|
||||
providers.Surfshark, providers.VPNSecure,
|
||||
providers.VPNUnlimited, providers.Vyprvpn:
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
||||
default:
|
||||
@@ -82,6 +82,9 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Protonvpn:
|
||||
allowedTCP = []uint16{443, 5995, 8443}
|
||||
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
|
||||
case providers.SlickVPN:
|
||||
allowedTCP = []uint16{443, 8080, 8888}
|
||||
allowedUDP = []uint16{443, 8080, 8888}
|
||||
case providers.Wevpn:
|
||||
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||
allowedUDP = []uint16{80, 1194, 1198}
|
||||
|
||||
@@ -48,18 +48,18 @@ func (p *PublicIP) copy() (copied PublicIP) {
|
||||
}
|
||||
|
||||
func (p *PublicIP) mergeWith(other PublicIP) {
|
||||
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
|
||||
p.Period = helpers.MergeWithDurationPtr(p.Period, other.Period)
|
||||
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
|
||||
}
|
||||
|
||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||
p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
|
||||
p.Period = helpers.OverrideWithDurationPtr(p.Period, other.Period)
|
||||
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
|
||||
}
|
||||
|
||||
func (p *PublicIP) setDefaults() {
|
||||
const defaultPeriod = 12 * time.Hour
|
||||
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
|
||||
p.Period = helpers.DefaultDurationPtr(p.Period, defaultPeriod)
|
||||
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ type ServerSelection struct { //nolint:maligned
|
||||
// FreeOnly is true if VPN servers that are not free should
|
||||
// be filtered. This is used with ProtonVPN and VPN Unlimited.
|
||||
FreeOnly *bool
|
||||
// PremiumOnly is true if VPN servers that are not premium should
|
||||
// be filtered. This is used with VPN Secure.
|
||||
// TODO extend to providers using FreeOnly.
|
||||
PremiumOnly *bool
|
||||
// StreamOnly is true if VPN servers not for streaming should
|
||||
// be filtered. This is used with VPNUnlimited.
|
||||
StreamOnly *bool
|
||||
@@ -63,8 +67,10 @@ type ServerSelection struct { //nolint:maligned
|
||||
var (
|
||||
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
|
||||
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
|
||||
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
|
||||
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
|
||||
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
||||
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
|
||||
)
|
||||
|
||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
@@ -103,6 +109,18 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
ErrFreeOnlyNotSupported, vpnServiceProvider)
|
||||
}
|
||||
|
||||
if *ss.PremiumOnly &&
|
||||
!helpers.IsOneOf(vpnServiceProvider,
|
||||
providers.VPNSecure,
|
||||
) {
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
ErrPremiumOnlyNotSupported, vpnServiceProvider)
|
||||
}
|
||||
|
||||
if *ss.FreeOnly && *ss.PremiumOnly {
|
||||
return ErrFreePremiumBothSet
|
||||
}
|
||||
|
||||
if *ss.StreamOnly &&
|
||||
!helpers.IsOneOf(vpnServiceProvider,
|
||||
providers.Protonvpn,
|
||||
@@ -194,6 +212,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
Numbers: helpers.CopyUint16Slice(ss.Numbers),
|
||||
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
|
||||
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
|
||||
PremiumOnly: helpers.CopyBoolPtr(ss.PremiumOnly),
|
||||
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
|
||||
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
|
||||
OpenVPN: ss.OpenVPN.copy(),
|
||||
@@ -213,6 +232,7 @@ func (ss *ServerSelection) mergeWith(other ServerSelection) {
|
||||
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
|
||||
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
|
||||
ss.PremiumOnly = helpers.MergeWithBool(ss.PremiumOnly, other.PremiumOnly)
|
||||
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
|
||||
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||
|
||||
@@ -232,6 +252,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
|
||||
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
|
||||
ss.PremiumOnly = helpers.OverrideWithBool(ss.PremiumOnly, other.PremiumOnly)
|
||||
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
|
||||
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||
@@ -243,6 +264,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) {
|
||||
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
|
||||
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
|
||||
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
|
||||
ss.PremiumOnly = helpers.DefaultBool(ss.PremiumOnly, false)
|
||||
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
|
||||
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
|
||||
ss.OpenVPN.setDefaults(vpnProvider)
|
||||
@@ -299,6 +321,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Free only servers: yes")
|
||||
}
|
||||
|
||||
if *ss.PremiumOnly {
|
||||
node.Appendf("Premium only servers: yes")
|
||||
}
|
||||
|
||||
if *ss.StreamOnly {
|
||||
node.Appendf("Stream only servers: yes")
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (s *Settings) Validate(storage Storage) (err error) {
|
||||
"version": s.Version.validate,
|
||||
// Pprof validation done in pprof constructor
|
||||
"VPN": func() error {
|
||||
return s.VPN.validate(storage)
|
||||
return s.VPN.Validate(storage)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (s *Settings) copy() (copied Settings) {
|
||||
System: s.System.copy(),
|
||||
Updater: s.Updater.copy(),
|
||||
Version: s.Version.copy(),
|
||||
VPN: s.VPN.copy(),
|
||||
VPN: s.VPN.Copy(),
|
||||
Pprof: s.Pprof.Copy(),
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
patchedSettings.System.overrideWith(other.System)
|
||||
patchedSettings.Updater.overrideWith(other.Updater)
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.overrideWith(other.VPN)
|
||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||
err = patchedSettings.Validate(storage)
|
||||
if err != nil {
|
||||
|
||||
@@ -67,6 +67,8 @@ func Test_Settings_String(t *testing.T) {
|
||||
├── Health settings:
|
||||
| ├── Server listening address: 127.0.0.1:9999
|
||||
| ├── Target address: cloudflare.com:443
|
||||
| ├── Read header timeout: 100ms
|
||||
| ├── Read timeout: 500ms
|
||||
| └── VPN wait durations:
|
||||
| ├── Initial duration: 6s
|
||||
| └── Additional duration: 5s
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *Updater) copy() (copied Updater) {
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (u *Updater) mergeWith(other Updater) {
|
||||
u.Period = helpers.MergeWithDuration(u.Period, other.Period)
|
||||
u.Period = helpers.MergeWithDurationPtr(u.Period, other.Period)
|
||||
u.DNSAddress = helpers.MergeWithString(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = helpers.MergeWithFloat64(u.MinRatio, other.MinRatio)
|
||||
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
|
||||
@@ -83,14 +83,14 @@ func (u *Updater) mergeWith(other Updater) {
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (u *Updater) overrideWith(other Updater) {
|
||||
u.Period = helpers.OverrideWithDuration(u.Period, other.Period)
|
||||
u.Period = helpers.OverrideWithDurationPtr(u.Period, other.Period)
|
||||
u.DNSAddress = helpers.OverrideWithString(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = helpers.OverrideWithFloat64(u.MinRatio, other.MinRatio)
|
||||
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
|
||||
}
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
u.Period = helpers.DefaultDuration(u.Period, 0)
|
||||
u.Period = helpers.DefaultDurationPtr(u.Period, 0)
|
||||
u.DNSAddress = helpers.DefaultString(u.DNSAddress, "1.1.1.1:53")
|
||||
|
||||
if u.MinRatio == 0 {
|
||||
|
||||
@@ -20,7 +20,7 @@ type VPN struct {
|
||||
}
|
||||
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (v *VPN) validate(storage Storage) (err error) {
|
||||
func (v *VPN) Validate(storage Storage) (err error) {
|
||||
// Validate Type
|
||||
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
||||
if !helpers.IsOneOf(v.Type, validVPNTypes...) {
|
||||
@@ -48,7 +48,7 @@ func (v *VPN) validate(storage Storage) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VPN) copy() (copied VPN) {
|
||||
func (v *VPN) Copy() (copied VPN) {
|
||||
return VPN{
|
||||
Type: v.Type,
|
||||
Provider: v.Provider.copy(),
|
||||
@@ -64,7 +64,7 @@ func (v *VPN) mergeWith(other VPN) {
|
||||
v.Wireguard.mergeWith(other.Wireguard)
|
||||
}
|
||||
|
||||
func (v *VPN) overrideWith(other VPN) {
|
||||
func (v *VPN) OverrideWith(other VPN) {
|
||||
v.Type = helpers.OverrideWithString(v.Type, other.Type)
|
||||
v.Provider.overrideWith(other.Provider)
|
||||
v.OpenVPN.overrideWith(other.OpenVPN)
|
||||
|
||||
40
internal/configuration/sources/env/openvpn.go
vendored
40
internal/configuration/sources/env/openvpn.go
vendored
@@ -11,7 +11,8 @@ import (
|
||||
func (r *Reader) readOpenVPN() (
|
||||
openVPN settings.OpenVPN, err error) {
|
||||
defer func() {
|
||||
err = unsetEnvKeys([]string{"OPENVPN_CLIENTKEY", "OPENVPN_CLIENTCRT"}, err)
|
||||
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT",
|
||||
"OPENVPN_KEY_PASSPHRASE", "OPENVPN_ENCRYPTED_KEY"}, err)
|
||||
}()
|
||||
|
||||
openVPN.Version = getCleanedEnv("OPENVPN_VERSION")
|
||||
@@ -30,8 +31,11 @@ func (r *Reader) readOpenVPN() (
|
||||
openVPN.Auth = &auth
|
||||
}
|
||||
|
||||
openVPN.ClientCrt = envToStringPtr("OPENVPN_CLIENTCRT")
|
||||
openVPN.ClientKey = envToStringPtr("OPENVPN_CLIENTKEY")
|
||||
openVPN.Cert = envToStringPtr("OPENVPN_CERT")
|
||||
openVPN.Key = envToStringPtr("OPENVPN_KEY")
|
||||
openVPN.EncryptedKey = envToStringPtr("OPENVPN_ENCRYPTED_KEY")
|
||||
|
||||
openVPN.KeyPassphrase = r.readOpenVPNKeyPassphrase()
|
||||
|
||||
openVPN.PIAEncPreset = r.readPIAEncryptionPreset()
|
||||
|
||||
@@ -65,17 +69,37 @@ func (r *Reader) readOpenVPN() (
|
||||
return openVPN, nil
|
||||
}
|
||||
|
||||
func (r *Reader) readOpenVPNUser() (user string) {
|
||||
_, user = r.getEnvWithRetro("OPENVPN_USER", "USER")
|
||||
func (r *Reader) readOpenVPNUser() (user *string) {
|
||||
user = new(string)
|
||||
_, *user = r.getEnvWithRetro("OPENVPN_USER", "USER")
|
||||
if *user == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
|
||||
return strings.ReplaceAll(user, " ", "")
|
||||
*user = strings.ReplaceAll(*user, " ", "")
|
||||
return user
|
||||
}
|
||||
|
||||
func (r *Reader) readOpenVPNPassword() (password string) {
|
||||
_, password = r.getEnvWithRetro("OPENVPN_PASSWORD", "PASSWORD")
|
||||
func (r *Reader) readOpenVPNPassword() (password *string) {
|
||||
password = new(string)
|
||||
_, *password = r.getEnvWithRetro("OPENVPN_PASSWORD", "PASSWORD")
|
||||
if *password == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
func (r *Reader) readOpenVPNKeyPassphrase() (passphrase *string) {
|
||||
passphrase = new(string)
|
||||
*passphrase = getCleanedEnv("OPENVPN_KEY_PASSPHRASE")
|
||||
if *passphrase == "" {
|
||||
return nil
|
||||
}
|
||||
return passphrase
|
||||
}
|
||||
|
||||
func (r *Reader) readPIAEncryptionPreset() (presetPtr *string) {
|
||||
_, preset := r.getEnvWithRetro(
|
||||
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
|
||||
|
||||
@@ -77,6 +77,12 @@ func (r *Reader) readServerSelection(vpnProvider, vpnType string) (
|
||||
return ss, fmt.Errorf("environment variable FREE_ONLY: %w", err)
|
||||
}
|
||||
|
||||
// VPNSecure only
|
||||
ss.PremiumOnly, err = envToBoolPtr("PREMIUM_ONLY")
|
||||
if err != nil {
|
||||
return ss, fmt.Errorf("environment variable PREMIUM_ONLY: %w", err)
|
||||
}
|
||||
|
||||
// VPNUnlimited only
|
||||
ss.MultiHopOnly, err = envToBoolPtr("MULTIHOP_ONLY")
|
||||
if err != nil {
|
||||
|
||||
@@ -11,18 +11,23 @@ const (
|
||||
OpenVPNClientKeyPath = "/gluetun/client.key"
|
||||
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
|
||||
OpenVPNClientCertificatePath = "/gluetun/client.crt"
|
||||
openVPNEncryptedKey = "/gluetun/openvpn_encrypted_key"
|
||||
)
|
||||
|
||||
func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) {
|
||||
settings.ClientKey, err = readPEMFile(OpenVPNClientKeyPath)
|
||||
settings.Key, err = readPEMFile(OpenVPNClientKeyPath)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("client key: %w", err)
|
||||
}
|
||||
|
||||
settings.ClientCrt, err = readPEMFile(OpenVPNClientCertificatePath)
|
||||
settings.Cert, err = readPEMFile(OpenVPNClientCertificatePath)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("client certificate: %w", err)
|
||||
}
|
||||
settings.EncryptedKey, err = readPEMFile(openVPNEncryptedKey)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("reading encrypted key file: %w", err)
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
@@ -28,21 +28,6 @@ func readSecretFileAsStringPtr(secretPathEnvKey, defaultSecretPath string) (
|
||||
return files.ReadFromFile(path)
|
||||
}
|
||||
|
||||
func readSecretFileAsString(secretPathEnvKey, defaultSecretPath string) (
|
||||
s string, err error) {
|
||||
path := getCleanedEnv(secretPathEnvKey)
|
||||
if path == "" {
|
||||
path = defaultSecretPath
|
||||
}
|
||||
stringPtr, err := files.ReadFromFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if stringPtr == nil {
|
||||
return "", nil
|
||||
}
|
||||
return *stringPtr, nil
|
||||
}
|
||||
|
||||
func readPEMSecretFile(secretPathEnvKey, defaultSecretPath string) (
|
||||
base64Ptr *string, err error) {
|
||||
pemData, err := readSecretFileAsStringPtr(secretPathEnvKey, defaultSecretPath)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func readOpenVPN() (
|
||||
settings settings.OpenVPN, err error) {
|
||||
settings.User, err = readSecretFileAsString(
|
||||
settings.User, err = readSecretFileAsStringPtr(
|
||||
"OPENVPN_USER_SECRETFILE",
|
||||
"/run/secrets/openvpn_user",
|
||||
)
|
||||
@@ -16,7 +16,7 @@ func readOpenVPN() (
|
||||
return settings, fmt.Errorf("cannot read user file: %w", err)
|
||||
}
|
||||
|
||||
settings.Password, err = readSecretFileAsString(
|
||||
settings.Password, err = readSecretFileAsStringPtr(
|
||||
"OPENVPN_PASSWORD_SECRETFILE",
|
||||
"/run/secrets/openvpn_password",
|
||||
)
|
||||
@@ -24,7 +24,7 @@ func readOpenVPN() (
|
||||
return settings, fmt.Errorf("cannot read password file: %w", err)
|
||||
}
|
||||
|
||||
settings.ClientKey, err = readPEMSecretFile(
|
||||
settings.Key, err = readPEMSecretFile(
|
||||
"OPENVPN_CLIENTKEY_SECRETFILE",
|
||||
"/run/secrets/openvpn_clientkey",
|
||||
)
|
||||
@@ -32,7 +32,23 @@ func readOpenVPN() (
|
||||
return settings, fmt.Errorf("cannot read client key file: %w", err)
|
||||
}
|
||||
|
||||
settings.ClientCrt, err = readPEMSecretFile(
|
||||
settings.EncryptedKey, err = readPEMSecretFile(
|
||||
"OPENVPN_ENCRYPTED_KEY_SECRETFILE",
|
||||
"/run/secrets/openvpn_encrypted_key",
|
||||
)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("reading encrypted key file: %w", err)
|
||||
}
|
||||
|
||||
settings.KeyPassphrase, err = readSecretFileAsStringPtr(
|
||||
"OPENVPN_KEY_PASSPHRASE_SECRETFILE",
|
||||
"/run/secrets/openvpn_key_passphrase",
|
||||
)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("reading key passphrase file: %w", err)
|
||||
}
|
||||
|
||||
settings.Cert, err = readPEMSecretFile(
|
||||
"OPENVPN_CLIENTCRT_SECRETFILE",
|
||||
"/run/secrets/openvpn_clientcrt",
|
||||
)
|
||||
|
||||
@@ -3,4 +3,7 @@ package openvpn
|
||||
const (
|
||||
// AuthConf is the file path to the OpenVPN auth file.
|
||||
AuthConf = "/etc/openvpn/auth.conf"
|
||||
// AskPassPath is the file path to the decryption passphrase for
|
||||
// and encrypted private key, which is pointed by `askpass`.
|
||||
AskPassPath = "/etc/openvpn/askpass" //nolint:gosec
|
||||
)
|
||||
|
||||
@@ -19,8 +19,10 @@ const (
|
||||
Privatevpn = "privatevpn"
|
||||
Protonvpn = "protonvpn"
|
||||
Purevpn = "purevpn"
|
||||
SlickVPN = "slickvpn"
|
||||
Surfshark = "surfshark"
|
||||
Torguard = "torguard"
|
||||
VPNSecure = "vpnsecure"
|
||||
VPNUnlimited = "vpn unlimited"
|
||||
Vyprvpn = "vyprvpn"
|
||||
Wevpn = "wevpn"
|
||||
@@ -44,8 +46,10 @@ func All() []string {
|
||||
Privatevpn,
|
||||
Protonvpn,
|
||||
Purevpn,
|
||||
SlickVPN,
|
||||
Surfshark,
|
||||
Torguard,
|
||||
VPNSecure,
|
||||
VPNUnlimited,
|
||||
Vyprvpn,
|
||||
Wevpn,
|
||||
|
||||
@@ -14,8 +14,10 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||
go s.runHealthcheckLoop(ctx, loopDone)
|
||||
|
||||
server := http.Server{
|
||||
Addr: s.config.ServerAddress,
|
||||
Handler: s.handler,
|
||||
Addr: s.config.ServerAddress,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: s.config.ReadHeaderTimeout,
|
||||
ReadTimeout: s.config.ReadTimeout,
|
||||
}
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
|
||||
@@ -23,7 +23,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
settings := l.state.GetSettings()
|
||||
server := New(runCtx, settings.ListeningAddress, l.logger,
|
||||
*settings.Stealth, *settings.Log, *settings.User,
|
||||
*settings.Password)
|
||||
*settings.Password, settings.ReadHeaderTimeout, settings.ReadTimeout)
|
||||
|
||||
errorCh := make(chan error)
|
||||
go server.Run(runCtx, errorCh)
|
||||
|
||||
@@ -8,25 +8,35 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
address string
|
||||
handler http.Handler
|
||||
logger infoErrorer
|
||||
internalWG *sync.WaitGroup
|
||||
address string
|
||||
handler http.Handler
|
||||
logger infoErrorer
|
||||
internalWG *sync.WaitGroup
|
||||
readHeaderTimeout time.Duration
|
||||
readTimeout time.Duration
|
||||
}
|
||||
|
||||
func New(ctx context.Context, address string, logger Logger,
|
||||
stealth, verbose bool, username, password string) *Server {
|
||||
stealth, verbose bool, username, password string,
|
||||
readHeaderTimeout, readTimeout time.Duration) *Server {
|
||||
wg := &sync.WaitGroup{}
|
||||
return &Server{
|
||||
address: address,
|
||||
handler: newHandler(ctx, wg, logger, stealth, verbose, username, password),
|
||||
logger: logger,
|
||||
internalWG: wg,
|
||||
address: address,
|
||||
handler: newHandler(ctx, wg, logger, stealth, verbose, username, password),
|
||||
logger: logger,
|
||||
internalWG: wg,
|
||||
readHeaderTimeout: readHeaderTimeout,
|
||||
readTimeout: readTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context, errorCh chan<- error) {
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
server := http.Server{
|
||||
Addr: s.address,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: s.readHeaderTimeout,
|
||||
ReadTimeout: s.readTimeout,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
|
||||
@@ -2,13 +2,10 @@ package httpserver
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func durationPtr(d time.Duration) *time.Duration { return &d }
|
||||
|
||||
var _ Logger = (*testLogger)(nil)
|
||||
|
||||
type testLogger struct{}
|
||||
|
||||
@@ -11,7 +11,12 @@ import (
|
||||
// The done channel has an error written to when the HTTP server
|
||||
// is terminated, and can be nil or not nil.
|
||||
func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- struct{}) {
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
server := http.Server{
|
||||
Addr: s.address,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: s.readHeaderTimeout,
|
||||
ReadTimeout: s.readTimeout,
|
||||
}
|
||||
|
||||
crashed := make(chan struct{})
|
||||
shutdownDone := make(chan struct{})
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
// Server is an HTTP server implementation, which uses
|
||||
// the HTTP handler provided.
|
||||
type Server struct {
|
||||
address string
|
||||
addressSet chan struct{}
|
||||
handler http.Handler
|
||||
logger Logger
|
||||
shutdownTimeout time.Duration
|
||||
address string
|
||||
addressSet chan struct{}
|
||||
handler http.Handler
|
||||
logger Logger
|
||||
readHeaderTimeout time.Duration
|
||||
readTimeout time.Duration
|
||||
shutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
// New creates a new HTTP server with the given settings.
|
||||
@@ -26,10 +28,12 @@ func New(settings Settings) (s *Server, err error) {
|
||||
}
|
||||
|
||||
return &Server{
|
||||
address: settings.Address,
|
||||
addressSet: make(chan struct{}),
|
||||
handler: settings.Handler,
|
||||
logger: settings.Logger,
|
||||
shutdownTimeout: *settings.ShutdownTimeout,
|
||||
address: settings.Address,
|
||||
addressSet: make(chan struct{}),
|
||||
handler: settings.Handler,
|
||||
logger: settings.Logger,
|
||||
readHeaderTimeout: settings.ReadHeaderTimeout,
|
||||
readTimeout: settings.ReadTimeout,
|
||||
shutdownTimeout: settings.ShutdownTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -29,16 +29,20 @@ func Test_New(t *testing.T) {
|
||||
},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: &Server{
|
||||
address: ":8001",
|
||||
handler: someHandler,
|
||||
logger: someLogger,
|
||||
shutdownTimeout: time.Second,
|
||||
address: ":8001",
|
||||
handler: someHandler,
|
||||
logger: someLogger,
|
||||
readHeaderTimeout: time.Second,
|
||||
readTimeout: time.Second,
|
||||
shutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,23 +22,34 @@ type Settings struct {
|
||||
// Logger is the logger to use.
|
||||
// It must be set and cannot be left to nil.
|
||||
Logger Logger
|
||||
// ReadHeaderTimeout is the HTTP header read timeout duration
|
||||
// of the HTTP server. It defaults to 3 seconds if left unset.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration
|
||||
// of the HTTP server. It defaults to 3 seconds if left unset.
|
||||
ReadTimeout time.Duration
|
||||
// ShutdownTimeout is the shutdown timeout duration
|
||||
// of the HTTP server. It defaults to 3 seconds.
|
||||
ShutdownTimeout *time.Duration
|
||||
// of the HTTP server. It defaults to 3 seconds if left unset.
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Address = helpers.DefaultString(s.Address, ":8000")
|
||||
const defaultReadTimeout = 3 * time.Second
|
||||
s.ReadHeaderTimeout = helpers.DefaultDuration(s.ReadHeaderTimeout, defaultReadTimeout)
|
||||
s.ReadTimeout = helpers.DefaultDuration(s.ReadTimeout, defaultReadTimeout)
|
||||
const defaultShutdownTimeout = 3 * time.Second
|
||||
s.ShutdownTimeout = helpers.DefaultDuration(s.ShutdownTimeout, defaultShutdownTimeout)
|
||||
}
|
||||
|
||||
func (s Settings) Copy() Settings {
|
||||
return Settings{
|
||||
Address: s.Address,
|
||||
Handler: s.Handler,
|
||||
Logger: s.Logger,
|
||||
ShutdownTimeout: helpers.CopyDurationPtr(s.ShutdownTimeout),
|
||||
Address: s.Address,
|
||||
Handler: s.Handler,
|
||||
Logger: s.Logger,
|
||||
ReadHeaderTimeout: s.ReadHeaderTimeout,
|
||||
ReadTimeout: s.ReadTimeout,
|
||||
ShutdownTimeout: s.ShutdownTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +59,8 @@ func (s *Settings) MergeWith(other Settings) {
|
||||
if s.Logger == nil {
|
||||
s.Logger = other.Logger
|
||||
}
|
||||
s.ReadHeaderTimeout = helpers.MergeWithDuration(s.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
s.ReadTimeout = helpers.MergeWithDuration(s.ReadTimeout, other.ReadTimeout)
|
||||
s.ShutdownTimeout = helpers.MergeWithDuration(s.ShutdownTimeout, other.ShutdownTimeout)
|
||||
}
|
||||
|
||||
@@ -57,13 +70,17 @@ func (s *Settings) OverrideWith(other Settings) {
|
||||
if other.Logger != nil {
|
||||
s.Logger = other.Logger
|
||||
}
|
||||
s.ReadHeaderTimeout = helpers.OverrideWithDuration(s.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
s.ReadTimeout = helpers.OverrideWithDuration(s.ReadTimeout, other.ReadTimeout)
|
||||
s.ShutdownTimeout = helpers.OverrideWithDuration(s.ShutdownTimeout, other.ShutdownTimeout)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrHandlerIsNotSet = errors.New("HTTP handler cannot be left unset")
|
||||
ErrLoggerIsNotSet = errors.New("logger cannot be left unset")
|
||||
ErrShutdownTimeoutTooSmall = errors.New("shutdown timeout is too small")
|
||||
ErrHandlerIsNotSet = errors.New("HTTP handler cannot be left unset")
|
||||
ErrLoggerIsNotSet = errors.New("logger cannot be left unset")
|
||||
ErrReadHeaderTimeoutTooSmall = errors.New("read header timeout is too small")
|
||||
ErrReadTimeoutTooSmall = errors.New("read timeout is too small")
|
||||
ErrShutdownTimeoutTooSmall = errors.New("shutdown timeout is too small")
|
||||
)
|
||||
|
||||
func (s Settings) Validate() (err error) {
|
||||
@@ -81,11 +98,24 @@ func (s Settings) Validate() (err error) {
|
||||
return ErrLoggerIsNotSet
|
||||
}
|
||||
|
||||
const minReadTimeout = time.Millisecond
|
||||
if s.ReadHeaderTimeout < minReadTimeout {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrReadHeaderTimeoutTooSmall,
|
||||
s.ReadHeaderTimeout, minReadTimeout)
|
||||
}
|
||||
|
||||
if s.ReadTimeout < minReadTimeout {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrReadTimeoutTooSmall,
|
||||
s.ReadTimeout, minReadTimeout)
|
||||
}
|
||||
|
||||
const minShutdownTimeout = 5 * time.Millisecond
|
||||
if *s.ShutdownTimeout < minShutdownTimeout {
|
||||
if s.ShutdownTimeout < minShutdownTimeout {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrShutdownTimeoutTooSmall,
|
||||
*s.ShutdownTimeout, minShutdownTimeout)
|
||||
s.ShutdownTimeout, minShutdownTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -94,7 +124,9 @@ func (s Settings) Validate() (err error) {
|
||||
func (s Settings) ToLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("HTTP server settings:")
|
||||
node.Appendf("Listening address: %s", s.Address)
|
||||
node.Appendf("Shutdown timeout: %s", *s.ShutdownTimeout)
|
||||
node.Appendf("Read header timeout: %s", s.ReadHeaderTimeout)
|
||||
node.Appendf("Read timeout: %s", s.ReadTimeout)
|
||||
node.Appendf("Shutdown timeout: %s", s.ShutdownTimeout)
|
||||
return node
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,24 @@ func Test_Settings_SetDefaults(t *testing.T) {
|
||||
"empty settings": {
|
||||
settings: Settings{},
|
||||
expected: Settings{
|
||||
Address: ":8000",
|
||||
ShutdownTimeout: durationPtr(defaultTimeout),
|
||||
Address: ":8000",
|
||||
ReadHeaderTimeout: defaultTimeout,
|
||||
ReadTimeout: defaultTimeout,
|
||||
ShutdownTimeout: defaultTimeout,
|
||||
},
|
||||
},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -62,16 +68,20 @@ func Test_Settings_Copy(t *testing.T) {
|
||||
"empty settings": {},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -102,30 +112,38 @@ func Test_Settings_MergeWith(t *testing.T) {
|
||||
"merge empty with empty": {},
|
||||
"merge empty with filled": {
|
||||
other: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
"merge filled with empty": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -156,48 +174,62 @@ func Test_Settings_OverrideWith(t *testing.T) {
|
||||
"override empty with empty": {},
|
||||
"override empty with filled": {
|
||||
other: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
"override filled with empty": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
"override filled with filled": {
|
||||
settings: Settings{
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
other: Settings{
|
||||
Address: ":8002",
|
||||
ShutdownTimeout: durationPtr(time.Hour),
|
||||
Address: ":8002",
|
||||
ReadHeaderTimeout: time.Hour,
|
||||
ReadTimeout: time.Hour,
|
||||
ShutdownTimeout: time.Hour,
|
||||
},
|
||||
expected: Settings{
|
||||
Address: ":8002",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Hour),
|
||||
Address: ":8002",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Hour,
|
||||
ReadTimeout: time.Hour,
|
||||
ShutdownTimeout: time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -247,22 +279,47 @@ func Test_Settings_Validate(t *testing.T) {
|
||||
errWrapped: ErrLoggerIsNotSet,
|
||||
errMessage: ErrLoggerIsNotSet.Error(),
|
||||
},
|
||||
"read header timeout too small": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Nanosecond,
|
||||
},
|
||||
errWrapped: ErrReadHeaderTimeoutTooSmall,
|
||||
errMessage: "read header timeout is too small: 1ns must be at least 1ms",
|
||||
},
|
||||
"read timeout too small": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Millisecond,
|
||||
ReadTimeout: time.Nanosecond,
|
||||
},
|
||||
errWrapped: ErrReadTimeoutTooSmall,
|
||||
errMessage: "read timeout is too small: 1ns must be at least 1ms",
|
||||
},
|
||||
"shutdown timeout too small": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Millisecond),
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Millisecond,
|
||||
ReadTimeout: time.Millisecond,
|
||||
ShutdownTimeout: time.Millisecond,
|
||||
},
|
||||
errWrapped: ErrShutdownTimeoutTooSmall,
|
||||
errMessage: "shutdown timeout is too small: 1ms must be at least 5ms",
|
||||
},
|
||||
"valid settings": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ReadHeaderTimeout: time.Millisecond,
|
||||
ReadTimeout: time.Millisecond,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -291,11 +348,15 @@ func Test_Settings_String(t *testing.T) {
|
||||
}{
|
||||
"all values": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8000",
|
||||
ReadHeaderTimeout: time.Millisecond,
|
||||
ReadTimeout: time.Millisecond,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
s: `HTTP server settings:
|
||||
├── Listening address: :8000
|
||||
├── Read header timeout: 1ms
|
||||
├── Read timeout: 1ms
|
||||
└── Shutdown timeout: 1s`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
numberHeader = "Number"
|
||||
ownedHeader = "Owned"
|
||||
portForwardHeader = "Port forwarding"
|
||||
premiumHeader = "Premium"
|
||||
regionHeader = "Region"
|
||||
streamHeader = "Stream"
|
||||
tcpHeader = "TCP"
|
||||
@@ -62,6 +63,8 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
|
||||
fields[i] = boolToMarkdown(s.Owned)
|
||||
case portForwardHeader:
|
||||
fields[i] = boolToMarkdown(s.PortForward)
|
||||
case premiumHeader:
|
||||
fields[i] = boolToMarkdown(s.Premium)
|
||||
case regionHeader:
|
||||
fields[i] = s.Region
|
||||
case streamHeader:
|
||||
@@ -123,10 +126,14 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, freeHeader}
|
||||
case providers.Purevpn:
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
||||
case providers.SlickVPN:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader}
|
||||
case providers.Surfshark:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
|
||||
case providers.Torguard:
|
||||
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
||||
case providers.VPNSecure:
|
||||
return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader}
|
||||
case providers.VPNUnlimited:
|
||||
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}
|
||||
case providers.Vyprvpn:
|
||||
|
||||
@@ -29,6 +29,7 @@ type Server struct {
|
||||
WgPubKey string `json:"wgpubkey,omitempty"`
|
||||
Free bool `json:"free,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Premium bool `json:"premium,omitempty"`
|
||||
PortForward bool `json:"port_forward,omitempty"`
|
||||
Keep bool `json:"keep,omitempty"`
|
||||
IPs []net.IP `json:"ips,omitempty"`
|
||||
|
||||
@@ -1,65 +1,55 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteAuthFile writes the OpenVPN auth file to disk with the right permissions.
|
||||
func (c *Configurator) WriteAuthFile(user, password string) error {
|
||||
file, err := os.Open(c.authFilePath)
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
file, err = os.OpenFile(c.authFilePath, os.O_WRONLY|os.O_CREATE, 0400)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.WriteString(user + "\n" + password)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
err = file.Chown(c.puid, c.pgid)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) > 1 && lines[0] == user && lines[1] == password {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.logger.Info("username and password changed in " + c.authFilePath)
|
||||
file, err = os.OpenFile(c.authFilePath, os.O_TRUNC|os.O_WRONLY, 0400)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.WriteString(user + "\n" + password)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
err = file.Chown(c.puid, c.pgid)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
content := strings.Join([]string{user, password}, "\n")
|
||||
return writeIfDifferent(c.authFilePath, content, c.puid, c.pgid)
|
||||
}
|
||||
|
||||
// WriteAskPassFile writes the OpenVPN askpass file to disk with the right permissions.
|
||||
func (c *Configurator) WriteAskPassFile(passphrase string) error {
|
||||
return writeIfDifferent(c.askPassPath, passphrase, c.puid, c.pgid)
|
||||
}
|
||||
|
||||
func writeIfDifferent(path, content string, puid, pgid int) (err error) {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("obtaining file information: %w", err)
|
||||
}
|
||||
|
||||
const perm = os.FileMode(0400)
|
||||
var writeData, setChown bool
|
||||
if os.IsNotExist(err) {
|
||||
writeData = true
|
||||
setChown = true
|
||||
} else {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
writeData = string(data) != content
|
||||
setChown = fileStat.Mode().Perm() != perm
|
||||
}
|
||||
|
||||
if writeData {
|
||||
err = os.WriteFile(path, []byte(content), perm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if setChown {
|
||||
err = os.Chown(path, puid, pgid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting file permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type Configurator struct {
|
||||
cmder command.RunStarter
|
||||
configPath string
|
||||
authFilePath string
|
||||
askPassPath string
|
||||
puid, pgid int
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ func New(logger Infoer, cmder command.RunStarter,
|
||||
cmder: cmder,
|
||||
configPath: configPath,
|
||||
authFilePath: openvpn.AuthConf,
|
||||
askPassPath: openvpn.AskPassPath,
|
||||
puid: puid,
|
||||
pgid: pgid,
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ package pprof
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func durationPtr(d time.Duration) *time.Duration { return &d }
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
var _ gomock.Matcher = (*regexMatcher)(nil)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package pprof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -29,7 +29,7 @@ func Test_Server(t *testing.T) {
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: address,
|
||||
Logger: logger,
|
||||
ShutdownTimeout: durationPtr(httpServerShutdownTimeout),
|
||||
ShutdownTimeout: httpServerShutdownTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func Test_Server(t *testing.T) {
|
||||
assert.Equalf(t, http.StatusOK, httpResult.response.StatusCode,
|
||||
"unexpected status code for URL %s: %s", httpResult.url, http.StatusText(httpResult.response.StatusCode))
|
||||
|
||||
b, err := ioutil.ReadAll(httpResult.response.Body)
|
||||
b, err := io.ReadAll(httpResult.response.Body)
|
||||
require.NoErrorf(t, err, "unexpected error for URL %s: %s", httpResult.url, err)
|
||||
assert.NotEmptyf(t, b, "response body is empty for URL %s", httpResult.url)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package pprof
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/httpserver"
|
||||
@@ -27,6 +28,8 @@ type Settings struct {
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Enabled = helpers.DefaultBool(s.Enabled, false)
|
||||
s.HTTPServer.Address = helpers.DefaultString(s.HTTPServer.Address, "localhost:6060")
|
||||
const defaultReadTimeout = 5 * time.Minute // for CPU profiling
|
||||
s.HTTPServer.ReadTimeout = helpers.DefaultDuration(s.HTTPServer.ReadTimeout, defaultReadTimeout)
|
||||
s.HTTPServer.SetDefaults()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ func Test_Settings_SetDefaults(t *testing.T) {
|
||||
expected: Settings{
|
||||
Enabled: boolPtr(false),
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: "localhost:6060",
|
||||
ShutdownTimeout: durationPtr(3 * time.Second),
|
||||
Address: "localhost:6060",
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
ShutdownTimeout: 3 * time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -32,8 +34,10 @@ func Test_Settings_SetDefaults(t *testing.T) {
|
||||
BlockProfileRate: 1,
|
||||
MutexProfileRate: 1,
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":6061",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":6061",
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
expected: Settings{
|
||||
@@ -41,8 +45,10 @@ func Test_Settings_SetDefaults(t *testing.T) {
|
||||
BlockProfileRate: 1,
|
||||
MutexProfileRate: 1,
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":6061",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":6061",
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,7 +81,7 @@ func Test_Settings_Copy(t *testing.T) {
|
||||
MutexProfileRate: 1,
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":6061",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
expected: Settings{
|
||||
@@ -84,7 +90,7 @@ func Test_Settings_Copy(t *testing.T) {
|
||||
MutexProfileRate: 1,
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":6061",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -278,10 +284,12 @@ func Test_Settings_Validate(t *testing.T) {
|
||||
"valid settings": {
|
||||
settings: Settings{
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":8000",
|
||||
Handler: http.NewServeMux(),
|
||||
Logger: &MockLogger{},
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
Address: ":8000",
|
||||
Handler: http.NewServeMux(),
|
||||
Logger: &MockLogger{},
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -321,7 +329,7 @@ func Test_Settings_String(t *testing.T) {
|
||||
MutexProfileRate: 1,
|
||||
HTTPServer: httpserver.Settings{
|
||||
Address: ":8000",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
ShutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
s: `Pprof settings:
|
||||
@@ -329,6 +337,8 @@ func Test_Settings_String(t *testing.T) {
|
||||
├── Mutex profile rate: 1
|
||||
└── HTTP server settings:
|
||||
├── Listening address: :8000
|
||||
├── Read header timeout: 0s
|
||||
├── Read timeout: 0s
|
||||
└── Shutdown timeout: 1s`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/provider/common (interfaces: ParallelResolver,Storage,Unzipper)
|
||||
// Source: github.com/qdm12/gluetun/internal/provider/common (interfaces: ParallelResolver,Storage,Unzipper,Warner)
|
||||
|
||||
// Package common is a generated GoMock package.
|
||||
package common
|
||||
@@ -144,3 +144,38 @@ func (mr *MockUnzipperMockRecorder) FetchAndExtract(arg0, arg1 interface{}) *gom
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAndExtract", reflect.TypeOf((*MockUnzipper)(nil).FetchAndExtract), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockWarner is a mock of Warner interface.
|
||||
type MockWarner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWarnerMockRecorder
|
||||
}
|
||||
|
||||
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||
type MockWarnerMockRecorder struct {
|
||||
mock *MockWarner
|
||||
}
|
||||
|
||||
// NewMockWarner creates a new mock instance.
|
||||
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||
mock := &MockWarner{ctrl: ctrl}
|
||||
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockWarner) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ package common
|
||||
|
||||
// Exceptionally, these mocks are exported since they are used by all
|
||||
// provider subpackages tests, and it reduces test code duplication a lot.
|
||||
//go:generate mockgen -destination=mocks.go -package $GOPACKAGE . ParallelResolver,Storage,Unzipper
|
||||
//go:generate mockgen -destination=mocks.go -package $GOPACKAGE . ParallelResolver,Storage,Unzipper,Warner
|
||||
|
||||
@@ -74,7 +74,7 @@ func modifyConfig(lines []string, connection models.Connection,
|
||||
modified = append(modified, "pull-filter ignore \"auth-token\"") // prevent auth failed loop
|
||||
modified = append(modified, "auth-retry nointeract")
|
||||
modified = append(modified, "suppress-timestamps")
|
||||
if settings.User != "" {
|
||||
if *settings.User != "" {
|
||||
modified = append(modified, "auth-user-pass "+openvpn.AuthConf)
|
||||
}
|
||||
modified = append(modified, "verb "+strconv.Itoa(*settings.Verbosity))
|
||||
|
||||
@@ -36,7 +36,7 @@ func Test_modifyConfig(t *testing.T) {
|
||||
"auth bla",
|
||||
},
|
||||
settings: settings.OpenVPN{
|
||||
User: "user",
|
||||
User: stringPtr("user"),
|
||||
Ciphers: []string{"cipher"},
|
||||
Auth: stringPtr("auth"),
|
||||
MSSFix: uint16Ptr(1000),
|
||||
|
||||
@@ -33,5 +33,11 @@ func getHostToURL(ctx context.Context, client *http.Client, protocol string) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openvpn.FetchMultiFiles(ctx, client, urls)
|
||||
const failEarly = true
|
||||
hostToURL, errors := openvpn.FetchMultiFiles(ctx, client, urls, failEarly)
|
||||
if len(errors) > 0 {
|
||||
return nil, errors[0]
|
||||
}
|
||||
|
||||
return hostToURL, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package updater
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Warner
|
||||
@@ -1,46 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/provider/ipvanish/updater (interfaces: Warner)
|
||||
|
||||
// Package ipvanish is a generated GoMock package.
|
||||
package updater
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockWarner is a mock of Warner interface.
|
||||
type MockWarner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWarnerMockRecorder
|
||||
}
|
||||
|
||||
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||
type MockWarnerMockRecorder struct {
|
||||
mock *MockWarner
|
||||
}
|
||||
|
||||
// NewMockWarner creates a new mock instance.
|
||||
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||
mock := &MockWarner{ctrl: ctrl}
|
||||
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockWarner) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"invalid proto": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("unknown protocol: invalid in badproto.ovpn")
|
||||
return warner
|
||||
},
|
||||
@@ -70,7 +70,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"no host": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("remote host not found in nohost.ovpn")
|
||||
return warner
|
||||
},
|
||||
@@ -80,7 +80,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"multiple hosts": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("only using the first host \"hosta\" and discarding 1 other hosts")
|
||||
return warner
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
},
|
||||
"resolve error": {
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("resolve warning")
|
||||
return warner
|
||||
},
|
||||
@@ -129,7 +129,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"filename parsing error": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("country code is unknown: unknown in ipvanish-unknown-City-A-hosta.ovpn")
|
||||
return warner
|
||||
},
|
||||
@@ -141,7 +141,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"success": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("resolve warning")
|
||||
return warner
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -32,11 +31,11 @@ func Test_fetchAPI(t *testing.T) {
|
||||
},
|
||||
"no server": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`{}`)),
|
||||
responseBody: io.NopCloser(strings.NewReader(`{}`)),
|
||||
},
|
||||
"success": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`{"servers":[
|
||||
responseBody: io.NopCloser(strings.NewReader(`{"servers":[
|
||||
{"country":"Country1","city":"City A","isp":"xyz","is_active":true,"hostnames":{"openvpn":"hosta"}},
|
||||
{"country":"Country2","city":"City B","isp":"abc","is_active":false,"hostnames":{"openvpn":"hostb"}}
|
||||
]}`)),
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package updater
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Warner
|
||||
@@ -1,46 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/provider/ivpn/updater (interfaces: Warner)
|
||||
|
||||
// Package ivpn is a generated GoMock package.
|
||||
package updater
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockWarner is a mock of Warner interface.
|
||||
type MockWarner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWarnerMockRecorder
|
||||
}
|
||||
|
||||
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||
type MockWarnerMockRecorder struct {
|
||||
mock *MockWarner
|
||||
}
|
||||
|
||||
// NewMockWarner creates a new mock instance.
|
||||
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||
mock := &MockWarner{ctrl: ctrl}
|
||||
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockWarner) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package updater
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -51,7 +51,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
},
|
||||
"resolve error": {
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("resolve warning")
|
||||
return warner
|
||||
},
|
||||
@@ -87,7 +87,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
"success": {
|
||||
minServers: 1,
|
||||
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
|
||||
warner := NewMockWarner(ctrl)
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
warner.EXPECT().Warn("resolve warning")
|
||||
return warner
|
||||
},
|
||||
@@ -145,7 +145,7 @@ func Test_Updater_GetServers(t *testing.T) {
|
||||
return &http.Response{
|
||||
StatusCode: testCase.responseStatus,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: ioutil.NopCloser(strings.NewReader(testCase.responseBody)),
|
||||
Body: io.NopCloser(strings.NewReader(testCase.responseBody)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/provider/privatevpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/protonvpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/purevpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/slickvpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark"
|
||||
"github.com/qdm12/gluetun/internal/provider/torguard"
|
||||
"github.com/qdm12/gluetun/internal/provider/vpnsecure"
|
||||
"github.com/qdm12/gluetun/internal/provider/vpnunlimited"
|
||||
"github.com/qdm12/gluetun/internal/provider/vyprvpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/wevpn"
|
||||
@@ -71,8 +73,10 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.VPNSecure: vpnsecure.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.VPNUnlimited: vpnunlimited.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Vyprvpn: vyprvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Wevpn: wevpn.New(storage, randSource, updaterWarner, parallelResolver),
|
||||
|
||||
13
internal/provider/slickvpn/connection.go
Normal file
13
internal/provider/slickvpn/connection.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) GetConnection(selection settings.ServerSelection) (
|
||||
connection models.Connection, err error) {
|
||||
defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:gomnd
|
||||
return utils.GetConnection(p.Name(), p.storage, selection, defaults, p.randSource)
|
||||
}
|
||||
30
internal/provider/slickvpn/openvpnconf.go
Normal file
30
internal/provider/slickvpn/openvpnconf.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
settings settings.OpenVPN) (lines []string) {
|
||||
const pingSeconds = 10
|
||||
const bufSize = 393216
|
||||
providerSettings := utils.OpenVPNProviderSettings{
|
||||
RemoteCertTLS: true,
|
||||
AuthUserPass: true,
|
||||
Ciphers: []string{
|
||||
openvpn.AES256cbc,
|
||||
},
|
||||
Ping: pingSeconds,
|
||||
SndBuf: bufSize,
|
||||
RcvBuf: bufSize,
|
||||
// Certificate found from https://www.slickvpn.com/tutorials/using-openvpn-configuration-files/
|
||||
CA: "MIIESDCCAzCgAwIBAgIJAKHK5bbBPSU2MA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNVBAYTAlVTMQwwCgYDVQQIEwNWUE4xDDAKBgNVBAcTA1ZQTjEMMAoGA1UEChMDVlBOMQwwCgYDVQQLEwNWUE4xDDAKBgNVBAMTA1ZQTjEMMAoGA1UEKRMDVlBOMRIwEAYJKoZIhvcNAQkBFgNWUE4wHhcNMjIwMjE0MjEzNDQwWhcNMzIwMjEyMjEzNDQwWjB1MQswCQYDVQQGEwJVUzEMMAoGA1UECBMDVlBOMQwwCgYDVQQHEwNWUE4xDDAKBgNVBAoTA1ZQTjEMMAoGA1UECxMDVlBOMQwwCgYDVQQDEwNWUE4xDDAKBgNVBCkTA1ZQTjESMBAGCSqGSIb3DQEJARYDVlBOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUl1XkfGo3c1uFsvgbO3C3yvu0+cHs9IUSsju5U9cPNCo53mqRHU/qntCC+ldIDKN+dNWn7eURIDszy+flutkgucs0qgETy5fzpXnVMtiKmMiOYWiJDor7j7QivRaxoT/O2fyjxvVCL8gMa60ekWSGBT6isYY8t8BnwTPVP0KvDm36wdaqLmubhf2XGvka/hhNx0SXMmz2x3OJ8BcoypZVLLk/+Qm6DJh1KxyDi4kI+jBC41QuaKKDNwb0kth1304eqZoUeCXtGkzl91y76ODAfdqzXf9WYJdgkXpOm53Cg7FtB42AqPRqHJVwYxDnQyrFwy4a3CWqFJnKtxJM/WlwIDAQABo4HaMIHXMB0GA1UdDgQWBBRSzxAtISfbSRPr0fmhwNZc8kOeKzCBpwYDVR0jBIGfMIGcgBRSzxAtISfbSRPr0fmhwNZc8kOeK6F5pHcwdTELMAkGA1UEBhMCVVMxDDAKBgNVBAgTA1ZQTjEMMAoGA1UEBxMDVlBOMQwwCgYDVQQKEwNWUE4xDDAKBgNVBAsTA1ZQTjEMMAoGA1UEAxMDVlBOMQwwCgYDVQQpEwNWUE4xEjAQBgkqhkiG9w0BCQEWA1ZQToIJAKHK5bbBPSU2MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGuKFW765F3D5wax5IFSQbEtr+rVHgjR8jiYTzxOCmbLaU4oj6phOhfQJiTTADQYgCIC/DN0HsAEEqrKkwEn8KdAoNiAWfqCV/eqnK83y7yRDGx6/zfsch+PAzKZouMJLrvR9RYbHq7m3adZv84YLge7FE1JqFk1j6rSa4dUUnGQPrQgr9Sasnz8O8KK45XH6fqKrsd4p485n+BXPDzWVsHl4M5dqQV7qUZTazbzzh4NyP5/RQ6Oh5jqMN7po4qiqWv1pu0EKDxUG6gcECc2cTQwHhIOPeCGdHS7uuI2FlLnHaCUFBgi8zTsZxaeaPuPch5M7Zxbdg0GBhS2SmNi+io=", //nolint:lll
|
||||
ExtraLines: []string{
|
||||
"redirect-gateway",
|
||||
},
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings)
|
||||
}
|
||||
33
internal/provider/slickvpn/provider.go
Normal file
33
internal/provider/slickvpn/provider.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/slickvpn/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.SlickVPN),
|
||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() string {
|
||||
return providers.SlickVPN
|
||||
}
|
||||
26
internal/provider/slickvpn/updater/helpers_test.go
Normal file
26
internal/provider/slickvpn/updater/helpers_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func parseTestHTML(t *testing.T, htmlString string) *html.Node {
|
||||
t.Helper()
|
||||
rootNode, err := html.Parse(strings.NewReader(htmlString))
|
||||
require.NoError(t, err)
|
||||
return rootNode
|
||||
}
|
||||
|
||||
func parseTestDataIndexHTML(t *testing.T) *html.Node {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile("testdata/index.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
return parseTestHTML(t, string(data))
|
||||
}
|
||||
28
internal/provider/slickvpn/updater/resolve.go
Normal file
28
internal/provider/slickvpn/updater/resolve.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
Hosts: hosts,
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
82
internal/provider/slickvpn/updater/servers.go
Normal file
82
internal/provider/slickvpn/updater/servers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
)
|
||||
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error) {
|
||||
hostToData, err := fetchServers(ctx, u.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching and parsing website: %w", err)
|
||||
}
|
||||
|
||||
openvpnURLs := make([]string, 0, len(hostToData))
|
||||
for _, data := range hostToData {
|
||||
openvpnURLs = append(openvpnURLs, data.ovpnURL)
|
||||
}
|
||||
|
||||
if len(openvpnURLs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(openvpnURLs), minServers)
|
||||
}
|
||||
|
||||
const failEarly = false // some URLs from the website are not valid
|
||||
udpHostToURL, errors := openvpn.FetchMultiFiles(ctx, u.client, openvpnURLs, failEarly)
|
||||
for _, err := range errors {
|
||||
u.warner.Warn(fmt.Sprintf("fetching OpenVPN files: %s", err))
|
||||
}
|
||||
|
||||
if len(udpHostToURL) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(udpHostToURL), minServers)
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(udpHostToURL))
|
||||
for host := range udpHostToURL {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
resolveSettings := parallelResolverSettings(hosts)
|
||||
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
for _, warning := range warnings {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving hosts: %w", err)
|
||||
}
|
||||
|
||||
if len(hostToIPs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(hosts), minServers)
|
||||
}
|
||||
|
||||
servers = make([]models.Server, 0, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
_, udp := udpHostToURL[host]
|
||||
|
||||
serverData := hostToData[host]
|
||||
|
||||
server := models.Server{
|
||||
VPN: vpn.OpenVPN,
|
||||
Region: serverData.region,
|
||||
Country: serverData.country,
|
||||
City: serverData.city,
|
||||
Hostname: host,
|
||||
UDP: udp,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sort.Sort(models.SortableServers(servers))
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
236
internal/provider/slickvpn/updater/testdata/index.html
vendored
Normal file
236
internal/provider/slickvpn/updater/testdata/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
22
internal/provider/slickvpn/updater/updater.go
Normal file
22
internal/provider/slickvpn/updater/updater.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(client *http.Client, warner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Updater {
|
||||
return &Updater{
|
||||
client: client,
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
149
internal/provider/slickvpn/updater/website.go
Normal file
149
internal/provider/slickvpn/updater/website.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
htmlutils "github.com/qdm12/gluetun/internal/updater/html"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func fetchServers(ctx context.Context, client *http.Client) (
|
||||
hostToData map[string]serverData, err error) {
|
||||
const url = "https://www.slickvpn.com/locations/"
|
||||
rootNode, err := htmlutils.Fetch(ctx, client, url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
||||
}
|
||||
|
||||
hostToData, err = parseHTML(rootNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing HTML: %w", err)
|
||||
}
|
||||
|
||||
return hostToData, nil
|
||||
}
|
||||
|
||||
type serverData struct {
|
||||
ovpnURL string
|
||||
country string
|
||||
region string
|
||||
city string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLocationTableNotFound = errors.New("HTML location table node not found")
|
||||
ErrTbodyNotFound = errors.New("HTML tbody node not found")
|
||||
ErrExtractOpenVPNURL = errors.New("failed extracting OpenVPN URL")
|
||||
)
|
||||
|
||||
func parseHTML(rootNode *html.Node) (hostToData map[string]serverData, err error) {
|
||||
locationTableNode := htmlutils.BFS(rootNode, matchLocationTable)
|
||||
if locationTableNode == nil {
|
||||
return nil, htmlutils.WrapError(ErrLocationTableNotFound, rootNode)
|
||||
}
|
||||
|
||||
tBodyNode := htmlutils.BFS(locationTableNode, matchTbody)
|
||||
if tBodyNode == nil {
|
||||
return nil, htmlutils.WrapError(ErrTbodyNotFound, rootNode)
|
||||
}
|
||||
|
||||
rowNodes := htmlutils.DirectChildren(tBodyNode, matchTr)
|
||||
hostToData = make(map[string]serverData, len(rowNodes))
|
||||
|
||||
for _, rowNode := range rowNodes {
|
||||
hostname, data, err := parseRowNode(rowNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing row node: %w", err)
|
||||
}
|
||||
hostToData[hostname] = data
|
||||
}
|
||||
|
||||
return hostToData, nil
|
||||
}
|
||||
|
||||
func parseRowNode(rowNode *html.Node) (hostname string, data serverData, err error) {
|
||||
columnIndex := 0
|
||||
const (
|
||||
columnIndexContinent = 0
|
||||
columnIndexCountry = 1
|
||||
columnIndexCity = 2
|
||||
columnIndexConfig = 3
|
||||
)
|
||||
for cellNode := rowNode.FirstChild; cellNode != nil; cellNode = cellNode.NextSibling {
|
||||
if cellNode.FirstChild == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch columnIndex {
|
||||
case columnIndexContinent:
|
||||
data.region = cellNode.FirstChild.Data
|
||||
case columnIndexCountry:
|
||||
data.country = cellNode.FirstChild.Data
|
||||
case columnIndexCity:
|
||||
data.city = cellNode.FirstChild.Data
|
||||
case columnIndexConfig:
|
||||
linkNodes := htmlutils.DirectChildren(cellNode, matchA)
|
||||
for _, linkNode := range linkNodes {
|
||||
if linkNode.FirstChild.Data != "OpenVPN" {
|
||||
continue
|
||||
}
|
||||
|
||||
data.ovpnURL = htmlutils.Attribute(linkNode, "href")
|
||||
if data.ovpnURL == "" {
|
||||
return "", data, htmlutils.WrapError(ErrExtractOpenVPNURL, linkNode)
|
||||
}
|
||||
|
||||
hostname, err = extractHostnameFromURL(data.ovpnURL)
|
||||
if err != nil {
|
||||
return "", data, fmt.Errorf("extracting hostname from url: %w", err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
columnIndex++
|
||||
if columnIndex == columnIndexConfig+1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hostname, data, nil
|
||||
}
|
||||
|
||||
func matchLocationTable(rootNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchID("location-table")(rootNode)
|
||||
}
|
||||
|
||||
func matchTbody(locationTableNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("tbody")(locationTableNode)
|
||||
}
|
||||
|
||||
func matchTr(tbodyNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("tr")(tbodyNode)
|
||||
}
|
||||
|
||||
func matchA(cellNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("a")(cellNode)
|
||||
}
|
||||
|
||||
var serverNameRegex = regexp.MustCompile(`^.+\/(?P<serverName>.+)\.ovpn$`)
|
||||
|
||||
var (
|
||||
ErrExtractHostnameFromURL = errors.New("cannot extract hostname from url")
|
||||
)
|
||||
|
||||
func extractHostnameFromURL(url string) (hostname string, err error) {
|
||||
matches := serverNameRegex.FindStringSubmatch(url)
|
||||
const minMatches = 2
|
||||
if len(matches) < minMatches {
|
||||
return "", fmt.Errorf("%w: %s has less than 2 matches for %s",
|
||||
ErrExtractHostnameFromURL, url, serverNameRegex)
|
||||
}
|
||||
hostname = matches[1]
|
||||
return hostname, nil
|
||||
}
|
||||
269
internal/provider/slickvpn/updater/website_test.go
Normal file
269
internal/provider/slickvpn/updater/website_test.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func Test_fetchServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
hostToData map[string]serverData
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"context canceled": {
|
||||
ctx: canceledCtx,
|
||||
errWrapped: context.Canceled,
|
||||
errMessage: `fetching HTML code: Get "https://www.slickvpn.com/locations/": context canceled`,
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
responseStatus: http.StatusOK,
|
||||
//nolint:lll
|
||||
responseBody: io.NopCloser(strings.NewReader(`
|
||||
<div>
|
||||
<table id="location-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>South America</td>
|
||||
<td>Chile</td>
|
||||
<td>Vina del Mar</td>
|
||||
<td> <a
|
||||
href="https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn">OpenVPN</a>
|
||||
| <a
|
||||
href="https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/viscosity/gw1.kna1.slickvpn.com_viscosity.ovpn">Viscosity</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`)),
|
||||
hostToData: map[string]serverData{
|
||||
"gw1.kna1.slickvpn.com": {
|
||||
ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn", //nolint:lll
|
||||
country: "Chile",
|
||||
region: "South America",
|
||||
city: "Vina del Mar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, r.URL.String(), "https://www.slickvpn.com/locations/")
|
||||
|
||||
ctxErr := r.Context().Err()
|
||||
if ctxErr != nil {
|
||||
return nil, ctxErr
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: testCase.responseBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
hostToData, err := fetchServers(testCase.ctx, client)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.hostToData, hostToData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
rootNode *html.Node
|
||||
hostToData map[string]serverData
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty html": {
|
||||
rootNode: parseTestHTML(t, ""),
|
||||
errWrapped: ErrLocationTableNotFound,
|
||||
errMessage: `HTML location table node not found: in HTML code: <html><head></head><body></body></html>`,
|
||||
},
|
||||
"test data": {
|
||||
rootNode: parseTestDataIndexHTML(t),
|
||||
//nolint:lll
|
||||
hostToData: map[string]serverData{
|
||||
"gw1.ams1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams1.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams2.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams3.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams4.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.arn1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.arn1.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Stockholm"},
|
||||
"gw1.arn3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.arn3.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Stockholm"},
|
||||
"gw1.ath1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ath1.slickvpn.com.ovpn", country: "Greece", region: "Europe", city: "Athens"},
|
||||
"gw1.atl1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.atl1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw1.atl3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.atl3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw1.beg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.beg1.slickvpn.com.ovpn", country: "Serbia", region: "Europe", city: "Belgrade"},
|
||||
"gw1.bkk1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bkk1.slickvpn.com.ovpn", country: "Thailand", region: "Asia", city: "Bangkok"},
|
||||
"gw1.blr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.blr1.slickvpn.com.ovpn", country: "India", region: "Asia", city: "Bangalore"},
|
||||
"gw1.bne1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bne1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Brisbane"},
|
||||
"gw1.bom1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bom1.slickvpn.com.ovpn", country: "India", region: "Asia", city: "Mumbai"},
|
||||
"gw1.bos1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bos1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Boston"},
|
||||
"gw1.bud1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bud1.slickvpn.com.ovpn", country: "Hungary", region: "Europe", city: "Budapest"},
|
||||
"gw1.buf1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.buf1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Buffalo"},
|
||||
"gw1.buh2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.buh2.slickvpn.com.ovpn", country: "Romania", region: "Europe", city: "Bucharest"},
|
||||
"gw1.cdg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cdg1.slickvpn.com.ovpn", country: "France", region: "Europe", city: "Paris"},
|
||||
"gw1.cgk1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cgk1.slickvpn.com.ovpn", country: "Indonesia", region: "Asia", city: "Jakarta"},
|
||||
"gw1.cmh1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cmh1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Columbus"},
|
||||
"gw1.cph1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cph1.slickvpn.com.ovpn", country: "Denmark", region: "Europe", city: "Copenhagen"},
|
||||
"gw1.cvt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cvt1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Coventry"},
|
||||
"gw1.dbq1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dbq1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dubuque"},
|
||||
"gw1.den1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.den1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Denver"},
|
||||
"gw1.dfw2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dfw2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw1.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw1.dub1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dub1.slickvpn.com.ovpn", country: "Ireland", region: "Europe", city: "Dublin"},
|
||||
"gw1.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw1.fra1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.fra1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw1.fra2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.fra2.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw1.gru2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.gru2.slickvpn.com.ovpn", country: "Brazil", region: "South America", city: "Sao Paulo"},
|
||||
"gw1.grz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.grz1.slickvpn.com.ovpn", country: "Austria", region: "Europe", city: "Graz"},
|
||||
"gw1.had2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.had2.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Halmstad"},
|
||||
"gw1.hkg2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.hkg2.slickvpn.com.ovpn", country: "Hong Kong", region: "Asia", city: "Hong Kong"},
|
||||
"gw1.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw1.iev1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iev1.slickvpn.com.ovpn", country: "Ukraine", region: "Europe", city: "Kiev"},
|
||||
"gw1.iom1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iom1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Isle Of Man"},
|
||||
"gw1.kiv1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kiv1.slickvpn.com.ovpn", country: "Moldova", region: "Europe", city: "Chisinau"},
|
||||
"gw1.kna1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn", country: "Chile", region: "South America", city: "Vina del Mar"},
|
||||
"gw1.kul1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kul1.slickvpn.com.ovpn", country: "Malaysia", region: "Asia", city: "Kuala Lumpur"},
|
||||
"gw1.las1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.las1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Las Vegas"},
|
||||
"gw1.lax1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lax1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw1.lax2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lax2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw1.led1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.led1.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "St Petersburg"},
|
||||
"gw1.lga1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lga1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "New York"},
|
||||
"gw1.lga2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lga2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "New York"},
|
||||
"gw1.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw1.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw1.lil1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lil1.slickvpn.com.ovpn", country: "France", region: "Europe", city: "Lille"},
|
||||
"gw1.lju1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lju1.slickvpn.com.ovpn", country: "Slovenia", region: "Europe", city: "Ljubljana"},
|
||||
"gw1.mad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mad1.slickvpn.com.ovpn", country: "Spain", region: "Europe", city: "Madrid"},
|
||||
"gw1.man2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.man2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Manchester"},
|
||||
"gw1.mci2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mci2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Kansas City"},
|
||||
"gw1.mrn1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mrn1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Morganton"},
|
||||
"gw1.mxp1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mxp1.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw1.mxp2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mxp2.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw1.nrt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.nrt1.slickvpn.com.ovpn", country: "Japan", region: "Asia", city: "Tokyo"},
|
||||
"gw1.nue1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.nue1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Nürnberg"},
|
||||
"gw1.ord3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ord3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw1.ord4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ord4.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw1.ost2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ost2.slickvpn.com.ovpn", country: "Belgium", region: "Europe", city: "Ostend"},
|
||||
"gw1.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw1.phx2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.phx2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Phoenix"},
|
||||
"gw1.prg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.prg1.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw1.prg2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.prg2.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw1.rcs1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.rcs1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Rochester"},
|
||||
"gw1.rkv1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.rkv1.slickvpn.com.ovpn", country: "Iceland", region: "Europe", city: "Reykjavik"},
|
||||
"gw1.san1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.san1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "San Diego"},
|
||||
"gw1.sea1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sea1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Seattle"},
|
||||
"gw1.sea2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sea2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Seattle"},
|
||||
"gw1.sin1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sin1.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw1.sin2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sin2.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw1.sjc2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sjc2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "San Jose"},
|
||||
"gw1.skg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.skg1.slickvpn.com.ovpn", country: "Greece", region: "Europe", city: "Thessaloniki"},
|
||||
"gw1.sou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sou1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Eastleigh near Southampton"},
|
||||
"gw1.stl1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.stl1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "St Louis"},
|
||||
"gw1.svo1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.svo1.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "Moscow"},
|
||||
"gw1.svo2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.svo2.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "Moscow"},
|
||||
"gw1.syd1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.syd1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw1.syd2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.syd2.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw1.tll1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tll1.slickvpn.com.ovpn", country: "Estonia", region: "Europe", city: "Tallinn"},
|
||||
"gw1.tlv2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tlv2.slickvpn.com.ovpn", country: "Israel", region: "Asia", city: "Tel Aviv Yafo"},
|
||||
"gw1.tpa1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tpa1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Tampa"},
|
||||
"gw1.trf1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.trf1.slickvpn.com.ovpn", country: "Norway", region: "Europe", city: "Torp"},
|
||||
"gw1.waw1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.waw1.slickvpn.com.ovpn", country: "Poland", region: "Europe", city: "Warsaw"},
|
||||
"gw1.yei1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yei1.slickvpn.com.ovpn", country: "Turkey", region: "Asia", city: "Bursa"},
|
||||
"gw1.yul1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yul1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw1.yul2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yul2.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw1.yvr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yvr1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Vancouver"},
|
||||
"gw1.yyz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yyz1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Toronto"},
|
||||
"gw1.zrh1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.zrh1.slickvpn.com.ovpn", country: "Switzerland", region: "Europe", city: "Zurich"},
|
||||
"gw2.ams3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ams3.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw2.atl3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.atl3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw2.bcn2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.bcn2.slickvpn.com.ovpn", country: "Spain", region: "Europe", city: "Barcelona"},
|
||||
"gw2.clt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.clt1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Charlotte"},
|
||||
"gw2.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw2.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw2.fra1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.fra1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw2.hou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.hou1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Houston"},
|
||||
"gw2.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw2.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw2.mel1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mel1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Melbourne"},
|
||||
"gw2.mia3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mia3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Miami"},
|
||||
"gw2.mia4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mia4.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Miami"},
|
||||
"gw2.mxp2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mxp2.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw2.ord1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ord1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw2.ost2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ost2.slickvpn.com.ovpn", country: "Belgium", region: "Europe", city: "Ostend"},
|
||||
"gw2.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw2.prg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.prg1.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw2.pty1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.pty1.slickvpn.com.ovpn", country: "Panama", region: "North America", city: "Panama City"},
|
||||
"gw2.sin2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.sin2.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw2.slc1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.slc1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Salt Lake City"},
|
||||
"gw2.syd2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.syd2.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw2.tpe1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.tpe1.slickvpn.com.ovpn", country: "Taiwan", region: "Asia", city: "Taipei"},
|
||||
"gw2.yul2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.yul2.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw2.yyz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.yyz1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Toronto"},
|
||||
"gw3.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw3.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw3.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw3.lax3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lax3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw3.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw3.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw3.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw3.per1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.per1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Perth"},
|
||||
"gw3.sou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.sou1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Eastleigh near Southampton"},
|
||||
"gw4.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw4.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hostToData, err := parseHTML(testCase.rootNode)
|
||||
|
||||
assert.Equal(t, testCase.hostToData, hostToData)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type ServerLocation struct {
|
||||
}
|
||||
|
||||
// TODO remove retroRegion and servers from API in v4.
|
||||
func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
func LocationData() (data []ServerLocation) {
|
||||
//nolint:lll
|
||||
return []ServerLocation{
|
||||
{Region: "Asia Pacific", Country: "Australia", City: "Adelaide", RetroLoc: "Australia Adelaide", Hostname: "au-adl.prod.surfshark.com", MultiHop: false},
|
||||
@@ -22,23 +22,9 @@ func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
{Region: "Asia Pacific", Country: "Australia", City: "Sydney", RetroLoc: "Australia Sydney", Hostname: "au-syd.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Azerbaijan", City: "Baku", RetroLoc: "Azerbaijan", Hostname: "az-bak.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Hong Kong", City: "Hong Kong", RetroLoc: "Hong Kong", Hostname: "hk-hkg.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "India", City: "Chennai", RetroLoc: "India Chennai", Hostname: "in-chn.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "India", City: "Indore", RetroLoc: "India Indore", Hostname: "in-idr.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "India", City: "Mumbai", RetroLoc: "India Mumbai", Hostname: "in-mum.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Indonesia", City: "Jakarta", RetroLoc: "Indonesia", Hostname: "id-jak.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st001", Hostname: "jp-tok-st001.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st002", Hostname: "jp-tok-st002.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st003", Hostname: "jp-tok-st003.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st004", Hostname: "jp-tok-st004.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st005", Hostname: "jp-tok-st005.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st006", Hostname: "jp-tok-st006.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st007", Hostname: "jp-tok-st007.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st008", Hostname: "jp-tok-st008.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st009", Hostname: "jp-tok-st009.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st010", Hostname: "jp-tok-st010.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st011", Hostname: "jp-tok-st011.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st012", Hostname: "jp-tok-st012.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo st013", Hostname: "jp-tok-st013.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", Hostname: "jp-tok-st014.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", Hostname: "jp-tok-st015.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", Hostname: "jp-tok-st016.prod.surfshark.com"},
|
||||
@@ -52,17 +38,12 @@ func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", Hostname: "jp-tok-st024.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", Hostname: "jp-tok-st025.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Japan", City: "Tokyo", RetroLoc: "Japan Tokyo", Hostname: "jp-tok.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Kazakhstan", City: "Oral", RetroLoc: "Kazakhstan", Hostname: "kz-ura.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Malaysia", City: "Kuala Lumpur", RetroLoc: "Malaysia", Hostname: "my-kul.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "New Zealand", City: "Auckland", RetroLoc: "New Zealand", Hostname: "nz-akl.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Philippines", City: "Manila", RetroLoc: "Philippines", Hostname: "ph-mnl.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore Hong Kong", City: "Hong Kong", RetroLoc: "Singapore Hong Kong", Hostname: "sg-hk.prod.surfshark.com", MultiHop: true},
|
||||
{Region: "Asia Pacific", Country: "Singapore in", City: "", RetroLoc: "Singapore in", Hostname: "sg-in.prod.surfshark.com", MultiHop: true},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", RetroLoc: "Singapore mp001", Hostname: "sg-sng-mp001.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", RetroLoc: "Singapore st001", Hostname: "sg-sng-st001.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", RetroLoc: "Singapore st002", Hostname: "sg-sng-st002.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", RetroLoc: "Singapore st003", Hostname: "sg-sng-st003.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", RetroLoc: "Singapore st004", Hostname: "sg-sng-st004.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", Hostname: "sg-sng-st005.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", Hostname: "sg-sng-st006.prod.surfshark.com"},
|
||||
{Region: "Asia Pacific", Country: "Singapore", City: "Singapore", Hostname: "sg-sng-st007.prod.surfshark.com"},
|
||||
@@ -97,6 +78,8 @@ func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main st003", Hostname: "de-fra-st003.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main st004", Hostname: "de-fra-st004.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main st005", Hostname: "de-fra-st005.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main st006", Hostname: "de-fra-st006.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main st007", Hostname: "de-fra-st007.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt am Main", Hostname: "de-fra.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Germany", City: "Frankfurt am Main", RetroLoc: "Germany Frankfurt mp001", Hostname: "de-fra-mp001.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Greece", City: "Athens", RetroLoc: "Greece", Hostname: "gr-ath.prod.surfshark.com", MultiHop: false},
|
||||
@@ -119,7 +102,6 @@ func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
{Region: "Europe", Country: "Portugal", City: "Lisbon", RetroLoc: "Portugal Lisbon", Hostname: "pt-lis.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Portugal", City: "Porto", RetroLoc: "Portugal Porto", Hostname: "pt-opo.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Romania", City: "Bucharest", RetroLoc: "Romania", Hostname: "ro-buc.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Russia", City: "Moscow", RetroLoc: "Russia Moscow", Hostname: "ru-mos.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Serbia", City: "Belgrade", RetroLoc: "Serbia", Hostname: "rs-beg.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "Europe", Country: "Singapore Netherlands", City: "", RetroLoc: "Singapore Netherlands", Hostname: "sg-nl.prod.surfshark.com", MultiHop: true},
|
||||
{Region: "Europe", Country: "Slovakia", City: "Bratislava", RetroLoc: "Slovekia", Hostname: "sk-bts.prod.surfshark.com", MultiHop: false},
|
||||
@@ -175,7 +157,6 @@ func LocationData() (data []ServerLocation) { //nolint:maintidx
|
||||
{Region: "The Americas", Country: "United States", City: "Las Vegas", RetroLoc: "US Las Vegas", Hostname: "us-las.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "Latham", RetroLoc: "US Latham", Hostname: "us-ltm.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "Los Angeles", RetroLoc: "US Los Angeles", Hostname: "us-lax.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "Manassas", RetroLoc: "US Maryland", Hostname: "us-mnz.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "Miami", RetroLoc: "US Miami", Hostname: "us-mia.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "New York", RetroLoc: "US New York City mp001", Hostname: "us-nyc-mp001.prod.surfshark.com", MultiHop: false},
|
||||
{Region: "The Americas", Country: "United States", City: "New York", RetroLoc: "US New York City st001", Hostname: "us-nyc-st001.prod.surfshark.com", MultiHop: false},
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -34,7 +33,7 @@ func Test_addServersFromAPI(t *testing.T) {
|
||||
"existinghost": {Hostname: "existinghost"},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[
|
||||
responseBody: io.NopCloser(strings.NewReader(`[
|
||||
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
|
||||
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
|
||||
]`)),
|
||||
@@ -111,12 +110,12 @@ func Test_fetchAPI(t *testing.T) {
|
||||
},
|
||||
"no server": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[]`)),
|
||||
responseBody: io.NopCloser(strings.NewReader(`[]`)),
|
||||
data: []serverData{},
|
||||
},
|
||||
"success": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[
|
||||
responseBody: io.NopCloser(strings.NewReader(`[
|
||||
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
|
||||
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
|
||||
]`)),
|
||||
|
||||
@@ -40,6 +40,10 @@ func filterServer(server models.Server,
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.PremiumOnly && !server.Premium {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.StreamOnly && !server.Stream {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -88,6 +88,19 @@ func Test_FilterServers(t *testing.T) {
|
||||
{Free: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by premium only": {
|
||||
selection: settings.ServerSelection{
|
||||
PremiumOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by stream only": {
|
||||
selection: settings.ServerSelection{
|
||||
StreamOnly: boolPtr(true),
|
||||
|
||||
@@ -188,12 +188,17 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
|
||||
lines.addLines(WrapOpenvpnTLSCrypt(provider.TLSCrypt))
|
||||
}
|
||||
|
||||
if *settings.ClientCrt != "" {
|
||||
lines.addLines(WrapOpenvpnCert(*settings.ClientCrt))
|
||||
if *settings.EncryptedKey != "" {
|
||||
lines.add("askpass", openvpn.AskPassPath)
|
||||
lines.addLines(WrapOpenvpnEncryptedKey(*settings.EncryptedKey))
|
||||
}
|
||||
|
||||
if *settings.ClientKey != "" {
|
||||
lines.addLines(WrapOpenvpnKey(*settings.ClientKey))
|
||||
if *settings.Cert != "" {
|
||||
lines.addLines(WrapOpenvpnCert(*settings.Cert))
|
||||
}
|
||||
|
||||
if *settings.Key != "" {
|
||||
lines.addLines(WrapOpenvpnKey(*settings.Key))
|
||||
}
|
||||
|
||||
lines.addLines(provider.ExtraLines)
|
||||
@@ -282,6 +287,16 @@ func WrapOpenvpnKey(clientKey string) (lines []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func WrapOpenvpnEncryptedKey(encryptedKey string) (lines []string) {
|
||||
return []string{
|
||||
"<key>",
|
||||
"-----BEGIN ENCRYPTED PRIVATE KEY-----",
|
||||
encryptedKey,
|
||||
"-----END ENCRYPTED PRIVATE KEY-----",
|
||||
"</key>",
|
||||
}
|
||||
}
|
||||
|
||||
func WrapOpenvpnRSAKey(rsaPrivateKey string) (lines []string) {
|
||||
return []string{
|
||||
"<key>",
|
||||
|
||||
14
internal/provider/vpnsecure/connection.go
Normal file
14
internal/provider/vpnsecure/connection.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package vpnsecure
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) GetConnection(selection settings.ServerSelection) (
|
||||
connection models.Connection, err error) {
|
||||
defaults := utils.NewConnectionDefaults(110, 1282, 0) //nolint:gomnd
|
||||
return utils.GetConnection(p.Name(),
|
||||
p.storage, selection, defaults, p.randSource)
|
||||
}
|
||||
26
internal/provider/vpnsecure/openvpnconf.go
Normal file
26
internal/provider/vpnsecure/openvpnconf.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package vpnsecure
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
settings settings.OpenVPN) (lines []string) {
|
||||
//nolint:gomnd
|
||||
providerSettings := utils.OpenVPNProviderSettings{
|
||||
RemoteCertTLS: true,
|
||||
AuthUserPass: true,
|
||||
Ping: 10,
|
||||
// note DES-CBC is not added since it's quite unsecure
|
||||
Ciphers: []string{openvpn.AES256cbc, openvpn.AES128cbc},
|
||||
ExtraLines: []string{
|
||||
"comp-lzo",
|
||||
"float",
|
||||
},
|
||||
CA: "MIIEJjCCAw6gAwIBAgIJAMkzh6p4m6XfMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsxFTATBgNVBAoTDHZwbnNlY3VyZS5tZTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEB2cG5zZWN1cmUubWUwIBcNMTcwNTA2MTMzMTQyWhgPMjkzODA4MjYxMzMxNDJaMGkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsxFTATBgNVBAoTDHZwbnNlY3VyZS5tZTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEB2cG5zZWN1cmUubWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiClT1wcZ6oovYjSxUJIQplrBSQRKB44uymC8evohzK7q67x0NE2sLz5Zn9ZiC7RnXQCtEqJfHqjuqjaH5MghjhUDnRbZS/8ElxdGKn9FPvs9b+aTVGSfrQm5KKoVigwAye3ilNiWAyy6MDlBeoKluQ4xW7SGiVZRxLcJbLAmjmfCjBS7eUGbtA8riTkIegFo4WFiy9G76zQWw1V26kDhyzcJNT4xO7USMPUeZthy13g+zi9+rcILhEAnl776sIil6w8UVK8xevFKBlOPk+YyXlo4eZiuppq300ogaS+fX/0mfD7DDE+Gk5/nCeACDNiBlfQ3ol/De8Cm60HWEUtZVAgMBAAGjgc4wgcswHQYDVR0OBBYEFBJyf4mpGT3dIu65/1zAFqCgGxZoMIGbBgNVHSMEgZMwgZCAFBJyf4mpGT3dIu65/1zAFqCgGxZooW2kazBpMQswCQYDVQQGEwJVUzELMAkGA1UECBMCTlkxETAPBgNVBAcTCE5ldyBZb3JrMRUwEwYDVQQKEwx2cG5zZWN1cmUubWUxIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAdnBuc2VjdXJlLm1lggkAyTOHqnibpd8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEArbTAibGQilY4Lu2RAVPjNx14SfojueBroeN7NIpAFUfbifPQRWvLamzRfxFTO0PXRc2pw/It7oa8yM7BsZj0vOiZY2p1JBHZwKom6tiSUVENDGW6JaYtiaE8XPyjfA5Yhfx4FefmaJ1veDYid18S+VVpt+Y+UIUxNmg1JB3CCUwbjl+dWlcvDBy4+jI+sZ7A1LF3uX64ZucDQ/XrpuopHhvDjw7g1PpKXsRqBYL+cpxUI7GrINBa/rGvXqv/NvFH8bguggknWKxKhd+jyMqkW3Ws258e0OwHz7gQ+tTJ909tR0TxJhZGkHatNSbpwW1Y52A972+9gYJMadSfm4bUHA==", //nolint:lll
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings)
|
||||
}
|
||||
33
internal/provider/vpnsecure/provider.go
Normal file
33
internal/provider/vpnsecure/provider.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package vpnsecure
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
"github.com/qdm12/gluetun/internal/provider/vpnsecure/updater"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.VPNSecure),
|
||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() string {
|
||||
return providers.VPNSecure
|
||||
}
|
||||
26
internal/provider/vpnsecure/updater/helpers_test.go
Normal file
26
internal/provider/vpnsecure/updater/helpers_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func parseTestHTML(t *testing.T, htmlString string) *html.Node {
|
||||
t.Helper()
|
||||
rootNode, err := html.Parse(strings.NewReader(htmlString))
|
||||
require.NoError(t, err)
|
||||
return rootNode
|
||||
}
|
||||
|
||||
func parseTestDataIndexHTML(t *testing.T) *html.Node {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile("testdata/index.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
return parseTestHTML(t, string(data))
|
||||
}
|
||||
38
internal/provider/vpnsecure/updater/hosttoserver.go
Normal file
38
internal/provider/vpnsecure/updater/hosttoserver.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
func (hts hostToServer) toHostsSlice() (hosts []string) {
|
||||
hosts = make([]string, 0, len(hts))
|
||||
for host := range hts {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
|
||||
for host, IPs := range hostToIPs {
|
||||
server := hts[host]
|
||||
server.IPs = IPs
|
||||
hts[host] = server
|
||||
}
|
||||
for host, server := range hts {
|
||||
if len(server.IPs) == 0 {
|
||||
delete(hts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hts hostToServer) toServersSlice() (servers []models.Server) {
|
||||
servers = make([]models.Server, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
26
internal/provider/vpnsecure/updater/resolve.go
Normal file
26
internal/provider/vpnsecure/updater/resolve.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxDuration = 5 * time.Second
|
||||
maxFailRatio = 0.1
|
||||
maxNoNew = 2
|
||||
maxFails = 3
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
Hosts: hosts,
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
57
internal/provider/vpnsecure/updater/servers.go
Normal file
57
internal/provider/vpnsecure/updater/servers.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
)
|
||||
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error) {
|
||||
servers, err = fetchServers(ctx, u.client, u.warner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch servers: %w", err)
|
||||
} else if len(servers) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
hts := make(hostToServer, len(servers))
|
||||
for _, server := range servers {
|
||||
hts[server.Hostname] = server
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
|
||||
resolveSettings := parallelResolverSettings(hosts)
|
||||
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
for _, warning := range warnings {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(hostToIPs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
for i := range servers {
|
||||
servers[i].VPN = vpn.OpenVPN
|
||||
servers[i].UDP = true
|
||||
servers[i].TCP = true
|
||||
}
|
||||
|
||||
sort.Sort(models.SortableServers(servers))
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
7345
internal/provider/vpnsecure/updater/testdata/index.html
vendored
Normal file
7345
internal/provider/vpnsecure/updater/testdata/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
22
internal/provider/vpnsecure/updater/updater.go
Normal file
22
internal/provider/vpnsecure/updater/updater.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(client *http.Client, warner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Updater {
|
||||
return &Updater{
|
||||
client: client,
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
239
internal/provider/vpnsecure/updater/website.go
Normal file
239
internal/provider/vpnsecure/updater/website.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
htmlutils "github.com/qdm12/gluetun/internal/updater/html"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func fetchServers(ctx context.Context, client *http.Client,
|
||||
warner common.Warner) (servers []models.Server, err error) {
|
||||
const url = "https://www.vpnsecure.me/vpn-locations/"
|
||||
rootNode, err := htmlutils.Fetch(ctx, client, url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
||||
}
|
||||
|
||||
servers, warnings, err := parseHTML(rootNode)
|
||||
for _, warning := range warnings {
|
||||
warner.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing HTML code: %w", err)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrHTMLServersDivNotFound = errors.New("HTML servers container div not found")
|
||||
)
|
||||
|
||||
const divString = "div"
|
||||
|
||||
func parseHTML(rootNode *html.Node) (servers []models.Server,
|
||||
warnings []string, err error) {
|
||||
// Find div container for all servers, searching with BFS.
|
||||
serversDiv := findServersDiv(rootNode)
|
||||
if serversDiv == nil {
|
||||
return nil, nil, htmlutils.WrapError(ErrHTMLServersDivNotFound, rootNode)
|
||||
}
|
||||
|
||||
for countryNode := serversDiv.FirstChild; countryNode != nil; countryNode = countryNode.NextSibling {
|
||||
if countryNode.Data != divString {
|
||||
// empty line(s) and tab(s)
|
||||
continue
|
||||
}
|
||||
|
||||
country := findCountry(countryNode)
|
||||
if country == "" {
|
||||
warnings = append(warnings, htmlutils.WrapWarning("country not found", countryNode))
|
||||
continue
|
||||
}
|
||||
|
||||
grid := htmlutils.BFS(countryNode, matchGridDiv)
|
||||
if grid == nil {
|
||||
warnings = append(warnings, htmlutils.WrapWarning("grid div not found", countryNode))
|
||||
continue
|
||||
}
|
||||
|
||||
gridItems := htmlutils.DirectChildren(grid, matchGridItem)
|
||||
if len(gridItems) == 0 {
|
||||
warnings = append(warnings, htmlutils.WrapWarning("no grid item found", grid))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, gridItem := range gridItems {
|
||||
server, warning := parseHTMLGridItem(gridItem)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
server.Country = country
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func parseHTMLGridItem(gridItem *html.Node) (
|
||||
server models.Server, warning string) {
|
||||
gridItemDT := htmlutils.DirectChild(gridItem, matchDT)
|
||||
if gridItemDT == nil {
|
||||
return server, htmlutils.WrapWarning("grid item <dt> not found", gridItem)
|
||||
}
|
||||
|
||||
host := findHost(gridItemDT)
|
||||
if host == "" {
|
||||
return server, htmlutils.WrapWarning("host not found", gridItemDT)
|
||||
}
|
||||
|
||||
status := findStatus(gridItemDT)
|
||||
if !strings.EqualFold(status, "up") {
|
||||
warning := fmt.Sprintf("skipping server with host %s which has status %q", host, status)
|
||||
warning = htmlutils.WrapWarning(warning, gridItemDT)
|
||||
return server, warning
|
||||
}
|
||||
|
||||
gridItemDD := htmlutils.DirectChild(gridItem, matchDD)
|
||||
if gridItemDD == nil {
|
||||
return server, htmlutils.WrapWarning("grid item dd not found", gridItem)
|
||||
}
|
||||
|
||||
region := findSpanStrong(gridItemDD, "Region:")
|
||||
if region == "" {
|
||||
warning := fmt.Sprintf("region for host %s not found", host)
|
||||
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||
}
|
||||
|
||||
city := findSpanStrong(gridItemDD, "City:")
|
||||
if city == "" {
|
||||
warning := fmt.Sprintf("region for host %s not found", host)
|
||||
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||
}
|
||||
|
||||
premiumString := findSpanStrong(gridItemDD, "Premium:")
|
||||
if premiumString == "" {
|
||||
warning := fmt.Sprintf("premium for host %s not found", host)
|
||||
return server, htmlutils.WrapWarning(warning, gridItemDD)
|
||||
}
|
||||
|
||||
return models.Server{
|
||||
Region: region,
|
||||
City: city,
|
||||
Hostname: host + ".isponeder.com",
|
||||
Premium: strings.EqualFold(premiumString, "yes"),
|
||||
}, ""
|
||||
}
|
||||
|
||||
func findCountry(countryNode *html.Node) (country string) {
|
||||
for node := countryNode.FirstChild; node != nil; node = node.NextSibling {
|
||||
if node.Data != "a" {
|
||||
continue
|
||||
}
|
||||
for subNode := node.FirstChild; subNode != nil; subNode = subNode.NextSibling {
|
||||
if subNode.Data != "h4" {
|
||||
continue
|
||||
}
|
||||
return subNode.FirstChild.Data
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findServersDiv(rootNode *html.Node) (serversDiv *html.Node) {
|
||||
locationsDiv := htmlutils.BFS(rootNode, matchLocationsListDiv)
|
||||
if locationsDiv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return htmlutils.BFS(locationsDiv, matchServersDiv)
|
||||
}
|
||||
|
||||
func findHost(gridItemDT *html.Node) (host string) {
|
||||
hostNode := htmlutils.DirectChild(gridItemDT, matchText)
|
||||
return strings.TrimSpace(hostNode.Data)
|
||||
}
|
||||
|
||||
func matchText(node *html.Node) (match bool) {
|
||||
if node.Type != html.TextNode {
|
||||
return false
|
||||
}
|
||||
data := strings.TrimSpace(node.Data)
|
||||
return data != ""
|
||||
}
|
||||
|
||||
func findStatus(gridItemDT *html.Node) (status string) {
|
||||
statusNode := htmlutils.DirectChild(gridItemDT, matchStatusSpan)
|
||||
return strings.TrimSpace(statusNode.FirstChild.Data)
|
||||
}
|
||||
|
||||
func matchServersDiv(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == divString &&
|
||||
htmlutils.HasClassStrings(node, "blk__i")
|
||||
}
|
||||
|
||||
func matchLocationsListDiv(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == divString &&
|
||||
htmlutils.HasClassStrings(node, "locations-list")
|
||||
}
|
||||
|
||||
func matchGridDiv(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == divString &&
|
||||
htmlutils.HasClassStrings(node, "grid--locations")
|
||||
}
|
||||
|
||||
func matchGridItem(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == "dl" &&
|
||||
htmlutils.HasClassStrings(node, "grid__i")
|
||||
}
|
||||
|
||||
func matchDT(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == "dt"
|
||||
}
|
||||
|
||||
func matchDD(node *html.Node) (match bool) {
|
||||
return node != nil && node.Data == "dd"
|
||||
}
|
||||
|
||||
func matchStatusSpan(node *html.Node) (match bool) {
|
||||
return node.Data == "span" && htmlutils.HasClassStrings(node, "status")
|
||||
}
|
||||
|
||||
func findSpanStrong(gridItemDD *html.Node, spanData string) (
|
||||
strongValue string) {
|
||||
spanFound := false
|
||||
for child := gridItemDD.FirstChild; child != nil; child = child.NextSibling {
|
||||
if !htmlutils.MatchData("div")(child) {
|
||||
continue
|
||||
}
|
||||
|
||||
for subchild := child.FirstChild; subchild != nil; subchild = subchild.NextSibling {
|
||||
if htmlutils.MatchData("span")(subchild) && subchild.FirstChild.Data == spanData {
|
||||
spanFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !spanFound {
|
||||
continue
|
||||
}
|
||||
|
||||
for subchild := child.FirstChild; subchild != nil; subchild = subchild.NextSibling {
|
||||
if htmlutils.MatchData("strong")(subchild) {
|
||||
return subchild.FirstChild.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
234
internal/provider/vpnsecure/updater/website_test.go
Normal file
234
internal/provider/vpnsecure/updater/website_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func Test_fetchServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
servers []models.Server
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"context canceled": {
|
||||
ctx: canceledCtx,
|
||||
errWrapped: context.Canceled,
|
||||
errMessage: `fetching HTML code: Get "https://www.vpnsecure.me/vpn-locations/": context canceled`,
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: io.NopCloser(strings.NewReader(`
|
||||
<div class="blk blk--white locations-list">
|
||||
<div class="blk__i">
|
||||
<div>
|
||||
<a href="https://www.vpnsecure.me/vpn-locations/australia/">
|
||||
<h4>Australia</h4>
|
||||
</a>
|
||||
<div class="grid grid--3 grid--locations">
|
||||
<dl class="grid__i">
|
||||
<dt>
|
||||
au1
|
||||
<span class="status status--up">up</span>
|
||||
</dt>
|
||||
<dd>
|
||||
<div><span>City:</span> <strong>City</strong></div>
|
||||
<div><span>Region:</span> <strong>Region</strong></div>
|
||||
<div><span>Premium:</span> <strong>YES</strong></div>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)),
|
||||
servers: []models.Server{
|
||||
{
|
||||
Country: "Australia",
|
||||
City: "City",
|
||||
Region: "Region",
|
||||
Hostname: "au1.isponeder.com",
|
||||
Premium: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, r.URL.String(), "https://www.vpnsecure.me/vpn-locations/")
|
||||
|
||||
ctxErr := r.Context().Err()
|
||||
if ctxErr != nil {
|
||||
return nil, ctxErr
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: testCase.responseBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
warner := common.NewMockWarner(ctrl)
|
||||
|
||||
servers, err := fetchServers(testCase.ctx, client, warner)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.servers, servers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
rootNode *html.Node
|
||||
servers []models.Server
|
||||
warnings []string
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty html": {
|
||||
rootNode: parseTestHTML(t, ""),
|
||||
errWrapped: ErrHTMLServersDivNotFound,
|
||||
errMessage: `HTML servers container div not found: in HTML code: <html><head></head><body></body></html>`,
|
||||
},
|
||||
"test data": {
|
||||
rootNode: parseTestDataIndexHTML(t),
|
||||
warnings: []string{
|
||||
"no grid item found: in HTML code: <div class=\"grid grid--3 grid--locations\">\n </div>",
|
||||
},
|
||||
//nolint:lll
|
||||
servers: []models.Server{
|
||||
{Country: "Australia", Region: "Queensland", City: "Brisbane", Hostname: "au1.isponeder.com", Premium: true},
|
||||
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au2.isponeder.com"},
|
||||
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au3.isponeder.com"},
|
||||
{Country: "Australia", Region: "New South Wales", City: "Sydney", Hostname: "au4.isponeder.com", Premium: true},
|
||||
{Country: "Austria", Region: "Vienna", City: "Vienna", Hostname: "at1.isponeder.com", Premium: true},
|
||||
{Country: "Austria", Region: "Vienna", City: "Vienna", Hostname: "at2.isponeder.com"},
|
||||
{Country: "Brazil", Region: "Sao Paulo", City: "Sao Paulo", Hostname: "br1.isponeder.com", Premium: true},
|
||||
{Country: "Belgium", Region: "Flanders", City: "Zaventem", Hostname: "be1.isponeder.com"},
|
||||
{Country: "Belgium", Region: "Brussels Hoofdstedelijk Gewest", City: "Brussel", Hostname: "be2.isponeder.com"},
|
||||
{Country: "Canada", Region: "Ontario", City: "Richmond Hill", Hostname: "ca1.isponeder.com"},
|
||||
{Country: "Canada", Region: "Ontario", City: "Richmond Hill", Hostname: "ca2.isponeder.com"},
|
||||
{Country: "Canada", Region: "Quebec", City: "Montréal", Hostname: "ca3.isponeder.com", Premium: true},
|
||||
{Country: "Denmark", Region: "Capital Region", City: "Copenhagen", Hostname: "dk1.isponeder.com", Premium: true},
|
||||
{Country: "Denmark", Region: "Capital Region", City: "Copenhagen", Hostname: "dk2.isponeder.com", Premium: true},
|
||||
{Country: "Denmark", Region: "Capital Region", City: "Ballerup", Hostname: "dk3.isponeder.com"},
|
||||
{Country: "France", Region: "Île-de-France", City: "Paris", Hostname: "fr1.isponeder.com"},
|
||||
{Country: "France", Region: "Île-de-France", City: "Paris", Hostname: "fr2.isponeder.com"},
|
||||
{Country: "France", Region: "Grand Est", City: "Strasbourg", Hostname: "fr3.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de1.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de2.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de3.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de4.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Limburg an der Lahn", Hostname: "de5.isponeder.com"},
|
||||
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", Hostname: "de6.isponeder.com"},
|
||||
{Country: "Hungary", Region: "Budapest", City: "Budapest", Hostname: "hu1.isponeder.com", Premium: true},
|
||||
{Country: "India", Region: "Karnataka", City: "Doddaballapura", Hostname: "in1.isponeder.com"},
|
||||
{Country: "Indonesia", Region: "Special Capital Region of Jakarta", City: "Jakarta", Hostname: "id1.isponeder.com"},
|
||||
{Country: "Ireland", Region: "Dublin City", City: "Dublin", Hostname: "ie1.isponeder.com"},
|
||||
{Country: "Israel", Region: "Tel Aviv", City: "Tel Aviv", Hostname: "il1.isponeder.com", Premium: true},
|
||||
{Country: "Italy", Region: "Lombardy", City: "Milan", Hostname: "it1.isponeder.com", Premium: true},
|
||||
{Country: "Japan", Region: "Tokyo", City: "Tokyo", Hostname: "jp2.isponeder.com", Premium: true},
|
||||
{Country: "Mexico", Region: "México", City: "Ampliación San Mateo (Colonia Solidaridad)", Hostname: "mx1.isponeder.com"},
|
||||
{Country: "Netherlands", Region: "North Holland", City: "Haarlem", Hostname: "nl1.isponeder.com"},
|
||||
{Country: "Netherlands", Region: "South Holland", City: "Naaldwijk", Hostname: "nl2.isponeder.com"},
|
||||
{Country: "New Zealand", Region: "Auckland", City: "Auckland", Hostname: "nz1.isponeder.com"},
|
||||
{Country: "Norway", Region: "Oslo", City: "Oslo", Hostname: "no1.isponeder.com", Premium: true},
|
||||
{Country: "Norway", Region: "Stockholm", City: "Stockholm", Hostname: "no2.isponeder.com", Premium: true},
|
||||
{Country: "Poland", Region: "Mazovia", City: "Warsaw", Hostname: "pl1.isponeder.com", Premium: true},
|
||||
{Country: "Romania", Region: "Bucure?ti", City: "Bucharest", Hostname: "ro1.isponeder.com", Premium: true},
|
||||
{Country: "Russia", Region: "Moscow", City: "Moscow", Hostname: "ru1.isponeder.com", Premium: true},
|
||||
{Country: "Singapore", Region: "Singapore", City: "Singapore", Hostname: "sg1.isponeder.com", Premium: true},
|
||||
{Country: "South Africa", Region: "Western Cape", City: "Cape Town", Hostname: "za1.isponeder.com", Premium: true},
|
||||
{Country: "Spain", Region: "Madrid", City: "Madrid", Hostname: "es2.isponeder.com"},
|
||||
{Country: "Spain", Region: "Valencia", City: "Valencia", Hostname: "se1.isponeder.com"},
|
||||
{Country: "Sweden", Region: "Stockholm", City: "Stockholm", Hostname: "se2.isponeder.com", Premium: true},
|
||||
{Country: "Sweden", Region: "Stockholm", City: "Stockholm", Hostname: "se3.isponeder.com"},
|
||||
{Country: "Switzerland", Region: "Vaud", City: "Lausanne", Hostname: "ch1.isponeder.com"},
|
||||
{Country: "Switzerland", Region: "Geneva", City: "Geneva", Hostname: "ch1.isponeder.com", Premium: true},
|
||||
{Country: "Switzerland", Region: "Geneva", City: "Genève", Hostname: "ch2.isponeder.com", Premium: true},
|
||||
{Country: "Ukraine", Region: "Poltavs'ka Oblast'", City: "Kremenchuk", Hostname: "ua1.isponeder.com", Premium: true},
|
||||
{Country: "United Arab Emirates", Region: "Maharashtra", City: "Mumbai", Hostname: "ae1.isponeder.com", Premium: true},
|
||||
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk2.isponeder.com"},
|
||||
{Country: "United Kingdom", Region: "England", City: "Kent", Hostname: "uk3.isponeder.com"},
|
||||
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk4.isponeder.com"},
|
||||
{Country: "United Kingdom", Region: "England", City: "London", Hostname: "uk5.isponeder.com"},
|
||||
{Country: "United Kingdom", Region: "Brent", City: "Harlesden", Hostname: "uk6.isponeder.com"},
|
||||
{Country: "United Kingdom", Region: "England", City: "Manchester", Hostname: "uk7.isponeder.com"},
|
||||
{Country: "United States", Region: "New Jersey", City: "Secaucus", Hostname: "us1.isponeder.com"},
|
||||
{Country: "United States", Region: "New York", City: "New York City", Hostname: "us10.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us11.isponeder.com"},
|
||||
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us12.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us13.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us14.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us15.isponeder.com"},
|
||||
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us16.isponeder.com"},
|
||||
{Country: "United States", Region: "New York", City: "New York City", Hostname: "us2.isponeder.com"},
|
||||
{Country: "United States", Region: "Oregon", City: "Portland", Hostname: "us3.isponeder.com", Premium: true},
|
||||
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us4.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us5.isponeder.com"},
|
||||
{Country: "United States", Region: "California", City: "Los Angeles", Hostname: "us6.isponeder.com"},
|
||||
{Country: "United States", Region: "Illinois", City: "Chicago", Hostname: "us7.isponeder.com"},
|
||||
{Country: "United States", Region: "Georgia", City: "Atlanta", Hostname: "us8.isponeder.com"},
|
||||
{Country: "United States", Region: "Georgia", City: "Atlanta", Hostname: "us9.isponeder.com"},
|
||||
{Country: "Hong Kong", Region: "Central and Western", City: "Hong Kong", Hostname: "hk1.isponeder.com"},
|
||||
{Country: "United States West", Region: "California", City: "Los Angeles", Hostname: "us3.isponeder.com", Premium: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers, warnings, err := parseHTML(testCase.rootNode)
|
||||
|
||||
assert.Equal(t, testCase.servers, servers)
|
||||
assert.Equal(t, testCase.warnings, warnings)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
x5090Name := group.OvpnX509
|
||||
wgPubKey := group.WgPubKey
|
||||
for _, node := range group.Nodes {
|
||||
ips := make([]net.IP, 0, 2) // nolint:gomnd
|
||||
ips := make([]net.IP, 0, 2) //nolint:gomnd
|
||||
if node.IP != nil {
|
||||
ips = append(ips, node.IP)
|
||||
}
|
||||
|
||||
@@ -8,18 +8,34 @@ import (
|
||||
)
|
||||
|
||||
type NetLinker interface {
|
||||
Addresser
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Addresser interface {
|
||||
AddrList(link netlink.Link, family int) (
|
||||
addresses []netlink.Addr, err error)
|
||||
AddrAdd(link netlink.Link, addr *netlink.Addr) error
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(link netlink.Link, family int) (
|
||||
routes []netlink.Route, err error)
|
||||
RouteAdd(route *netlink.Route) error
|
||||
RouteDel(route *netlink.Route) error
|
||||
RouteReplace(route *netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleList(family int) (rules []netlink.Rule, err error)
|
||||
RuleAdd(rule *netlink.Rule) error
|
||||
RuleDel(rule *netlink.Rule) error
|
||||
}
|
||||
|
||||
type Linker interface {
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkByIndex(index int) (link netlink.Link, err error)
|
||||
|
||||
@@ -32,10 +32,10 @@ func (h *dnsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodPut:
|
||||
h.setStatus(w, r)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "route "+r.RequestURI+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,18 @@ func newHandler(ctx context.Context, logger infoWarner, logging bool,
|
||||
unboundLooper DNSLoop,
|
||||
updaterLooper UpdaterLooper,
|
||||
publicIPLooper PublicIPLoop,
|
||||
storage Storage,
|
||||
) http.Handler {
|
||||
handler := &handler{}
|
||||
|
||||
vpn := newVPNHandler(ctx, vpnLooper, storage, logger)
|
||||
openvpn := newOpenvpnHandler(ctx, vpnLooper, pfGetter, logger)
|
||||
dns := newDNSHandler(ctx, unboundLooper, logger)
|
||||
updater := newUpdaterHandler(ctx, updaterLooper, logger)
|
||||
publicip := newPublicIPHandler(publicIPLooper, logger)
|
||||
|
||||
handler.v0 = newHandlerV0(ctx, logger, vpnLooper, unboundLooper, updaterLooper)
|
||||
handler.v1 = newHandlerV1(logger, buildInfo, openvpn, dns, updater, publicip)
|
||||
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip)
|
||||
|
||||
handlerWithLog := withLogMiddleware(handler, logger, logging)
|
||||
handler.setLogEnabled = handlerWithLog.setEnabled
|
||||
|
||||
@@ -28,7 +28,7 @@ type handlerV0 struct {
|
||||
|
||||
func (h *handlerV0) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "unversioned API: only supports GET method", http.StatusNotFound)
|
||||
http.Error(w, "unversioned API: only supports GET method", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch r.RequestURI {
|
||||
@@ -63,6 +63,6 @@ func (h *handlerV0) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Warn(err.Error())
|
||||
}
|
||||
default:
|
||||
http.Error(w, "unversioned API: requested URI not found", http.StatusNotFound)
|
||||
http.Error(w, "unversioned API: requested URI not found", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
)
|
||||
|
||||
func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
||||
openvpn, dns, updater, publicip http.Handler) http.Handler {
|
||||
vpn, openvpn, dns, updater, publicip http.Handler) http.Handler {
|
||||
return &handlerV1{
|
||||
warner: w,
|
||||
buildInfo: buildInfo,
|
||||
vpn: vpn,
|
||||
openvpn: openvpn,
|
||||
dns: dns,
|
||||
updater: updater,
|
||||
@@ -24,6 +25,7 @@ func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
||||
type handlerV1 struct {
|
||||
warner warner
|
||||
buildInfo models.BuildInformation
|
||||
vpn http.Handler
|
||||
openvpn http.Handler
|
||||
dns http.Handler
|
||||
updater http.Handler
|
||||
@@ -34,6 +36,8 @@ func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.RequestURI == "/version" && r.Method == http.MethodGet:
|
||||
h.getVersion(w)
|
||||
case strings.HasPrefix(r.RequestURI, "/vpn"):
|
||||
h.vpn.ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.RequestURI, "/openvpn"):
|
||||
h.openvpn.ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.RequestURI, "/dns"):
|
||||
@@ -44,7 +48,7 @@ func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.publicip.ServeHTTP(w, r)
|
||||
default:
|
||||
errString := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
|
||||
http.Error(w, errString, http.StatusNotFound)
|
||||
http.Error(w, errString, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type VPNLooper interface {
|
||||
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error)
|
||||
GetSettings() (settings settings.VPN)
|
||||
SetSettings(ctx context.Context, settings settings.VPN) (outcome string)
|
||||
}
|
||||
|
||||
type DNSLoop interface {
|
||||
@@ -27,3 +28,7 @@ type PortForwardedGetter interface {
|
||||
type PublicIPLoop interface {
|
||||
GetData() (data models.PublicIP)
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
GetFilterChoices(provider string) models.FilterChoices
|
||||
}
|
||||
|
||||
@@ -34,24 +34,24 @@ func (h *openvpnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodPut:
|
||||
h.setStatus(w, r)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
case "/settings":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.getSettings(w)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
case "/portforwarded":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.getPortForwarded(w)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "route "+r.RequestURI+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,6 @@ func (h *openvpnHandler) setStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *openvpnHandler) getSettings(w http.ResponseWriter) {
|
||||
vpnSettings := h.looper.GetSettings()
|
||||
settings := vpnSettings.OpenVPN
|
||||
settings.User = "redacted"
|
||||
settings.Password = "redacted"
|
||||
encoder := json.NewEncoder(w)
|
||||
if err := encoder.Encode(settings); err != nil {
|
||||
h.warner.Warn(err.Error())
|
||||
|
||||
@@ -26,10 +26,10 @@ func (h *publicIPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodGet:
|
||||
h.getPublicIP(w)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "route "+r.RequestURI+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
func New(ctx context.Context, address string, logEnabled bool, logger Logger,
|
||||
buildInfo models.BuildInformation, openvpnLooper VPNLooper,
|
||||
pfGetter PortForwardedGetter, unboundLooper DNSLoop,
|
||||
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop) (server *httpserver.Server, err error) {
|
||||
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop, storage Storage) (
|
||||
server *httpserver.Server, err error) {
|
||||
handler := newHandler(ctx, logger, logEnabled, buildInfo,
|
||||
openvpnLooper, pfGetter, unboundLooper, updaterLooper, publicIPLooper)
|
||||
openvpnLooper, pfGetter, unboundLooper, updaterLooper, publicIPLooper, storage)
|
||||
|
||||
httpServerSettings := httpserver.Settings{
|
||||
Address: address,
|
||||
|
||||
@@ -44,10 +44,10 @@ func (h *updaterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodPut:
|
||||
h.setStatus(w, r)
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "method "+r.Method+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.Error(w, "route "+r.RequestURI+" not supported", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user