chiark / gitweb /
zone.lisp (:dkim): Replace a loop with `format' trickery.
[zone] / zone.lisp
index c800f1f4491d6850773703b2001ca44e43f4e501..b1e3050bb148d9a31cd3931095080f89fcdb76f7 100644 (file)
--- a/zone.lisp
+++ b/zone.lisp
@@ -26,7 +26,7 @@
 
 (defpackage #:zone
   (:use #:common-lisp
-       #:mdw.base #:mdw.str #:collect #:safely
+       #:mdw.base #:mdw.str #:anaphora #:collect #:safely
        #:net #:services)
   (:import-from #:net #:round-down #:round-up))
 
@@ -35,6 +35,14 @@ (in-package #:zone)
 ;;;--------------------------------------------------------------------------
 ;;; Various random utilities.
 
+(export '*zone-config*)
+(defparameter *zone-config* nil
+  "A list of configuration variables.
+
+   This is for the benefit of the frontend, which will dynamically bind them
+   so that input files can override them independently.  Not intended for use
+   by users.")
+
 (defun to-integer (x)
   "Convert X to an integer in the most straightforward way."
   (floor (rational x)))
@@ -229,10 +237,6 @@ (defstruct (soa (:predicate soap))
   min-ttl
   serial)
 
-(export 'zone-text-name)
-(defun zone-text-name (zone)
-  (princ-to-string (zone-name zone)))
-
 (export 'mx)
 (defstruct (mx (:predicate mxp))
   "Mail-exchange record information."
@@ -247,6 +251,10 @@ (defstruct (zone (:predicate zonep))
   name
   records)
 
+(export 'zone-text-name)
+(defun zone-text-name (zone)
+  (princ-to-string (zone-name zone)))
+
 ;;;--------------------------------------------------------------------------
 ;;; Zone defaults.  It is intended that scripts override these.
 
@@ -257,28 +265,28 @@ (defvar *default-zone-source*
   "The default zone source: the current host's name.")
 
 (export '*default-zone-refresh*)
-(defvar *default-zone-refresh* (* 24 60 60)
-  "Default zone refresh interval: one day.")
+(defvar *default-zone-refresh* (* 8 60 60)
+  "Default zone refresh interval: eight hours.")
 
 (export '*default-zone-admin*)
 (defvar *default-zone-admin* nil
   "Default zone administrator's email address.")
 
 (export '*default-zone-retry*)
-(defvar *default-zone-retry* (* 60 60)
-  "Default znoe retry interval: one hour.")
+(defvar *default-zone-retry* (* 20 60)
+  "Default zone retry interval: twenty minutes.")
 
 (export '*default-zone-expire*)
-(defvar *default-zone-expire* (* 14 24 60 60)
-  "Default zone expiry time: two weeks.")
+(defvar *default-zone-expire* (* 3 24 60 60)
+  "Default zone expiry time: three days.")
 
 (export '*default-zone-min-ttl*)
 (defvar *default-zone-min-ttl* (* 4 60 60)
-  "Default zone minimum TTL/negative TTL: four hours.")
+  "Default zone minimum/negative TTL: four hours.")
 
 (export '*default-zone-ttl*)
-(defvar *default-zone-ttl* (* 8 60 60)
-  "Default zone TTL (for records without explicit TTLs): 8 hours.")
+(defvar *default-zone-ttl* (* 4 60 60)
+  "Default zone TTL (for records without explicit TTLs): four hours.")
 
 (export '*default-mx-priority*)
 (defvar *default-mx-priority* 50
@@ -489,7 +497,7 @@ (defun zone-process-records (rec ttl func)
                                      :ttl ttl :records (cdr r))
                                     sub)))
                         (t
-                         (error "Unexpected record form ~A" (car r))))))))
+                         (error "Unexpected record form ~A" r)))))))
 
           (process (rec dom ttl)
             ;; Recursirvely process the record list REC, with a list DOM of
@@ -544,7 +552,7 @@ (defun zone-parse-head (head)
        (retry *default-zone-retry*)
        (expire *default-zone-expire*)
        (min-ttl *default-zone-min-ttl*)
-       (ttl min-ttl)
+       (ttl *default-zone-ttl*)
        (serial (make-zone-serial raw-zname))
        &aux
        (zname (zone-parse-host raw-zname root-domain)))
@@ -679,15 +687,15 @@ (export 'defrevzone)
 (defmacro defrevzone (head &body zf)
   "Define a reverse zone, with the correct name."
   (destructuring-bind (nets &rest args
-                           &key &allow-other-keys
-                                (family '*address-family*)
-                                prefix-bits)
+                           &key (family '*address-family*)
+                                prefix-bits
+                                &allow-other-keys)
       (listify head)
     (with-gensyms (ipn)
       `(dolist (,ipn (net-parse-to-ipnets ',nets ,family))
         (let ((*address-family* (ipnet-family ,ipn)))
-          (zone-create `((,(format nil "~A." (reverse-domain ,ipn
-                                                             ,prefix-bits))
+          (zone-create `((,(format nil "~A" (reverse-domain ,ipn
+                                                            ,prefix-bits))
                            ,@',(loop for (k v) on args by #'cddr
                                      unless (member k
                                                     '(:family :prefix-bits))
@@ -768,7 +776,7 @@ (defun rec-u32 (value)
 
 (export 'rec-raw-string)
 (defun rec-raw-string (s &key (start 0) end)
-  "Append (a (substring of) a raw string S to the current record.
+  "Append (a substring of) a raw string S to the current record.
 
    No arrangement is made for reporting the length of the string.  That must
    be done by the caller, if necessary."
@@ -852,13 +860,44 @@ (defzoneparse :cname (name data rec :zname zname)
   ":cname HOST"
   (rec :data (zone-parse-host data zname)))
 
+(defzoneparse :dname (name data rec :zname zname)
+  ":dname HOST"
+  (rec :data (zone-parse-host data zname)))
+
 (defmethod zone-record-rrdata ((type (eql :cname)) zr)
   (rec-name (zr-data zr))
   5)
 
+(defun split-txt-data (data)
+  (collecting ()
+    (let ((i 0) (n (length data)))
+      (loop
+       (let ((end (+ i 255)))
+         (when (<= n end) (return))
+         (let ((split (acond ((position #\; data :from-end t
+                                        :start i :end end)
+                              (+ it 1))
+                             ((position #\space data :from-end t
+                                        :start i :end end)
+                              (+ it 1))
+                             (t end))))
+           (loop
+             (when (or (>= split end)
+                       (char/= (char data split) #\space))
+               (return))
+             (incf split))
+           (collect (subseq data i split))
+           (setf i split))))
+      (collect (subseq data i)))))
+
 (defzoneparse :txt (name data rec)
   ":txt (TEXT*)"
-  (rec :data (listify data)))
+  (rec :data (cond ((stringp data) (split-txt-data data))
+                  (t
+                   (dolist (piece data)
+                     (unless (<= (length piece) 255)
+                       (error "`:txt' record piece `~A' too long" piece)))
+                   data))))
 
 (defmethod zone-record-rrdata ((type (eql :txt)) zr)
   (mapc #'rec-string (zr-data zr))
@@ -868,92 +907,70 @@ (export '*dkim-pathname-defaults*)
 (defvar *dkim-pathname-defaults*
   (make-pathname :directory '(:relative "keys")
                 :type "dkim"))
+(pushnew '*dkim-pathname-defaults* *zone-config*)
 
 (defzoneparse :dkim (name data rec)
   ":dkim (KEYFILE {:TAG VALUE}*)"
   (destructuring-bind (file &rest plist) (listify data)
-    (let ((things nil) (out nil))
-      (labels ((flush ()
-                (when out
-                  (push (get-output-stream-string out) things)
-                  (setf out nil)))
-              (emit (text)
-                (let ((len (length text)))
-                  (when (and out (> (+ (file-position out)
-                                       (length text))
-                                    64))
-                    (flush))
-                  (when (plusp len)
-                    (cond ((< len 64)
-                           (unless out
-                             (setf out (make-string-output-stream)))
-                           (write-string text out))
-                          (t
-                           (do ((i 0 j)
-                                (j 64 (+ j 64)))
-                               ((>= i len))
-                             (push (subseq text i (min j len))
-                                   things))))))))
-       (do ((p plist (cddr p)))
-           ((endp p))
-         (emit (format nil "~(~A~)=~A;" (car p) (cadr p))))
-       (emit (with-output-to-string (out)
-               (write-string "p=" out)
-               (when file
-                 (with-open-file
-                     (in (merge-pathnames file *dkim-pathname-defaults*))
-                   (loop
-                     (when (string= (read-line in)
-                                    "-----BEGIN PUBLIC KEY-----")
-                       (return)))
-                   (loop
-                     (let ((line (read-line in)))
-                       (if (string= line "-----END PUBLIC KEY-----")
-                           (return)
-                           (write-string line out)))))))))
-      (rec :type :txt
-          :data (nreverse things)))))
-
-(defenum sshfp-algorithm () (:rsa 1) (:dsa 2) (:ecdsa 3))
+    (rec :type :txt
+        :data
+        (split-txt-data
+         (with-output-to-string (out)
+           (format out "~{~(~A~)=~A; ~}" plist)
+           (write-string "p=" out)
+           (when file
+             (with-open-file
+                 (in (merge-pathnames file *dkim-pathname-defaults*))
+               (loop
+                 (when (string= (read-line in)
+                                "-----BEGIN PUBLIC KEY-----")
+                   (return)))
+               (loop
+                 (let ((line (read-line in)))
+                   (when (string= line "-----END PUBLIC KEY-----")
+                     (return))
+                   (write-string line out))))))))))
+
+(defenum sshfp-algorithm () (:rsa 1) (:dsa 2) (:ecdsa 3) (:ed25519 4))
 (defenum sshfp-type () (:sha-1 1) (:sha-256 2))
 
 (export '*sshfp-pathname-defaults*)
 (defvar *sshfp-pathname-defaults*
-  (make-pathname :directory '(:relative "keys")
-                :type "sshfp"))
+  (make-pathname :directory '(:relative "keys") :type "sshfp")
+  "Default pathname components for SSHFP records.")
+(pushnew '*sshfp-pathname-defaults* *zone-config*)
 
 (defzoneparse :sshfp (name data rec)
   ":sshfp { FILENAME | ((FPR :alg ALG :type HASH)*) }"
-  (if (stringp data)
-      (with-open-file (in (merge-pathnames data *sshfp-pathname-defaults*))
-       (loop (let ((line (read-line in nil)))
-               (unless line (return))
-               (let ((words (str-split-words line)))
-                 (pop words)
-                 (when (string= (car words) "IN") (pop words))
-                 (unless (and (string= (car words) "SSHFP")
-                              (= (length words) 4))
-                   (error "Invalid SSHFP record."))
-                 (pop words)
-                 (destructuring-bind (alg type fpr) words
-                   (rec :data (list (parse-integer alg)
-                                    (parse-integer type)
-                                    fpr)))))))
-      (dolist (item (listify data))
-       (destructuring-bind (fpr &key (alg 'rsa) (type 'sha-1))
-           (listify item)
-         (rec :data (list (lookup-enum alg 'sshfp-algorithm :min 0 :max 255)
-                          (lookup-enum type 'sshfp-type :min 0 :max 255)
-                          fpr))))))
+  (typecase data
+    ((or string pathname)
+     (with-open-file (in (merge-pathnames data *sshfp-pathname-defaults*))
+       (loop (let ((line (read-line in nil)))
+              (unless line (return))
+              (let ((words (str-split-words line)))
+                (pop words)
+                (when (string= (car words) "IN") (pop words))
+                (unless (and (string= (car words) "SSHFP")
+                             (= (length words) 4))
+                  (error "Invalid SSHFP record."))
+                (pop words)
+                (destructuring-bind (alg type fprhex) words
+                  (rec :data (list (parse-integer alg)
+                                   (parse-integer type)
+                                   (decode-hex fprhex)))))))))
+    (t
+     (dolist (item (listify data))
+       (destructuring-bind (fprhex &key (alg 'rsa) (type 'sha-1))
+          (listify item)
+        (rec :data (list (lookup-enum alg 'sshfp-algorithm :min 0 :max 255)
+                         (lookup-enum type 'sshfp-type :min 0 :max 255)
+                         (decode-hex fprhex))))))))
 
 (defmethod zone-record-rrdata ((type (eql :sshfp)) zr)
   (destructuring-bind (alg type fpr) (zr-data zr)
     (rec-u8 alg)
     (rec-u8 type)
-    (do ((i 0 (+ i 2))
-        (n (length fpr)))
-       ((>= i n))
-      (rec-u8 (parse-integer fpr :start i :end (+ i 2) :radix 16))))
+    (rec-octet-vector fpr))
   44)
 
 (defenum tlsa-usage ()
@@ -1065,6 +1082,16 @@ (defun identify-tlsa-selector-file (file)
                       (return value)))
                   'tlsa-selector))))))
 
+(export '*tlsa-pathname-defaults*)
+(defvar *tlsa-pathname-defaults*
+  (list (make-pathname :directory '(:relative "certs") :type "cert")
+       (make-pathname :directory '(:relative "keys") :type "pub"))
+  "Default pathname components for TLSA records.")
+(pushnew '*tlsa-pathname-defaults* *zone-config*)
+
+(defparameter *tlsa-data-cache* (make-hash-table :test #'equal)
+  "Cache for TLSA association data; keys are (DATA SELECTOR MATCH).")
+
 (defun convert-tlsa-selector-data (data selector match)
   "Convert certificate association DATA as required by SELECTOR and MATCH.
 
@@ -1085,10 +1112,19 @@ (defun convert-tlsa-selector-data (data selector match)
                (reverse-enum 'tlsa-match match)))
        bin))
     (pathname
-     (with-temporary-files (context :base "tmpfile.tmp")
-       (let* ((kind (identify-tlsa-selector-file data))
-             (raw (raw-tlsa-assoc-data kind selector data context)))
-        (read-tlsa-match-data match raw context))))))
+     (let ((key (list data selector match)))
+       (or (gethash key *tlsa-data-cache*)
+          (with-temporary-files (context :base (make-pathname :type "tmp"))
+            (let* ((file (or (find-if #'probe-file
+                                      (mapcar (lambda (template)
+                                                (merge-pathnames data
+                                                                 template))
+                                              *tlsa-pathname-defaults*))
+                             (error "Couldn't find TLSA file `~A'" data)))
+                   (kind (identify-tlsa-selector-file file))
+                   (raw (raw-tlsa-assoc-data kind selector file context))
+                   (binary (read-tlsa-match-data match raw context)))
+              (setf (gethash key *tlsa-data-cache*) binary))))))))
 
 (defzoneparse :tlsa (name data rec)
   ":tlsa (((SERVICE|PORT &key :protocol)*) (USAGE SELECTOR MATCH DATA)*)"
@@ -1132,6 +1168,39 @@ (defmethod zone-record-rrdata ((type (eql :tlsa)) zr)
     (rec-octet-vector data))
   52)
 
+(defenum dnssec-algorithm ()
+  (:rsamd5 1)
+  (:dh 2)
+  (:dsa 3)
+  (:rsasha1 5)
+  (:dsa-nsec3-sha1 6)
+  (:rsasha1-nsec3-sha1 7)
+  (:rsasha256 8)
+  (:rsasha512 10)
+  (:ecc-gost 12)
+  (:ecdsap256sha256 13)
+  (:ecdsap384sha384 14))
+
+(defenum dnssec-digest ()
+  (:sha1 1)
+  (:sha256 2))
+
+(defzoneparse :ds (name data rec)
+  ":ds ((TAG ALGORITHM DIGEST-TYPE DIGEST)*)"
+  (dolist (ds data)
+    (destructuring-bind (tag alg hashtype hash) ds
+      (rec :data (list tag
+                      (lookup-enum 'dnssec-algorithm alg :min 0 :max 255)
+                      (lookup-enum 'dnssec-digest hashtype :min 0 :max 255)
+                      (decode-hex hash))))))
+
+(defmethod zone-record-rrdata ((type (eql :ds)) zr)
+  (destructuring-bind (tag alg hashtype hash) zr
+    (rec-u16 tag)
+    (rec-u8 alg)
+    (rec-u8 hashtype)
+    (rec-octet-vector hash)))
+
 (defzoneparse :mx (name data rec :zname zname)
   ":mx ((HOST :prio INT :ip IPADDR)*)"
   (dolist (mx (listify data))
@@ -1209,6 +1278,29 @@ (defmethod zone-record-rrdata ((type (eql :srv)) zr)
     (rec-name host))
   33)
 
+(defenum caa-flag () (:critical 128))
+
+(defzoneparse :caa (name data rec)
+  ":caa ((TAG VALUE FLAG*)*)"
+  (dolist (prop data)
+    (destructuring-bind (tag value &rest flags) prop
+      (setf flags (reduce #'logior
+                         (mapcar (lambda (item)
+                                   (lookup-enum 'caa-flag item
+                                                :min 0 :max 255))
+                                 flags)))
+      (ecase tag
+       ((:issue :issuewild :iodef)
+        (rec :name name
+             :data (list flags tag value)))))))
+
+(defmethod zone-record-rrdata ((type (eql :caa)) zr)
+  (destructuring-bind (flags tag value) (zr-data zr)
+    (rec-u8 flags)
+    (rec-string (string-downcase tag))
+    (rec-raw-string value))
+  257)
+
 (defzoneparse :net (name data rec)
   ":net (NETWORK*)"
   (dolist (net (listify data))
@@ -1531,6 +1623,9 @@ (defmethod zone-write-record ((format (eql :bind)) (type (eql :ptr)) zr)
 (defmethod zone-write-record ((format (eql :bind)) (type (eql :cname)) zr)
   (bind-format-record zr "~A~%" (bind-hostname (zr-data zr))))
 
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :dname)) zr)
+  (bind-format-record zr "~A~%" (bind-hostname (zr-data zr))))
+
 (defmethod zone-write-record ((format (eql :bind)) (type (eql :ns)) zr)
   (bind-format-record zr "~A~%" (bind-hostname (zr-data zr))))
 
@@ -1545,13 +1640,24 @@ (defmethod zone-write-record ((format (eql :bind)) (type (eql :srv)) zr)
                        prio weight port (bind-hostname host))))
 
 (defmethod zone-write-record ((format (eql :bind)) (type (eql :sshfp)) zr)
-  (bind-format-record zr "~{~2D ~2D ~A~}~%" (zr-data zr)))
+  (destructuring-bind (alg type fpr) (zr-data zr)
+    (bind-format-record zr "~2D ~2D " alg type)
+    (bind-write-hex fpr 12)))
 
 (defmethod zone-write-record ((format (eql :bind)) (type (eql :tlsa)) zr)
   (destructuring-bind (usage selector match data) (zr-data zr)
     (bind-format-record zr "~2D ~2D ~2D " usage selector match)
     (bind-write-hex data 12)))
 
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :caa)) zr)
+  (destructuring-bind (flags tag value) (zr-data zr)
+    (bind-format-record zr "~3D ~(~A~) ~S~%" flags tag value)))
+
+(defmethod zone-write-record ((format (eql :bind)) (type (eql :ds)) zr)
+  (destructuring-bind (tag alg hashtype hash) (zr-data zr)
+    (bind-format-record zr "~5D ~2D ~2D " tag alg hashtype)
+    (bind-write-hex hash 12)))
+
 (defmethod zone-write-record ((format (eql :bind)) (type (eql :txt)) zr)
   (bind-format-record zr "~{~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]~}~%"
                      (zr-data zr)))