package server import ( "bufio" "crypto/tls" "errors" "net" "net/http" "net/netip" "strings" "sync" "time" ) // sniffListener serves TLS and plain HTTP on the same port: it looks at a // connection's first byte and hands the server a *tls.Conn when it is a // TLS handshake (0x16), the plain connection otherwise. That way housecarld // can start serving HTTPS when a certificate turns up (one it obtains, // docs/certificates.md) without a restart, and a plain-HTTP bookmark gets // a redirect instead of an error. type sniffListener struct { net.Listener cfg *tls.Config ch chan net.Conn errc chan error done chan struct{} once sync.Once } func newSniffListener(ln net.Listener, cfg *tls.Config) *sniffListener { l := &sniffListener{Listener: ln, cfg: cfg, ch: make(chan net.Conn), errc: make(chan error, 1), done: make(chan struct{})} go l.loop() return l } func (l *sniffListener) loop() { for { c, err := l.Listener.Accept() if err != nil { var ne net.Error if errors.As(err, &ne) && ne.Timeout() { time.Sleep(50 * time.Millisecond) continue } select { case l.errc <- err: default: } return } go l.classify(c) } } func (l *sniffListener) classify(c net.Conn) { c.SetReadDeadline(time.Now().Add(10 * time.Second)) br := bufio.NewReader(c) b, err := br.Peek(1) c.SetReadDeadline(time.Time{}) if err != nil { c.Close() return } var out net.Conn = &peekedConn{Conn: c, r: br} if b[0] == 0x16 { out = tls.Server(out, l.cfg) } select { case l.ch <- out: case <-l.done: c.Close() } } func (l *sniffListener) Accept() (net.Conn, error) { select { case c := <-l.ch: return c, nil case err := <-l.errc: return nil, err case <-l.done: return nil, net.ErrClosed } } func (l *sniffListener) Close() error { l.once.Do(func() { close(l.done) }) return l.Listener.Close() } // peekedConn reads through the buffer that holds the peeked byte. type peekedConn struct { net.Conn r *bufio.Reader } func (c *peekedConn) Read(p []byte) (int, error) { return c.r.Read(p) } // webCertificate is the certificate housecarld serves: tls.cert_file's, or // the one it obtains itself (tls.acme). func (s *Server) webCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { if s.certs != nil { return s.certs.get(hello) } if s.cfg.ACME != nil { if c := s.cfg.ACME.Certificate(); c != nil { return c, nil } } return nil, errNoCertificate } var errNoCertificate = errors.New("no certificate for HTTPS yet") // httpsAvailable reports whether housecarld has a certificate to serve. func (s *Server) httpsAvailable() bool { return s.certs != nil || s.cfg.ACME != nil && s.cfg.ACME.Certificate() != nil } // requireHTTPS sends plain-HTTP requests to HTTPS once there is a // certificate: browsers (GET and HEAD) get a redirect, anything else an // error, so tokens and passwords aren't sent in the clear. Requests from // loopback (tools on this server) and from trusted proxies (which // terminate TLS themselves) are served as they are. func (s *Server) requireHTTPS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS != nil || !s.httpsAvailable() { next.ServeHTTP(w, r) return } host, _, _ := net.SplitHostPort(r.RemoteAddr) if a, err := netip.ParseAddr(host); err == nil && (a.Unmap().IsLoopback() || s.trusted(a.Unmap())) { next.ServeHTTP(w, r) return } target := "https://" + r.Host + r.URL.RequestURI() if r.Method == http.MethodGet || r.Method == http.MethodHead { http.Redirect(w, r, target, http.StatusPermanentRedirect) return } msg := "housecarld serves HTTPS on this port; use " + strings.TrimSuffix(target, r.URL.RequestURI()) if r.Header.Get("Authorization") != "" { msg += ". This request's token crossed the network unencrypted; if that network isn't trusted, rotate the token" } writeError(w, http.StatusBadRequest, "https_required", msg) }) }