|
@@ -0,0 +1,288 @@
|
|
|
|
|
+// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
|
+// Use of this source code is governed by a BSD-style
|
|
|
|
|
+// license that can be found in the LICENSE file.
|
|
|
|
|
+
|
|
|
|
|
+//go:build linux
|
|
|
|
|
+// +build linux
|
|
|
|
|
+
|
|
|
|
|
+package tshttpproxy
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "io/ioutil"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/url"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+func TestSynologyProxyFromConfigCached(t *testing.T) {
|
|
|
|
|
+ req, err := http.NewRequest("GET", "https://example.org/", nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var orig string
|
|
|
|
|
+ orig, synologyProxyConfigPath = synologyProxyConfigPath, filepath.Join(t.TempDir(), "proxy.conf")
|
|
|
|
|
+ defer func() { synologyProxyConfigPath = orig }()
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("no config file", func(t *testing.T) {
|
|
|
|
|
+ if _, err := os.Stat(synologyProxyConfigPath); err == nil {
|
|
|
|
|
+ t.Fatalf("%s must not exist for this test", synologyProxyConfigPath)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cache.updated = time.Time{}
|
|
|
|
|
+ cache.proxy = nil
|
|
|
|
|
+
|
|
|
|
|
+ if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
|
|
|
|
|
+ t.Fatalf("got %s, %v; want nil, nil", val, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got, want := cache.updated, time.Unix(0, 0); got != want {
|
|
|
|
|
+ t.Fatalf("got %s, want %s", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ if cache.proxy != nil {
|
|
|
|
|
+ t.Fatalf("got %s, want nil", cache.proxy)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("config file updated", func(t *testing.T) {
|
|
|
|
|
+ cache.updated = time.Now()
|
|
|
|
|
+ cache.proxy = nil
|
|
|
|
|
+
|
|
|
|
|
+ if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
|
|
|
|
|
+proxy_enabled=yes
|
|
|
|
|
+http_host=10.0.0.55
|
|
|
|
|
+http_port=80
|
|
|
|
|
+ `), 0600); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val, err := synologyProxyFromConfigCached(req)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
|
|
|
|
|
+ t.Fatalf("got %s; want %s", val, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("config file removed", func(t *testing.T) {
|
|
|
|
|
+ cache.updated = time.Now()
|
|
|
|
|
+ cache.proxy = urlMustParse("http://127.0.0.1/")
|
|
|
|
|
+
|
|
|
|
|
+ if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val, err := synologyProxyFromConfigCached(req)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if val != nil {
|
|
|
|
|
+ t.Fatalf("got %s; want nil", val)
|
|
|
|
|
+ }
|
|
|
|
|
+ if cache.proxy != nil {
|
|
|
|
|
+ t.Fatalf("got %s, want nil", cache.proxy)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSynologyProxyFromConfig(t *testing.T) {
|
|
|
|
|
+ var (
|
|
|
|
|
+ openReader io.ReadCloser
|
|
|
|
|
+ openErr error
|
|
|
|
|
+ )
|
|
|
|
|
+ var origOpen func() (io.ReadCloser, error)
|
|
|
|
|
+ origOpen, openSynologyProxyConf = openSynologyProxyConf, func() (io.ReadCloser, error) {
|
|
|
|
|
+ return openReader, openErr
|
|
|
|
|
+ }
|
|
|
|
|
+ defer func() { openSynologyProxyConf = origOpen }()
|
|
|
|
|
+
|
|
|
|
|
+ req, err := http.NewRequest("GET", "https://example.com/", nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("with config", func(t *testing.T) {
|
|
|
|
|
+ mc := &mustCloser{Reader: strings.NewReader(`
|
|
|
|
|
+proxy_user=foo
|
|
|
|
|
+proxy_pwd=bar
|
|
|
|
|
+proxy_enabled=yes
|
|
|
|
|
+adv_enabled=yes
|
|
|
|
|
+bypass_enabled=yes
|
|
|
|
|
+auth_enabled=yes
|
|
|
|
|
+https_host=10.0.0.66
|
|
|
|
|
+https_port=8443
|
|
|
|
|
+http_host=10.0.0.55
|
|
|
|
|
+http_port=80
|
|
|
|
|
+ `)}
|
|
|
|
|
+ defer mc.check(t)
|
|
|
|
|
+ openReader = mc
|
|
|
|
|
+
|
|
|
|
|
+ proxyURL, err := synologyProxyFromConfig(req)
|
|
|
|
|
+
|
|
|
|
|
+ if got, want := err, openErr; got != want {
|
|
|
|
|
+ t.Fatalf("got %s, want %s", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got, want := proxyURL, urlMustParse("https://foo:[email protected]:8443"); got.String() != want.String() {
|
|
|
|
|
+ t.Fatalf("got %s, want %s", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("non-existent config", func(t *testing.T) {
|
|
|
|
|
+ openReader = nil
|
|
|
|
|
+ openErr = os.ErrNotExist
|
|
|
|
|
+
|
|
|
|
|
+ proxyURL, err := synologyProxyFromConfig(req)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("expected no error, got %s", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if proxyURL != nil {
|
|
|
|
|
+ t.Fatalf("expected no url, got %s", proxyURL)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("error opening config", func(t *testing.T) {
|
|
|
|
|
+ openReader = nil
|
|
|
|
|
+ openErr = errors.New("example error")
|
|
|
|
|
+
|
|
|
|
|
+ proxyURL, err := synologyProxyFromConfig(req)
|
|
|
|
|
+ if err != openErr {
|
|
|
|
|
+ t.Fatalf("expected %s, got %s", openErr, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if proxyURL != nil {
|
|
|
|
|
+ t.Fatalf("expected no url, got %s", proxyURL)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestParseSynologyConfig(t *testing.T) {
|
|
|
|
|
+ cases := map[string]struct {
|
|
|
|
|
+ input string
|
|
|
|
|
+ url *url.URL
|
|
|
|
|
+ err error
|
|
|
|
|
+ }{
|
|
|
|
|
+ "populated": {
|
|
|
|
|
+ input: `
|
|
|
|
|
+proxy_user=foo
|
|
|
|
|
+proxy_pwd=bar
|
|
|
|
|
+proxy_enabled=yes
|
|
|
|
|
+adv_enabled=yes
|
|
|
|
|
+bypass_enabled=yes
|
|
|
|
|
+auth_enabled=yes
|
|
|
|
|
+https_host=10.0.0.66
|
|
|
|
|
+https_port=8443
|
|
|
|
|
+http_host=10.0.0.55
|
|
|
|
|
+http_port=80
|
|
|
|
|
+`,
|
|
|
|
|
+ url: urlMustParse("https://foo:[email protected]:8443"),
|
|
|
|
|
+ err: nil,
|
|
|
|
|
+ },
|
|
|
|
|
+ "no-auth": {
|
|
|
|
|
+ input: `
|
|
|
|
|
+proxy_user=foo
|
|
|
|
|
+proxy_pwd=bar
|
|
|
|
|
+proxy_enabled=yes
|
|
|
|
|
+adv_enabled=yes
|
|
|
|
|
+bypass_enabled=yes
|
|
|
|
|
+auth_enabled=no
|
|
|
|
|
+https_host=10.0.0.66
|
|
|
|
|
+https_port=8443
|
|
|
|
|
+http_host=10.0.0.55
|
|
|
|
|
+http_port=80
|
|
|
|
|
+`,
|
|
|
|
|
+ url: urlMustParse("https://10.0.0.66:8443"),
|
|
|
|
|
+ err: nil,
|
|
|
|
|
+ },
|
|
|
|
|
+ "http": {
|
|
|
|
|
+ input: `
|
|
|
|
|
+proxy_user=foo
|
|
|
|
|
+proxy_pwd=bar
|
|
|
|
|
+proxy_enabled=yes
|
|
|
|
|
+adv_enabled=yes
|
|
|
|
|
+bypass_enabled=yes
|
|
|
|
|
+auth_enabled=yes
|
|
|
|
|
+https_host=
|
|
|
|
|
+https_port=8443
|
|
|
|
|
+http_host=10.0.0.55
|
|
|
|
|
+http_port=80
|
|
|
|
|
+`,
|
|
|
|
|
+ url: urlMustParse("http://foo:[email protected]:80"),
|
|
|
|
|
+ err: nil,
|
|
|
|
|
+ },
|
|
|
|
|
+ "empty": {
|
|
|
|
|
+ input: `
|
|
|
|
|
+proxy_user=
|
|
|
|
|
+proxy_pwd=
|
|
|
|
|
+proxy_enabled=
|
|
|
|
|
+adv_enabled=
|
|
|
|
|
+bypass_enabled=
|
|
|
|
|
+auth_enabled=
|
|
|
|
|
+https_host=
|
|
|
|
|
+https_port=
|
|
|
|
|
+http_host=
|
|
|
|
|
+http_port=
|
|
|
|
|
+`,
|
|
|
|
|
+ url: nil,
|
|
|
|
|
+ err: nil,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, example := range cases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ url, err := parseSynologyConfig(strings.NewReader(example.input))
|
|
|
|
|
+ if err != example.err {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if example.err != nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if url == nil && example.url == nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if example.url == nil {
|
|
|
|
|
+ if url != nil {
|
|
|
|
|
+ t.Fatalf("got %s, want nil", url)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got, want := example.url.String(), url.String(); got != want {
|
|
|
|
|
+ t.Fatalf("got %s, want %s", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+func urlMustParse(u string) *url.URL {
|
|
|
|
|
+ r, err := url.Parse(u)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ panic(fmt.Sprintf("urlMustParse: %s", err))
|
|
|
|
|
+ }
|
|
|
|
|
+ return r
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type mustCloser struct {
|
|
|
|
|
+ io.Reader
|
|
|
|
|
+ closed bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *mustCloser) Close() error {
|
|
|
|
|
+ m.closed = true
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *mustCloser) check(t *testing.T) {
|
|
|
|
|
+ if !m.closed {
|
|
|
|
|
+ t.Errorf("mustCloser wrapping %#v was not closed at time of check", m.Reader)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|