Files
SocksRevive-PC/internal/dnsttclient/http.go
2026-05-16 00:18:06 -03:00

177 lines
5.7 KiB
Go

package dnsttclient
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"sync"
"time"
"socksrevivepc/internal/dnsttcore/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
// 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.
*turbotunnel.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: 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 := 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 := 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
}