tls.acme names the certificate and jarl: the Jarl server and a token that may only write the challenge record. Steward gets the certificate with DNS-01 (lego), writing the record through Jarl's API into every view that has the zone, renews it with a third of its lifetime left and swaps it in without a restart. A check runs before every order: Jarl, the zone, the token's permission, a test record, the zone's name servers, public resolvers, clean-up; each step says what to fix. Staging by default. With a certificate, each listener serves HTTPS and plain HTTP on one port (redirecting plain HTTP, except from the server itself). steward cert / cert check / cert renew, Server -> Certificate in the web UI, GET /certificate, POST /certificate/check|renew. docs/certificates.md and docs/jarl.md (the Jarl user and the token's grant). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
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 stewardd
|
|
// 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 stewardd 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 stewardd 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 := "stewardd 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)
|
|
})
|
|
}
|