package main import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" "embed" "encoding/base64" "encoding/pem" "errors" "fmt" "io/fs" "io/ioutil" "log" "math/rand" "net/http" "os" "regexp" "strings" "sync" "time" "golang.org/x/crypto/openpgp/clearsign" "gopkg.in/djherbis/times.v1" ) type ( // HTTPServer is the webservice main object with its configuration parameters HTTPServer struct { Port string key string cert string crl string decodedCrl *pkix.CertificateList decodedCA []*x509.Certificate decodedCrlTime time.Time authProfiles map[string]AuthProfile pdnsAcls []*PdnsACL jrpcAPIACls []*JSONRPCACL certPool *x509.CertPool debug bool m sync.RWMutex dns *PowerDNS nonceGen string certCache map[string]time.Time zoneProfiles map[string]*zoneProfile } zoneProfile struct { Default bool NameServers []string SOA string DefaultEntries []*JSONInput Regexp []*regexp.Regexp AutoInc bool } ) // initalize local files // //go:embed web/* var embeddedFS embed.FS // JSONRPCNewError return a valid json rpc 2.0 error func JSONRPCNewError(code, id int, message string) JSONRPCError { e := JSONRPCError{ID: id, JSONRPC: "2.0"} e.Error.Code = code e.Error.Message = message return e } func (e JSONRPCError) String() string { return string(printJSON(e)) } // NewHTTPServer initializes HTTPServer func NewHTTPServer(port, key, cert, crl, ca, pdnsServer, pdnsKey string, timeout, ttl int) *HTTPServer { rand.Seed(time.Now().UnixNano()) h := HTTPServer{ Port: port, key: key, cert: cert, crl: crl, nonceGen: NewSalt(25), zoneProfiles: map[string]*zoneProfile{}, certCache: map[string]time.Time{}, authProfiles: map[string]AuthProfile{}, pdnsAcls: []*PdnsACL{}, certPool: x509.NewCertPool(), decodedCA: []*x509.Certificate{}, } rawCA, err := ioutil.ReadFile(ca) if err != nil { log.Fatal("ca:", err) } for len(rawCA) > 0 { var block *pem.Block block, rawCA = pem.Decode(rawCA) if block == nil { break } if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { continue } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { continue } h.certPool.AddCert(cert) h.decodedCA = append(h.decodedCA, cert) } if h.dns, err = NewClient(pdnsServer, pdnsKey, timeout, ttl); err != nil { log.Fatal(err) } if _, err := h.RefreshCRL(); err != nil { log.Fatal(err) } return &h } // NewZoneProfile add zone profiles to the structure func (h *HTTPServer) NewZoneProfile(zoneType, soa string, isDefault, autoInc bool, nameServers, rules []string) error { for t, profile := range h.zoneProfiles { if t == zoneType { return fmt.Errorf("zone type %s already defined", t) } if isDefault && profile.Default { return fmt.Errorf("zone type %s is already the default one", t) } } if len(nameServers) == 0 { return errors.New("no nameservers in the configuration") } for i := range nameServers { nameServers[i] = addPoint(nameServers[i]) } z := &zoneProfile{ Default: isDefault, NameServers: nameServers, Regexp: []*regexp.Regexp{}, SOA: soa, AutoInc: autoInc, } for _, r := range rules { re, err := regexp.Compile(r) if err != nil { return err } z.Regexp = append(z.Regexp, re) } h.lock() defer h.unlock() h.zoneProfiles[zoneType] = z return nil } func (z *zoneProfile) addDefaultEntry(name, action, value string) { params := JSONInputParams{ Name: name, Value: value, TTL: 172800, } input := &JSONInput{Method: action, ignoreBadDomain: true, Params: params} z.DefaultEntries = append(z.DefaultEntries, input) } // Lock the structure func (h *HTTPServer) lock() { h.m.Lock() } // ...or unlock it func (h *HTTPServer) unlock() { h.m.Unlock() } // Debug facilities func (h *HTTPServer) Debug() { h.debug = true h.dns.Debug() } // verifyNonce check that the once is valid and less than 10s old func (h *HTTPServer) verifyNonce(nonce string) bool { // cannot appear in production, but useful for tests if len(h.nonceGen) == 0 { return true } if len(nonce) < 47 { return false } now := time.Now().Unix() salt := nonce[:4] for i := 0; i < 10; i++ { if salt+ComputeHmac256(fmt.Sprintf("%s%d", salt, now-int64(i)), h.nonceGen) == nonce { return true } } return false } // sendNonce return a valid nonce to the user func (h *HTTPServer) sendNonce(w http.ResponseWriter, r *http.Request) { now := time.Now().Unix() salt := NewSalt(4) fmt.Fprintf(w, salt+ComputeHmac256(fmt.Sprintf("%s%d", salt, now), h.nonceGen)) } // AddAuthProfile adds a profile in the server config func (h *HTTPServer) AddAuthProfile(name string, p AuthProfile) { h.lock() defer h.unlock() h.authProfiles[name] = p } // AddPdnsACL adds an acl in the server config func (h *HTTPServer) AddPdnsACL(a *PdnsACL) { h.lock() defer h.unlock() h.pdnsAcls = append(h.pdnsAcls, a) } // AddjsonRPCACL adds a JRPC acl in the server config func (h *HTTPServer) AddjsonRPCACL(a *JSONRPCACL) { h.lock() defer h.unlock() h.jrpcAPIACls = append(h.jrpcAPIACls, a) } // getPgpProfiles returns the list of profiles validated by the signed payload, // and the name of the cert func (h *HTTPServer) getPgpProfiles(message, signature []byte) (string, []string) { valid := []string{} signer := "" for name, p := range h.authProfiles { if keyUser := PgpMessageVerify(message, signature, p.PgpKeys()); keyUser != "" { valid = append(valid, name) signer = keyUser } } return signer, valid } // getProfiles returns the list of profiles validated for a certificate subject func (h *HTTPServer) getProfiles(subject string) []string { valid := []string{} for name, p := range h.authProfiles { ok, err := p.Match(subject) if err == nil && ok { valid = append(valid, name) } if err != nil { log.Println(err) } } return valid } // nativeValidAuth validates that a user can access a particular path with a specified method func (h *HTTPServer) nativeValidAuth(path, user, method string) bool { profiles := h.getProfiles(user) // no profile validated, no need to continue if len(profiles) == 0 { return false } // check every acl for _, acl := range h.pdnsAcls { if acl.Match(path, method, profiles) { return true } } log.Println("Could not find profile/acl for ", user, method, path) return false } // nativeValidAuth validates that a user can access a particular path with a specified method func (h *HTTPServer) jsonrpcValidAuth(j JSONArray, message, signature []byte, certUser string) (bool, string) { // if the DryRun flag is set on all commands, let's authorize it // in debug mode, since we will do nothing if isDryRun(j) && h.debug && len(signature) == 0 { return true, certUser } var pgpProfiles, sslProfiles []string if len(j) == 0 { // nothing to check return false, "" } // try to get some profile switch { case h.verifyNonce(j[0].Params.Nonce): certUser, pgpProfiles = h.getPgpProfiles(message, signature) case certUser != "": sslProfiles = h.getProfiles(certUser) } // we need at least one profile if len(sslProfiles)+len(pgpProfiles) == 0 { log.Printf("[jsonRPC API] User %s was not authorized to execute anything", certUser) return false, certUser } // get the available acl for the user if there is a search or list listFilters := map[string][]*regexp.Regexp{} for _, acl := range h.jrpcAPIACls { for _, method := range []string{"list", "search"} { listFilters[method] = append(listFilters[method], acl.GetListFilters(pgpProfiles, sslProfiles, method)...) } } // check every acl on every action for i, action := range j { // for list and search, we do the checking after the fact if action.Method == "list" || action.Method == "search" { j[i].listFilters = listFilters[action.Method] continue } // the domain method is always permitted if action.Method == "domain" { continue } ok := false for _, acl := range h.jrpcAPIACls { if acl.Match(action.Method, action.Params.Name, pgpProfiles, sslProfiles) { ok = true break } } if !ok { log.Printf("[jsonRPC API] User %s was not authorized to execute \"%s\"", certUser, action.String()) return false, certUser } } return true, certUser } // Get Certificate user Name from the http.Request func (h *HTTPServer) getCertificate(r *http.Request) (bool, string, error) { // Check if the TLS user certificate is valid var sslUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} opts := x509.VerifyOptions{Roots: h.certPool, KeyUsages: sslUsage} if len(r.TLS.PeerCertificates) == 0 { return false, "", nil } if _, err := r.TLS.PeerCertificates[0].Verify(opts); err != nil { return false, "", err } if err := h.checkCRL(r.TLS.PeerCertificates); err != nil { return false, "", err } return true, strings.Replace(r.TLS.PeerCertificates[0].Subject.CommonName, " ", "", -1), nil } // decode the http request and return the payload, username and signature func (h *HTTPServer) jrpcDecodeQuery(body []byte) (JSONArray, []byte, []byte, bool, error) { // the payload can be a PGP signed payload // if it's not, message will contain the whole payload b, message := clearsign.Decode(body) // this is not a valid PGP signed payload, meaning message is all we got if b == nil { jsonRPC, wasArray, err := ParsejsonRPCRequest(message, h.dns) return jsonRPC, message, nil, wasArray, err } // this is a valid PGP signed payload, we can extract the real payload and // use the signature to authenticate message = b.Plaintext // there is a newline appended to the payload somehow if message[len(message)-1] == 10 { message = message[:len(message)-1] } // we need the signature to be a []byte and not an Reader, since we // may have to use it several times signature, err := ioutil.ReadAll(b.ArmoredSignature.Body) if err != nil { return JSONArray{}, message, signature, false, err } jsonRPC, wasArray, err := ParsejsonRPCRequest(message, h.dns) return jsonRPC, message, signature, wasArray, err } // PowerDNS json RPC API support. Support Cert Auth or PGP signed messages func (h *HTTPServer) jsonRPCServe(w http.ResponseWriter, r *http.Request) { // This API is POST only if r.Method != "POST" { http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 405) return } w.Header().Set("content-type", "application/json") username := "" _, username, certError := h.getCertificate(r) if certError != nil { http.Error(w, JSONRPCNewError(-32008, 0, certError.Error()).String(), 403) return } body, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 500) return } // decode the body jsonRPC, message, signature, wasArray, err := h.jrpcDecodeQuery(body) if err != nil { log.Println(err) http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 500) return } // try to validate the query valid, username := h.jsonrpcValidAuth(jsonRPC, message, signature, username) if !valid { http.Error(w, JSONRPCNewError(-32004, 0, "You are not authorized").String(), 403) return } fmt.Fprintf(w, jsonRPC.Run(h, username, wasArray, r.Header.Get("PDNS-Output") == "plaintext")) } // PowerDNS native API support. Add certificate support for auth func (h *HTTPServer) nativeAPIServe(w http.ResponseWriter, r *http.Request) { // Check if the user/server is allowed to perform the required action ok, commonName, certError := h.getCertificate(r) if !ok { http.Error(w, certError.Error(), 403) return } log.Println("[Native API] User", commonName, "requested", r.Method, "on", r.RequestURI) if !h.nativeValidAuth(strings.TrimPrefix(r.RequestURI, h.dns.apiURL+"/"), commonName, r.Method) { http.Error(w, "The user "+commonName+" is not authorized to perform this action", 403) log.Println("[Native API] User ", commonName, " was not authorized to perform the action") return } h.dns.Proxy(w, r) } // GetZoneConfig returns a configuration for a new zone func (h *HTTPServer) GetZoneConfig(s string) (string, string, []string, JSONArray, bool, error) { valid := "" def := JSONArray{} for zoneType, profile := range h.zoneProfiles { for _, re := range profile.Regexp { if !re.MatchString(s) { continue } if valid != "" { return "", "", nil, nil, false, errors.New("Multiple profiles matched, check the config") } valid = zoneType } } if valid != "" { profile := h.zoneProfiles[valid] for _, e := range profile.DefaultEntries { def = append(def, e) } return valid, profile.SOA, profile.NameServers, def, profile.AutoInc, nil } // check for the default for zoneType, profile := range h.zoneProfiles { if profile.Default { for _, e := range profile.DefaultEntries { def = append(def, e) } return zoneType, profile.SOA, profile.NameServers, def, profile.AutoInc, nil } } return "", "", nil, nil, false, errors.New("no valid configuration found") } // RefreshCRL decodes the crl file if it's configured, and store it for later use func (h *HTTPServer) RefreshCRL() (*pkix.CertificateList, error) { // if there is no crl, no need to do anything if h.crl == "" { return nil, nil } t, err := times.Stat(h.crl) if err != nil { return nil, err } mtime := t.ModTime() if h.decodedCrlTime.Equal(mtime) { return h.decodedCrl, nil } rawCrl, err := ioutil.ReadFile(h.crl) if err != nil { return nil, err } decoded, err := x509.ParseCRL(rawCrl) if err != nil { return nil, err } ok := false for _, ca := range h.decodedCA { if err := ca.CheckCRLSignature(decoded); err == nil { ok = true } } if !ok { return nil, fmt.Errorf("CRL issued with the wrong CA") } h.lock() defer h.unlock() h.decodedCrlTime = mtime h.decodedCrl = decoded return h.decodedCrl, nil } func (h *HTTPServer) checkCRL(allCerts []*x509.Certificate) error { crl, err := h.RefreshCRL() if err != nil || crl == nil { return err } now := time.Now() expire := now.Add(24 * time.Hour) for _, cert := range allCerts { sign := base64.StdEncoding.EncodeToString(cert.Signature) if t, ok := h.certCache[sign]; ok && t.Before(expire) { continue } for _, revoked := range h.decodedCrl.TBSCertList.RevokedCertificates { if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 { return fmt.Errorf("Certificate for %s is revoked", cert.Subject.CommonName) } } h.certCache[sign] = now } return nil } // Run the server func (h *HTTPServer) Run() { mux := http.NewServeMux() server := &http.Server{ Addr: h.Port, Handler: mux, TLSConfig: &tls.Config{ ClientAuth: tls.RequestClientCert, ClientCAs: h.certPool, }, } serverRoot, err := fs.Sub(embeddedFS, "static") if err != nil { log.Fatal(err) } // give acces part of the native API mux.HandleFunc("/api/v1/servers/localhost/", h.nativeAPIServe) // but not all mux.Handle("/api/v1/", http.NotFoundHandler()) // new json RPC api mux.HandleFunc("/jsonrpc", h.jsonRPCServe) // needed for security mux.HandleFunc("/nonce", h.sendNonce) // status page mux.HandleFunc("/stats/", h.nativeAPIServe) mux.HandleFunc("/style.css", h.nativeAPIServe) if _, err := os.Stat("./web"); err == nil && h.debug { h.dns.LogDebug("serving local files") mux.Handle("/", http.FileServer(http.Dir("web"))) } else { mux.Handle("/", http.FileServer(http.FS(serverRoot))) } log.Println("Ready to serve") log.Fatal(server.ListenAndServeTLS(h.cert, h.key)) }