Skip to content

Commit 3f822b6

Browse files
authored
golink: listen on HTTPS and redirect HTTP traffic (#99)
golink: listen on HTTPS and redirect HTTP traffic Updates #9 Fixes #29 On tailnets with HTTPS enabled golink will serve the primary endpoints via HTTPS. With HTTPS enabled golink will respond to HTTP traffic with a separate redirectHandler which redirects requests to their HTTPS equivalent. Update documented examples of `curl` to include the `-L` flog to follow these redirects if present. Add a HTTPS section to the README documenting all of the above. Signed-off-by: Patrick O'Doherty <[email protected]>
1 parent 0abea01 commit 3f822b6

File tree

4 files changed

+123
-4
lines changed

4 files changed

+123
-4
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,16 @@ If you're using Firefox, you might want to configure two options to make it easy
146146
with a value of _true_
147147

148148
* if you use HTTPS-Only Mode, [add an exception](https://support.mozilla.org/en-US/kb/https-only-prefs#w_add-exceptions-for-http-websites-when-youre-in-https-only-mode)
149+
150+
## HTTPS
151+
152+
When golink joins your tailnet it will check to see if HTTPS is enabled and
153+
begin serving HTTPS traffic it detects that it is. When HTTPS is enabled golink
154+
will redirect all requests received by the HTTP endpoint first to their internal
155+
HTTPS equivalent before redirecting to the external link destination.
156+
157+
**NB:** If you use `curl` to interact with the API of a golink instance with HTTPS
158+
enabled over its HTTP interface you _must_ specify the `-L` flag to follow these
159+
redirects or else your request will terminate early with an empty response. We
160+
recommend the use of the `-L` flag in all deployments regardless of current
161+
HTTPS status to avoid accidental outages should it be enabled in the future.

golink.go

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"tailscale.com/ipn"
3737
"tailscale.com/tailcfg"
3838
"tailscale.com/tsnet"
39+
"tailscale.com/util/dnsname"
3940
)
4041

4142
const defaultHostname = "go"
@@ -158,6 +159,7 @@ func Run() error {
158159
return errors.New("--hostname, if specified, cannot be empty")
159160
}
160161

162+
// create tsNet server and wait for it to be ready & connected.
161163
srv := &tsnet.Server{
162164
ControlURL: *controlURL,
163165
Hostname: *hostname,
@@ -169,17 +171,55 @@ func Run() error {
169171
if err := srv.Start(); err != nil {
170172
return err
171173
}
174+
172175
localClient, _ = srv.LocalClient()
176+
out:
177+
for {
178+
upCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
179+
defer cancel()
180+
status, err := srv.Up(upCtx)
181+
if err == nil && status != nil {
182+
break out
183+
}
184+
}
173185

174-
l80, err := srv.Listen("tcp", ":80")
186+
statusCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
187+
defer cancel()
188+
status, err := localClient.Status(statusCtx)
175189
if err != nil {
176190
return err
177191
}
192+
enableTLS := status.Self.HasCap(tailcfg.CapabilityHTTPS)
193+
fqdn := strings.TrimSuffix(status.Self.DNSName, ".")
178194

195+
httpHandler := serveHandler()
196+
if enableTLS {
197+
httpsHandler := HSTS(httpHandler)
198+
httpHandler = redirectHandler(fqdn)
199+
200+
httpsListener, err := srv.ListenTLS("tcp", ":443")
201+
if err != nil {
202+
return err
203+
}
204+
log.Println("Listening on :443")
205+
go func() {
206+
log.Printf("Serving https://%s/ ...", fqdn)
207+
if err := http.Serve(httpsListener, httpsHandler); err != nil {
208+
log.Fatal(err)
209+
}
210+
}()
211+
}
212+
213+
httpListener, err := srv.Listen("tcp", ":80")
214+
log.Println("Listening on :80")
215+
if err != nil {
216+
return err
217+
}
179218
log.Printf("Serving http://%s/ ...", *hostname)
180-
if err := http.Serve(l80, serveHandler()); err != nil {
219+
if err := http.Serve(httpListener, httpHandler); err != nil {
181220
return err
182221
}
222+
183223
return nil
184224
}
185225

@@ -286,6 +326,34 @@ func deleteLinkStats(link *Link) {
286326
db.DeleteStats(link.Short)
287327
}
288328

329+
// redirectHandler returns the http.Handler for serving all plaintext HTTP
330+
// requests. It redirects all requests to the HTTPs version of the same URL.
331+
func redirectHandler(hostname string) http.Handler {
332+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
333+
http.Redirect(w, r, (&url.URL{Scheme: "https", Host: hostname, Path: r.URL.Path}).String(), http.StatusFound)
334+
})
335+
}
336+
337+
// HSTS wraps the provided handler and sets Strict-Transport-Security header on
338+
// responses. It inspects the Host header to ensure we do not specify HSTS
339+
// response on non fully qualified domain name origins.
340+
func HSTS(h http.Handler) http.Handler {
341+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
342+
host, found := r.Header["Host"]
343+
if found {
344+
host := host[0]
345+
fqdn, err := dnsname.ToFQDN(host)
346+
if err == nil {
347+
segCount := fqdn.NumLabels()
348+
if segCount > 1 {
349+
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
350+
}
351+
}
352+
}
353+
h.ServeHTTP(w, r)
354+
})
355+
}
356+
289357
// serverHandler returns the main http.Handler for serving all requests.
290358
func serveHandler() http.Handler {
291359
mux := http.NewServeMux()

golink_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,41 @@ func TestResolveLink(t *testing.T) {
524524
})
525525
}
526526
}
527+
528+
func TestNoHSTSShortDomain(t *testing.T) {
529+
var err error
530+
db, err = NewSQLiteDB(":memory:")
531+
if err != nil {
532+
t.Fatal(err)
533+
}
534+
db.Save(&Link{Short: "foobar", Long: "http://foobar/"})
535+
536+
tests := []struct {
537+
host string
538+
expectHsts bool
539+
}{
540+
{
541+
host: "go",
542+
expectHsts: false,
543+
},
544+
{
545+
host: "go.prawn-universe.ts.net",
546+
expectHsts: true,
547+
},
548+
}
549+
for _, tt := range tests {
550+
name := "HSTS: " + tt.host
551+
t.Run(name, func(t *testing.T) {
552+
r := httptest.NewRequest("GET", "/foobar", nil)
553+
r.Header.Add("Host", tt.host)
554+
555+
w := httptest.NewRecorder()
556+
HSTS(serveHandler()).ServeHTTP(w, r)
557+
558+
_, found := w.Header()["Strict-Transport-Security"]
559+
if found != tt.expectHsts {
560+
t.Errorf("HSTS expectation: domain %s want: %t got: %t", tt.host, tt.expectHsts, found)
561+
}
562+
})
563+
}
564+
}

