Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

golink: listen on HTTPS and redirect HTTP traffic #99

Merged
merged 10 commits into from
Dec 18, 2023
67 changes: 65 additions & 2 deletions golink.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func Run() error {
return errors.New("--hostname, if specified, cannot be empty")
}

// create tsNet server and wait for it to be ready & connected.
srv := &tsnet.Server{
ControlURL: *controlURL,
Hostname: *hostname,
Expand All @@ -169,17 +170,55 @@ func Run() error {
if err := srv.Start(); err != nil {
return err
}

localClient, _ = srv.LocalClient()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're here, maybe now is a good time to

localClient, err = srv.LocalClient()
if err != nil {
  return err
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the server is started, LocalClient promises not to report an error in this context.

out:
for {
upCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
status, err := srv.Up(upCtx)
if err == nil && status != nil {
break out
}
}

l80, err := srv.Listen("tcp", ":80")
statusCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
status, err := localClient.Status(statusCtx)
if err != nil {
return err
}
enableTLS := status.Self.HasCap(tailcfg.CapabilityHTTPS)
fqdn := strings.TrimSuffix(status.Self.DNSName, ".")

httpHandler := serveHandler()
if enableTLS {
httpsHandler := HSTS(httpHandler)
httpHandler = redirectHandler(fqdn)

httpsListener, err := srv.ListenTLS("tcp", ":443")
if err != nil {
return err
}
log.Println("Listening on :443")
go func() {
log.Printf("Serving https://%s/ ...", fqdn)
if err := http.Serve(httpsListener, httpsHandler); err != nil {
log.Fatal(err)
}
}()
}

httpListener, err := srv.Listen("tcp", ":80")
log.Println("Listening on :80")
if err != nil {
return err
}
log.Printf("Serving http://%s/ ...", *hostname)
if err := http.Serve(l80, serveHandler()); err != nil {
if err := http.Serve(httpListener, httpHandler); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -286,6 +325,30 @@ func deleteLinkStats(link *Link) {
db.DeleteStats(link.Short)
}

// redirectHandler returns the http.Handler for serving all plaintext HTTP
// requests. It redirects all requests to the HTTPs version of the same URL.
func redirectHandler(hostname string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, (&url.URL{Scheme: "https", Host: hostname, Path: r.URL.Path}).String(), http.StatusFound)
})
}

// HSTS wraps the provided handler and sets Strict-Transport-Security header on
// responses. It inspects the Host header to ensure we do not specify HSTS
// response on non fully qualified domain name origins.
func HSTS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, found := r.Header["Host"]
if found {
host := host[0]
if strings.Contains(host, ".") {
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
}
}
h.ServeHTTP(w, r)
})
}

// serverHandler returns the main http.Handler for serving all requests.
func serveHandler() http.Handler {
mux := http.NewServeMux()
Expand Down
38 changes: 38 additions & 0 deletions golink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,41 @@ func TestResolveLink(t *testing.T) {
})
}
}

func TestNoHSTSShortDomain(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
db.Save(&Link{Short: "foobar", Long: "http://foobar/"})

tests := []struct {
host string
expectHsts bool
}{
{
host: "go",
expectHsts: false,
},
{
host: "go.prawn-universe.ts.net",
expectHsts: true,
},
}
for _, tt := range tests {
name := "HSTS: " + tt.host
t.Run(name, func(t *testing.T) {
r := httptest.NewRequest("GET", "/foobar", nil)
r.Header.Add("Host", tt.host)

w := httptest.NewRecorder()
HSTS(serveHandler()).ServeHTTP(w, r)

_, found := w.Header()["Strict-Transport-Security"]
if found != tt.expectHsts {
t.Errorf("HSTS expectation: domain %s want: %t got: %t", tt.host, tt.expectHsts, found)
}
})
}
}
4 changes: 2 additions & 2 deletions tmpl/help.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ <h2 id="api">Application Programming Interface (API)</h2>
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>.
This is useful to create data snapshots that can be restored later.

<pre>{{`$ curl go/.export
<pre>{{`$ curl -L go/.export
{"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}
{"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}`}}
</pre>

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

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

Expand Down
Loading