cmd/derper: move 204 handler from package main to derphttp

Updates #13038

Change-Id: I28a8284dbe49371cae0e9098205c7c5f17225b40
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/13060/head
Brad Fitzpatrick 4 months ago committed by Brad Fitzpatrick
parent a93dc6cdb1
commit 6ca078c46e

@ -237,7 +237,7 @@ func main() {
tsweb.AddBrowserHeaders(w) tsweb.AddBrowserHeaders(w)
io.WriteString(w, "User-agent: *\nDisallow: /\n") io.WriteString(w, "User-agent: *\nDisallow: /\n")
})) }))
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent)) mux.Handle("/generate_204", http.HandlerFunc(derphttp.ServeNoContent))
debug := tsweb.Debugger(mux) debug := tsweb.Debugger(mux)
debug.KV("TLS hostname", *hostname) debug.KV("TLS hostname", *hostname)
debug.KV("Mesh key", s.HasMeshKey()) debug.KV("Mesh key", s.HasMeshKey())
@ -337,7 +337,7 @@ func main() {
if *httpPort > -1 { if *httpPort > -1 {
go func() { go func() {
port80mux := http.NewServeMux() port80mux := http.NewServeMux()
port80mux.HandleFunc("/generate_204", serveNoContent) port80mux.HandleFunc("/generate_204", derphttp.ServeNoContent)
port80mux.Handle("/", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux})) port80mux.Handle("/", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
port80srv := &http.Server{ port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)), Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
@ -378,31 +378,6 @@ func main() {
} }
} }
const (
noContentChallengeHeader = "X-Tailscale-Challenge"
noContentResponseHeader = "X-Tailscale-Response"
)
// For captive portal detection
func serveNoContent(w http.ResponseWriter, r *http.Request) {
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
w.Header().Set(noContentResponseHeader, "response "+challenge)
}
}
w.WriteHeader(http.StatusNoContent)
}
func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
}
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`) var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
func prodAutocertHostPolicy(_ context.Context, host string) error { func prodAutocertHostPolicy(_ context.Context, host string) error {

@ -10,6 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"tailscale.com/derp/derphttp"
"tailscale.com/tstest/deptest" "tailscale.com/tstest/deptest"
) )
@ -76,20 +77,20 @@ func TestNoContent(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil) req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
if tt.input != "" { if tt.input != "" {
req.Header.Set(noContentChallengeHeader, tt.input) req.Header.Set(derphttp.NoContentChallengeHeader, tt.input)
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
serveNoContent(w, req) derphttp.ServeNoContent(w, req)
resp := w.Result() resp := w.Result()
if tt.want == "" { if tt.want == "" {
if h, found := resp.Header[noContentResponseHeader]; found { if h, found := resp.Header[derphttp.NoContentResponseHeader]; found {
t.Errorf("got %+v; expected no response header", h) t.Errorf("got %+v; expected no response header", h)
} }
return return
} }
if got := resp.Header.Get(noContentResponseHeader); got != tt.want { if got := resp.Header.Get(derphttp.NoContentResponseHeader); got != tt.want {
t.Errorf("got %q; want %q", got, tt.want) t.Errorf("got %q; want %q", got, tt.want)
} }
}) })

@ -18,6 +18,7 @@ import (
// following its HTTP request. // following its HTTP request.
const fastStartHeader = "Derp-Fast-Start" const fastStartHeader = "Derp-Fast-Start"
// Handler returns an http.Handler to be mounted at /derp, serving s.
func Handler(s *derp.Server) http.Handler { func Handler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// These are installed both here and in cmd/derper. The check here // These are installed both here and in cmd/derper. The check here
@ -79,3 +80,29 @@ func ProbeHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed) http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
} }
} }
// ServeNoContent generates the /generate_204 response used by Tailscale's
// captive portal detection.
func ServeNoContent(w http.ResponseWriter, r *http.Request) {
if challenge := r.Header.Get(NoContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
w.Header().Set(NoContentResponseHeader, "response "+challenge)
}
}
w.WriteHeader(http.StatusNoContent)
}
func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
}
const (
NoContentChallengeHeader = "X-Tailscale-Challenge"
NoContentResponseHeader = "X-Tailscale-Response"
)

Loading…
Cancel
Save