tmpl/help.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,15 @@ <h2 id="api">Application Programming Interface (API)</h2>
114114
Visit <a href="/.export">go/.export</a> to export all saved links and their metadata in <a href="https://jsonlines.org/">JSON Lines format</a>.
115115
This is useful to create data snapshots that can be restored later.
116116

117-
<pre>{{`$ curl go/.export
117+
<pre>{{`$ curl -L go/.export
118118
{"Short":"go","Long":"http://go","Created":"2022-05-31T13:04:44.741457796-07:00","LastEdit":"2022-05-31T13:04:44.741457796-07:00","Owner":"[email protected]","Clicks":1}
119119
{"Short":"slack","Long":"https://company.slack.com/{{if .Path}}channels/{{PathEscape .Path}}{{end}}","Created":"2022-06-17T18:05:43.562948451Z","LastEdit":"2022-06-17T18:06:35.811398Z","Owner":"[email protected]","Clicks":4}`}}
120120
</pre>
121121

122122
<p>
123123
Create a new link by sending a POST request with a <code>short</code> and <code>long</code> value:
124124

125-
<pre>{{`$ curl -d short=cs -d long=https://cs.github.com/ go
125+
<pre>{{`$ curl -L -d short=cs -d long=https://cs.github.com/ go
126126
{"Short":"cs","Long":"https://cs.github.com/","Created":"2022-06-03T22:15:29.993978392Z","LastEdit":"2022-06-03T22:15:29.993978392Z","Owner":"[email protected]"}`}}
127127
</pre>
128128

0 commit comments

Comments
 (0)