rrda

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

commit be1b378ac210c29e299fe77f23e19fd81fc332d7
Author: Frederic Cambus <fcambus@users.sourceforge.net>
Date:   Mon,  1 Jul 2013 12:58:06 +0200

Initial RRDA release

Diffstat:
AAUTHORS | 6++++++
ALICENSE | 28++++++++++++++++++++++++++++
AREADME.md | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATODO | 3+++
Arrda.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 324 insertions(+), 0 deletions(-)

diff --git a/AUTHORS b/AUTHORS @@ -0,0 +1,6 @@ +RRDA is developed by : + +Frederic Cambus <fcambus AT users DOT sourceforge DOT net> + +Site : http://www.cambus.net +Twitter : @fcambus diff --git a/LICENSE b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012-2013, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of RRDA nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md @@ -0,0 +1,104 @@ +## Description + +RRDA is a REST API written in Go allowing to perform DNS queries over HTTP, and to get reverse PTR records for both IPv4 and IPv6 addresses. It outputs JSON-encoded DNS responses. + +RRDA is a recursive acronym for "RRDA REST DNS API". + +## Requirements + +RRDA requires the following Go libraries : + +- dns : DNS library in Go - https://github.com/miekg/dns +- pat : pattern muxer for Go - https://github.com/bmizerany/pat + +## Installation + +Build and install with the `go` tool : + + go build rrda + go install rrda + +## Usage + + Usage of ./rrda: + -host="127.0.0.1": Set the server host + -port="8080": Set the server port + +## Making Queries + +The following examples assume there is a resolver on localhost listening on port 53. + +### Getting Resources Records + +URL Scheme : http://server:port/resolver:port/domain/querytype + +Example : http://127.0.0.1:8080/127.0.0.1:53/example.org/ns + +### Getting Reverse PTR Records (for both IPv4 and IPv6 addresses) + +URL Scheme : http://server:port/resolver:port/x/ip + +Example (IPv4) : http://127.0.0.1:8080/127.0.0.1:53/x/193.0.6.139 +Example (IPv6) : http://127.0.0.1:8080/127.0.0.1:53/x/2001:67c:2e8:22::c100:68b + +## JSON Output Schema + +The output is a JSON object containing the following arrays, representing the appropriate sections of DNS packets : + +- question +- answer +- authority (omitted if empty) +- additional (omitted if empty) + +### Question section + +- name +- type +- class + +### Answer, Authority, Additional sections + +- name +- type +- class +- ttl +- rdlength +- rdata + +## Client Errors + +When incorrect user input is entered, the server returns an HTTP 400 Error (Bad Request), along with a JSON-encoded error message. + +Code 401 : Input string could not be parsed +Code 402 : Input string is not a well-formed domain name +Code 403 : Input string is not a valid IP address +Code 404 : Invalid DNS query type + +## Server Errors + +When the DNS server cannot be reached or returns an error, the server returns an HTTP 500 Error (Internal Server Error), along with a JSON-encoded error message. + +Code 501 : DNS server could not be reached +Code 502 : The name server encountered an internal failure while processing this request (SERVFAIL) +Code 503 : Some name that ought to exist, does not exist (NXDOMAIN) +Code 505 : The name server refuses to perform the specified operation for policy or security reasons (REFUSED) + +## Sites using RRDA + +- StatDNS : Rest DNS API - http://www.statdns.com/api/ +- DNS-LG : Multilocation DNS Looking Glass - http://www.dns-lg.com + +## License + +RRDA is released under the BSD 3-Clause license. See `LICENSE` file for details. + +## Author + +RRDA is developed by Frederic Cambus + +- Site : http://www.cambus.net +- Twitter: http://twitter.com/fcambus + +## Resources + +Project Homepage : http://www.statdns.com+ \ No newline at end of file diff --git a/TODO b/TODO @@ -0,0 +1,3 @@ +- Add a Makefile to install binaries and init files +- Add automated testing (Error codes, Domains > 253 characters, Labels > 63 characters) +- Add DNS Header Flags and RCODEs to the JSON structure? diff --git a/rrda.go b/rrda.go @@ -0,0 +1,182 @@ +/*****************************************************************************/ +/* */ +/* RRDA (RRDA REST DNS API) 1.00 (c) by Frederic Cambus 2012-2013 */ +/* http://www.statdns.com */ +/* */ +/* Created: 2012/03/11 */ +/* Last Updated: 2013/07/01 */ +/* */ +/* RRDA is released under the BSD 3-Clause license. */ +/* See LICENSE file for details. */ +/* */ +/*****************************************************************************/ + +package main + +import ( + "code.google.com/p/go.net/idna" + "encoding/json" + "flag" + "fmt" + "github.com/bmizerany/pat" + "github.com/miekg/dns" + "io" + "net/http" + "os" + "strings" +) + +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type Question struct { + Name string `json:"name"` + Type string `json:"type"` + Class string `json:"class"` +} + +type Section struct { + Name string `json:"name"` + Type string `json:"type"` + Class string `json:"class"` + Ttl uint32 `json:"ttl"` + Rdlength uint16 `json:"rdlength"` + Rdata string `json:"rdata"` +} + +type Message struct { + Question []*Question `json:"question"` + Answer []*Section `json:"answer"` + Authority []*Section `json:"authority,omitempty"` + Additional []*Section `json:"additional,omitempty"` +} + +// Return rdata +func rdata(RR dns.RR) string { + return strings.Replace(RR.String(), RR.Header().String(), "", -1) +} + +// Return an HTTP Error along with a JSON-encoded error message +func error(w http.ResponseWriter, status int, code int, message string) { + if output, err := json.Marshal(Error{Code: code, Message: message}); err == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + fmt.Fprintln(w, string(output)) + } +} + +// Generate JSON output +func jsonify(w http.ResponseWriter, question []dns.Question, answer []dns.RR, authority []dns.RR, additional []dns.RR) { + var answerArray, authorityArray, additionalArray []*Section + + for _, answer := range answer { + 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)}) + } + + for _, authority := range authority { + 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)}) + } + + for _, additional := range additional { + 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)}) + } + + if output, err := json.MarshalIndent(Message{[]*Question{&Question{question[0].Name, dns.TypeToString[question[0].Qtype], dns.ClassToString[question[0].Qclass]}}, answerArray, authorityArray, additionalArray}, "", " "); err == nil { + io.WriteString(w, string(output)) + } +} + +// Perform DNS resolution +func resolve(w http.ResponseWriter, server string, domain string, querytype uint16) { + m := new(dns.Msg) + m.SetQuestion(domain, querytype) + m.MsgHdr.RecursionDesired = true + + w.Header().Set("Content-Type", "application/json") + + c := new(dns.Client) + +Redo: + if in, _, err := c.Exchange(m, server); err == nil { // Second return value is RTT, not used for now + if in.MsgHdr.Truncated { + c.Net = "tcp" + goto Redo + } + + switch in.MsgHdr.Rcode { + case dns.RcodeServerFailure: + error(w, 500, 502, "The name server encountered an internal failure while processing this request (SERVFAIL)") + case dns.RcodeNameError: + error(w, 500, 503, "Some name that ought to exist, does not exist (NXDOMAIN)") + case dns.RcodeRefused: + error(w, 500, 505, "The name server refuses to perform the specified operation for policy or security reasons (REFUSED)") + default: + jsonify(w, in.Question, in.Answer, in.Ns, in.Extra) + } + } else { + error(w, 500, 501, "DNS server could not be reached") + } +} + +// Handler for DNS queries +func query(w http.ResponseWriter, r *http.Request) { + server := r.URL.Query().Get(":server") + domain := dns.Fqdn(r.URL.Query().Get(":domain")) + querytype := r.URL.Query().Get(":querytype") + + if domain, err := idna.ToASCII(domain); err == nil { // ValidateValid IP address (IPv4 or IPv6) + if _, _, isDomain := dns.IsDomainName(domain); isDomain { // Well-formed domain name + if querytype, ok := dns.StringToType[strings.ToUpper(querytype)]; ok { // Valid DNS query type + resolve(w, server, domain, querytype) + } else { + error(w, 400, 404, "Invalid DNS query type") + } + } else { + error(w, 400, 402, "Input string is not a well-formed domain name") + } + } else { + error(w, 400, 401, "Input string could not be parsed") + } +} + +// Handler for reverse DNS queries +func ptr(w http.ResponseWriter, r *http.Request) { + server := r.URL.Query().Get(":server") + ip := r.URL.Query().Get(":ip") + + if arpa, err := dns.ReverseAddr(ip); err == nil { // Valid IP address (IPv4 or IPv6) + resolve(w, server, arpa, dns.TypePTR) + } else { + error(w, 400, 403, "Input string is not a valid IP address") + } +} + +func main() { + header := `------------------------------------------------------------------------------- +RRDA (RRDA REST DNS API) (c) by Frederic Cambus 2012-2013 +-------------------------------------------------------------------------------` + + host := flag.String("host", "127.0.0.1", "Set the server host") + port := flag.String("port", "8080", "Set the server port") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + fmt.Println(header) + + m := pat.New() + m.Get("/:server/x/:ip", http.HandlerFunc(ptr)) + m.Get("/:server/:domain/:querytype", http.HandlerFunc(query)) + + if err := http.ListenAndServe(*host+":"+*port, m); err != nil { + fmt.Println("\nERROR :", err) + os.Exit(1) + } + + fmt.Println("\nListening on port :", *port) +}