X-Git-Url: https://git.distorted.org.uk/~mdw/zone/blobdiff_plain/804882ca1c12315b7943c6f71f7bb43866a7a301..80b5c2ffcd36eb88b6b8f87dc711cf93ebfc27d9:/zone.lisp diff --git a/zone.lisp b/zone.lisp index b9da45e..b9c8fa1 100644 --- a/zone.lisp +++ b/zone.lisp @@ -286,6 +286,37 @@ (join-strings #\. (list prefix zone-name)) prefix)))) +(export 'zone-records-sorted) +(defun zone-records-sorted (zone) + "Return the ZONE's records, in a pleasant sorted order." + (sort (copy-seq (zone-records zone)) + (lambda (zr-a zr-b) + (let* ((name-a (zr-name zr-a)) (pos-a (length name-a)) + (name-b (zr-name zr-b)) (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))) + (cond ((string< name-a name-b + :start1 (1+ dot-a) :end1 pos-a + :start2 (1+ dot-b) :end2 pos-b) + (return t)) + ((string> name-a name-b + :start1 (1+ dot-a) :end1 pos-a + :start2 (1+ dot-b) :end2 pos-b) + (return nil)) + ((= dot-a dot-b -1) + (return (string< (zr-type zr-a) (zr-type zr-b)))) + ((= dot-a -1) + (return t)) + ((= dot-b -1) + (return nil)) + (t + (setf pos-a dot-a) + (setf pos-b dot-b))))))))) + ;;;-------------------------------------------------------------------------- ;;; Serial numbering. @@ -439,7 +470,7 @@ (ttl min-ttl) (serial (make-zone-serial zname))) (listify head) - (values zname + (values (string-downcase zname) (timespec-seconds ttl) (make-soa :admin admin :source (zone-parse-host source zname) @@ -632,8 +663,8 @@ (rec :data (zone-parse-host data zname))) (defzoneparse :txt (name data rec) - ":txt TEXT" - (rec :data data)) + ":txt (TEXT*)" + (rec :data (listify data))) (export '*dkim-pathname-defaults*) (defvar *dkim-pathname-defaults* @@ -821,7 +852,7 @@ :ttl (zr-ttl zr) :data (zr-name zr)) (setf (gethash name seen) t)))))))))) -(defzoneparse (:multi) (name data rec :zname zname :ttl ttl) +(defzoneparse :multi (name data rec :zname zname :ttl ttl) ":multi (((NET*) &key :start :end :family :suffix) . REC) Output multiple records covering a portion of the reverse-resolution @@ -883,6 +914,73 @@ :make-ptr-p (zr-make-ptr-p zr))))))))))) ;;;-------------------------------------------------------------------------- +;;; Building raw record vectors. + +(defvar *record-vector* nil + "The record vector under construction.") + +(defun rec-ensure (n) + "Ensure that at least N octets are spare in the current record." + (let ((want (+ n (fill-pointer *record-vector*))) + (have (array-dimension *record-vector* 0))) + (unless (<= want have) + (adjust-array *record-vector* + (do ((new (* 2 have) (* 2 new))) + ((<= want new) new)))))) + +(defun rec-byte (octets value) + "Append an unsigned byte, OCTETS octets wide, with VALUE, to the record." + (rec-ensure octets) + (do ((i (1- octets) (1- i))) + ((minusp i)) + (vector-push (ldb (byte 8 (* 8 i)) value) *record-vector*))) + +(defun rec-u8 (value) + "Append an 8-bit VALUE to the current record." + (rec-byte 1 value)) +(defun rec-u16 (value) + "Append a 16-bit VALUE to the current record." + (rec-byte 2 value)) +(defun rec-u32 (value) + "Append a 32-bit VALUE to the current record." + (rec-byte 4 value)) + +(defun rec-raw-string (s &key (start 0) end) + "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." + (setf-default end (length s)) + (rec-ensure (- end start)) + (do ((i start (1+ i))) + ((>= i end)) + (vector-push (char-code (char s i)) *record-vector*))) + +(defun rec-name (s) + "Append a domain name S. + + 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-u8 (- lim i)) + (rec-raw-string s :start i :end lim) + (if dot + (setf i (1+ dot)) + (return)))) + (when (< i n) + (rec-u8 0)))) + +(defmacro build-record (&body body) + "Build a raw record, and return it as a vector of octets." + `(let ((*record-vector* (make-array 256 + :element-type '(unsigned-byte 8) + :fill-pointer 0 + :adjustable t))) + ,@body + (copy-seq *record-vector*))) + +;;;-------------------------------------------------------------------------- ;;; Zone file output. (export 'zone-write) @@ -919,20 +1017,33 @@ ;;;-------------------------------------------------------------------------- ;;; Bind format output. +(defvar *bind-last-record-name* nil + "The previously emitted record name. + + Used for eliding record names on output.") + (export 'bind-hostname) (defun bind-hostname (hostname) - (if (not 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* ((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 "."))))) + +(export 'bind-output-hostname) +(defun bind-output-hostname (hostname) + (let ((name (bind-hostname hostname))) + (cond ((and *bind-last-record-name* + (string= name *bind-last-record-name*)) + "") + (t + (setf *bind-last-record-name* name) + name)))) (export 'bind-record) (defgeneric bind-record (type zr)) @@ -947,7 +1058,8 @@ $TTL ~2@*~D~2%" (zone-name zone) (iso-date :now :datep t :timep t) (zone-default-ttl zone)) - (let* ((soa (zone-soa zone)) + (let* ((*bind-last-record-name* nil) + (soa (zone-soa zone)) (admin (let* ((name (soa-admin soa)) (at (position #\@ name)) (copy (format nil "~(~A~)." name))) @@ -962,22 +1074,22 @@ $TTL ~2@*~D~2%" ~45T~10D~60T ;retry ~45T~10D~60T ;expire ~45T~10D )~60T ;min-ttl~2%" - (bind-hostname (zone-name zone)) + (bind-output-hostname (zone-name zone)) (bind-hostname (soa-source soa)) admin (soa-serial soa) (soa-refresh soa) (soa-retry soa) (soa-expire soa) - (soa-min-ttl soa))) - (dolist (zr (zone-records zone)) - (bind-record (zr-type zr) zr))) + (soa-min-ttl soa)) + (dolist (zr (zone-records-sorted zone)) + (bind-record (zr-type zr) zr)))) (export 'bind-format-record) (defun bind-format-record (name ttl type format args) (format *zone-output-stream* "~A~20T~@[~8D~]~30TIN ~A~40T~?~%" - (bind-hostname name) + (bind-output-hostname name) (and (/= ttl (zone-default-ttl *writing-zone*)) ttl) (string-upcase (symbol-name type)) @@ -1003,7 +1115,7 @@ $TTL ~2@*~D~2%" (cons "~2D ~2D ~A" data)) (:method ((type (eql :txt)) data) (cons "~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]" - (mapcar #'stringify (listify data))))) + (mapcar #'stringify data)))) (defmethod bind-record (type zr) (destructuring-bind (format &rest args) @@ -1013,4 +1125,90 @@ $TTL ~2@*~D~2%" (bind-record-type type) format args))) +;;;-------------------------------------------------------------------------- +;;; tinydns-data output format. + +(defun tinydns-output (code &rest fields) + (format *zone-output-stream* "~C~{~@[~A~]~^:~}~%" code fields)) + +(defun tinydns-raw-record (type zr data) + (tinydns-output #\: (zr-name zr) type + (with-output-to-string (out) + (dotimes (i (length data)) + (let ((byte (aref data i))) + (if (or (<= byte 32) + (>= byte 128) + (member byte '(#\: #\\) :key #'char-code)) + (format out "\\~3,'0O" byte) + (write-char (code-char byte) out))))) + (zr-ttl zr))) + +(defgeneric tinydns-record (type zr) + (:method ((type (eql :a)) zr) + (tinydns-output #\+ (zr-name zr) + (ipaddr-string (zr-data zr)) (zr-ttl zr))) + (:method ((type (eql :aaaa)) zr) + (tinydns-output #\3 (zr-name zr) + (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr))) + (zr-ttl zr))) + (:method ((type (eql :ptr)) zr) + (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr))) + (:method ((type (eql :cname)) zr) + (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr))) + (:method ((type (eql :ns)) zr) + (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr))) + (:method ((type (eql :mx)) zr) + (let ((name (car (zr-data zr))) + (prio (cdr (zr-data zr)))) + (tinydns-output #\@ (zr-name zr) nil name prio (zr-ttl zr)))) + (:method ((type (eql :txt)) zr) + (tinydns-raw-record 16 zr + (build-record + (dolist (s (zr-data zr)) + (rec-u8 (length s)) + (rec-raw-string s))))) + (:method ((type (eql :srv)) zr) + (destructuring-bind (prio weight port host) (zr-data zr) + (tinydns-raw-record 33 zr + (build-record + (rec-u16 prio) + (rec-u16 weight) + (rec-u16 port) + (rec-name host))))) + (:method ((type (eql :sshfp)) zr) + (destructuring-bind (alg type fpr) (zr-data zr) + (tinydns-raw-record 44 zr + (build-record + (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)))))))) + +(defmethod zone-write ((format (eql :tinydns)) zone stream) + (format stream "~ +### Zone file `~(~A~)' +### (generated ~A) +~%" + (zone-name zone) + (iso-date :now :datep t :timep t)) + (let ((soa (zone-soa zone))) + (tinydns-output #\Z + (zone-name zone) + (soa-source soa) + (let* ((name (copy-seq (soa-admin soa))) + (at (position #\@ name))) + (when at (setf (char name at) #\.)) + name) + (soa-serial soa) + (soa-refresh soa) + (soa-expire soa) + (soa-min-ttl soa) + (zone-default-ttl zone))) + (dolist (zr (zone-records-sorted zone)) + (tinydns-record (zr-type zr) zr))) + ;;;----- That's all, folks --------------------------------------------------