568 lines
15 KiB
Go
568 lines
15 KiB
Go
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))
|
|
}
|