1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2015 David Thompson <davet@gnu.org>
3 ;;; Copyright © 2020 by Amar M. Singh <nly@disroot.org>
4 ;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
5 ;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
6 ;;; Copyright © 2021 Simon Tournier <zimon.toutoune@gmail.com>
8 ;;; This file is part of GNU Guix.
10 ;;; GNU Guix is free software; you can redistribute it and/or modify it
11 ;;; under the terms of the GNU General Public License as published by
12 ;;; the Free Software Foundation; either version 3 of the License, or (at
13 ;;; your option) any later version.
15 ;;; GNU Guix is distributed in the hope that it will be useful, but
16 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;;; GNU General Public License for more details.
20 ;;; You should have received a copy of the GNU General Public License
21 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
23 (define-module (guix scripts publish)
24 #:use-module ((system repl server) #:prefix repl:)
25 #:use-module (ice-9 binary-ports)
26 #:use-module (ice-9 format)
27 #:use-module (ice-9 match)
28 #:use-module (ice-9 poll)
29 #:use-module (ice-9 regex)
30 #:use-module (ice-9 rdelim)
31 #:use-module (ice-9 threads)
32 #:use-module (rnrs bytevectors)
33 #:use-module (srfi srfi-1)
34 #:use-module (srfi srfi-2)
35 #:use-module (srfi srfi-9)
36 #:use-module (srfi srfi-9 gnu)
37 #:use-module (srfi srfi-11)
38 #:use-module (srfi srfi-19)
39 #:use-module (srfi srfi-26)
40 #:use-module (srfi srfi-34)
41 #:use-module (srfi srfi-37)
42 #:use-module (web http)
43 #:use-module (web request)
44 #:use-module (web response)
45 #:use-module (web server)
46 #:use-module (web uri)
47 #:autoload (sxml simple) (sxml->xml)
48 #:autoload (guix avahi) (avahi-publish-service-thread)
49 #:use-module (guix base32)
50 #:use-module (guix base64)
51 #:use-module (guix config)
52 #:use-module (guix derivations)
53 #:use-module (gcrypt hash)
54 #:use-module (guix pki)
55 #:use-module (gcrypt pk-crypto)
56 #:use-module (guix workers)
57 #:use-module (guix store)
58 #:use-module ((guix serialization) #:select (write-file))
60 #:autoload (lzlib) (call-with-lzip-output-port
61 make-lzip-output-port)
62 #:autoload (zstd) (call-with-zstd-output-port
63 make-zstd-output-port)
64 #:use-module (guix cache)
65 #:use-module (guix ui)
66 #:use-module (guix scripts)
67 #:use-module ((guix utils)
68 #:select (with-atomic-file-output compressed-file?))
69 #:use-module ((guix build utils)
70 #:select (dump-port mkdir-p find-files))
71 #:use-module ((guix build syscalls) #:select (set-thread-name))
72 #:export (%default-gzip-compression
84 (format #t (G_ "Usage: guix publish [OPTION]...
85 Publish ~a over HTTP.\n") %store-directory)
87 -p, --port=PORT listen on PORT"))
89 --listen=HOST listen on the network interface for HOST"))
91 -u, --user=USER change privileges to USER as soon as possible"))
93 -a, --advertise advertise on the local network"))
95 -C, --compression[=METHOD:LEVEL]
96 compress archives with METHOD at LEVEL"))
98 -c, --cache=DIRECTORY cache published items to DIRECTORY"))
100 --cache-bypass-threshold=SIZE
101 serve store items below SIZE even when not cached"))
103 --workers=N use N workers to bake items"))
105 --ttl=TTL announce narinfos can be cached for TTL seconds"))
107 --negative-ttl=TTL announce missing narinfos can be cached for TTL seconds"))
109 --nar-path=PATH use PATH as the prefix for nar URLs"))
111 --public-key=FILE use FILE as the public key for signatures"))
113 --private-key=FILE use FILE as the private key for signatures"))
115 -r, --repl[=PORT] spawn REPL server on PORT"))
118 -h, --help display this help and exit"))
120 -V, --version display version information and exit"))
122 (show-bug-report-information))
124 (define (getaddrinfo* host)
125 "Like 'getaddrinfo', but properly report errors."
126 (catch 'getaddrinfo-error
130 (leave (G_ "lookup of host '~a' failed: ~a~%")
131 host (gai-strerror error)))))
133 ;; Nar compression parameters.
134 (define-record-type <compression>
135 (compression type level)
137 (type compression-type)
138 (level compression-level))
140 (define %no-compression
141 (compression 'none 0))
143 (define %default-gzip-compression
144 ;; Since we compress on the fly, default to fast compression.
145 (compression 'gzip 3))
147 (define (default-compression type)
148 (compression type 3))
150 (define (actual-compressions item requested)
151 "Return the actual compressions used for ITEM, which may be %NO-COMPRESSION
152 if ITEM is already compressed."
153 (if (compressed-file? item)
154 (list %no-compression)
157 (define (low-compression c)
158 "Return <compression> of the same type as C, but optimized for low CPU
160 (compression (compression-type c)
161 (min (compression-level c) 2)))
164 (list (option '(#\h "help") #f #f
168 (option '(#\V "version") #f #f
170 (show-version-and-exit "guix publish")))
171 (option '(#\a "advertise") #f #f
172 (lambda (opt name arg result)
173 (alist-cons 'advertise? #t result)))
174 (option '(#\u "user") #t #f
175 (lambda (opt name arg result)
176 (alist-cons 'user arg result)))
177 (option '(#\p "port") #t #f
178 (lambda (opt name arg result)
179 (alist-cons 'port (string->number* arg) result)))
180 (option '("listen") #t #f
181 (lambda (opt name arg result)
182 (match (getaddrinfo* arg)
184 (alist-cons 'address (addrinfo:addr info)
187 (leave (G_ "lookup of host '~a' returned nothing")
189 (option '(#\C "compression") #f #t
190 (lambda (opt name arg result)
191 (let* ((colon (string-index arg #\:))
193 (colon (string-take arg colon))
194 ((string->number arg) "gzip")
198 (string-drop arg (+ 1 colon)))
199 (or (string->number arg) 3))))
202 (alist-cons 'compression %no-compression result))
204 (match (string->compression-type type)
206 (alist-cons 'compression
207 (compression type level)
210 (warning (G_ "~a: unsupported compression type~%")
213 (option '(#\c "cache") #t #f
214 (lambda (opt name arg result)
215 (alist-cons 'cache arg result)))
216 (option '("cache-bypass-threshold") #t #f
217 (lambda (opt name arg result)
218 (alist-cons 'cache-bypass-threshold (size->number arg)
220 (option '("workers") #t #f
221 (lambda (opt name arg result)
222 (alist-cons 'workers (string->number* arg)
224 (option '("ttl") #t #f
225 (lambda (opt name arg result)
226 (let ((duration (string->duration arg)))
228 (leave (G_ "~a: invalid duration~%") arg))
229 (alist-cons 'narinfo-ttl (time-second duration)
231 (option '("negative-ttl") #t #f
232 (lambda (opt name arg result)
233 (let ((duration (string->duration arg)))
235 (leave (G_ "~a: invalid duration~%") arg))
236 (alist-cons 'narinfo-negative-ttl (time-second duration)
238 (option '("nar-path") #t #f
239 (lambda (opt name arg result)
240 (alist-cons 'nar-path arg result)))
241 (option '("public-key") #t #f
242 (lambda (opt name arg result)
243 (alist-cons 'public-key-file arg result)))
244 (option '("private-key" "secret-key") #t #f
245 (lambda (opt name arg result)
246 (alist-cons 'private-key-file arg result)))
247 (option '(#\r "repl") #f #t
248 (lambda (opt name arg result)
249 ;; If port unspecified, use default Guile REPL port.
250 (let ((port (and arg (string->number* arg))))
251 (alist-cons 'repl (or port 37146) result))))))
253 (define %default-options
256 ;; By default, serve nars under "/nar".
259 (public-key-file . ,%public-key-file)
260 (private-key-file . ,%private-key-file)
262 ;; Default number of workers when caching is enabled.
263 (workers . ,(current-processor-count))
265 (address . ,(make-socket-address AF_INET INADDR_ANY 0))
268 ;; The key pair used to sign narinfos.
274 (define %nix-cache-info
275 `(("StoreDir" . ,%store-directory)
276 ("WantMassQuery" . 0)
279 ;;; A common buffer size value used for the TCP socket SO_SNDBUF option and
280 ;;; the gzip compressor buffer size.
281 (define %default-buffer-size
284 (define %default-socket-options
285 ;; List of options passed to 'setsockopt' when transmitting files.
286 (list (list SO_SNDBUF %default-buffer-size)))
288 (define* (configure-socket socket #:key (level SOL_SOCKET)
289 (options %default-socket-options))
290 "Apply multiple option tuples in OPTIONS to SOCKET, using LEVEL."
291 (for-each (cut apply setsockopt socket level <>)
294 (define (signed-string s)
295 "Sign the hash of the string S with the daemon's key. Return a canonical
296 sexp for the signature."
297 (let* ((public-key (%public-key))
298 (hash (bytevector->hash-data (sha256 (string->utf8 s))
299 #:key-type (key-type public-key))))
300 (signature-sexp hash (%private-key) public-key)))
302 (define base64-encode-string
303 (compose base64-encode string->utf8))
305 (define* (store-item->recutils store-item
308 (compression %no-compression)
310 "Return the 'Compression' and 'URL' fields of the narinfo for STORE-ITEM,
311 with COMPRESSION, starting at NAR-PATH."
312 (let ((url (encode-and-join-uri-path
313 `(,@(split-and-decode-uri-path nar-path)
315 (($ <compression> 'none)
317 (($ <compression> type)
318 (list (symbol->string type))))
319 ,(basename store-item)))))
320 (format #f "URL: ~a~%Compression: ~a~%~@[FileSize: ~a~%~]"
321 url (compression-type compression) file-size)))
323 (define* (narinfo-string store store-path key
324 #:key (compressions (list %no-compression))
325 (nar-path "nar") (file-sizes '()))
326 "Generate a narinfo key/value string for STORE-PATH; an exception is raised
327 if STORE-PATH is invalid. Produce a URL that corresponds to COMPRESSION. The
328 narinfo is signed with KEY. NAR-PATH specifies the prefix for nar URLs.
330 Optionally, FILE-SIZES is a list of compression/integer pairs, where the
331 integer is size in bytes of the compressed NAR; it informs the client of how
332 much needs to be downloaded."
333 (let* ((path-info (query-path-info store store-path))
334 (compressions (actual-compressions store-path compressions))
335 (hash (bytevector->nix-base32-string
336 (path-info-hash path-info)))
337 (size (path-info-nar-size path-info))
338 (file-sizes `((,%no-compression . ,size) ,@file-sizes))
339 (references (string-join
340 (map basename (path-info-references path-info))
342 (deriver (path-info-deriver path-info))
343 (base-info (format #f
351 (map (lambda (compression)
352 (let ((size (assoc-ref file-sizes
354 (store-item->recutils store-path
360 hash size references))
361 ;; Do not render a "Deriver" or "System" line if we are rendering
362 ;; info for a derivation.
363 (info (if (not deriver)
367 (let ((drv (read-derivation-from-file deriver)))
368 (format #f "~aSystem: ~a~%Deriver: ~a~%"
369 base-info (derivation-system drv)
370 (basename deriver))))
372 ;; DERIVER might be missing, but that's fine:
373 ;; it's only used for <substitutable> where it's
374 ;; optional. 'System' is currently unused.
375 (if (= ENOENT (system-error-errno args))
377 (apply throw args))))))
378 (signature (base64-encode-string
379 (canonical-sexp->string (signed-string info)))))
380 (format #f "~aSignature: 1;~a;~a~%" info (gethostname) signature)))
382 (define* (not-found request
383 #:key (phrase "Resource not found")
385 "Render 404 response for REQUEST."
386 (values (build-response #:code 404
388 `((cache-control (max-age . ,ttl)))
390 (string-append phrase ": "
391 (uri-path (request-uri request)))))
393 (define (render-nix-cache-info)
394 "Render server information."
395 (values '((content-type . (text/plain)))
397 (for-each (match-lambda
399 (format port "~a: ~a~%" key value)))
402 (define* (render-narinfo store request hash
403 #:key ttl (compressions (list %no-compression))
404 (nar-path "nar") negative-ttl)
405 "Render metadata for the store path corresponding to HASH. If TTL is true,
406 advertise it as the maximum validity period (in seconds) via the
407 'Cache-Control' header. This allows 'guix substitute' to cache it for an
408 appropriate duration. NAR-PATH specifies the prefix for nar URLs."
409 (let ((store-path (hash-part->path store hash)))
410 (if (string-null? store-path)
411 (not-found request #:phrase "" #:ttl negative-ttl)
412 (values `((content-type . (application/x-nix-narinfo))
414 `((cache-control (max-age . ,ttl)))
417 (narinfo-string store store-path (%private-key)
419 #:compressions compressions)
422 (define* (nar-cache-file directory item
423 #:key (compression %no-compression))
424 (string-append directory "/"
425 (symbol->string (compression-type compression))
426 "/" (basename item) ".nar"))
428 (define* (narinfo-cache-file directory item
429 #:key (compression %no-compression))
430 (string-append directory "/"
431 (symbol->string (compression-type compression))
435 (define (hash-part-mapping-cache-file directory hash)
436 (string-append directory "/hashes/" hash))
438 (define run-single-baker
439 (let ((baking (make-weak-value-hash-table))
440 (mutex (make-mutex)))
442 "Run THUNK, which is supposed to bake ITEM, but make sure only one
443 thread is baking ITEM at a given time."
446 (and (not (hash-ref baking item))
448 (hash-set! baking item (current-thread))
457 (hash-remove! baking item))))))))
459 (define-syntax-rule (single-baker item exp ...)
460 "Bake ITEM by evaluating EXP, but make sure there's only one baker for ITEM
462 (run-single-baker item (lambda () exp ...)))
465 (define (narinfo-files cache)
466 "Return the list of .narinfo files under CACHE."
467 (if (file-is-directory? cache)
470 (string-suffix? ".narinfo" file)))
473 (define (nar-expiration-time ttl)
474 "Return the narinfo expiration time (in seconds since the Epoch). The
475 expiration time is +inf.0 when passed an item that is still in the store; in
476 other cases, it is the last-access time of the item plus TTL.
478 This policy allows us to keep cached nars that correspond to valid store
479 items. Failing that, we could eventually have to recompute them and return
480 404 in the meantime."
481 (let ((expiration-time (file-expiration-time ttl)))
483 (let ((item (string-append (%store-prefix) "/"
484 (basename file ".narinfo"))))
485 ;; Note: We don't need to use 'valid-path?' here because FILE would
486 ;; not exist if ITEM were not valid in the first place.
487 (if (file-exists? item)
489 (expiration-time file))))))
491 (define (hash-part->path* store hash cache)
492 "Like 'hash-part->path' but cache results under CACHE. This ensures we can
493 still map HASH to the corresponding store file name, even if said store item
494 vanished from the store in the meantime."
495 (let ((cached (hash-part-mapping-cache-file cache hash)))
498 (call-with-input-file cached read-string))
500 (if (= ENOENT (system-error-errno args))
501 (match (hash-part->path store hash)
504 (mkdir-p (dirname cached))
505 (call-with-output-file (string-append cached ".tmp")
507 (display result port)))
508 (rename-file (string-append cached ".tmp") cached)
510 (apply throw args))))))
512 (define cache-bypass-threshold
513 ;; Maximum size of a store item that may be served by the '/cached' handlers
514 ;; below even when not in cache.
515 (make-parameter (* 10 (expt 2 20))))
517 (define (bypass-cache? store item)
518 "Return true if we allow ITEM to be downloaded before it is cached. ITEM is
519 interpreted as the basename of a store item."
520 (guard (c ((store-error? c) #f))
521 (< (path-info-nar-size (query-path-info store item))
522 (cache-bypass-threshold))))
524 (define* (render-narinfo/cached store request hash
525 #:key ttl (compressions (list %no-compression))
526 (nar-path "nar") negative-ttl
528 "Respond to the narinfo request for REQUEST. If the narinfo is available in
529 CACHE, then send it; otherwise, return 404 and \"bake\" that nar and narinfo
530 requested using POOL."
531 (define (delete-entry narinfo)
532 ;; Delete NARINFO and the corresponding nar from CACHE.
533 (let* ((nar (string-append (string-drop-right narinfo
534 (string-length ".narinfo"))
536 (base (basename narinfo ".narinfo"))
537 (hash (string-take base (string-index base #\-)))
538 (mapping (hash-part-mapping-cache-file cache hash)))
539 (delete-file* narinfo)
541 (delete-file* mapping)))
543 (let* ((item (hash-part->path* store hash cache))
544 (compressions (actual-compressions item compressions))
545 (cached (and (not (string-null? item))
546 (narinfo-cache-file cache item
548 (first compressions)))))
549 (cond ((string-null? item)
550 (not-found request #:ttl negative-ttl))
551 ((file-exists? cached)
552 ;; Narinfo is in cache, send it.
553 (values `((content-type . (application/x-nix-narinfo))
555 `((cache-control (max-age . ,ttl)))
558 (display (call-with-input-file cached
561 ((and (file-exists? item) ;cheaper than the 'valid-path?' RPC
562 (valid-path? store item))
563 ;; Nothing in cache: bake the narinfo and nar in the background and
567 ;; Check whether CACHED has been produced in the meantime.
568 (unless (file-exists? cached)
569 ;; (format #t "baking ~s~%" item)
570 (bake-narinfo+nar cache item
572 #:compressions compressions
573 #:nar-path nar-path)))
576 (single-baker 'cache-cleanup
577 (maybe-remove-expired-cache-entries cache
580 (nar-expiration-time ttl)
581 #:delete-entry delete-entry
582 #:cleanup-period ttl))))
584 ;; If ITEM passes 'bypass-cache?', render a temporary narinfo right
585 ;; away, with a short TTL. The narinfo is temporary because it
586 ;; lacks 'FileSize', for instance, which the cached narinfo will
587 ;; have. Chances are that the nar will be baked by the time the
588 ;; client asks for it.
589 (if (bypass-cache? store item)
590 (render-narinfo store request hash
593 #:compressions compressions)
595 #:phrase "We're baking it"
596 #:ttl 300))) ;should be available within 5m
598 (not-found request #:phrase "" #:ttl negative-ttl)))))
600 (define (compress-nar cache item compression)
601 "Save in directory CACHE the nar for ITEM compressed with COMPRESSION."
603 (nar-cache-file cache item #:compression compression))
605 (define (write-compressed-file call-with-compressed-output-port)
606 ;; Note: the file port gets closed along with the compressed port.
607 (call-with-compressed-output-port (open-output-file (string-append nar ".tmp"))
609 (write-file item port))
610 #:level (compression-level compression))
611 (rename-file (string-append nar ".tmp") nar))
613 (mkdir-p (dirname nar))
614 (match (compression-type compression)
616 (write-compressed-file call-with-gzip-output-port))
618 (write-compressed-file call-with-lzip-output-port))
620 (write-compressed-file call-with-zstd-output-port))
622 ;; Cache nars even when compression is disabled so that we can
623 ;; guarantee the TTL (see <https://bugs.gnu.org/28664>.)
624 (with-atomic-file-output nar
626 (write-file item port)
627 ;; Make the file world-readable, contrary to what
628 ;; 'with-atomic-file-output' does.
629 (chmod port (logand #o644 (lognot (umask)))))))))
631 (define* (bake-narinfo+nar cache item
632 #:key ttl (compressions (list %no-compression))
634 "Write the narinfo and nar for ITEM to CACHE."
635 (define (compressed-nar-size compression)
636 (let* ((nar (nar-cache-file cache item #:compression compression))
637 (stat (stat nar #f)))
639 (cons compression (stat:size stat)))))
641 (let ((compression (actual-compressions item compressions)))
643 (for-each (cut compress-nar cache item <>) compressions)
647 (let ((narinfo (narinfo-cache-file cache item
648 #:compression main)))
649 (with-atomic-file-output narinfo
651 ;; Open a new connection to the store. We cannot reuse the main
652 ;; thread's connection to the store since we would end up sending
653 ;; stuff concurrently on the same channel.
655 (let ((sizes (filter-map compressed-nar-size compression)))
656 (display (narinfo-string store item
659 #:compressions compressions
663 ;; Make the cached narinfo world-readable, contrary to what
664 ;; 'with-atomic-file-output' does, so that other users can rsync
666 (chmod port (logand #o644 (lognot (umask))))))
668 ;; Make narinfo files for OTHERS hard links to NARINFO such that the
669 ;; atime-based cache eviction considers either all the nars or none
670 ;; of them as candidates.
671 (for-each (lambda (other)
672 (let ((other (narinfo-cache-file cache item
673 #:compression other)))
674 (link narinfo other)))
677 ;; XXX: Declare the 'X-Nar-Compression' HTTP header, which is in fact for
678 ;; internal consumption: it allows us to pass the compression info to
679 ;; 'http-write', as part of the workaround to <http://bugs.gnu.org/21093>.
680 (declare-header! "X-Nar-Compression"
682 (match (call-with-input-string str read)
683 (('compression type level)
684 (compression type level))))
686 (lambda (compression port)
688 (($ <compression> type level)
689 (write `(compression ,type ,level) port)))))
691 (define* (render-nar store request store-item
692 #:key (compression %no-compression))
693 "Render archive of the store path corresponding to STORE-ITEM."
694 (let ((store-path (string-append %store-directory "/" store-item)))
695 ;; The ISO-8859-1 charset *must* be used otherwise HTTP clients will
696 ;; interpret the byte stream as UTF-8 and arbitrarily change invalid byte
698 (if (valid-path? store store-path)
699 (values `((content-type . (application/x-nix-archive
700 (charset . "ISO-8859-1")))
701 (x-nar-compression . ,compression))
702 ;; XXX: We're not returning the actual contents, deferring
703 ;; instead to 'http-write'. This is a hack to work around
704 ;; <http://bugs.gnu.org/21093>.
706 (not-found request))))
708 (define* (render-nar/cached store cache request store-item
709 #:key ttl (compression %no-compression))
710 "Respond to REQUEST with a nar for STORE-ITEM. If the nar is in CACHE,
711 return it; otherwise, return 404. When TTL is true, use it as the
712 'Cache-Control' expiration time."
713 (let ((cached (nar-cache-file cache store-item
714 #:compression compression)))
715 (cond ((file-exists? cached)
716 (values `((content-type . (application/octet-stream
717 (charset . "ISO-8859-1")))
719 `((cache-control (max-age . ,ttl)))
722 ;; XXX: We're not returning the actual contents, deferring
723 ;; instead to 'http-write'. This is a hack to work around
724 ;; <http://bugs.gnu.org/21093>.
725 (x-raw-file . ,cached))
727 ((let* ((hash (and=> (string-index store-item #\-)
728 (cut string-take store-item <>)))
730 (guard (c ((store-error? c) #f))
731 (hash-part->path store hash)))))
732 (and item (not (string-null? item))
733 (bypass-cache? store item)))
734 ;; Render STORE-ITEM live. We reach this because STORE-ITEM is
735 ;; being baked but clients are already asking for it. Thus, we're
736 ;; duplicating work, but doing so allows us to reduce delays.
737 (render-nar store request store-item
738 #:compression (low-compression compression)))
740 (not-found request)))))
742 (define (render-content-addressed-file store request
744 "Return the content of the result of the fixed-output derivation NAME that
745 has the given HASH of type ALGO."
746 ;; TODO: Support other hash algorithms.
747 (if (and (eq? algo 'sha256) (= 32 (bytevector-length hash)))
748 (let ((item (fixed-output-path name hash
751 (if (valid-path? store item)
752 (values `((content-type . (application/octet-stream
753 (charset . "ISO-8859-1")))
754 ;; XXX: We're not returning the actual contents,
755 ;; deferring instead to 'http-write'. This is a hack to
756 ;; work around <http://bugs.gnu.org/21093>.
757 (x-raw-file . ,item))
759 (not-found request)))
760 (not-found request)))
762 (define (render-log-file store request name)
763 "Render the log file for NAME, the base name of a store item. Don't attempt
764 to compress or decompress the log file; just return it as-is."
765 (define (response-headers file)
766 ;; XXX: We're not returning the actual contents, deferring instead to
767 ;; 'http-write'. This is a hack to work around
768 ;; <http://bugs.gnu.org/21093>.
769 (cond ((string-suffix? ".gz" file)
770 `((content-type . (text/plain (charset . "UTF-8")))
771 (content-encoding . (gzip))
772 (x-raw-file . ,file)))
773 ((string-suffix? ".bz2" file)
774 `((content-type . (application/x-bzip2
775 (charset . "ISO-8859-1")))
776 (x-raw-file . ,file)))
778 `((content-type . (text/plain (charset . "UTF-8")))
779 (x-raw-file . ,file)))))
781 (let ((log (log-file store
782 (string-append (%store-prefix) "/" name))))
784 (values (response-headers log) log)
785 (not-found request))))
787 (define (render-signing-key)
788 "Render signing key."
789 (let ((file %public-key-file))
790 (values `((content-type . (text/plain (charset . "UTF-8")))
791 (x-raw-file . ,file))
794 (define (render-home-page request)
795 "Render the home page."
796 (values `((content-type . (text/html (charset . "UTF-8"))))
797 (call-with-output-string
800 (head (title "GNU Guix Substitute Server"))
802 (h1 "GNU Guix Substitute Server")
805 "https://guix.gnu.org/manual/en/html_node/Invoking-guix-publish.html"))
807 " speaking. Welcome!")
812 " for this server. Knock yourselves out!")))
815 (define (extract-narinfo-hash str)
816 "Return the hash within the narinfo resource string STR, or false if STR
818 (and (string-suffix? ".narinfo" str)
819 (let ((base (string-drop-right str 8)))
820 (and (string-every %nix-base32-charset base)
823 (define (get-request? request)
824 "Return #t if REQUEST uses the GET method."
825 (eq? (request-method request) 'GET))
827 (define (request-path-components request)
828 "Split the URI path of REQUEST into a list of component strings. For
829 example: \"/foo/bar\" yields '(\"foo\" \"bar\")."
830 (split-and-decode-uri-path (uri-path (request-uri request))))
838 (@@ (web server http) http-write))
840 (define (strip-headers response)
841 "Return RESPONSE's headers minus 'Content-Length' and our internal headers."
843 (response-headers response)
844 '(content-length x-raw-file x-nar-compression)))
846 (define (sans-content-length response)
847 "Return RESPONSE without its 'content-length' header."
848 (set-field response (response-headers)
849 (strip-headers response)))
851 (define (with-content-length response length)
852 "Return RESPONSE with a 'content-length' header set to LENGTH."
853 (set-field response (response-headers)
854 (alist-cons 'content-length length
855 (strip-headers response))))
857 (define-syntax-rule (swallow-EPIPE exp ...)
858 "Swallow EPIPE errors raised by EXP..."
863 (if (= EPIPE (system-error-errno args))
865 (apply throw args)))))
867 (define-syntax-rule (swallow-zlib-error exp ...)
868 "Swallow 'zlib-error' exceptions raised by EXP..."
874 (define (nar-compressed-port port compression)
875 "Return a port on which to write the body of the response of a /nar request,
876 according to COMPRESSION."
878 (($ <compression> 'gzip level)
879 ;; Note: We cannot used chunked encoding here because
880 ;; 'make-gzip-output-port' wants a file port.
881 (make-gzip-output-port port
883 #:buffer-size %default-buffer-size))
884 (($ <compression> 'lzip level)
885 (make-lzip-output-port port
887 (($ <compression> 'zstd level)
888 (make-zstd-output-port port
890 (($ <compression> 'none)
895 (define (http-write server client response body)
896 "Write RESPONSE and BODY to CLIENT, possibly in a separate thread to avoid
898 ;; XXX: The default Guile web server implementation supports the keep-alive
899 ;; mechanism. However, as we run our own modified version of the http-write
900 ;; procedure, we need to access a few server implementation details to keep
902 (define *error-events*
903 (logior POLLHUP POLLERR))
905 (define *read-events*
909 (logior *error-events* *read-events*))
911 ;; Access the server poll set variable.
912 (define http-poll-set
913 (@@ (web server http) http-poll-set))
915 ;; Copied from (web server http).
916 (define (keep-alive? response)
917 (let ((v (response-version response)))
918 (and (or (< (response-code response) 400)
919 (= (response-code response) 404))
923 ((1) (not (memq 'close (response-connection response))))
924 ((0) (memq 'keep-alive (response-connection response)))))
927 (define (keep-alive port)
928 "Add the given PORT the server poll set."
930 (poll-set-add! (http-poll-set server) port *events*))
933 (assoc-ref (response-headers response) 'x-nar-compression))
935 (match (response-content-type response)
936 (('application/x-nix-archive . _)
937 ;; When compressing the NAR on the go, we cannot announce its size
938 ;; beforehand to the client. Hence, the keep-alive mechanism cannot work
940 (let ((keep-alive? (and (eq? (compression-type compression) 'none)
941 (keep-alive? response))))
942 ;; Add the client to the server poll set, so that we can receive
943 ;; further requests without closing the connection.
946 ;; Sending the the whole archive can take time so do it in a separate
947 ;; thread so that the main thread can keep working in the meantime.
948 (call-with-new-thread
950 (set-thread-name "publish nar")
951 (let* ((response (write-response (sans-content-length response)
954 (force-output client)
955 (configure-socket client)
956 ;; Duplicate the response port, so that it is
957 ;; not automatically closed when closing the
958 ;; returned port. This is needed for the
959 ;; keep-alive mechanism.
962 (response-port response) "w+0b")
964 ;; XXX: Given our ugly workaround for <http://bugs.gnu.org/21093>
965 ;; in 'render-nar', BODY here is just the file name of the store
966 ;; item. We call 'write-file' from here because we know that's
967 ;; the only way to avoid building the whole nar in memory, which
968 ;; could quickly become a real problem. As a bonus, we even do
969 ;; sendfile(2) directly from the store files to the socket.
972 (write-file (utf8->string body) port)))
976 (close-port client)))
979 (match (assoc-ref (response-headers response) 'x-raw-file)
981 (when (keep-alive? response)
983 ;; Send a raw file in a separate thread.
984 (call-with-new-thread
986 (set-thread-name "publish file")
989 (call-with-input-file file
991 (let* ((size (stat:size (stat input)))
992 (response (write-response
993 (with-content-length response size)
995 (output (response-port response)))
996 (configure-socket client)
997 (if (file-port? output)
998 (sendfile output input size)
999 (dump-port input output))
1000 (unless (keep-alive? response)
1001 (close-port output))
1004 ;; If the file was GC'd behind our back, that's fine. Likewise
1005 ;; if the client closes the connection.
1006 (unless (memv (system-error-errno args)
1007 (list ENOENT EPIPE ECONNRESET))
1011 ;; Handle other responses sequentially.
1012 (%http-write server client response body))))))
1014 (define-server-impl concurrent-http-server
1015 ;; A variant of Guile's built-in HTTP server that offloads possibly long
1016 ;; responses to a different thread.
1017 (@@ (web server http) http-open)
1018 (@@ (web server http) http-read)
1020 (@@ (web server http) http-close))
1022 (define (string->compression-type string)
1023 "Return a symbol denoting the compression method expressed by STRING; return
1024 #f if STRING doesn't match any supported method."
1031 (define (effective-compression requested-type compressions)
1032 "Given the REQUESTED-TYPE for compression and the set of chosen COMPRESSION
1033 methods, return the applicable compression."
1034 (or (find (match-lambda
1035 (($ <compression> type)
1036 (and (eq? type requested-type)
1039 (default-compression requested-type)))
1041 (define (preserve-connection-headers request response)
1042 "Add REQUEST's 'connection' header, if any, to HEADERS, a list of response
1044 (if (pair? response)
1046 (assq 'connection (request-headers request))))
1053 (define* (make-request-handler store
1056 narinfo-ttl narinfo-negative-ttl
1058 (compressions (list %no-compression)))
1059 (define compression-type?
1060 string->compression-type)
1063 (let ((expected (split-and-decode-uri-path nar-path)))
1064 (cut equal? expected <>)))
1066 (define (handle request body)
1067 (format #t "~a ~a~%"
1068 (request-method request)
1069 (uri-path (request-uri request)))
1070 (if (get-request? request) ;reject POST, PUT, etc.
1071 (match (request-path-components request)
1074 (render-nix-cache-info))
1076 ((or () ("index.html"))
1077 (render-home-page request))
1079 (("signing-key.pub")
1080 (render-signing-key))
1082 (((= extract-narinfo-hash (? string? hash)))
1084 (render-narinfo/cached store request hash
1088 #:negative-ttl narinfo-negative-ttl
1090 #:compressions compressions)
1091 (render-narinfo store request hash
1093 #:negative-ttl narinfo-negative-ttl
1095 #:compressions compressions)))
1096 ;; /nar/file/NAME/sha256/HASH
1097 (("file" name "sha256" hash)
1098 (guard (c ((invalid-base32-character? c)
1099 (not-found request)))
1100 (let ((hash (nix-base32-string->bytevector hash)))
1101 (render-content-addressed-file store request
1102 name 'sha256 hash))))
1106 (render-log-file store request name))
1108 ;; Use different URLs depending on the compression type. This
1109 ;; guarantees that /nar URLs remain valid even when 'guix publish'
1110 ;; is restarted with different compression parameters.
1112 ;; /nar/gzip/<store-item>
1113 ((components ... (? compression-type? type) store-item)
1114 (if (nar-path? components)
1115 (let* ((compression-type (string->compression-type type))
1116 (compression (effective-compression compression-type
1119 (render-nar/cached store cache request store-item
1121 #:compression compression)
1122 (render-nar store request store-item
1123 #:compression compression)))
1124 (not-found request)))
1126 ;; /nar/<store-item>
1127 ((components ... store-item)
1128 (if (nar-path? components)
1130 (render-nar/cached store cache request store-item
1132 #:compression %no-compression)
1133 (render-nar store request store-item
1134 #:compression %no-compression))
1135 (not-found request)))
1137 (x (not-found request)))
1138 (not-found request)))
1140 ;; Preserve the request's 'connection' header in the response, so that the
1141 ;; server can close the connection if this is requested by the client.
1142 (lambda (request body)
1143 (let-values (((response response-body)
1144 (handle request body)))
1145 (values (preserve-connection-headers request response)
1148 (define (service-name)
1149 "Return the Avahi service name of the server."
1150 (string-append "guix-publish-" (gethostname)))
1152 (define publish-service-type
1153 ;; Return the Avahi service type of the server.
1154 "_guix_publish._tcp")
1156 (define* (run-publish-server socket store
1159 (compressions (list %no-compression))
1160 (nar-path "nar") narinfo-ttl narinfo-negative-ttl
1163 (let ((name (service-name)))
1164 ;; XXX: Use a callback from Guile-Avahi here, as Avahi can pick a
1165 ;; different name to avoid name clashes.
1166 (info (G_ "Advertising ~a~%.") name)
1167 (avahi-publish-service-thread name
1168 #:type publish-service-type
1171 (run-server (make-request-handler store
1175 #:narinfo-ttl narinfo-ttl
1176 #:narinfo-negative-ttl narinfo-negative-ttl
1177 #:compressions compressions)
1178 concurrent-http-server
1179 `(#:socket ,socket)))
1181 (define (open-server-socket address)
1182 "Return a TCP socket bound to ADDRESS, a socket address."
1183 (let ((sock (socket (sockaddr:fam address) SOCK_STREAM 0)))
1184 (configure-socket sock #:options (cons (list SO_REUSEADDR 1)
1185 %default-socket-options))
1189 (define (gather-user-privileges user)
1190 "Switch to the identity of USER, a user name."
1193 (let ((user (getpw user)))
1195 (setgid (passwd:gid user))
1196 (setuid (passwd:uid user))))
1197 (lambda (key proc message args . rest)
1198 (leave (G_ "user '~a' not found: ~a~%")
1199 user (apply format #f message args)))))
1206 (define-command (guix-publish . args)
1207 (category packaging)
1208 (synopsis "publish build results over HTTP")
1210 (with-error-handling
1211 (let* ((opts (parse-command-line args %options (list %default-options)
1214 (lambda (arg result)
1215 (leave (G_ "~A: extraneous argument~%") arg))))
1216 (advertise? (assoc-ref opts 'advertise?))
1217 (user (assoc-ref opts 'user))
1218 (port (assoc-ref opts 'port))
1219 (ttl (assoc-ref opts 'narinfo-ttl))
1220 (negative-ttl (assoc-ref opts 'narinfo-negative-ttl))
1221 (compressions (match (filter-map (match-lambda
1222 (('compression . compression)
1227 ;; Default to fast & low compression.
1228 (list %default-gzip-compression))
1229 (lst (reverse lst))))
1230 (address (let ((addr (assoc-ref opts 'address)))
1231 (make-socket-address (sockaddr:fam addr)
1232 (sockaddr:addr addr)
1234 (socket (open-server-socket address))
1235 (nar-path (assoc-ref opts 'nar-path))
1236 (repl-port (assoc-ref opts 'repl))
1237 (cache (assoc-ref opts 'cache))
1238 (workers (assoc-ref opts 'workers))
1240 ;; Read the key right away so that (1) we fail early on if we can't
1241 ;; access them, and (2) we can then drop privileges.
1242 (public-key (read-file-sexp (assoc-ref opts 'public-key-file)))
1243 (private-key (read-file-sexp (assoc-ref opts 'private-key-file))))
1246 ;; Now that we've read the key material and opened the socket, we can
1248 (gather-user-privileges user))
1250 (when (zero? (getuid))
1251 (warning (G_ "server running as root; \
1252 consider using the '--user' option!~%")))
1254 (parameterize ((%public-key public-key)
1255 (%private-key private-key)
1256 (cache-bypass-threshold
1257 (or (assoc-ref opts 'cache-bypass-threshold)
1258 (cache-bypass-threshold))))
1259 (info (G_ "publishing ~a on ~a, port ~d~%")
1261 (inet-ntop (sockaddr:fam address) (sockaddr:addr address))
1262 (sockaddr:port address))
1264 (for-each (lambda (compression)
1265 (info (G_ "using '~a' compression method, level ~a~%")
1266 (compression-type compression)
1267 (compression-level compression)))
1271 (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port)))
1273 ;; Set the name of the main thread.
1274 (set-thread-name "guix publish")
1277 (run-publish-server socket store
1278 #:advertise? advertise?
1281 #:pool (and cache (make-pool workers
1285 #:compressions compressions
1286 #:narinfo-negative-ttl negative-ttl
1287 #:narinfo-ttl ttl))))))
1289 ;;; Local Variables:
1290 ;;; eval: (put 'single-baker 'scheme-indent-function 1)