/
http.go
175 lines (161 loc) · 5.75 KB
/
http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package requester
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"strconv"
"sync"
"time"
"github.com/refraction-networking/conjure/pkg/registrars/dns-registrar/queuepacketconn"
)
// A default Retry-After delay to use when there is no explicit Retry-After
// header in an HTTP response.
const defaultRetryAfter = 10 * time.Second
// HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
// HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
// requests and responses.
//
// HTTPPacketConn deals only with already formatted DNS messages. It does not
// handle encoding information into the messages. That is rather the
// responsibility of DNSPacketConn.
//
// https://tools.ietf.org/html/rfc8484
type HTTPPacketConn struct {
// client is the http.Client used to make requests. We use this instead
// of http.DefaultClient in order to support setting a timeout and a
// uTLS fingerprint.
client *http.Client
// urlString is the URL to which HTTP requests will be sent, for example
// "https://doh.example/dns-query".
urlString string
// notBefore, if not zero, is a time before which we may not send any
// queries; queries are buffered or dropped until that time. notBefore
// is set when we get a 429 Too Many Requests HTTP response or other
// unexpected status code that causes us to need to slow down. It is set
// according to the Retry-After header if available, otherwise it is set
// to defaultRetryAfter in the future. notBeforeLock controls access to
// notBefore.
notBefore time.Time
notBeforeLock sync.RWMutex
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
// sendLoop, via send, removes messages from the outgoing queue that
// were placed there by WriteTo, and inserts messages into the incoming
// queue to be returned from ReadFrom.
*queuepacketconn.QueuePacketConn
}
// NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
// server at urlString as a DNS over HTTP resolver. client is the http.Client
// that will be used to make requests. urlString should include any necessary
// path components; e.g., "/dns-query". numSenders is the number of concurrent
// sender-receiver goroutines to run.
func NewHTTPPacketConn(rt http.RoundTripper, urlString string, numSenders int) (*HTTPPacketConn, error) {
c := &HTTPPacketConn{
client: &http.Client{
Transport: rt,
Timeout: 1 * time.Minute,
},
urlString: urlString,
QueuePacketConn: queuepacketconn.NewQueuePacketConn(queuepacketconn.DummyAddr{}, 0),
}
for i := 0; i < numSenders; i++ {
go c.sendLoop()
}
return c, nil
}
// send sends a message in an HTTP request, and queues the body HTTP response to
// be returned from a future call to ReadFrom.
func (c *HTTPPacketConn) send(p []byte) error {
req, err := http.NewRequest("POST", c.urlString, bytes.NewReader(p))
if err != nil {
return err
}
req.Header.Set("Accept", "application/dns-message")
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
if ct := resp.Header.Get("Content-Type"); ct != "application/dns-message" {
return fmt.Errorf("unknown HTTP response Content-Type %+q", ct)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 64000))
if err == nil {
c.QueuePacketConn.QueueIncoming(body, queuepacketconn.DummyAddr{})
}
// Ignore err != nil; don't report an error if we at least
// managed to send.
default:
// We primarily are thinking of 429 Too Many Requests here, but
// any other unexpected response codes will also cause us to
// rate-limit ourselves and emit a log message.
// https://developers.google.com/speed/public-dns/docs/doh/#errors
now := time.Now()
var retryAfter time.Time
if value := resp.Header.Get("Retry-After"); value != "" {
var err error
retryAfter, err = parseRetryAfter(value, now)
if err != nil {
log.Printf("cannot parse Retry-After value %+q", value)
}
}
if retryAfter.IsZero() {
// Supply a default.
retryAfter = now.Add(defaultRetryAfter)
}
if retryAfter.Before(now) {
log.Printf("got %+q, but Retry-After is %v in the past",
resp.Status, now.Sub(retryAfter))
} else {
c.notBeforeLock.Lock()
if retryAfter.Before(c.notBefore) {
log.Printf("got %+q, but Retry-After is %v earlier than already received Retry-After",
resp.Status, c.notBefore.Sub(retryAfter))
} else {
log.Printf("got %+q; ceasing sending for %v",
resp.Status, retryAfter.Sub(now))
c.notBefore = retryAfter
}
c.notBeforeLock.Unlock()
}
}
return nil
}
// sendLoop loops over the contents of the outgoing queue and passes them to
// send. It drops packets while c.notBefore is in the future.
func (c *HTTPPacketConn) sendLoop() {
for p := range c.QueuePacketConn.OutgoingQueue(queuepacketconn.DummyAddr{}) {
// Stop sending while we are rate-limiting ourselves (as a
// result of a Retry-After response header, for example).
c.notBeforeLock.RLock()
notBefore := c.notBefore
c.notBeforeLock.RUnlock()
if wait := time.Until(notBefore); wait > 0 {
// Drop it.
continue
}
err := c.send(p)
if err != nil {
log.Printf("sendLoop: %v", err)
}
}
}
// parseRetryAfter parses the value of a Retry-After header as an absolute
// time.Time.
func parseRetryAfter(value string, now time.Time) (time.Time, error) {
// May be a date string or an integer number of seconds.
// https://tools.ietf.org/html/rfc7231#section-7.1.3
if t, err := http.ParseTime(value); err == nil {
return t, nil
}
i, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return time.Time{}, err
}
return now.Add(time.Duration(i) * time.Second), nil
}