Skip to content

Commit 7f9bd0b

Browse files
tsrelay: break up routes into their own files (#112)
Our `main.go` is over 700 lines of code. This PR breaks up our routes into their own files/package so that future tests and work can be easier to handle.
1 parent de8514b commit 7f9bd0b

17 files changed

Lines changed: 759 additions & 620 deletions

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/tailscale-dev/vscode-tailscale
33
go 1.20
44

55
require (
6+
github.com/go-chi/chi/v5 v5.0.10
67
github.com/gorilla/websocket v1.5.0
78
github.com/mitchellh/go-ps v1.0.0
89
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df

go.sum

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
1010
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
1111
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
1212
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
13+
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
14+
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
1315
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1416
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1517
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -18,8 +20,6 @@ github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7H
1820
github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
1921
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
2022
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
21-
github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
22-
github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
2323
github.com/jsimonetti/rtnetlink v1.3.3 h1:ycpm3z8XlAzmaacVRjdUT3x6MM1o3YBXsXc7DXSRNCE=
2424
github.com/jsimonetti/rtnetlink v1.3.3/go.mod h1:mW4xSP3wkiqWxHMlfG/gOufp3XnhAxu7EhfABmrWSh8=
2525
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -37,42 +37,24 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H
3737
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
3838
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo=
3939
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
40-
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
41-
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
4240
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
4341
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
44-
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
45-
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
4642
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
4743
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
48-
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
49-
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
5044
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
5145
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
52-
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
53-
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
5446
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
5547
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
56-
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
57-
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
5848
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
5949
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
6050
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
61-
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
62-
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6351
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
6452
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65-
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
66-
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
6753
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
6854
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
69-
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
70-
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
7155
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
7256
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
7357
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
7458
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
75-
tailscale.com v1.1.1-0.20230607142209-f8f0b981ac9c h1:w69HAs+pB67hX+o3pvP7RSnOU8CaiPY9s1XkqwXkBaU=
76-
tailscale.com v1.1.1-0.20230607142209-f8f0b981ac9c/go.mod h1:1t0LGzRAX3TBG4k5XtXaAEmYxKnx6b2k18m7j71G3Po=
7759
tailscale.com v1.44.0 h1:MPos9n30kJvdyfL52045gVFyNg93K+bwgDsr8gqKq2o=
7860
tailscale.com v1.44.0/go.mod h1:+iYwTdeHyVJuNDu42Zafwihq1Uqfh+pW7pRaY1GD328=

tsrelay/handler/auth_middleware.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package handler
2+
3+
import "net/http"
4+
5+
func (h *handler) authMiddleware(next http.Handler) http.Handler {
6+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
7+
user, _, ok := r.BasicAuth()
8+
9+
// TODO: consider locking down to vscode-webviews://* URLs by checking
10+
// r.Header.Get("Origin") only in production builds.
11+
w.Header().Set("Access-Control-Allow-Origin", "*")
12+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
13+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
14+
15+
if r.Method == http.MethodOptions {
16+
// Handle preflight request
17+
w.WriteHeader(http.StatusNoContent)
18+
return
19+
}
20+
21+
if !ok {
22+
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
23+
}
24+
25+
if user != h.nonce {
26+
// TODO: return JSON for all errors
27+
http.Error(w, "unauthorized", http.StatusUnauthorized)
28+
return
29+
}
30+
next.ServeHTTP(w, r)
31+
})
32+
}

