| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package main
- import (
- "bytes"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "strconv"
- "sync"
- "time"
- "www.bamsoftware.com/git/dnstt.git/turbotunnel"
- )
- // A default Retry-After delay to use when there is no explicit Retry-After
- // header in an HTTP response.
- const defaultRetryAfter = 10 * time.Second
- // The *http.Client shared by instances of HTTPPacketConn. We use this instead
- // of http.DefaultClient in order to set a timeout.
- var httpClient = &http.Client{Timeout: 1 * time.Minute}
- // 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 {
- // 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.
- *turbotunnel.QueuePacketConn
- }
- // NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
- // server at urlString as a DNS over HTTP resolver. urlString should include any
- // necessary path components; e.g., "/dns-query". numSenders is the number of
- // concurrent sender-receiver goroutines to run.
- func NewHTTPPacketConn(urlString string, numSenders int) (*HTTPPacketConn, error) {
- c := &HTTPPacketConn{
- urlString: urlString,
- QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.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 := httpClient.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 := ioutil.ReadAll(io.LimitReader(resp.Body, 64000))
- if err == nil {
- c.QueuePacketConn.QueueIncoming(body, turbotunnel.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(turbotunnel.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 := notBefore.Sub(time.Now()); 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
- }
|