chiark / gitweb /
zone.lisp: Escape DEL in TinyDNS output.
[zone] / zone.lisp
index e6863224a0d78feb488b32e58812037d041055aa..3aa25ba0794e3b9337fc3594a4bfd42aab11c114 100644 (file)
--- a/zone.lisp
+++ b/zone.lisp
@@ -111,93 +111,66 @@ (defun iso-date (&optional time &key datep timep (sep #\ ))
       (when timep
        (format s "~2,'0D:~2,'0D:~2,'0D" hr min sec)))))
 
-(defun natural-string< (string1 string2
-                       &key (start1 0) (end1 nil)
-                       (start2 0) (end2 nil))
-  "Answer whether STRING1 precedes STRING2 in a vaguely natural ordering.
-
-   In particular, digit sequences are handled in a moderately sensible way.
-   Split the strings into maximally long alternating sequences of non-numeric
-   and numeric characters, such that the non-numeric sequences are
-   non-empty.  Compare these lexicographically; numeric sequences order
-   according to their integer values, non-numeric sequences in the usual
-   lexicographic ordering.
-
-   Returns two values: whether STRING1 strictly precedes STRING2, and whether
-   STRING1 strictly follows STRING2."
-
-  (let ((end1 (or end1 (length string1)))
-       (end2 (or end2 (length string2))))
-    (loop
-      (cond ((>= start1 end1)
-            (let ((eqp (>= start2 end2)))
-              (return (values (not eqp) nil))))
-           ((>= start2 end2)
-            (return (values nil t)))
-           ((and (digit-char-p (char string1 start1))
-                 (digit-char-p (char string2 start2)))
-            (let* ((lim1 (or (position-if-not #'digit-char-p string1
-                                              :start start1 :end end1)
-                             end1))
-                   (n1 (parse-integer string1 :start start1 :end lim1))
-                   (lim2 (or (position-if-not #'digit-char-p string2
-                                              :start start2 :end end2)
-                             end2))
-                   (n2 (parse-integer string2 :start start2 :end lim2)))
-              (cond ((< n1 n2) (return (values t nil)))
-                    ((> n1 n2) (return (values nil t))))
-              (setf start1 lim1
-                    start2 lim2)))
-           (t
-            (let ((lim1 (or (position-if #'digit-char-p string1
-                                         :start start1 :end end1)
-                            end1))
-                  (lim2 (or (position-if #'digit-char-p string2
-                                         :start start2 :end end2)
-                            end2)))
-              (cond ((string< string1 string2
-                              :start1 start1 :end1 lim1
-                              :start2 start2 :end2 lim2)
-                     (return (values t nil)))
-                    ((string> string1 string2
-                              :start1 start1 :end1 lim1
-                              :start2 start2 :end2 lim2)
-                     (return (values nil t))))
-              (setf start1 lim1
-                    start2 lim2)))))))
-
-(defun domain-name< (name-a name-b)
-  "Answer whether NAME-A precedes NAME-B in an ordering of domain names.
-
-   Split the names into labels at the dots, and then lexicographically
-   compare the sequences of labels, right to left, using `natural-string<'.
-
-   Returns two values: whether NAME-A strictly precedes NAME-B, and whether
-   NAME-A strictly follows NAME-B."
-  (let ((pos-a (length name-a))
-       (pos-b (length name-b)))
-    (loop (let ((dot-a (or (position #\. name-a
-                                    :from-end t :end pos-a)
-                          -1))
-               (dot-b (or (position #\. name-b
-                                    :from-end t :end pos-b)
-                          -1)))
-           (multiple-value-bind (precp follp)
-               (natural-string< name-a name-b
-                                :start1 (1+ dot-a) :end1 pos-a
-                                :start2 (1+ dot-b) :end2 pos-b)
-             (cond (precp
-                    (return (values t nil)))
-                   (follp
-                    (return (values nil t)))
-                   ((= dot-a -1)
-                    (let ((eqp (= dot-b -1)))
-                      (return (values (not eqp) nil))))
-                   ((= dot-b -1)
-                    (return (values nil t)))
-                   (t
-                    (setf pos-a dot-a
-                          pos-b dot-b))))))))
+(defmacro defenum (name (&key export) &body values)
+  "Set up symbol properties for manifest constants.
+
+   The VALUES are a list of (TAG VALUE) pairs. Each TAG is a symbol; we set
+   the NAME property on TAG to VALUE, and export TAG.  There are also handy
+   hash-tables mapping in the forward and reverse directions, in the name
+   symbol's `enum-forward' and `enum-reverse' properties."
+  `(eval-when (:compile-toplevel :load-toplevel :execute)
+     ,(let*/gensyms (export)
+       (with-gensyms (forward reverse valtmp)
+         `(let ((,forward (make-hash-table))
+                (,reverse (make-hash-table)))
+            (when ,export (export ',name))
+            ,@(mapcar (lambda (item)
+                        (destructuring-bind (tag value) item
+                          (let ((constant
+                                 (intern (concatenate 'string
+                                                      (symbol-name name)
+                                                      "/"
+                                                      (symbol-name tag)))))
+                            `(let ((,valtmp ,value))
+                               (when ,export
+                                 (export ',constant)
+                                 (when (eq (symbol-package ',tag) *package*)
+                                   (export ',tag)))
+                               (defconstant ,constant ,valtmp)
+                               (setf (get ',tag ',name) ,value
+                                     (gethash ',tag ,forward) ,valtmp
+                                     (gethash ,valtmp ,reverse) ',tag)))))
+                      values)
+            (setf (get ',name 'enum-forward) ,forward
+                  (get ',name 'enum-reverse) ,reverse))))))
+
+(defun lookup-enum (name tag &key min max)
+  "Look up a TAG in an enumeration.
+
+   If TAG is a symbol, check its NAME property; if it's a fixnum then take it
+   as it is.  Make sure that it's between MIN and MAX, if they're not nil."
+  (let ((value (etypecase tag
+               (fixnum tag)
+               (symbol (or (get tag name)
+                           (error "~S is not a known ~A" tag name))))))
+    (unless (and (or (null min) (<= min value))
+               (or (null max) (<= value max)))
+      (error "Value ~S out of range for ~A" value name))
+    value))
+
+(defun reverse-enum (name value)
+  "Reverse-lookup of a VALUE in enumeration NAME.
+
+   If a tag for the VALUE is found, return it and `t'; otherwise return VALUE
+   unchanged and `nil'."
+  (multiple-value-bind (tag foundp) (gethash value (get name 'enum-reverse))
+    (if foundp
+       (values tag t)
+       (values value nil))))
+
+(defun mapenum (func name)
+  "Call FUNC on TAG/VALUE pairs from the enumeration called NAME."
+  (maphash func (get name 'enum-forward)))
 
 ;;;--------------------------------------------------------------------------
 ;;; Zone types.
@@ -213,6 +186,10 @@ (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."
@@ -347,32 +324,20 @@ (defmacro preferred-subnet-case (&body clauses)
              clauses)))
 
 (export 'zone-parse-host)
-(defun zone-parse-host (f zname)
-  "Parse a host name F.
-
-   If F ends in a dot then it's considered absolute; otherwise it's relative
-   to ZNAME."
-  (setf f (stringify f))
-  (cond ((string= f "@") (stringify zname))
-       ((and (plusp (length f))
-             (char= (char f (1- (length f))) #\.))
-        (string-downcase (subseq f 0 (1- (length f)))))
-       (t (string-downcase (concatenate 'string f "."
-                                        (stringify zname))))))
-
-(export 'zone-make-name)
-(defun zone-make-name (prefix zone-name)
-  "Compute a full domain name from a PREFIX and a ZONE-NAME.
-
-   If the PREFIX ends with `.' then it's absolute already; otherwise, append
-   the ZONE-NAME, separated with a `.'.  If PREFIX is nil, or `@', then
-   return the ZONE-NAME only."
-  (if (or (not prefix) (string= prefix "@"))
-      zone-name
-      (let ((len (length prefix)))
-       (if (or (zerop len) (char/= (char prefix (1- len)) #\.))
-           (join-strings #\. (list prefix zone-name))
-           prefix))))
+(defun zone-parse-host (form &optional tail)
+  "Parse a host name FORM from a value in a zone form.
+
+   The underlying parsing is done using `parse-domain-name'.  Here, we
+   interpret various kinds of Lisp object specially.  In particular: `nil'
+   refers to the TAIL zone (just like a plain `@'); and a symbol is downcased
+   before use."
+  (let ((name (etypecase form
+               (null (make-domain-name :labels nil :absolutep nil))
+               (domain-name form)
+               (symbol (parse-domain-name (string-downcase form)))
+               (string (parse-domain-name form)))))
+    (if (null tail) name
+       (domain-name-concat name tail))))
 
 (export 'zone-records-sorted)
 (defun zone-records-sorted (zone)
@@ -476,9 +441,9 @@ (defun zone-process-records (rec ttl func)
                                   top))
                         ((listp r)
                          (dolist (name (listify (car r)))
-                           (collect (make-zone-subdomain :name name
-                                                         :ttl ttl
-                                                         :records (cdr r))
+                           (collect (make-zone-subdomain
+                                     :name (zone-parse-host name)
+                                     :ttl ttl :records (cdr r))
                                     sub)))
                         (t
                          (error "Unexpected record form ~A" (car r))))))))
@@ -492,24 +457,25 @@ (defun zone-process-records (rec ttl func)
             (multiple-value-bind (top sub) (sift rec ttl)
               (if (and dom (null top) sub)
                   (let ((preferred
-                         (or (find-if (lambda (s)
-                                        (some #'zone-preferred-subnet-p
-                                              (listify (zs-name s))))
-                                      sub)
+                         (or (find-if
+                              (lambda (s)
+                                (let ((ll (domain-name-labels (zs-name s))))
+                                  (and (consp ll) (null (cdr ll))
+                                       (zone-preferred-subnet-p (car ll)))))
+                              sub)
                              (car sub))))
                     (when preferred
                       (process (zs-records preferred)
                                dom
                                (zs-ttl preferred))))
-                  (let ((name (and dom
-                                   (string-downcase
-                                    (join-strings #\. (reverse dom))))))
+                  (let ((name dom))
                     (dolist (zr top)
                       (setf (zr-name zr) name)
                       (funcall func zr))))
               (dolist (s sub)
                 (process (zs-records s)
-                         (cons (zs-name s) dom)
+                         (if (null dom) (zs-name s)
+                             (domain-name-concat dom (zs-name s)))
                          (zs-ttl s))))))
 
     ;; Process the records we're given with no prefix.
@@ -526,19 +492,21 @@ (defun zone-parse-head (head)
    though a singleton NAME needn't be a list.  Returns the default TTL and an
    soa structure representing the zone head."
   (destructuring-bind
-      (zname
+      (raw-zname
        &key
        (source *default-zone-source*)
        (admin (or *default-zone-admin*
-                 (format nil "hostmaster@~A" zname)))
+                 (format nil "hostmaster@~A" raw-zname)))
        (refresh *default-zone-refresh*)
        (retry *default-zone-retry*)
        (expire *default-zone-expire*)
        (min-ttl *default-zone-min-ttl*)
        (ttl min-ttl)
-       (serial (make-zone-serial zname)))
+       (serial (make-zone-serial raw-zname))
+       &aux
+       (zname (zone-parse-host raw-zname root-domain)))
       (listify head)
-    (values (string-downcase zname)
+    (values zname
            (timespec-seconds ttl)
            (make-soa :admin admin
                      :source (zone-parse-host source zname)
@@ -583,6 +551,7 @@ (defmacro defzoneparse (types (name data list
 
    These (except MAKE-PTR-P, which defaults to nil) default to the above
    arguments (even if you didn't accept the arguments)."
+
   (setf types (listify types))
   (let* ((type (car types))
         (func (intern (format nil "ZONE-PARSE/~:@(~A~)" type))))
@@ -594,7 +563,8 @@ (defmacro defzoneparse (types (name data list
           (defun ,func (,prefix ,zname ,data ,ttl ,col)
             ,@doc
             ,@decls
-            (let ((,name (zone-make-name ,prefix ,zname)))
+            (let ((,name (if (null ,prefix) ,zname
+                             (domain-name-concat ,prefix ,zname))))
               (flet ((,list (&key ((:name ,tname) ,name)
                                   ((:type ,ttype) ,type)
                                   ((:data ,tdata) ,data)
@@ -621,7 +591,7 @@ (defun zone-parse-records (zname ttl records)
             (let ((func (or (get (zr-type zr) 'zone-parse)
                             (error "No parser for record ~A."
                                    (zr-type zr))))
-                  (name (and (zr-name zr) (stringify (zr-name zr)))))
+                  (name (and (zr-name zr) (zr-name zr))))
               (funcall func name zname (zr-data zr) (zr-ttl zr) rec))))
       (zone-process-records records ttl #'parse-record))))
 
@@ -645,10 +615,11 @@ (defun zone-parse (zf)
 
 (export 'zone-create)
 (defun zone-create (zf)
-  "Zone construction function.  Given a zone form ZF, construct the zone and
-   add it to the table."
+  "Zone construction function.
+
+   Given a zone form ZF, construct the zone and add it to the table."
   (let* ((zone (zone-parse zf))
-        (name (zone-name zone)))
+        (name (zone-text-name zone)))
     (setf (zone-find name) zone)
     name))
 
@@ -672,7 +643,8 @@ (defmacro defrevzone (head &body zf)
     (with-gensyms (ipn)
       `(dolist (,ipn (net-parse-to-ipnets ',nets ,family))
         (let ((*address-family* (ipnet-family ,ipn)))
-          (zone-create `((,(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))
@@ -763,19 +735,13 @@ (defun rec-string (s &key (start 0) end (max 255))
     (rec-raw-string s :start start :end end)))
 
 (export 'rec-name)
-(defun rec-name (s)
-  "Append a domain name S.
+(defun rec-name (name)
+  "Append a domain NAME.
 
    No attempt is made to perform compression of the name."
-  (let ((i 0) (n (length s)))
-    (loop (let* ((dot (position #\. s :start i))
-                (lim (or dot n)))
-           (rec-string s :start i :end lim :max 63)
-           (if dot
-               (setf i (1+ dot))
-               (return))))
-    (when (< i n)
-      (rec-u8 0))))
+  (dolist (label (reverse (domain-name-labels name)))
+    (rec-string label :max 63))
+  (rec-u8 0))
 
 (export 'build-record)
 (defmacro build-record (&body body)
@@ -866,13 +832,15 @@ (defzoneparse :dkim (name data rec)
                     (flush))
                   (when (plusp len)
                     (cond ((< len 64)
-                           (unless out (setf out (make-string-output-stream)))
+                           (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))))))))
+                             (push (subseq text i (min j len))
+                                   things))))))))
        (do ((p plist (cddr p)))
            ((endp p))
          (emit (format nil "~(~A~)=~A;" (car p) (cadr p))))
@@ -893,15 +861,8 @@ (defzoneparse :dkim (name data rec)
       (rec :type :txt
           :data (nreverse things)))))
 
-(eval-when (:load-toplevel :execute)
-  (dolist (item '((sshfp-algorithm rsa 1)
-                 (sshfp-algorithm dsa 2)
-                 (sshfp-algorithm ecdsa 3)
-                 (sshfp-type sha-1 1)
-                 (sshfp-type sha-256 2)))
-    (destructuring-bind (prop sym val) item
-      (setf (get sym prop) val)
-      (export sym))))
+(defenum sshfp-algorithm (rsa 1) (dsa 2) (ecdsa 3))
+(defenum sshfp-type (sha-1 1) (sha-256 2))
 
 (export '*sshfp-pathname-defaults*)
 (defvar *sshfp-pathname-defaults*
@@ -925,17 +886,12 @@ (defzoneparse :sshfp (name data rec)
                    (rec :data (list (parse-integer alg)
                                     (parse-integer type)
                                     fpr)))))))
-      (flet ((lookup (what prop)
-              (etypecase what
-                (fixnum what)
-                (symbol (or (get what prop)
-                            (error "~S is not a known ~A" what prop))))))
-       (dolist (item (listify data))
-         (destructuring-bind (fpr &key (alg 'rsa) (type 'sha-1))
-             (listify item)
-           (rec :data (list (lookup alg 'sshfp-algorithm)
-                            (lookup type 'sshfp-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))))))
 
 (defmethod zone-record-rrdata ((type (eql :sshfp)) zr)
   (destructuring-bind (alg type fpr) (zr-data zr)
@@ -986,7 +942,8 @@ (defzoneparse :alias (name data rec :zname zname)
         :data name)))
 
 (defzoneparse :srv (name data rec :zname zname)
-  ":srv (((SERVICE &key :port) (PROVIDER &key :port :prio :weight :ip)*)*)"
+  ":srv (((SERVICE &key :port :protocol)
+         (PROVIDER &key :port :prio :weight :ip)*)*)"
   (dolist (srv data)
     (destructuring-bind (servopts &rest providers) srv
       (destructuring-bind
@@ -995,7 +952,12 @@ (defzoneparse :srv (name data rec :zname zname)
        (unless default-port
          (let ((serv (serv-by-name service protocol)))
            (setf default-port (and serv (serv-port serv)))))
-       (let ((rname (format nil "~(_~A._~A~).~A" service protocol name)))
+       (let ((rname (flet ((prepend (tag tail)
+                             (domain-name-concat
+                              (make-domain-name
+                               :labels (list (format nil "_~(~A~)" tag)))
+                              tail)))
+                      (prepend service (prepend protocol name)))))
          (dolist (prov providers)
            (destructuring-bind
                (srvname
@@ -1054,11 +1016,12 @@ (defzoneparse (:rev :reverse) (name data rec)
                       (ipaddr-networkp (ipaddr-addr (zr-data zr)) ipn))
              (let* ((frag (reverse-domain-fragment (zr-data zr)
                                                    0 frag-len))
-                    (name (concatenate 'string frag "." name)))
-               (unless (gethash name seen)
+                    (name (domain-name-concat frag name))
+                    (name-string (princ-to-string name)))
+               (unless (gethash name-string seen)
                  (rec :name name :type :ptr
                       :ttl (zr-ttl zr) :data (zr-name zr))
-                 (setf (gethash name seen) t))))))))))
+                 (setf (gethash name-string seen) t))))))))))
 
 (defzoneparse :multi (name data rec :zname zname :ttl ttl)
   ":multi (((NET*) &key :start :end :family :suffix) . REC)
@@ -1084,42 +1047,45 @@ (defzoneparse :multi (name data rec :zname zname :ttl ttl)
 
    Obviously, nested `:multi' records won't work well."
 
-  (destructuring-bind (nets &key start end (family *address-family*) suffix)
+  (destructuring-bind (nets
+                      &key start end ((:suffix raw-suffix))
+                      (family *address-family*))
       (listify (car data))
-    (dolist (net (listify nets))
-      (dolist (ipn (net-parse-to-ipnets net family))
-       (let* ((addr (ipnet-net ipn))
-              (width (ipaddr-width addr))
-              (comp-width (reverse-domain-component-width addr))
-              (end (round-up (or end
-                                 (ipnet-changeable-bits width
-                                                        (ipnet-mask ipn)))
-                             comp-width))
-              (start (round-down (or start (- end comp-width))
-                                 comp-width))
-              (map (ipnet-host-map ipn)))
-         (multiple-value-bind (host-step host-limit)
-             (ipnet-index-bounds map start end)
-           (do ((index 0 (+ index host-step)))
-               ((> index host-limit))
-             (let* ((addr (ipnet-index-host map index))
-                    (frag (reverse-domain-fragment addr start end))
-                    (target (concatenate 'string
-                                         (zone-make-name
-                                          (if (not suffix) frag
-                                              (concatenate 'string
-                                                           frag "." suffix))
-                                          zname)
-                                         ".")))
-               (dolist (zr (zone-parse-records (zone-make-name frag zname)
-                                               ttl
-                                               (subst target '*
-                                                      (cdr data))))
-                 (rec :name (zr-name zr)
-                      :type (zr-type zr)
-                      :data (zr-data zr)
-                      :ttl (zr-ttl zr)
-                      :make-ptr-p (zr-make-ptr-p zr)))))))))))
+    (let ((suffix (if (not raw-suffix)
+                     (make-domain-name :labels nil :absolutep nil)
+                     (zone-parse-host raw-suffix))))
+      (dolist (net (listify nets))
+       (dolist (ipn (net-parse-to-ipnets net family))
+         (let* ((addr (ipnet-net ipn))
+                (width (ipaddr-width addr))
+                (comp-width (reverse-domain-component-width addr))
+                (end (round-up (or end
+                                   (ipnet-changeable-bits width
+                                                          (ipnet-mask ipn)))
+                               comp-width))
+                (start (round-down (or start (- end comp-width))
+                                   comp-width))
+                (map (ipnet-host-map ipn)))
+           (multiple-value-bind (host-step host-limit)
+               (ipnet-index-bounds map start end)
+             (do ((index 0 (+ index host-step)))
+                 ((> index host-limit))
+               (let* ((addr (ipnet-index-host map index))
+                      (frag (reverse-domain-fragment addr start end))
+                      (target (reduce #'domain-name-concat
+                                      (list frag suffix zname)
+                                      :from-end t
+                                      :initial-value root-domain)))
+                 (dolist (zr (zone-parse-records (domain-name-concat frag
+                                                                     zname)
+                                                 ttl
+                                                 (subst target '*
+                                                        (cdr data))))
+                   (rec :name (zr-name zr)
+                        :type (zr-type zr)
+                        :data (zr-data zr)
+                        :ttl (zr-ttl zr)
+                        :make-ptr-p (zr-make-ptr-p zr))))))))))))
 
 ;;;--------------------------------------------------------------------------
 ;;; Zone file output.
@@ -1213,16 +1179,19 @@ (defvar *bind-last-record-name* nil
 
 (export 'bind-hostname)
 (defun bind-hostname (hostname)
-  (let* ((h (string-downcase (stringify hostname)))
-        (hl (length h))
-        (r (string-downcase (zone-name *writing-zone*)))
-        (rl (length r)))
-    (cond ((string= r h) "@")
-         ((and (> hl rl)
-               (char= (char h (- hl rl 1)) #\.)
-               (string= h r :start1 (- hl rl)))
-          (subseq h 0 (- hl rl 1)))
-         (t (concatenate 'string h ".")))))
+  (let ((zone (domain-name-labels (zone-name *writing-zone*)))
+       (name (domain-name-labels hostname)))
+    (loop
+      (unless (and zone name (string= (car zone) (car name)))
+       (return))
+      (pop zone) (pop name))
+    (flet ((stitch (labels absolutep)
+            (format nil "~{~A~^.~}~@[.~]"
+                    (reverse (mapcar #'quotify-label labels))
+                    absolutep)))
+      (cond (zone (stitch (domain-name-labels hostname) t))
+           (name (stitch name nil))
+           (t "@")))))
 
 (export 'bind-output-hostname)
 (defun bind-output-hostname (hostname)
@@ -1235,6 +1204,7 @@ (defun bind-output-hostname (hostname)
           name))))
 
 (defmethod zone-write :around ((format (eql :bind)) zone stream)
+  (declare (ignorable zone stream))
   (let ((*bind-last-record-name* nil))
     (call-next-method)))
 
@@ -1352,7 +1322,7 @@ (defmethod zone-write-raw-rrdata ((format (eql :tinydns)) zr type data)
                    (dotimes (i (length data))
                      (let ((byte (aref data i)))
                        (if (or (<= byte 32)
-                               (>= byte 128)
+                               (>= byte 127)
                                (member byte '(#\: #\\) :key #'char-code))
                            (format out "\\~3,'0O" byte)
                            (write-char (code-char byte) out)))))