tsrelay/handler/create_serve.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"strings"
11+
12+
"tailscale.com/client/tailscale"
13+
"tailscale.com/ipn"
14+
)
15+
16+
type serveRequest struct {
17+
Protocol string
18+
Source string
19+
Port uint16
20+
MountPoint string
21+
Funnel bool
22+
}
23+
24+
func (h *handler) createServeHandler(w http.ResponseWriter, r *http.Request) {
25+
if err := h.createServe(r.Context(), r.Body); err != nil {
26+
var re RelayError
27+
if errors.As(err, &re) {
28+
w.WriteHeader(re.statusCode)
29+
json.NewEncoder(w).Encode(re)
30+
return
31+
}
32+
h.l.Println("error creating serve:", err)
33+
http.Error(w, err.Error(), 500)
34+
return
35+
}
36+
w.Write([]byte(`{}`))
37+
}
38+
39+
// createServe is the programtic equivalent of "tailscale serve --set-raw"
40+
// it returns the config as json in case of an error.
41+
func (h *handler) createServe(ctx context.Context, body io.Reader) error {
42+
var req serveRequest
43+
err := json.NewDecoder(body).Decode(&req)
44+
if err != nil {
45+
return fmt.Errorf("error decoding request body: %w", err)
46+
}
47+
if req.Protocol != "https" {
48+
return fmt.Errorf("unsupported protocol: %q", req.Protocol)
49+
}
50+
sc, dns, err := h.serveConfigDNS(ctx)
51+
if err != nil {
52+
return fmt.Errorf("error getting config: %w", err)
53+
}
54+
hostPort := ipn.HostPort(fmt.Sprintf("%s:%d", dns, req.Port))
55+
setHandler(sc, hostPort, req)
56+
if req.Funnel {
57+
if sc.AllowFunnel == nil {
58+
sc.AllowFunnel = make(map[ipn.HostPort]bool)
59+
}
60+
sc.AllowFunnel[hostPort] = true
61+
} else {
62+
delete(sc.AllowFunnel, hostPort)
63+
}
64+
err = h.setServeCfg(ctx, sc)
65+
if err != nil {
66+
if tailscale.IsAccessDeniedError(err) {
67+
cfgJSON, err := json.Marshal(sc)
68+
if err != nil {
69+
return fmt.Errorf("error marshaling own config: %w", err)
70+
}
71+
re := RelayError{
72+
statusCode: http.StatusForbidden,
73+
Errors: []Error{{
74+
Type: RequiresSudo,
75+
Command: fmt.Sprintf(`echo %s | sudo tailscale serve --set-raw`, cfgJSON),
76+
}},
77+
}
78+
return re
79+
}
80+
if err != nil {
81+
return fmt.Errorf("error marshaling config: %w", err)
82+
}
83+
return fmt.Errorf("error setting serve config: %w", err)
84+
}
85+
return nil
86+
}
87+
88+
func (h *handler) serveConfigDNS(ctx context.Context) (*ipn.ServeConfig, string, error) {
89+
st, sc, err := h.getConfigs(ctx)
90+
if err != nil {
91+
return nil, "", fmt.Errorf("error getting configs: %w", err)
92+
}
93+
if sc == nil {
94+
sc = &ipn.ServeConfig{}
95+
}
96+
dns := strings.TrimSuffix(st.Self.DNSName, ".")
97+
return sc, dns, nil
98+
}
99+
100+
func setHandler(sc *ipn.ServeConfig, newHP ipn.HostPort, req serveRequest) {
101+
if sc.TCP == nil {
102+
sc.TCP = make(map[uint16]*ipn.TCPPortHandler)
103+
}
104+
if _, ok := sc.TCP[req.Port]; !ok {
105+
sc.TCP[req.Port] = &ipn.TCPPortHandler{
106+
HTTPS: true,
107+
}
108+
}
109+
if sc.Web == nil {
110+
sc.Web = make(map[ipn.HostPort]*ipn.WebServerConfig)
111+
}
112+
wsc, ok := sc.Web[newHP]
113+
if !ok {
114+
wsc = &ipn.WebServerConfig{}
115+
sc.Web[newHP] = wsc
116+
}
117+
if wsc.Handlers == nil {
118+
wsc.Handlers = make(map[string]*ipn.HTTPHandler)
119+
}
120+
wsc.Handlers[req.MountPoint] = &ipn.HTTPHandler{
121+
Proxy: req.Source,
122+
}
123+
}

