Launch
This commit is contained in:
176
internal/dnsttclient/http.go
Normal file
176
internal/dnsttclient/http.go
Normal file
@@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user