rrda

REST API allowing to perform DNS queries over HTTP
Log | Files | Refs | README | LICENSE

rrda.go (6216B)


      1 /*
      2  * RRDA (RRDA REST DNS API) 1.1.0
      3  * Copyright (c) 2012-2020, Frederic Cambus
      4  * https://www.statdns.com
      5  *
      6  * Created: 2012-03-11
      7  * Last Updated: 2020-03-16
      8  *
      9  * RRDA is released under the BSD 2-Clause license.
     10  * See LICENSE file for details.
     11  *
     12  * SPDX-License-Identifier: BSD-2-Clause
     13  */
     14 
     15 package main
     16 
     17 import (
     18 	"encoding/json"
     19 	"flag"
     20 	"fmt"
     21 	"github.com/bmizerany/pat"
     22 	"github.com/miekg/dns"
     23 	"golang.org/x/net/idna"
     24 	"io"
     25 	"net"
     26 	"net/http"
     27 	"net/http/fcgi"
     28 	"os"
     29 	"strings"
     30 )
     31 
     32 type Error struct {
     33 	Code    int    `json:"code"`
     34 	Message string `json:"message"`
     35 }
     36 
     37 type Question struct {
     38 	Name  string `json:"name"`
     39 	Type  string `json:"type"`
     40 	Class string `json:"class"`
     41 }
     42 
     43 type Section struct {
     44 	Name     string `json:"name"`
     45 	Type     string `json:"type"`
     46 	Class    string `json:"class"`
     47 	Ttl      uint32 `json:"ttl"`
     48 	Rdlength uint16 `json:"rdlength"`
     49 	Rdata    string `json:"rdata"`
     50 }
     51 
     52 type Message struct {
     53 	Question   []*Question `json:"question"`
     54 	Answer     []*Section  `json:"answer"`
     55 	Authority  []*Section  `json:"authority,omitempty"`
     56 	Additional []*Section  `json:"additional,omitempty"`
     57 }
     58 
     59 // Return rdata
     60 func rdata(RR dns.RR) string {
     61 	return strings.Replace(RR.String(), RR.Header().String(), "", -1)
     62 }
     63 
     64 // Return an HTTP Error along with a JSON-encoded error message
     65 func error(w http.ResponseWriter, status int, code int, message string) {
     66 	if output, err := json.Marshal(Error{Code: code, Message: message}); err == nil {
     67 		w.Header().Set("Content-Type", "application/json")
     68 		w.WriteHeader(status)
     69 		fmt.Fprintln(w, string(output))
     70 	}
     71 }
     72 
     73 // Generate JSON output
     74 func jsonify(w http.ResponseWriter, r *http.Request, question []dns.Question, answer []dns.RR, authority []dns.RR, additional []dns.RR) {
     75 	var answerArray, authorityArray, additionalArray []*Section
     76 
     77 	callback := r.URL.Query().Get("callback")
     78 
     79 	for _, answer := range answer {
     80 		answerArray = append(answerArray, &Section{answer.Header().Name, dns.TypeToString[answer.Header().Rrtype], dns.ClassToString[answer.Header().Class], answer.Header().Ttl, answer.Header().Rdlength, rdata(answer)})
     81 	}
     82 
     83 	for _, authority := range authority {
     84 		authorityArray = append(authorityArray, &Section{authority.Header().Name, dns.TypeToString[authority.Header().Rrtype], dns.ClassToString[authority.Header().Class], authority.Header().Ttl, authority.Header().Rdlength, rdata(authority)})
     85 	}
     86 
     87 	for _, additional := range additional {
     88 		additionalArray = append(additionalArray, &Section{additional.Header().Name, dns.TypeToString[additional.Header().Rrtype], dns.ClassToString[additional.Header().Class], additional.Header().Ttl, additional.Header().Rdlength, rdata(additional)})
     89 	}
     90 
     91 	if json, err := json.MarshalIndent(Message{[]*Question{&Question{question[0].Name, dns.TypeToString[question[0].Qtype], dns.ClassToString[question[0].Qclass]}}, answerArray, authorityArray, additionalArray}, "", "    "); err == nil {
     92 		if callback != "" {
     93 			io.WriteString(w, callback+"("+string(json)+");")
     94 		} else {
     95 			io.WriteString(w, string(json))
     96 		}
     97 	}
     98 }
     99 
    100 // Perform DNS resolution
    101 func resolve(w http.ResponseWriter, r *http.Request, server string, domain string, querytype uint16) {
    102 	m := new(dns.Msg)
    103 	m.SetQuestion(domain, querytype)
    104 	m.MsgHdr.RecursionDesired = true
    105 
    106 	w.Header().Set("Content-Type", "application/json")
    107 	w.Header().Set("Access-Control-Allow-Origin", "*")
    108 
    109 	c := new(dns.Client)
    110 
    111 Redo:
    112 	if in, _, err := c.Exchange(m, server); err == nil { // Second return value is RTT, not used for now
    113 		if in.MsgHdr.Truncated {
    114 			c.Net = "tcp"
    115 			goto Redo
    116 		}
    117 
    118 		switch in.MsgHdr.Rcode {
    119 		case dns.RcodeServerFailure:
    120 			error(w, 500, 502, "The name server encountered an internal failure while processing this request (SERVFAIL)")
    121 		case dns.RcodeNameError:
    122 			error(w, 500, 503, "Some name that ought to exist, does not exist (NXDOMAIN)")
    123 		case dns.RcodeRefused:
    124 			error(w, 500, 505, "The name server refuses to perform the specified operation for policy or security reasons (REFUSED)")
    125 		default:
    126 			jsonify(w, r, in.Question, in.Answer, in.Ns, in.Extra)
    127 		}
    128 	} else {
    129 		error(w, 500, 501, "DNS server could not be reached")
    130 	}
    131 }
    132 
    133 // Handler for DNS queries
    134 func query(w http.ResponseWriter, r *http.Request) {
    135 	server := r.URL.Query().Get(":server")
    136 	domain := dns.Fqdn(r.URL.Query().Get(":domain"))
    137 	querytype := r.URL.Query().Get(":querytype")
    138 
    139 	if domain, err := idna.ToASCII(domain); err == nil { // Valid domain name (ASCII or IDN)
    140 		if _, isDomain := dns.IsDomainName(domain); isDomain { // Well-formed domain name
    141 			if querytype, ok := dns.StringToType[strings.ToUpper(querytype)]; ok { // Valid DNS query type
    142 				resolve(w, r, server, domain, querytype)
    143 			} else {
    144 				error(w, 400, 404, "Invalid DNS query type")
    145 			}
    146 		} else {
    147 			error(w, 400, 402, "Input string is not a well-formed domain name")
    148 		}
    149 	} else {
    150 		error(w, 400, 401, "Input string could not be parsed")
    151 	}
    152 }
    153 
    154 // Handler for reverse DNS queries
    155 func ptr(w http.ResponseWriter, r *http.Request) {
    156 	server := r.URL.Query().Get(":server")
    157 	ip := r.URL.Query().Get(":ip")
    158 
    159 	if arpa, err := dns.ReverseAddr(ip); err == nil { // Valid IP address (IPv4 or IPv6)
    160 		resolve(w, r, server, arpa, dns.TypePTR)
    161 	} else {
    162 		error(w, 400, 403, "Input string is not a valid IP address")
    163 	}
    164 }
    165 
    166 func main() {
    167 	fastcgi := flag.Bool("fastcgi", false, "Enable FastCGI mode")
    168 	host := flag.String("host", "127.0.0.1", "Set the server host")
    169 	port := flag.String("port", "8080", "Set the server port")
    170 	version := flag.Bool("version", false, "Display version")
    171 
    172 	mode := "HTTP"
    173 
    174 	flag.Usage = func() {
    175 		fmt.Println("\nUSAGE:")
    176 		flag.PrintDefaults()
    177 	}
    178 	flag.Parse()
    179 
    180 	if *version {
    181 		fmt.Println("RRDA 1.1.0")
    182 		os.Exit(0)
    183 	}
    184 
    185 	if *fastcgi {
    186 		mode = "FastCGI"
    187 	}
    188 
    189 	address := *host + ":" + *port
    190 
    191 	fmt.Println("Listening on ("+mode+" mode):", address)
    192 
    193 	m := pat.New()
    194 	m.Get("/:server/x/:ip", http.HandlerFunc(ptr))
    195 	m.Get("/:server/:domain/:querytype", http.HandlerFunc(query))
    196 
    197 	if *fastcgi {
    198 		listener, _ := net.Listen("tcp", address)
    199 
    200 		if err := fcgi.Serve(listener, m); err != nil {
    201 			fmt.Println("\nERROR:", err)
    202 			os.Exit(1)
    203 		}
    204 	} else {
    205 		if err := http.ListenAndServe(address, m); err != nil {
    206 			fmt.Println("\nERROR:", err)
    207 			os.Exit(1)
    208 		}
    209 	}
    210 }