X-Git-Url: https://git.distorted.org.uk/~mdw/zone/blobdiff_plain/a2267e14628e71bdac3a67f1aca1686ee03eccc4..db43369d61d700b1d0100998a2d9ecefe28ff8d4:/zone.lisp diff --git a/zone.lisp b/zone.lisp index d25986a..602f1f2 100644 --- a/zone.lisp +++ b/zone.lisp @@ -125,6 +125,10 @@ 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." @@ -259,32 +263,31 @@ 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) + "Return the ZONE's records, in a pleasant sorted order." + (sort (copy-seq (zone-records zone)) + (lambda (zr-a zr-b) + (multiple-value-bind (precp follp) + (domain-name< (zr-name zr-a) (zr-name zr-b)) + (cond (precp t) + (follp nil) + (t (string< (zr-type zr-a) (zr-type zr-b)))))))) ;;;-------------------------------------------------------------------------- ;;; Serial numbering. @@ -377,9 +380,9 @@ 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)))))))) @@ -393,24 +396,25 @@ (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. @@ -427,17 +431,19 @@ 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 zname (timespec-seconds ttl) @@ -484,6 +490,7 @@ 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)))) @@ -495,7 +502,8 @@ (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) @@ -522,7 +530,7 @@ (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)))) @@ -546,10 +554,11 @@ (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)) @@ -573,19 +582,22 @@ (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)) nconc (list k v))) ,@',zf))))))) +(export 'map-host-addresses) (defun map-host-addresses (func addr &key (family *address-family*)) "Call FUNC for each address denoted by ADDR (a `host-parse' address)." (dolist (a (host-addrs (host-parse addr family))) (funcall func a))) +(export 'do-host) (defmacro do-host ((addr spec &key (family *address-family*)) &body body) "Evaluate BODY, binding ADDR to each address denoted by SPEC." `(dolist (,addr (host-addrs (host-parse ,spec ,family))) @@ -603,16 +615,109 @@ (apply rec :type (ipaddr-rrtype addr) :data addr key-args)))) ;;;-------------------------------------------------------------------------- +;;; 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)))))) + +(export 'rec-byte) +(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*))) + +(export 'rec-u8) +(defun rec-u8 (value) + "Append an 8-bit VALUE to the current record." + (rec-byte 1 value)) + +(export 'rec-u16) +(defun rec-u16 (value) + "Append a 16-bit VALUE to the current record." + (rec-byte 2 value)) + +(export 'rec-u32) +(defun rec-u32 (value) + "Append a 32-bit VALUE to the current record." + (rec-byte 4 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. + + 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*))) + +(export 'rec-string) +(defun rec-string (s &key (start 0) end (max 255)) + (let* ((end (or end (length s))) + (len (- end start))) + (unless (<= len max) + (error "String `~A' too long" (subseq s start end))) + (rec-u8 (- end start)) + (rec-raw-string s :start start :end end))) + +(export 'rec-name) +(defun rec-name (name) + "Append a domain NAME. + + No attempt is made to perform compression of the name." + (dolist (label (reverse (domain-name-labels name))) + (rec-string label :max 63)) + (rec-u8 0)) + +(export 'build-record) +(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*))) + +(export 'zone-record-rrdata) +(defgeneric zone-record-rrdata (type zr) + (:documentation "Emit (using the `build-record' protocol) RRDATA for ZR. + + The TYPE is a keyword naming the record type. Return the numeric RRTYPE + code.")) + +;;;-------------------------------------------------------------------------- ;;; Zone record parsers. (defzoneparse :a (name data rec) ":a IPADDR" (zone-set-address #'rec data :make-ptr-p t :family :ipv4)) +(defmethod zone-record-rrdata ((type (eql :a)) zr) + (rec-u32 (ipaddr-addr (zr-data zr))) + 1) + (defzoneparse :aaaa (name data rec) ":aaaa IPADDR" (zone-set-address #'rec data :make-ptr-p t :family :ipv6)) +(defmethod zone-record-rrdata ((type (eql :aaaa)) zr) + (rec-byte 16 (ipaddr-addr (zr-data zr))) + 28) + (defzoneparse :addr (name data rec) ":addr IPADDR" (zone-set-address #'rec data :make-ptr-p t)) @@ -625,13 +730,25 @@ ":ptr HOST" (rec :data (zone-parse-host data zname))) +(defmethod zone-record-rrdata ((type (eql :ptr)) zr) + (rec-name (zr-data zr)) + 12) + (defzoneparse :cname (name data rec :zname zname) ":cname HOST" (rec :data (zone-parse-host data zname))) +(defmethod zone-record-rrdata ((type (eql :cname)) zr) + (rec-name (zr-data zr)) + 5) + (defzoneparse :txt (name data rec) - ":txt TEXT" - (rec :data data)) + ":txt (TEXT*)" + (rec :data (listify data))) + +(defmethod zone-record-rrdata ((type (eql :txt)) zr) + (mapc #'rec-string (zr-data zr)) + 16) (export '*dkim-pathname-defaults*) (defvar *dkim-pathname-defaults* @@ -725,6 +842,16 @@ (lookup type 'sshfp-type) fpr))))))) +(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)))) + 44) + (defzoneparse :mx (name data rec :zname zname) ":mx ((HOST :prio INT :ip IPADDR)*)" (dolist (mx (listify data)) @@ -735,6 +862,13 @@ (when ip (zone-set-address #'rec ip :name host)) (rec :data (cons host prio)))))) +(defmethod zone-record-rrdata ((type (eql :mx)) zr) + (let ((name (car (zr-data zr))) + (prio (cdr (zr-data zr)))) + (rec-u16 prio) + (rec-name name)) + 15) + (defzoneparse :ns (name data rec :zname zname) ":ns ((HOST :ip IPADDR)*)" (dolist (ns (listify data)) @@ -745,6 +879,10 @@ (when ip (zone-set-address #'rec ip :name host)) (rec :data host))))) +(defmethod zone-record-rrdata ((type (eql :ns)) zr) + (rec-name (zr-data zr)) + 2) + (defzoneparse :alias (name data rec :zname zname) ":alias (LABEL*)" (dolist (a (listify data)) @@ -762,7 +900,12 @@ (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 @@ -777,6 +920,14 @@ (rec :name rname :data (list prio weight port host)))))))))) +(defmethod zone-record-rrdata ((type (eql :srv)) zr) + (destructuring-bind (prio weight port host) (zr-data zr) + (rec-u16 prio) + (rec-u16 weight) + (rec-u16 port) + (rec-name host)) + 33) + (defzoneparse :net (name data rec) ":net (NETWORK*)" (dolist (net (listify data)) @@ -813,13 +964,14 @@ (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) +(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 @@ -843,42 +995,45 @@ 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. @@ -893,11 +1048,59 @@ (defvar *zone-output-stream* nil "Stream to write zone data on.") -(defmethod zone-write :around (format zone stream) - (declare (ignore format)) +(export 'zone-write-raw-rrdata) +(defgeneric zone-write-raw-rrdata (format zr type data) + (:documentation "Write an otherwise unsupported record in a given FORMAT. + + ZR gives the record object, which carries the name and TTL; the TYPE is + the numeric RRTYPE code; and DATA is an octet vector giving the RRDATA. + This is used by the default `zone-write-record' method to handle record + types which aren't directly supported by the format driver.")) + +(export 'zone-write-header) +(defgeneric zone-write-header (format zone) + (:documentation "Emit the header for a ZONE, in a given FORMAT. + + The header includes any kind of initial comment, the SOA record, and any + other necessary preamble. There is no default implementation. + + This is part of the protocol used by the default method on `zone-write'; + if you override that method.")) + +(export 'zone-write-trailer) +(defgeneric zone-write-trailer (format zone) + (:documentation "Emit the header for a ZONE, in a given FORMAT. + + The footer may be empty, and is so by default. + + This is part of the protocol used by the default method on `zone-write'; + if you override that method.") + (:method (format zone) + (declare (ignore format zone)) + nil)) + +(export 'zone-write-record) +(defgeneric zone-write-record (format type zr) + (:documentation "Emit a record of the given TYPE (a keyword). + + The default implementation builds the raw RRDATA and passes it to + `zone-write-raw-rrdata'.") + (:method (format type zr) + (let* (code + (data (build-record (setf code (zone-record-rrdata type zr))))) + (zone-write-raw-rrdata format zr code data)))) + +(defmethod zone-write (format zone stream) + "This default method calls `zone-write-header', then `zone-write-record' + for each record in the zone, and finally `zone-write-trailer'. While it's + running, `*writing-zone*' is bound to the zone object, and + `*zone-output-stream*' to the output stream." (let ((*writing-zone* zone) (*zone-output-stream* stream)) - (call-next-method))) + (zone-write-header format zone) + (dolist (zr (zone-records-sorted zone)) + (zone-write-record format (zr-type zr) zr)) + (zone-write-trailer format zone))) (export 'zone-save) (defun zone-save (zones &key (format :bind)) @@ -917,26 +1120,43 @@ ;;;-------------------------------------------------------------------------- ;;; 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 ".")))))) - -(export 'bind-record) -(defgeneric bind-record (type zr)) - -(defmethod zone-write ((format (eql :bind)) zone stream) - (format stream "~ + (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) + (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)))) + +(defmethod zone-write :around ((format (eql :bind)) zone stream) + (let ((*bind-last-record-name* nil)) + (call-next-method))) + +(defmethod zone-write-header ((format (eql :bind)) zone) + (format *zone-output-stream* "~ ;;; Zone file `~(~A~)' ;;; (generated ~A) @@ -952,62 +1172,151 @@ $TTL ~2@*~D~2%" (when at (setf (char copy at) #\.)) copy))) - (format stream "~ -~A~30TIN SOA~40T~A ~A ( + (format *zone-output-stream* "~ +~A~30TIN SOA~40T~A ( +~55@A~60T ;administrator ~45T~10D~60T ;serial ~45T~10D~60T ;refresh ~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)))) (export 'bind-format-record) -(defun bind-format-record (name ttl type format args) +(defun bind-format-record (zr format &rest args) (format *zone-output-stream* "~A~20T~@[~8D~]~30TIN ~A~40T~?~%" - (bind-hostname name) - (and (/= ttl (zone-default-ttl *writing-zone*)) - ttl) - (string-upcase (symbol-name type)) + (bind-output-hostname (zr-name zr)) + (let ((ttl (zr-ttl zr))) + (and (/= ttl (zone-default-ttl *writing-zone*)) + ttl)) + (string-upcase (symbol-name (zr-type zr))) format args)) -(export 'bind-record-type) -(defgeneric bind-record-type (type) - (:method (type) type)) - -(export 'bind-record-format-args) -(defgeneric bind-record-format-args (type data) - (:method ((type (eql :a)) data) (list "~A" (ipaddr-string data))) - (:method ((type (eql :aaaa)) data) (list "~A" (ipaddr-string data))) - (:method ((type (eql :ptr)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :cname)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :ns)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :mx)) data) - (list "~2D ~A" (cdr data) (bind-hostname (car data)))) - (:method ((type (eql :srv)) data) - (destructuring-bind (prio weight port host) data - (list "~2D ~5D ~5D ~A" prio weight port (bind-hostname host)))) - (:method ((type (eql :sshfp)) data) - (cons "~2D ~2D ~A" data)) - (:method ((type (eql :txt)) data) - (cons "~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]" - (mapcar #'stringify (listify data))))) - -(defmethod bind-record (type zr) - (destructuring-bind (format &rest args) - (bind-record-format-args type (zr-data zr)) - (bind-format-record (zr-name zr) - (zr-ttl zr) - (bind-record-type type) - format args))) +(defmethod zone-write-raw-rrdata ((format (eql :bind)) zr type data) + (format *zone-output-stream* + "~A~20T~@[~8D~]~30TIN TYPE~A~40T\\# ~A" + (bind-output-hostname (zr-name zr)) + (let ((ttl (zr-ttl zr))) + (and (/= ttl (zone-default-ttl *writing-zone*)) + ttl)) + type + (length data)) + (let* ((hex (with-output-to-string (out) + (dotimes (i (length data)) + (format out "~(~2,'0X~)" (aref data i))))) + (len (length hex))) + (cond ((< len 24) + (format *zone-output-stream* " ~A~%" hex)) + (t + (format *zone-output-stream* " (") + (let ((i 0)) + (loop + (when (>= i len) (return)) + (let ((j (min (+ i 64) len))) + (format *zone-output-stream* "~%~8T~A" (subseq hex i j)) + (setf i j)))) + (format *zone-output-stream* " )~%"))))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :a)) zr) + (bind-format-record zr "~A" (ipaddr-string (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :aaaa)) zr) + (bind-format-record zr "~A" (ipaddr-string (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :ptr)) zr) + (bind-format-record zr "~A" (bind-hostname (zr-data 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 :ns)) zr) + (bind-format-record zr "~A" (bind-hostname (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :mx)) zr) + (bind-format-record zr "~2D ~A" + (cdr (zr-data zr)) + (bind-hostname (car (zr-data zr))))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :srv)) zr) + (destructuring-bind (prio weight port host) (zr-data zr) + (bind-format-record zr "~2D ~5D ~5D ~A" + 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))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :txt)) zr) + (bind-format-record zr "~{~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]~}" (zr-data zr))) + +;;;-------------------------------------------------------------------------- +;;; tinydns-data output format. + +(export 'tinydns-output) +(defun tinydns-output (code &rest fields) + (format *zone-output-stream* "~C~{~@[~A~]~^:~}~%" code fields)) + +(defmethod zone-write-raw-rrdata ((format (eql :tinydns)) zr type 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))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :a)) zr) + (tinydns-output #\+ (zr-name zr) + (ipaddr-string (zr-data zr)) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :aaaa)) zr) + (tinydns-output #\3 (zr-name zr) + (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr))) + (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ptr)) zr) + (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :cname)) zr) + (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ns)) zr) + (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (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)))) + +(defmethod zone-write-header ((format (eql :tinydns)) zone) + (format *zone-output-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)))) ;;;----- That's all, folks --------------------------------------------------