Download docker/protocol.lisp from Snapkitty/nekomata: direct link, hf CLI and curl.
- Browser
- Download file 9.49 kB
-
https://huggingface.co/Snapkitty/nekomata/resolve/main/docker/protocol.lisp
- Command line
-
hf download hf://Snapkitty/nekomata/docker/protocol.lisp
-
curl -L -o protocol.lisp https://huggingface.co/Snapkitty/nekomata/resolve/main/docker/protocol.lisp
9.49 kB
| (in-package #:nekod.docker) | |
| ;;; Incremental HTTP/1.1 parser for Docker Engine API over Unix socket | |
| (defvar *max-header-size* (* 64 1024)) | |
| (defvar *connection-policy* :close) | |
| (defstruct http-response | |
| (version "" :type string) | |
| (status 0 :type integer) | |
| (reason "" :type string) | |
| (headers nil :type list) | |
| (body #() :type (vector (unsigned-byte 8)))) | |
| (defstruct http-parser-state | |
| (buffer (make-array 4096 :element-type '(unsigned-byte 8) | |
| :adjustable t :fill-pointer 0)) | |
| (header-complete nil :type boolean) | |
| (content-length nil) | |
| (chunked nil :type boolean) | |
| (body-bytes-read 0 :type integer) | |
| (complete nil :type boolean)) | |
| ;;; --- HTTP request building --- | |
| (defun build-http-request (method path &optional body) | |
| "Build an HTTP/1.1 request string for the Docker API." | |
| (let* ((body-octets (if body | |
| (nekod.docker::string-to-octets body) | |
| #())) | |
| (content-length (length body-octets)) | |
| (connection (if (eq *connection-policy* :keep-alive) "keep-alive" "close"))) | |
| (format nil "~a ~a HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ~a\r\nConnection: ~a\r\n\r\n~@[~a~]" | |
| method path content-length connection body))) | |
| ;;; --- Incremental HTTP response parsing --- | |
| (defun parse-status-line (line) | |
| "Parse 'HTTP/1.1 200 OK' into (values version status reason)." | |
| (let* ((sp1 (position #\Space line)) | |
| (sp2 (and sp1 (position #\Space line :start (1+ sp1))))) | |
| (unless (and sp1 sp2) | |
| (error 'nekod:http-protocol-error | |
| :message (format nil "Malformed status line: ~a" line))) | |
| (values (subseq line 0 sp1) | |
| (parse-integer (subseq line (1+ sp1) sp2)) | |
| (subseq line (1+ sp2))))) | |
| (defun parse-http-headers (header-text) | |
| "Parse header block into alist of (name . value)." | |
| (let ((headers nil) | |
| (lines (remove "" (mapcar (lambda (l) (string-trim '(#\Return) l)) | |
| (uiop:split-string header-text :separator '(#\Newline))) | |
| :test #'string=))) | |
| (dolist (line lines) | |
| (let ((colon (position #\: line))) | |
| (when colon | |
| (push (cons (string-downcase (subseq line 0 colon)) | |
| (string-trim '(#\Space) (subseq line (1+ colon)))) | |
| headers)))) | |
| (nreverse headers))) | |
| (defun header-value (headers name) | |
| "Get header value by lowercase name from alist." | |
| (cdr (assoc name headers :test #'string=))) | |
| (defun decode-chunk-size (line) | |
| "Decode hex chunk size from a chunk header line." | |
| (parse-integer (string-trim '(#\Return #\Newline #\Space) line) | |
| :radix 16 :junk-allowed t)) | |
| ;;; --- Body reading strategies --- | |
| (defun read-content-length-body (sock content-length) | |
| "Read exactly content-length bytes from socket." | |
| (let ((body (make-array content-length :element-type '(unsigned-byte 8))) | |
| (total-read 0)) | |
| (loop while (< total-read content-length) | |
| do (let ((n (socket-read-octets sock body | |
| :start total-read | |
| :end content-length))) | |
| (when (<= n total-read) | |
| (error 'nekod:http-body-truncated | |
| :message "Connection closed before body complete" | |
| :expected content-length | |
| :received total-read)) | |
| (setf total-read n))) | |
| body)) | |
| (defun read-chunked-body (sock) | |
| "Read chunked transfer-encoding body incrementally." | |
| (let ((result (make-array 0 :element-type '(unsigned-byte 8) | |
| :adjustable t :fill-pointer 0))) | |
| (loop | |
| (let* ((size-line-octets (socket-read-until sock (format nil "~c~c" #\Return #\Newline))) | |
| (size-line (nekod.docker::octets-to-string size-line-octets :external-format :utf-8)) | |
| (chunk-size (decode-chunk-size size-line))) | |
| (when (or (null chunk-size) (zerop chunk-size)) | |
| (socket-read-until sock (format nil "~c~c" #\Return #\Newline)) | |
| (return result)) | |
| (let ((chunk (make-array chunk-size :element-type '(unsigned-byte 8))) | |
| (read-so-far 0)) | |
| (loop while (< read-so-far chunk-size) | |
| do (let ((n (socket-read-octets sock chunk | |
| :start read-so-far | |
| :end chunk-size))) | |
| (when (<= n read-so-far) | |
| (error 'nekod:http-body-truncated | |
| :message "Chunk truncated" | |
| :expected chunk-size | |
| :received read-so-far)) | |
| (setf read-so-far n))) | |
| (loop for b across chunk do (vector-push-extend b result)) | |
| (socket-read-until sock (format nil "~c~c" #\Return #\Newline))))) | |
| result)) | |
| ;;; --- Full response reader --- | |
| (defun read-http-response (sock) | |
| "Read a complete HTTP response from socket. Handles fragmented reads." | |
| (let* ((header-octets (socket-read-until sock (format nil "~c~c~c~c" | |
| #\Return #\Newline | |
| #\Return #\Newline) | |
| :max-bytes *max-header-size*)) | |
| (header-text (nekod.docker::octets-to-string header-octets :external-format :utf-8)) | |
| (header-end (search (format nil "~c~c~c~c" #\Return #\Newline #\Return #\Newline) | |
| header-text)) | |
| (status-end (position #\Newline header-text)) | |
| (status-line (subseq header-text 0 (and status-end | |
| (if (and status-end | |
| (> status-end 0) | |
| (char= (char header-text (1- status-end)) #\Return)) | |
| (1- status-end) | |
| status-end)))) | |
| (header-block (if header-end | |
| (subseq header-text (1+ status-end) header-end) | |
| ""))) | |
| (multiple-value-bind (version status reason) (parse-status-line status-line) | |
| (let* ((headers (parse-http-headers header-block)) | |
| (cl-val (header-value headers "content-length")) | |
| (te-val (header-value headers "transfer-encoding")) | |
| (content-length (and cl-val (parse-integer cl-val :junk-allowed t))) | |
| (chunked-p (and te-val (search "chunked" te-val))) | |
| (body (cond | |
| ((member status '(204 304 101)) | |
| (make-array 0 :element-type '(unsigned-byte 8))) | |
| (chunked-p | |
| (read-chunked-body sock)) | |
| (content-length | |
| (read-content-length-body sock content-length)) | |
| (t | |
| (make-array 0 :element-type '(unsigned-byte 8)))))) | |
| (make-http-response :version version | |
| :status status | |
| :reason reason | |
| :headers headers | |
| :body body))))) | |
| ;;; --- Convenience API (backward compatible) --- | |
| (defun parse-http-response (raw) | |
| "Legacy: Parse HTTP response string. Extract status code and body." | |
| (let* ((header-end (search (format nil "~c~c~c~c" #\Return #\Newline #\Return #\Newline) raw)) | |
| (status-line (subseq raw 0 (position #\Return raw))) | |
| (sp1 (position #\Space status-line)) | |
| (sp2 (and sp1 (position #\Space status-line :start (1+ sp1)))) | |
| (status-code (and sp1 sp2 (parse-integer (subseq status-line (1+ sp1) sp2) | |
| :junk-allowed t))) | |
| (body (if header-end (subseq raw (+ header-end 4)) ""))) | |
| (values (or status-code 0) body))) | |
| (defun docker-request (sock method path &optional body) | |
| "Send HTTP request and read full response via real socket transport. | |
| Returns http-response struct." | |
| (let ((req (build-http-request method path body))) | |
| (socket-write sock req) | |
| (read-http-response sock))) | |
| (defun docker-get (sock path) | |
| "GET request to Docker API. Returns (values status body-string)." | |
| (let ((resp (docker-request sock "GET" path))) | |
| (values (http-response-status resp) | |
| (nekod.docker::octets-to-string (http-response-body resp) | |
| :external-format :utf-8)))) | |
| (defun docker-post (sock path &optional body) | |
| "POST request to Docker API. Returns (values status body-string)." | |
| (let ((resp (docker-request sock "POST" path body))) | |
| (values (http-response-status resp) | |
| (nekod.docker::octets-to-string (http-response-body resp) | |
| :external-format :utf-8)))) | |
| (defun docker-delete (sock path) | |
| "DELETE request to Docker API. Returns (values status body-string)." | |
| (let ((resp (docker-request sock "DELETE" path))) | |
| (values (http-response-status resp) | |
| (nekod.docker::octets-to-string (http-response-body resp) | |
| :external-format :utf-8)))) | |