tsrelay/handler/delete_serve.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
11+
"tailscale.com/ipn"
12+
)
13+
14+
func (h *handler) deleteServeHandler(w http.ResponseWriter, r *http.Request) {
15+
if err := h.deleteServe(r.Context(), r.Body); err != nil {
16+
var re RelayError
17+
if errors.As(err, &re) {
18+
w.WriteHeader(re.statusCode)
19+
json.NewEncoder(w).Encode(re)
20+
return
21+
}
22+
h.l.Println("error deleting serve:", err)
23+
http.Error(w, err.Error(), 500)
24+
return
25+
}
26+
w.Write([]byte(`{}`))
27+
}
28+
29+
func (h *handler) deleteServe(ctx context.Context, body io.Reader) error {
30+
var req serveRequest
31+
if body != nil && body != http.NoBody {
32+
err := json.NewDecoder(body).Decode(&req)
33+
if err != nil {
34+
return fmt.Errorf("error decoding request body: %w", err)
35+
}
36+
}
37+
38+
// reset serve config if no request body
39+
if (req == serveRequest{}) {
40+
sc := &ipn.ServeConfig{}
41+
err := h.setServeCfg(ctx, sc)
42+
if err != nil {
43+
return fmt.Errorf("error setting serve config: %w", err)
44+
}
45+
return nil
46+
}
47+
48+
if req.Protocol != "https" {
49+
return fmt.Errorf("unsupported protocol: %q", req.Protocol)
50+
}
51+
sc, dns, err := h.serveConfigDNS(ctx)
52+
if err != nil {
53+
return fmt.Errorf("error getting config: %w", err)
54+
}
55+
hostPort := ipn.HostPort(fmt.Sprintf("%s:%d", dns, req.Port))
56+
deleteFromConfig(sc, hostPort, req)
57+
delete(sc.AllowFunnel, hostPort)
58+
if len(sc.AllowFunnel) == 0 {
59+
sc.AllowFunnel = nil
60+
}
61+
err = h.setServeCfg(ctx, sc)
62+
if err != nil {
63+
return fmt.Errorf("error setting serve config: %w", err)
64+
}
65+
return nil
66+
}
67+
68+
func deleteFromConfig(sc *ipn.ServeConfig, newHP ipn.HostPort, req serveRequest) {
69+
delete(sc.AllowFunnel, newHP)
70+
if sc.TCP != nil {
71+
delete(sc.TCP, req.Port)
72+
}
73+
if sc.Web == nil {
74+
return
75+
}
76+
if sc.Web[newHP] == nil {
77+
return
78+
}
79+
wsc, ok := sc.Web[newHP]
80+
if !ok {
81+
return
82+
}
83+
if wsc.Handlers == nil {
84+
return
85+
}
86+
_, ok = wsc.Handlers[req.MountPoint]
87+
if !ok {
88+
return
89+
}
90+
delete(wsc.Handlers, req.MountPoint)
91+
if len(wsc.Handlers) == 0 {
92+
delete(sc.Web, newHP)
93+
}
94+
}

tsrelay/handler/error.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package handler
2+
3+
// ErrorTypes for signaling
4+
// invalid states to the VSCode
5+
// extension.
6+
const (
7+
// FunnelOff means the user does not have
8+
// funnel in their ACLs.
9+
FunnelOff = "FUNNEL_OFF"
10+
// HTTPSOff means the user has not enabled
11+
// https in the DNS section of the UI
12+
HTTPSOff = "HTTPS_OFF"
13+
// Offline can mean a user is not logged in
14+
// or is logged in but their key has expired.
15+
Offline = "OFFLINE"
16+
// RequiresSudo for when LocalBackend is run
17+
// with sudo but tsrelay is not
18+
RequiresSudo = "REQUIRES_SUDO"
19+
// NotRunning indicates tailscaled is
20+
// not running
21+
NotRunning = "NOT_RUNNING"
22+
// FlatpakRequiresRestart indicates that the flatpak
23+
// container needs to be fully restarted
24+
FlatpakRequiresRestart = "FLATPAK_REQUIRES_RESTART"
25+
)
26+
27+
// RelayError is a wrapper for Error
28+
type RelayError struct {
29+
statusCode int
30+
Errors []Error
31+
}
32+
33+
// Error implements error. It returns a
34+
// static string as it is only needed to be
35+
// used for programatic type assertion.
36+
func (RelayError) Error() string {
37+
return "relay error"
38+
}
39+
40+
// Error is a programmable error returned
41+
// to the typescript client
42+
type Error struct {
43+
Type string `json:",omitempty"`
44+
Command string `json:",omitempty"`
45+
}

0 commit comments

Comments
 (0)