Files
AdGuardDNS/internal/dnsserver/serverhttpsjson.go
Andrey Meshkov 5690301129 Sync v2.7.0
2024-06-07 14:27:46 +03:00

222 lines
5.8 KiB
Go

package dnsserver
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/miekg/dns"
)
// JSONMsg represents a *dns.Msg in the JSON format defined here:
// https://developers.google.com/speed/public-dns/docs/doh/json#dns_response_in_json
// Note, that we do not implement some parts of it. There is no "Comment" field
// and there's no "edns_client_subnet".
type JSONMsg struct {
Question []JSONQuestion `json:"Question"`
Answer []JSONAnswer `json:"Answer"`
Extra []JSONAnswer `json:"Extra"`
Truncated bool `json:"TC"`
RecursionDesired bool `json:"RD"`
RecursionAvailable bool `json:"RA"`
AuthenticatedData bool `json:"AD"`
CheckingDisabled bool `json:"CD"`
Status int `json:"Status"`
}
// JSONQuestion is a part of JSONMsg definition.
type JSONQuestion struct {
Name string `json:"name"`
Type uint16 `json:"type"`
}
// JSONAnswer is a part of JSONMsg definition.
type JSONAnswer struct {
Name string `json:"name"`
Data string `json:"data"`
TTL uint32 `json:"TTL"`
Type uint16 `json:"type"`
Class uint16 `json:"class"`
}
// DNSMsgToJSONMsg converts the *dns.Msg to the JSON format (*JSONMsg).
func DNSMsgToJSONMsg(m *dns.Msg) (msg *JSONMsg) {
msg = &JSONMsg{
Status: m.Rcode,
Truncated: m.Truncated,
RecursionDesired: m.RecursionDesired,
RecursionAvailable: m.RecursionAvailable,
AuthenticatedData: m.AuthenticatedData,
CheckingDisabled: m.CheckingDisabled,
}
for _, q := range m.Question {
msg.Question = append(msg.Question, JSONQuestion{
Name: q.Name,
Type: q.Qtype,
})
}
for _, rr := range m.Answer {
msg.Answer = append(msg.Answer, rrToJSON(rr))
}
for _, rr := range m.Extra {
msg.Extra = append(msg.Extra, rrToJSON(rr))
}
return msg
}
// rrToJSON converts the specified rr to JSONAnswer.
func rrToJSON(rr dns.RR) (j JSONAnswer) {
hdr := rr.Header()
// Extracting the RR value is a bit tricky since miekg/dns does not
// expose the necessary methods. This way we can benefit from the
// proper string serialization code that's used inside miekg/dns.
hdrStr := hdr.String()
valStr := rr.String()
data := strings.TrimLeft(strings.TrimPrefix(valStr, hdrStr), " ")
return JSONAnswer{
Name: hdr.Name,
Type: hdr.Rrtype,
TTL: hdr.Ttl,
Class: hdr.Class,
Data: data,
}
}
// dnsMsgToJSON converts the *dns.Msg to the JSON format (JSONMsg) and returns
// it in the serialized form.
func dnsMsgToJSON(m *dns.Msg) (b []byte, err error) {
msg := DNSMsgToJSONMsg(m)
return json.Marshal(msg)
}
// httpRequestToMsgJSON builds a DNS message from the request parameters.
// We use the same parameters as the ones defined here:
// https://developers.google.com/speed/public-dns/docs/doh/json#supported_parameters
// Some parameters are not supported: "ct", "edns_client_subnet".
func httpRequestToMsgJSON(req *http.Request) (b []byte, err error) {
q := req.URL.Query()
// Query name, the only required parameter.
name := q.Get("name")
if name == "" {
// Indicate that the argument is invalid
return nil, ErrInvalidArgument
}
// RR type can be represented as a number in [1, 65535] or a
// canonical string (case-insensitive, such as A or AAAA).
var t uint16
t, err = urlQueryParameterToUint16(q, "type", dns.TypeA, dns.StringToType)
if err != nil {
return nil, err
}
// Query class can be represented as a number in [1, 65535] or a
// canonical string (case-insensitive).
var qc uint16
qc, err = urlQueryParameterToUint16(q, "qc", dns.ClassINET, dns.StringToClass)
if err != nil {
return nil, err
}
// The CD (Checking Disabled) flag. Use cd=1, or cd=true to disable
// DNSSEC validation; use cd=0, cd=false, or no cd parameter to
// enable DNSSEC validation.
var cd bool
cd, err = urlQueryParameterToBoolean(q, "cd", false)
if err != nil {
return nil, err
}
// The DO (DNSSEC OK) flag. Use do=1, or do=true to include DNSSEC
// records (RRSIG, NSEC, NSEC3); use do=0, do=false, or no do parameter
// to omit DNSSEC records.
var do bool
do, err = urlQueryParameterToBoolean(q, "do", false)
if err != nil {
return nil, err
}
// Now build a DNS message with all those parameters
r := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
CheckingDisabled: cd,
RecursionDesired: true,
},
Question: []dns.Question{
{
Name: dns.Fqdn(name),
Qtype: t,
Qclass: qc,
},
},
}
if do {
r.SetEdns0(dns.MaxMsgSize, do)
}
return r.Pack()
}
// urlQueryParameterToUint16 is a helper function that extracts a uint16 value
// from a query parameter. See httpRequestToMsgJSON to see how it's used.
func urlQueryParameterToUint16(
q url.Values,
name string,
defaultValue uint16,
strValuesMap map[string]uint16,
) (v uint16, err error) {
strValue := q.Get(name)
uintValue, convErr := strconv.ParseUint(strValue, 10, 16)
switch {
case strValue == "":
// use default value if nothing was specified.
v = defaultValue
case convErr == nil:
// use the specified value if it is a valid uint16.
v = uint16(uintValue)
default:
// check if the specified string value is in the lookup map.
var ok bool
v, ok = strValuesMap[strings.ToUpper(strValue)]
if !ok {
// specified type is invalid.
return 0, ErrInvalidArgument
}
}
return v, nil
}
// urlQueryParameterToBoolean is a helper function that extracts a boolean value
// from a query parameter. See httpRequestToMsgJSON to see how it's used.
func urlQueryParameterToBoolean(
q url.Values,
name string,
defaultValue bool,
) (v bool, err error) {
strValue := q.Get(name)
switch strValue {
case "1", "true", "True":
v = true
case "0", "false", "False":
v = false
case "":
v = defaultValue
default:
return defaultValue, ErrInvalidArgument
}
return v, nil
}