ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/acme.go

SHA-256: 3304512e46dc44685a0ce96cc5106706616b889ee2c9cf7fcffa19a1de82266e
137 líneas · 3931 bytes · Ver en GitHub

1package main
2
3import (
4 "bufio"
5 "bytes"
6 "io"
7 "net"
8 "net/http"
9 "os"
10 "path/filepath"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15)
16
17// Port 80 serves ONE thing: the HTTP-01 challenge files of the node's own certificate, from the webroot its certificate client writes. It never
18// forwards a byte to a device (a certificate for a device is issued over DNS-01 and the CAA allows nothing else), it answers only for the node's own
19// name, and everything else is a 404.
20
21const (
22 maxACMEHeadBytes = 8 << 10
23
24 resp404 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
25)
26
27var acmePathRE = regexp.MustCompile(`^/\.well-known/acme-challenge/([A-Za-z0-9_-]{1,128})$`)
28
29// hostOnly strips an optional :port and normalizes a Host header value.
30func hostOnly(h string) (string, bool) {
31 if i := strings.LastIndexByte(h, ':'); i >= 0 && !strings.Contains(h[i:], "]") {
32 h = h[:i]
33 }
34 return normalizeName(h)
35}
36
37// readHTTPHead reads up to the end of the request head (CRLF CRLF).
38func readHTTPHead(c net.Conn, timeout time.Duration) ([]byte, error) {
39 _ = c.SetReadDeadline(time.Now().Add(timeout))
40 defer func() { _ = c.SetReadDeadline(time.Time{}) }()
41 buf := make([]byte, 0, 1024)
42 searched := 0
43 for {
44 if len(buf) == cap(buf) {
45 if cap(buf) >= maxACMEHeadBytes {
46 return nil, errHelloTooLarge
47 }
48 grown := make([]byte, len(buf), min(2*cap(buf), maxACMEHeadBytes))
49 copy(grown, buf)
50 buf = grown
51 }
52 n, err := c.Read(buf[len(buf):cap(buf)])
53 buf = buf[:len(buf)+n]
54 from := max(0, searched-3)
55 if i := bytes.Index(buf[from:], []byte("\r\n\r\n")); i >= 0 {
56 return buf[:from+i+4], nil
57 }
58 searched = len(buf)
59 if err != nil {
60 return nil, err
61 }
62 }
63}
64
65// acmeChallenge reads the challenge file of a token from the webroot: a regular file (never a link) no larger than the cap.
66func (s *Server) acmeChallenge(token string) ([]byte, bool) {
67 if s.cfg.ACMEWebroot == "" {
68 return nil, false
69 }
70 path := filepath.Join(s.cfg.ACMEWebroot, ".well-known", "acme-challenge", token)
71 st, err := os.Lstat(path)
72 if err != nil || !st.Mode().IsRegular() || st.Size() > s.cfg.ACMEMaxBytes {
73 return nil, false
74 }
75 f, err := os.Open(path)
76 if err != nil {
77 return nil, false
78 }
79 defer f.Close()
80 body, err := io.ReadAll(io.LimitReader(f, s.cfg.ACMEMaxBytes+1))
81 if err != nil || int64(len(body)) > s.cfg.ACMEMaxBytes {
82 return nil, false
83 }
84 return body, true
85}
86
87// handleHTTP serves only GET/HEAD /.well-known/acme-challenge/<token> for the node's own name; everything else is a 404.
88func (s *Server) handleHTTP(c net.Conn) {
89 defer s.wg.Done()
90 defer c.Close()
91 // The same sources, the same limits as :443: a source that floods one port does not get a second budget on the other.
92 if peer, ok, fromOpenAI := s.sourceOf(c); ok {
93 release, why := s.ips.admit(peer, fromOpenAI)
94 if why != "" {
95 s.met.rejected.inc(why)
96 return
97 }
98 defer release()
99 }
100 if !tryAcquire(&s.unauth, int64(s.cfg.MaxUnauth)) {
101 return
102 }
103 defer s.unauth.Add(-1)
104
105 head, err := readHTTPHead(c, s.cfg.HelloTimeout)
106 if err != nil {
107 return
108 }
109 req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(head)))
110 if err != nil {
111 return
112 }
113 _ = c.SetWriteDeadline(time.Now().Add(s.cfg.ACMETimeout))
114 reply404 := func(reason string) {
115 s.log.Debug("acme_reject", "reason", reason)
116 _, _ = io.WriteString(c, resp404)
117 }
118 host, hostOK := hostOnly(req.Host)
119 if !hostOK || host != s.cfg.NodeHost {
120 reply404("not_own_host")
121 return
122 }
123 m := acmePathRE.FindStringSubmatch(req.RequestURI)
124 if (req.Method != http.MethodGet && req.Method != http.MethodHead) || m == nil {
125 reply404("not_acme_path")
126 return
127 }
128 body, found := s.acmeChallenge(m[1])
129 if !found {
130 reply404("no_challenge")
131 return
132 }
133 _, _ = io.WriteString(c, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "+strconv.Itoa(len(body))+"\r\nConnection: close\r\n\r\n")
134 if req.Method == http.MethodGet {
135 _, _ = c.Write(body)
136 }
137}