summaryrefslogtreecommitdiffhomepage
path: root/net/tshttpproxy/tshttpproxy_synology.go
blob: a632753f7bc1b55b6d7421a30e2723a2a6f25c3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package tshttpproxy

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"tailscale.com/util/lineiter"
)

// These vars are overridden for tests.
var (
	synologyProxyConfigPath = "/etc/proxy.conf"

	openSynologyProxyConf = func() (io.ReadCloser, error) {
		return os.Open(synologyProxyConfigPath)
	}
)

var cache struct {
	sync.Mutex
	httpProxy  *url.URL
	httpsProxy *url.URL
	updated    time.Time
}

func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
	if req.URL == nil {
		return nil, nil
	}

	cache.Lock()
	defer cache.Unlock()

	var err error
	modtime := mtime(synologyProxyConfigPath)

	if !modtime.Equal(cache.updated) {
		cache.httpProxy, cache.httpsProxy, err = synologyProxiesFromConfig()
		cache.updated = modtime
	}

	if req.URL.Scheme == "https" {
		return cache.httpsProxy, err
	}
	return cache.httpProxy, err
}

func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
	r, err := openSynologyProxyConf()
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil, nil
		}
		return nil, nil, err
	}
	defer r.Close()

	return parseSynologyConfig(r)
}

// parseSynologyConfig parses the Synology proxy configuration, and returns any
// http proxy, and any https proxy respectively, or an error if parsing fails.
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
	cfg := map[string]string{}

	for lr := range lineiter.Reader(r) {
		line, err := lr.Value()
		if err != nil {
			return nil, nil, err
		}
		// accept and skip over empty lines
		line = bytes.TrimSpace(line)
		if len(line) == 0 {
			continue
		}

		key, value, ok := strings.Cut(string(line), "=")
		if !ok {
			return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
		}
		cfg[string(key)] = string(value)
	}

	if cfg["proxy_enabled"] != "yes" {
		return nil, nil, nil
	}

	httpProxyURL := new(url.URL)
	httpsProxyURL := new(url.URL)
	if cfg["auth_enabled"] == "yes" {
		httpProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
		httpsProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
	}

	// As far as we are aware, synology does not support tls proxies.
	httpProxyURL.Scheme = "http"
	httpsProxyURL.Scheme = "http"

	httpsProxyURL = addHostPort(httpsProxyURL, cfg["https_host"], cfg["https_port"])
	httpProxyURL = addHostPort(httpProxyURL, cfg["http_host"], cfg["http_port"])

	return httpProxyURL, httpsProxyURL, nil
}

// addHostPort adds to u the given host and port and returns the updated url, or
// if host is empty, it returns nil.
func addHostPort(u *url.URL, host, port string) *url.URL {
	if host == "" {
		return nil
	}

	if port == "" {
		u.Host = host
	} else {
		u.Host = net.JoinHostPort(host, port)
	}
	return u
}

// mtime stat's path and returns its modification time. If path does not exist,
// it returns the unix epoch.
func mtime(path string) time.Time {
	fi, err := os.Stat(path)
	if err != nil {
		return time.Unix(0, 0)
	}
	return fi.ModTime()
}