304 lines
9.6 KiB
Go
304 lines
9.6 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/rand"
|
||
|
"encoding/hex"
|
||
|
"encoding/json"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"github.com/ANSSI-FR/transdep/dependency"
|
||
|
"github.com/ANSSI-FR/transdep/graph"
|
||
|
dependency2 "github.com/ANSSI-FR/transdep/messages/dependency"
|
||
|
"github.com/ANSSI-FR/transdep/tools"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// handleRequest is common to all request handlers. The difference lies in the reqConf parameter whose value varies
|
||
|
// depending on the request handler.
|
||
|
func handleRequest(
|
||
|
params url.Values, reqConf *tools.RequestConfig, reqChan chan<- *dependency2.Request,
|
||
|
w http.ResponseWriter, req *http.Request,
|
||
|
) {
|
||
|
// Get requested domain
|
||
|
domain, ok := params["domain"]
|
||
|
if !ok || len(domain) != 1 {
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Submit the request
|
||
|
depReq := dependency2.NewRequest(domain[0], true, false, reqConf.Exceptions)
|
||
|
select {
|
||
|
case <-tools.StartTimeout(20 * time.Second):
|
||
|
w.WriteHeader(http.StatusRequestTimeout)
|
||
|
return
|
||
|
case reqChan <- depReq:
|
||
|
res, err := depReq.Result()
|
||
|
if err != nil {
|
||
|
bstr, err := json.Marshal(err)
|
||
|
if err != nil {
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
w.Header().Add("content-type", "application/json+error")
|
||
|
w.Write(bstr)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
rootNode, ok := res.(*graph.RelationshipNode)
|
||
|
if !ok {
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var queryResult *graph.WorkerAnalysisResult
|
||
|
allNamesResult, allNamesNo4Result, allNamesNo6Result, dnssecResult, dnssecNo4Result, dnssecNo6Result :=
|
||
|
graph.PerformAnalyseOnResult(rootNode, reqConf, nil)
|
||
|
|
||
|
if reqConf.AnalysisCond.DNSSEC == false {
|
||
|
if reqConf.AnalysisCond.NoV4 {
|
||
|
queryResult = allNamesNo4Result
|
||
|
} else if reqConf.AnalysisCond.NoV6 {
|
||
|
queryResult = allNamesNo6Result
|
||
|
} else {
|
||
|
queryResult = allNamesResult
|
||
|
}
|
||
|
} else {
|
||
|
if reqConf.AnalysisCond.NoV4 {
|
||
|
queryResult = dnssecNo4Result
|
||
|
} else if reqConf.AnalysisCond.NoV6 {
|
||
|
queryResult = dnssecNo6Result
|
||
|
} else {
|
||
|
queryResult = dnssecResult
|
||
|
}
|
||
|
}
|
||
|
if queryResult.Err != nil {
|
||
|
bstr, jsonErr := json.Marshal(queryResult.Err)
|
||
|
if jsonErr != nil {
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
w.Header().Add("content-type", "application/json+error")
|
||
|
w.Write(bstr)
|
||
|
return
|
||
|
}
|
||
|
bstr, jsonErr := json.Marshal(queryResult.Nodes)
|
||
|
if jsonErr != nil {
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
w.Header().Add("content-type", "application/json+nodes")
|
||
|
w.Write(bstr)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// getRequestConf uses the parameters from the query string to define the request configuration, notably concerning
|
||
|
// deemed acceptable DNS violations
|
||
|
func getRequestConf(params url.Values) *tools.RequestConfig {
|
||
|
var RFC8020, AcceptServfail bool
|
||
|
|
||
|
paramRFC8020, ok := params["rfc8020"]
|
||
|
if !ok || len(paramRFC8020) != 1 {
|
||
|
RFC8020 = false
|
||
|
} else if i, err := strconv.ParseInt(paramRFC8020[0], 10, 0); err != nil {
|
||
|
RFC8020 = false
|
||
|
} else {
|
||
|
RFC8020 = i != 0
|
||
|
}
|
||
|
|
||
|
paramAcceptServfail, ok := params["servfail"]
|
||
|
if !ok || len(paramAcceptServfail) != 1 {
|
||
|
AcceptServfail = false
|
||
|
} else if i, err := strconv.ParseInt(paramAcceptServfail[0], 10, 0); err != nil {
|
||
|
AcceptServfail = false
|
||
|
} else {
|
||
|
AcceptServfail = i != 0
|
||
|
}
|
||
|
|
||
|
// Prepare request-specific configuration based on rfc8020 and servfail query string parameters presence and value
|
||
|
reqConf := &tools.RequestConfig{
|
||
|
AnalysisCond: tools.AnalysisConditions{
|
||
|
All: false,
|
||
|
DNSSEC: false,
|
||
|
NoV4: false,
|
||
|
NoV6: false,
|
||
|
},
|
||
|
Exceptions: tools.Exceptions{
|
||
|
RFC8020: RFC8020,
|
||
|
AcceptServFailAsNoData: AcceptServfail,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
return reqConf
|
||
|
}
|
||
|
|
||
|
func handleAllNamesRequests(reqChan chan<- *dependency2.Request, w http.ResponseWriter, req *http.Request) {
|
||
|
params, err := url.ParseQuery(req.URL.RawQuery)
|
||
|
if err != nil {
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Prepare request-specific configuration
|
||
|
reqConf := getRequestConf(params)
|
||
|
handleRequest(params, reqConf, reqChan, w, req)
|
||
|
}
|
||
|
|
||
|
func handleDNSSECRequests(reqChan chan<- *dependency2.Request, w http.ResponseWriter, req *http.Request) {
|
||
|
params, err := url.ParseQuery(req.URL.RawQuery)
|
||
|
if err != nil {
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Prepare request-specific configuration
|
||
|
reqConf := getRequestConf(params)
|
||
|
reqConf.AnalysisCond.DNSSEC = true
|
||
|
handleRequest(params, reqConf, reqChan, w, req)
|
||
|
}
|
||
|
|
||
|
func handleNo4Requests(reqChan chan<- *dependency2.Request, w http.ResponseWriter, req *http.Request) {
|
||
|
params, err := url.ParseQuery(req.URL.RawQuery)
|
||
|
if err != nil {
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Prepare request-specific configuration
|
||
|
reqConf := getRequestConf(params)
|
||
|
reqConf.AnalysisCond.NoV4 = true
|
||
|
handleRequest(params, reqConf, reqChan, w, req)
|
||
|
}
|
||
|
|
||
|
func handleNo6Requests(reqChan chan<- *dependency2.Request, w http.ResponseWriter, req *http.Request) {
|
||
|
params, err := url.ParseQuery(req.URL.RawQuery)
|
||
|
if err != nil {
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Prepare request-specific configuration
|
||
|
reqConf := getRequestConf(params)
|
||
|
reqConf.AnalysisCond.NoV6 = true
|
||
|
handleRequest(params, reqConf, reqChan, w, req)
|
||
|
}
|
||
|
|
||
|
func stopFinder(df *dependency.Finder, reqChan chan<- *dependency2.Request, secret string, w http.ResponseWriter, req *http.Request) {
|
||
|
params, err := url.ParseQuery(req.URL.RawQuery)
|
||
|
if err == nil {
|
||
|
if secretParam, ok := params["secret"]; ok && len(secretParam) == 1 && secretParam[0] == secret {
|
||
|
// Secret is correct, initiating graceful stop
|
||
|
go func() {
|
||
|
// Wait for 1 second, just to give the time to send the web confirmation page
|
||
|
time.Sleep(1 * time.Second)
|
||
|
fmt.Printf("Stopping the finder: ")
|
||
|
// Will print dots until os.Exit kills the process
|
||
|
go func() {
|
||
|
for {
|
||
|
fmt.Printf(".")
|
||
|
time.Sleep(100 * time.Millisecond)
|
||
|
}
|
||
|
}()
|
||
|
close(reqChan)
|
||
|
// Perform a graceful stop of the dependency finder, which will flush caches on disk
|
||
|
df.Stop()
|
||
|
fmt.Printf("OK\n")
|
||
|
os.Exit(0)
|
||
|
}()
|
||
|
// Returns a webpage confirming shutdown
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
buf := new(bytes.Buffer)
|
||
|
buf.WriteString("Stopping.")
|
||
|
w.Write(buf.Bytes())
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reject all requests that are missing the secret parameter or whose secret value is different from the "secret"
|
||
|
// function parameter.
|
||
|
w.WriteHeader(http.StatusForbidden)
|
||
|
}
|
||
|
|
||
|
// runWebWorker is a go routine that handles dependency requests received from the web handlers.
|
||
|
func runWebWorker(df *dependency.Finder, reqChan <-chan *dependency2.Request) {
|
||
|
for req := range reqChan {
|
||
|
df.Handle(req)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
var transdepConf tools.TransdepConfig
|
||
|
var ip string
|
||
|
var port int
|
||
|
secret := make([]byte, 16)
|
||
|
var secretString string
|
||
|
|
||
|
tmpdir := os.Getenv("TMPDIR")
|
||
|
if tmpdir == "" {
|
||
|
tmpdir = "/tmp"
|
||
|
}
|
||
|
|
||
|
flag.IntVar(&transdepConf.JobCount, "jobs", 5, "Indicates the maximum number of concurrent workers")
|
||
|
flag.IntVar(&transdepConf.LRUSizes.DependencyFinder, "dflrusize", 2000, "Indicates the maximum number of concurrent Dependency Finder workers")
|
||
|
flag.IntVar(&transdepConf.LRUSizes.ZoneCutFinder, "zcflrusize", 10000, "Indicates the maximum number of concurrent Zone Cut Finder workers")
|
||
|
flag.IntVar(&transdepConf.LRUSizes.NameResolverFinder, "nrlrusize", 10000, "Indicates the maximum number of concurrent Name Resolver workers")
|
||
|
flag.StringVar(&transdepConf.CacheRootDir, "cachedir", tmpdir, "Specifies the cache directory")
|
||
|
flag.StringVar(&transdepConf.RootHintsFile, "hints", "", "An updated DNS root hint file. If left unspecified, some hardcoded values will be used.")
|
||
|
flag.StringVar(&ip, "bind", "127.0.0.1", "IP address to which the HTTP server will bind and listen")
|
||
|
flag.IntVar(&port, "port", 5000, "Port on which the HTTP server will bind and listen")
|
||
|
flag.Parse()
|
||
|
|
||
|
// A single dependency finder is shared between all web clients. This allows for cache sharing.
|
||
|
df := dependency.NewFinder(&transdepConf, nil)
|
||
|
|
||
|
reqChan := make(chan *dependency2.Request)
|
||
|
for i := 0; i < transdepConf.JobCount; i++ {
|
||
|
go runWebWorker(df, reqChan)
|
||
|
}
|
||
|
|
||
|
// A secret is generated from random. The point of this secret is to be an authentication token allowing graceful
|
||
|
// shutdown
|
||
|
rand.Read(secret[:])
|
||
|
secretString = hex.EncodeToString(secret)
|
||
|
// The URL to call to perform a graceful shutodown is printed on stdout
|
||
|
fmt.Printf("To stop the server, send a query to http://%s:%d/stop?secret=%s\n", ip, port, secretString)
|
||
|
|
||
|
// handles all requests where we want a list of all SPOF, even domain names that are not protected by DNSSEC
|
||
|
http.HandleFunc("/allnames", func(w http.ResponseWriter, req *http.Request) {
|
||
|
handleAllNamesRequests(reqChan, w, req)
|
||
|
})
|
||
|
|
||
|
// handles all requests where we want a list of all SPOF (domain are considered a SPOF candidates only if they are DNSSEC-protected)
|
||
|
http.HandleFunc("/dnssec", func(w http.ResponseWriter, req *http.Request) {
|
||
|
handleDNSSECRequests(reqChan, w, req)
|
||
|
})
|
||
|
|
||
|
// handles all requests where we want a list of all SPOF when IPv4 addresses are unreachable
|
||
|
http.HandleFunc("/break4", func(w http.ResponseWriter, req *http.Request) {
|
||
|
handleNo4Requests(reqChan, w, req)
|
||
|
})
|
||
|
|
||
|
// handles all requests where we want a list of all SPOF when IPv6 addresses are unreachable
|
||
|
http.HandleFunc("/break6", func(w http.ResponseWriter, req *http.Request) {
|
||
|
handleNo6Requests(reqChan, w, req)
|
||
|
})
|
||
|
|
||
|
// handles requests to stop graceful this webservice
|
||
|
http.HandleFunc("/stop", func(w http.ResponseWriter, req *http.Request) {
|
||
|
stopFinder(df, reqChan, secretString, w, req)
|
||
|
})
|
||
|
|
||
|
// start web server and log fatal error that may arise during execution
|
||
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", ip, port), nil))
|
||
|
}
|