publish.scm 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  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 Ludovic Courtès <ludo@gnu.org>
  5. ;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  6. ;;; Copyright © 2021 Simon Tournier <zimon.toutoune@gmail.com>
  7. ;;;
  8. ;;; This file is part of GNU Guix.
  9. ;;;
  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.
  14. ;;;
  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.
  19. ;;;
  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/>.
  22. (define-module (guix scripts publish)
  23. #:use-module ((system repl server) #:prefix repl:)
  24. #:use-module (ice-9 binary-ports)
  25. #:use-module (ice-9 format)
  26. #:use-module (ice-9 match)
  27. #:use-module (ice-9 regex)
  28. #:use-module (ice-9 rdelim)
  29. #:use-module (ice-9 threads)
  30. #:use-module (rnrs bytevectors)
  31. #:use-module (srfi srfi-1)
  32. #:use-module (srfi srfi-2)
  33. #:use-module (srfi srfi-9)
  34. #:use-module (srfi srfi-9 gnu)
  35. #:use-module (srfi srfi-19)
  36. #:use-module (srfi srfi-26)
  37. #:use-module (srfi srfi-34)
  38. #:use-module (srfi srfi-37)
  39. #:use-module (web http)
  40. #:use-module (web request)
  41. #:use-module (web response)
  42. #:use-module (web server)
  43. #:use-module (web uri)
  44. #:autoload (sxml simple) (sxml->xml)
  45. #:autoload (guix avahi) (avahi-publish-service-thread)
  46. #:use-module (guix base32)
  47. #:use-module (guix base64)
  48. #:use-module (guix config)
  49. #:use-module (guix derivations)
  50. #:use-module (gcrypt hash)
  51. #:use-module (guix pki)
  52. #:use-module (gcrypt pk-crypto)
  53. #:use-module (guix workers)
  54. #:use-module (guix store)
  55. #:use-module ((guix serialization) #:select (write-file))
  56. #:use-module (zlib)
  57. #:autoload (lzlib) (call-with-lzip-output-port
  58. make-lzip-output-port)
  59. #:autoload (zstd) (call-with-zstd-output-port
  60. make-zstd-output-port)
  61. #:use-module (guix cache)
  62. #:use-module (guix ui)
  63. #:use-module (guix scripts)
  64. #:use-module ((guix utils)
  65. #:select (with-atomic-file-output compressed-file?))
  66. #:use-module ((guix build utils)
  67. #:select (dump-port mkdir-p find-files))
  68. #:use-module ((guix build syscalls) #:select (set-thread-name))
  69. #:export (%default-gzip-compression
  70. %public-key
  71. %private-key
  72. signed-string
  73. open-server-socket
  74. publish-service-type
  75. run-publish-server
  76. guix-publish))
  77. (define (show-help)
  78. (format #t (G_ "Usage: guix publish [OPTION]...
  79. Publish ~a over HTTP.\n") %store-directory)
  80. (display (G_ "
  81. -p, --port=PORT listen on PORT"))
  82. (display (G_ "
  83. --listen=HOST listen on the network interface for HOST"))
  84. (display (G_ "
  85. -u, --user=USER change privileges to USER as soon as possible"))
  86. (display (G_ "
  87. -a, --advertise advertise on the local network"))
  88. (display (G_ "
  89. -C, --compression[=METHOD:LEVEL]
  90. compress archives with METHOD at LEVEL"))
  91. (display (G_ "
  92. -c, --cache=DIRECTORY cache published items to DIRECTORY"))
  93. (display (G_ "
  94. --cache-bypass-threshold=SIZE
  95. serve store items below SIZE even when not cached"))
  96. (display (G_ "
  97. --workers=N use N workers to bake items"))
  98. (display (G_ "
  99. --ttl=TTL announce narinfos can be cached for TTL seconds"))
  100. (display (G_ "
  101. --nar-path=PATH use PATH as the prefix for nar URLs"))
  102. (display (G_ "
  103. --public-key=FILE use FILE as the public key for signatures"))
  104. (display (G_ "
  105. --private-key=FILE use FILE as the private key for signatures"))
  106. (display (G_ "
  107. -r, --repl[=PORT] spawn REPL server on PORT"))
  108. (newline)
  109. (display (G_ "
  110. -h, --help display this help and exit"))
  111. (display (G_ "
  112. -V, --version display version information and exit"))
  113. (newline)
  114. (show-bug-report-information))
  115. (define (getaddrinfo* host)
  116. "Like 'getaddrinfo', but properly report errors."
  117. (catch 'getaddrinfo-error
  118. (lambda ()
  119. (getaddrinfo host))
  120. (lambda (key error)
  121. (leave (G_ "lookup of host '~a' failed: ~a~%")
  122. host (gai-strerror error)))))
  123. ;; Nar compression parameters.
  124. (define-record-type <compression>
  125. (compression type level)
  126. compression?
  127. (type compression-type)
  128. (level compression-level))
  129. (define %no-compression
  130. (compression 'none 0))
  131. (define %default-gzip-compression
  132. ;; Since we compress on the fly, default to fast compression.
  133. (compression 'gzip 3))
  134. (define (default-compression type)
  135. (compression type 3))
  136. (define (actual-compressions item requested)
  137. "Return the actual compressions used for ITEM, which may be %NO-COMPRESSION
  138. if ITEM is already compressed."
  139. (if (compressed-file? item)
  140. (list %no-compression)
  141. requested))
  142. (define (low-compression c)
  143. "Return <compression> of the same type as C, but optimized for low CPU
  144. usage."
  145. (compression (compression-type c)
  146. (min (compression-level c) 2)))
  147. (define %options
  148. (list (option '(#\h "help") #f #f
  149. (lambda _
  150. (show-help)
  151. (exit 0)))
  152. (option '(#\V "version") #f #f
  153. (lambda _
  154. (show-version-and-exit "guix publish")))
  155. (option '(#\a "advertise") #f #f
  156. (lambda (opt name arg result)
  157. (alist-cons 'advertise? #t result)))
  158. (option '(#\u "user") #t #f
  159. (lambda (opt name arg result)
  160. (alist-cons 'user arg result)))
  161. (option '(#\p "port") #t #f
  162. (lambda (opt name arg result)
  163. (alist-cons 'port (string->number* arg) result)))
  164. (option '("listen") #t #f
  165. (lambda (opt name arg result)
  166. (match (getaddrinfo* arg)
  167. ((info _ ...)
  168. (alist-cons 'address (addrinfo:addr info)
  169. result))
  170. (()
  171. (leave (G_ "lookup of host '~a' returned nothing")
  172. name)))))
  173. (option '(#\C "compression") #f #t
  174. (lambda (opt name arg result)
  175. (let* ((colon (string-index arg #\:))
  176. (type (cond
  177. (colon (string-take arg colon))
  178. ((string->number arg) "gzip")
  179. (else arg)))
  180. (level (if colon
  181. (string->number*
  182. (string-drop arg (+ 1 colon)))
  183. (or (string->number arg) 3))))
  184. (match level
  185. (0
  186. (alist-cons 'compression %no-compression result))
  187. (level
  188. (match (string->compression-type type)
  189. ((? symbol? type)
  190. (alist-cons 'compression
  191. (compression type level)
  192. result))
  193. (_
  194. (warning (G_ "~a: unsupported compression type~%")
  195. type)
  196. result)))))))
  197. (option '(#\c "cache") #t #f
  198. (lambda (opt name arg result)
  199. (alist-cons 'cache arg result)))
  200. (option '("cache-bypass-threshold") #t #f
  201. (lambda (opt name arg result)
  202. (alist-cons 'cache-bypass-threshold (size->number arg)
  203. result)))
  204. (option '("workers") #t #f
  205. (lambda (opt name arg result)
  206. (alist-cons 'workers (string->number* arg)
  207. result)))
  208. (option '("ttl") #t #f
  209. (lambda (opt name arg result)
  210. (let ((duration (string->duration arg)))
  211. (unless duration
  212. (leave (G_ "~a: invalid duration~%") arg))
  213. (alist-cons 'narinfo-ttl (time-second duration)
  214. result))))
  215. (option '("nar-path") #t #f
  216. (lambda (opt name arg result)
  217. (alist-cons 'nar-path arg result)))
  218. (option '("public-key") #t #f
  219. (lambda (opt name arg result)
  220. (alist-cons 'public-key-file arg result)))
  221. (option '("private-key" "secret-key") #t #f
  222. (lambda (opt name arg result)
  223. (alist-cons 'private-key-file arg result)))
  224. (option '(#\r "repl") #f #t
  225. (lambda (opt name arg result)
  226. ;; If port unspecified, use default Guile REPL port.
  227. (let ((port (and arg (string->number* arg))))
  228. (alist-cons 'repl (or port 37146) result))))))
  229. (define %default-options
  230. `((port . 8080)
  231. ;; By default, serve nars under "/nar".
  232. (nar-path . "nar")
  233. (public-key-file . ,%public-key-file)
  234. (private-key-file . ,%private-key-file)
  235. ;; Default number of workers when caching is enabled.
  236. (workers . ,(current-processor-count))
  237. (address . ,(make-socket-address AF_INET INADDR_ANY 0))
  238. (repl . #f)))
  239. ;; The key pair used to sign narinfos.
  240. (define %private-key
  241. (make-parameter #f))
  242. (define %public-key
  243. (make-parameter #f))
  244. (define %nix-cache-info
  245. `(("StoreDir" . ,%store-directory)
  246. ("WantMassQuery" . 0)
  247. ("Priority" . 100)))
  248. ;;; A common buffer size value used for the TCP socket SO_SNDBUF option and
  249. ;;; the gzip compressor buffer size.
  250. (define %default-buffer-size
  251. (* 208 1024))
  252. (define %default-socket-options
  253. ;; List of options passed to 'setsockopt' when transmitting files.
  254. (list (list SO_SNDBUF %default-buffer-size)))
  255. (define* (configure-socket socket #:key (level SOL_SOCKET)
  256. (options %default-socket-options))
  257. "Apply multiple option tuples in OPTIONS to SOCKET, using LEVEL."
  258. (for-each (cut apply setsockopt socket level <>)
  259. options))
  260. (define (signed-string s)
  261. "Sign the hash of the string S with the daemon's key. Return a canonical
  262. sexp for the signature."
  263. (let* ((public-key (%public-key))
  264. (hash (bytevector->hash-data (sha256 (string->utf8 s))
  265. #:key-type (key-type public-key))))
  266. (signature-sexp hash (%private-key) public-key)))
  267. (define base64-encode-string
  268. (compose base64-encode string->utf8))
  269. (define* (store-item->recutils store-item
  270. #:key
  271. (nar-path "nar")
  272. (compression %no-compression)
  273. file-size)
  274. "Return the 'Compression' and 'URL' fields of the narinfo for STORE-ITEM,
  275. with COMPRESSION, starting at NAR-PATH."
  276. (let ((url (encode-and-join-uri-path
  277. `(,@(split-and-decode-uri-path nar-path)
  278. ,@(match compression
  279. (($ <compression> 'none)
  280. '())
  281. (($ <compression> type)
  282. (list (symbol->string type))))
  283. ,(basename store-item)))))
  284. (format #f "URL: ~a~%Compression: ~a~%~@[FileSize: ~a~%~]"
  285. url (compression-type compression) file-size)))
  286. (define* (narinfo-string store store-path key
  287. #:key (compressions (list %no-compression))
  288. (nar-path "nar") (file-sizes '()))
  289. "Generate a narinfo key/value string for STORE-PATH; an exception is raised
  290. if STORE-PATH is invalid. Produce a URL that corresponds to COMPRESSION. The
  291. narinfo is signed with KEY. NAR-PATH specifies the prefix for nar URLs.
  292. Optionally, FILE-SIZES is a list of compression/integer pairs, where the
  293. integer is size in bytes of the compressed NAR; it informs the client of how
  294. much needs to be downloaded."
  295. (let* ((path-info (query-path-info store store-path))
  296. (compressions (actual-compressions store-path compressions))
  297. (hash (bytevector->nix-base32-string
  298. (path-info-hash path-info)))
  299. (size (path-info-nar-size path-info))
  300. (file-sizes `((,%no-compression . ,size) ,@file-sizes))
  301. (references (string-join
  302. (map basename (path-info-references path-info))
  303. " "))
  304. (deriver (path-info-deriver path-info))
  305. (base-info (format #f
  306. "\
  307. StorePath: ~a
  308. ~{~a~}\
  309. NarHash: sha256:~a
  310. NarSize: ~d
  311. References: ~a~%"
  312. store-path
  313. (map (lambda (compression)
  314. (let ((size (assoc-ref file-sizes
  315. compression)))
  316. (store-item->recutils store-path
  317. #:file-size size
  318. #:nar-path nar-path
  319. #:compression
  320. compression)))
  321. compressions)
  322. hash size references))
  323. ;; Do not render a "Deriver" or "System" line if we are rendering
  324. ;; info for a derivation.
  325. (info (if (not deriver)
  326. base-info
  327. (catch 'system-error
  328. (lambda ()
  329. (let ((drv (read-derivation-from-file deriver)))
  330. (format #f "~aSystem: ~a~%Deriver: ~a~%"
  331. base-info (derivation-system drv)
  332. (basename deriver))))
  333. (lambda args
  334. ;; DERIVER might be missing, but that's fine:
  335. ;; it's only used for <substitutable> where it's
  336. ;; optional. 'System' is currently unused.
  337. (if (= ENOENT (system-error-errno args))
  338. base-info
  339. (apply throw args))))))
  340. (signature (base64-encode-string
  341. (canonical-sexp->string (signed-string info)))))
  342. (format #f "~aSignature: 1;~a;~a~%" info (gethostname) signature)))
  343. (define* (not-found request
  344. #:key (phrase "Resource not found")
  345. ttl)
  346. "Render 404 response for REQUEST."
  347. (values (build-response #:code 404
  348. #:headers (if ttl
  349. `((cache-control (max-age . ,ttl)))
  350. '()))
  351. (string-append phrase ": "
  352. (uri-path (request-uri request)))))
  353. (define (render-nix-cache-info)
  354. "Render server information."
  355. (values '((content-type . (text/plain)))
  356. (lambda (port)
  357. (for-each (match-lambda
  358. ((key . value)
  359. (format port "~a: ~a~%" key value)))
  360. %nix-cache-info))))
  361. (define* (render-narinfo store request hash
  362. #:key ttl (compressions (list %no-compression))
  363. (nar-path "nar"))
  364. "Render metadata for the store path corresponding to HASH. If TTL is true,
  365. advertise it as the maximum validity period (in seconds) via the
  366. 'Cache-Control' header. This allows 'guix substitute' to cache it for an
  367. appropriate duration. NAR-PATH specifies the prefix for nar URLs."
  368. (let ((store-path (hash-part->path store hash)))
  369. (if (string-null? store-path)
  370. (not-found request #:phrase "")
  371. (values `((content-type . (application/x-nix-narinfo))
  372. ,@(if ttl
  373. `((cache-control (max-age . ,ttl)))
  374. '()))
  375. (cut display
  376. (narinfo-string store store-path (%private-key)
  377. #:nar-path nar-path
  378. #:compressions compressions)
  379. <>)))))
  380. (define* (nar-cache-file directory item
  381. #:key (compression %no-compression))
  382. (string-append directory "/"
  383. (symbol->string (compression-type compression))
  384. "/" (basename item) ".nar"))
  385. (define* (narinfo-cache-file directory item
  386. #:key (compression %no-compression))
  387. (string-append directory "/"
  388. (symbol->string (compression-type compression))
  389. "/" (basename item)
  390. ".narinfo"))
  391. (define (hash-part-mapping-cache-file directory hash)
  392. (string-append directory "/hashes/" hash))
  393. (define run-single-baker
  394. (let ((baking (make-weak-value-hash-table))
  395. (mutex (make-mutex)))
  396. (lambda (item thunk)
  397. "Run THUNK, which is supposed to bake ITEM, but make sure only one
  398. thread is baking ITEM at a given time."
  399. (define selected?
  400. (with-mutex mutex
  401. (and (not (hash-ref baking item))
  402. (begin
  403. (hash-set! baking item (current-thread))
  404. #t))))
  405. (when selected?
  406. (dynamic-wind
  407. (const #t)
  408. thunk
  409. (lambda ()
  410. (with-mutex mutex
  411. (hash-remove! baking item))))))))
  412. (define-syntax-rule (single-baker item exp ...)
  413. "Bake ITEM by evaluating EXP, but make sure there's only one baker for ITEM
  414. at a time."
  415. (run-single-baker item (lambda () exp ...)))
  416. (define (narinfo-files cache)
  417. "Return the list of .narinfo files under CACHE."
  418. (if (file-is-directory? cache)
  419. (find-files cache
  420. (lambda (file stat)
  421. (string-suffix? ".narinfo" file)))
  422. '()))
  423. (define (nar-expiration-time ttl)
  424. "Return the narinfo expiration time (in seconds since the Epoch). The
  425. expiration time is +inf.0 when passed an item that is still in the store; in
  426. other cases, it is the last-access time of the item plus TTL.
  427. This policy allows us to keep cached nars that correspond to valid store
  428. items. Failing that, we could eventually have to recompute them and return
  429. 404 in the meantime."
  430. (let ((expiration-time (file-expiration-time ttl)))
  431. (lambda (file)
  432. (let ((item (string-append (%store-prefix) "/"
  433. (basename file ".narinfo"))))
  434. ;; Note: We don't need to use 'valid-path?' here because FILE would
  435. ;; not exist if ITEM were not valid in the first place.
  436. (if (file-exists? item)
  437. +inf.0
  438. (expiration-time file))))))
  439. (define (hash-part->path* store hash cache)
  440. "Like 'hash-part->path' but cache results under CACHE. This ensures we can
  441. still map HASH to the corresponding store file name, even if said store item
  442. vanished from the store in the meantime."
  443. (let ((cached (hash-part-mapping-cache-file cache hash)))
  444. (catch 'system-error
  445. (lambda ()
  446. (call-with-input-file cached read-string))
  447. (lambda args
  448. (if (= ENOENT (system-error-errno args))
  449. (match (hash-part->path store hash)
  450. ("" "")
  451. (result
  452. (mkdir-p (dirname cached))
  453. (call-with-output-file (string-append cached ".tmp")
  454. (lambda (port)
  455. (display result port)))
  456. (rename-file (string-append cached ".tmp") cached)
  457. result))
  458. (apply throw args))))))
  459. (define cache-bypass-threshold
  460. ;; Maximum size of a store item that may be served by the '/cached' handlers
  461. ;; below even when not in cache.
  462. (make-parameter (* 10 (expt 2 20))))
  463. (define (bypass-cache? store item)
  464. "Return true if we allow ITEM to be downloaded before it is cached. ITEM is
  465. interpreted as the basename of a store item."
  466. (guard (c ((store-error? c) #f))
  467. (< (path-info-nar-size (query-path-info store item))
  468. (cache-bypass-threshold))))
  469. (define* (render-narinfo/cached store request hash
  470. #:key ttl (compressions (list %no-compression))
  471. (nar-path "nar")
  472. cache pool)
  473. "Respond to the narinfo request for REQUEST. If the narinfo is available in
  474. CACHE, then send it; otherwise, return 404 and \"bake\" that nar and narinfo
  475. requested using POOL."
  476. (define (delete-entry narinfo)
  477. ;; Delete NARINFO and the corresponding nar from CACHE.
  478. (let* ((nar (string-append (string-drop-right narinfo
  479. (string-length ".narinfo"))
  480. ".nar"))
  481. (base (basename narinfo ".narinfo"))
  482. (hash (string-take base (string-index base #\-)))
  483. (mapping (hash-part-mapping-cache-file cache hash)))
  484. (delete-file* narinfo)
  485. (delete-file* nar)
  486. (delete-file* mapping)))
  487. (let* ((item (hash-part->path* store hash cache))
  488. (compressions (actual-compressions item compressions))
  489. (cached (and (not (string-null? item))
  490. (narinfo-cache-file cache item
  491. #:compression
  492. (first compressions)))))
  493. (cond ((string-null? item)
  494. (not-found request))
  495. ((file-exists? cached)
  496. ;; Narinfo is in cache, send it.
  497. (values `((content-type . (application/x-nix-narinfo))
  498. ,@(if ttl
  499. `((cache-control (max-age . ,ttl)))
  500. '()))
  501. (lambda (port)
  502. (display (call-with-input-file cached
  503. read-string)
  504. port))))
  505. ((and (file-exists? item) ;cheaper than the 'valid-path?' RPC
  506. (valid-path? store item))
  507. ;; Nothing in cache: bake the narinfo and nar in the background and
  508. ;; return 404.
  509. (eventually pool
  510. (single-baker item
  511. ;; Check whether CACHED has been produced in the meantime.
  512. (unless (file-exists? cached)
  513. ;; (format #t "baking ~s~%" item)
  514. (bake-narinfo+nar cache item
  515. #:ttl ttl
  516. #:compressions compressions
  517. #:nar-path nar-path)))
  518. (when ttl
  519. (single-baker 'cache-cleanup
  520. (maybe-remove-expired-cache-entries cache
  521. narinfo-files
  522. #:entry-expiration
  523. (nar-expiration-time ttl)
  524. #:delete-entry delete-entry
  525. #:cleanup-period ttl))))
  526. ;; If ITEM passes 'bypass-cache?', render a temporary narinfo right
  527. ;; away, with a short TTL. The narinfo is temporary because it
  528. ;; lacks 'FileSize', for instance, which the cached narinfo will
  529. ;; have. Chances are that the nar will be baked by the time the
  530. ;; client asks for it.
  531. (if (bypass-cache? store item)
  532. (render-narinfo store request hash
  533. #:ttl 300 ;temporary
  534. #:nar-path nar-path
  535. #:compressions compressions)
  536. (not-found request
  537. #:phrase "We're baking it"
  538. #:ttl 300))) ;should be available within 5m
  539. (else
  540. (not-found request #:phrase "")))))
  541. (define (compress-nar cache item compression)
  542. "Save in directory CACHE the nar for ITEM compressed with COMPRESSION."
  543. (define nar
  544. (nar-cache-file cache item #:compression compression))
  545. (define (write-compressed-file call-with-compressed-output-port)
  546. ;; Note: the file port gets closed along with the compressed port.
  547. (call-with-compressed-output-port (open-output-file (string-append nar ".tmp"))
  548. (lambda (port)
  549. (write-file item port))
  550. #:level (compression-level compression))
  551. (rename-file (string-append nar ".tmp") nar))
  552. (mkdir-p (dirname nar))
  553. (match (compression-type compression)
  554. ('gzip
  555. (write-compressed-file call-with-gzip-output-port))
  556. ('lzip
  557. (write-compressed-file call-with-lzip-output-port))
  558. ('zstd
  559. (write-compressed-file call-with-zstd-output-port))
  560. ('none
  561. ;; Cache nars even when compression is disabled so that we can
  562. ;; guarantee the TTL (see <https://bugs.gnu.org/28664>.)
  563. (with-atomic-file-output nar
  564. (lambda (port)
  565. (write-file item port)
  566. ;; Make the file world-readable, contrary to what
  567. ;; 'with-atomic-file-output' does.
  568. (chmod port (logand #o644 (lognot (umask)))))))))
  569. (define* (bake-narinfo+nar cache item
  570. #:key ttl (compressions (list %no-compression))
  571. (nar-path "/nar"))
  572. "Write the narinfo and nar for ITEM to CACHE."
  573. (define (compressed-nar-size compression)
  574. (let* ((nar (nar-cache-file cache item #:compression compression))
  575. (stat (stat nar #f)))
  576. (and stat
  577. (cons compression (stat:size stat)))))
  578. (let ((compression (actual-compressions item compressions)))
  579. (for-each (cut compress-nar cache item <>) compressions)
  580. (match compressions
  581. ((main others ...)
  582. (let ((narinfo (narinfo-cache-file cache item
  583. #:compression main)))
  584. (with-atomic-file-output narinfo
  585. (lambda (port)
  586. ;; Open a new connection to the store. We cannot reuse the main
  587. ;; thread's connection to the store since we would end up sending
  588. ;; stuff concurrently on the same channel.
  589. (with-store store
  590. (let ((sizes (filter-map compressed-nar-size compression)))
  591. (display (narinfo-string store item
  592. (%private-key)
  593. #:nar-path nar-path
  594. #:compressions compressions
  595. #:file-sizes sizes)
  596. port)))
  597. ;; Make the cached narinfo world-readable, contrary to what
  598. ;; 'with-atomic-file-output' does, so that other users can rsync
  599. ;; the whole cache.
  600. (chmod port (logand #o644 (lognot (umask))))))
  601. ;; Make narinfo files for OTHERS hard links to NARINFO such that the
  602. ;; atime-based cache eviction considers either all the nars or none
  603. ;; of them as candidates.
  604. (for-each (lambda (other)
  605. (let ((other (narinfo-cache-file cache item
  606. #:compression other)))
  607. (link narinfo other)))
  608. others))))))
  609. ;; XXX: Declare the 'X-Nar-Compression' HTTP header, which is in fact for
  610. ;; internal consumption: it allows us to pass the compression info to
  611. ;; 'http-write', as part of the workaround to <http://bugs.gnu.org/21093>.
  612. (declare-header! "X-Nar-Compression"
  613. (lambda (str)
  614. (match (call-with-input-string str read)
  615. (('compression type level)
  616. (compression type level))))
  617. compression?
  618. (lambda (compression port)
  619. (match compression
  620. (($ <compression> type level)
  621. (write `(compression ,type ,level) port)))))
  622. (define* (render-nar store request store-item
  623. #:key (compression %no-compression))
  624. "Render archive of the store path corresponding to STORE-ITEM."
  625. (let ((store-path (string-append %store-directory "/" store-item)))
  626. ;; The ISO-8859-1 charset *must* be used otherwise HTTP clients will
  627. ;; interpret the byte stream as UTF-8 and arbitrarily change invalid byte
  628. ;; sequences.
  629. (if (valid-path? store store-path)
  630. (values `((content-type . (application/x-nix-archive
  631. (charset . "ISO-8859-1")))
  632. (x-nar-compression . ,compression))
  633. ;; XXX: We're not returning the actual contents, deferring
  634. ;; instead to 'http-write'. This is a hack to work around
  635. ;; <http://bugs.gnu.org/21093>.
  636. store-path)
  637. (not-found request))))
  638. (define* (render-nar/cached store cache request store-item
  639. #:key ttl (compression %no-compression))
  640. "Respond to REQUEST with a nar for STORE-ITEM. If the nar is in CACHE,
  641. return it; otherwise, return 404. When TTL is true, use it as the
  642. 'Cache-Control' expiration time."
  643. (let ((cached (nar-cache-file cache store-item
  644. #:compression compression)))
  645. (cond ((file-exists? cached)
  646. (values `((content-type . (application/octet-stream
  647. (charset . "ISO-8859-1")))
  648. ,@(if ttl
  649. `((cache-control (max-age . ,ttl)))
  650. '())
  651. ;; XXX: We're not returning the actual contents, deferring
  652. ;; instead to 'http-write'. This is a hack to work around
  653. ;; <http://bugs.gnu.org/21093>.
  654. (x-raw-file . ,cached))
  655. #f))
  656. ((let* ((hash (and=> (string-index store-item #\-)
  657. (cut string-take store-item <>)))
  658. (item (and hash
  659. (guard (c ((store-error? c) #f))
  660. (hash-part->path store hash)))))
  661. (and item (not (string-null? item))
  662. (bypass-cache? store item)))
  663. ;; Render STORE-ITEM live. We reach this because STORE-ITEM is
  664. ;; being baked but clients are already asking for it. Thus, we're
  665. ;; duplicating work, but doing so allows us to reduce delays.
  666. (render-nar store request store-item
  667. #:compression (low-compression compression)))
  668. (else
  669. (not-found request)))))
  670. (define (render-content-addressed-file store request
  671. name algo hash)
  672. "Return the content of the result of the fixed-output derivation NAME that
  673. has the given HASH of type ALGO."
  674. ;; TODO: Support other hash algorithms.
  675. (if (and (eq? algo 'sha256) (= 32 (bytevector-length hash)))
  676. (let ((item (fixed-output-path name hash
  677. #:hash-algo algo
  678. #:recursive? #f)))
  679. (if (valid-path? store item)
  680. (values `((content-type . (application/octet-stream
  681. (charset . "ISO-8859-1")))
  682. ;; XXX: We're not returning the actual contents,
  683. ;; deferring instead to 'http-write'. This is a hack to
  684. ;; work around <http://bugs.gnu.org/21093>.
  685. (x-raw-file . ,item))
  686. #f)
  687. (not-found request)))
  688. (not-found request)))
  689. (define (render-log-file store request name)
  690. "Render the log file for NAME, the base name of a store item. Don't attempt
  691. to compress or decompress the log file; just return it as-is."
  692. (define (response-headers file)
  693. ;; XXX: We're not returning the actual contents, deferring instead to
  694. ;; 'http-write'. This is a hack to work around
  695. ;; <http://bugs.gnu.org/21093>.
  696. (cond ((string-suffix? ".gz" file)
  697. `((content-type . (text/plain (charset . "UTF-8")))
  698. (content-encoding . (gzip))
  699. (x-raw-file . ,file)))
  700. ((string-suffix? ".bz2" file)
  701. `((content-type . (application/x-bzip2
  702. (charset . "ISO-8859-1")))
  703. (x-raw-file . ,file)))
  704. (else ;uncompressed
  705. `((content-type . (text/plain (charset . "UTF-8")))
  706. (x-raw-file . ,file)))))
  707. (let ((log (log-file store
  708. (string-append (%store-prefix) "/" name))))
  709. (if log
  710. (values (response-headers log) log)
  711. (not-found request))))
  712. (define (render-signing-key)
  713. "Render signing key."
  714. (let ((file %public-key-file))
  715. (values `((content-type . (text/plain (charset . "UTF-8")))
  716. (x-raw-file . ,file))
  717. file)))
  718. (define (render-home-page request)
  719. "Render the home page."
  720. (values `((content-type . (text/html (charset . "UTF-8"))))
  721. (call-with-output-string
  722. (lambda (port)
  723. (sxml->xml '(html
  724. (head (title "GNU Guix Substitute Server"))
  725. (body
  726. (h1 "GNU Guix Substitute Server")
  727. (p "Hi, "
  728. (a (@ (href
  729. "https://guix.gnu.org/manual/en/html_node/Invoking-guix-publish.html"))
  730. (tt "guix publish"))
  731. " speaking. Welcome!")
  732. (p "Here is the "
  733. (a (@ (href
  734. "signing-key.pub"))
  735. (tt "signing key"))
  736. " for this server. Knock yourselves out!")))
  737. port)))))
  738. (define (extract-narinfo-hash str)
  739. "Return the hash within the narinfo resource string STR, or false if STR
  740. is invalid."
  741. (and (string-suffix? ".narinfo" str)
  742. (let ((base (string-drop-right str 8)))
  743. (and (string-every %nix-base32-charset base)
  744. base))))
  745. (define (get-request? request)
  746. "Return #t if REQUEST uses the GET method."
  747. (eq? (request-method request) 'GET))
  748. (define (request-path-components request)
  749. "Split the URI path of REQUEST into a list of component strings. For
  750. example: \"/foo/bar\" yields '(\"foo\" \"bar\")."
  751. (split-and-decode-uri-path (uri-path (request-uri request))))
  752. ;;;
  753. ;;; Server.
  754. ;;;
  755. (define %http-write
  756. (@@ (web server http) http-write))
  757. (define (strip-headers response)
  758. "Return RESPONSE's headers minus 'Content-Length' and our internal headers."
  759. (fold alist-delete
  760. (response-headers response)
  761. '(content-length x-raw-file x-nar-compression)))
  762. (define (sans-content-length response)
  763. "Return RESPONSE without its 'content-length' header."
  764. (set-field response (response-headers)
  765. (strip-headers response)))
  766. (define (with-content-length response length)
  767. "Return RESPONSE with a 'content-length' header set to LENGTH."
  768. (set-field response (response-headers)
  769. (alist-cons 'content-length length
  770. (strip-headers response))))
  771. (define-syntax-rule (swallow-EPIPE exp ...)
  772. "Swallow EPIPE errors raised by EXP..."
  773. (catch 'system-error
  774. (lambda ()
  775. exp ...)
  776. (lambda args
  777. (if (= EPIPE (system-error-errno args))
  778. (values)
  779. (apply throw args)))))
  780. (define-syntax-rule (swallow-zlib-error exp ...)
  781. "Swallow 'zlib-error' exceptions raised by EXP..."
  782. (catch 'zlib-error
  783. (lambda ()
  784. exp ...)
  785. (const #f)))
  786. (define (nar-response-port response compression)
  787. "Return a port on which to write the body of RESPONSE, the response of a
  788. /nar request, according to COMPRESSION."
  789. (match compression
  790. (($ <compression> 'gzip level)
  791. ;; Note: We cannot used chunked encoding here because
  792. ;; 'make-gzip-output-port' wants a file port.
  793. (make-gzip-output-port (response-port response)
  794. #:level level
  795. #:buffer-size %default-buffer-size))
  796. (($ <compression> 'lzip level)
  797. (make-lzip-output-port (response-port response)
  798. #:level level))
  799. (($ <compression> 'zstd level)
  800. (make-zstd-output-port (response-port response)
  801. #:level level))
  802. (($ <compression> 'none)
  803. (response-port response))
  804. (#f
  805. (response-port response))))
  806. (define (http-write server client response body)
  807. "Write RESPONSE and BODY to CLIENT, possibly in a separate thread to avoid
  808. blocking."
  809. (match (response-content-type response)
  810. (('application/x-nix-archive . _)
  811. ;; Sending the the whole archive can take time so do it in a separate
  812. ;; thread so that the main thread can keep working in the meantime.
  813. (call-with-new-thread
  814. (lambda ()
  815. (set-thread-name "publish nar")
  816. (let* ((compression (assoc-ref (response-headers response)
  817. 'x-nar-compression))
  818. (response (write-response (sans-content-length response)
  819. client))
  820. (port (begin
  821. (force-output client)
  822. (configure-socket client)
  823. (nar-response-port response compression))))
  824. ;; XXX: Given our ugly workaround for <http://bugs.gnu.org/21093> in
  825. ;; 'render-nar', BODY here is just the file name of the store item.
  826. ;; We call 'write-file' from here because we know that's the only
  827. ;; way to avoid building the whole nar in memory, which could
  828. ;; quickly become a real problem. As a bonus, we even do
  829. ;; sendfile(2) directly from the store files to the socket.
  830. (swallow-zlib-error
  831. (swallow-EPIPE
  832. (write-file (utf8->string body) port)))
  833. (swallow-zlib-error
  834. (close-port port))
  835. (values)))))
  836. (_
  837. (match (assoc-ref (response-headers response) 'x-raw-file)
  838. ((? string? file)
  839. ;; Send a raw file in a separate thread.
  840. (call-with-new-thread
  841. (lambda ()
  842. (set-thread-name "publish file")
  843. (catch 'system-error
  844. (lambda ()
  845. (call-with-input-file file
  846. (lambda (input)
  847. (let* ((size (stat:size (stat input)))
  848. (response (write-response (with-content-length response
  849. size)
  850. client))
  851. (output (response-port response)))
  852. (configure-socket client)
  853. (if (file-port? output)
  854. (sendfile output input size)
  855. (dump-port input output))
  856. (close-port output)
  857. (values)))))
  858. (lambda args
  859. ;; If the file was GC'd behind our back, that's fine. Likewise if
  860. ;; the client closes the connection.
  861. (unless (memv (system-error-errno args)
  862. (list ENOENT EPIPE ECONNRESET))
  863. (apply throw args))
  864. (values))))))
  865. (#f
  866. ;; Handle other responses sequentially.
  867. (%http-write server client response body))))))
  868. (define-server-impl concurrent-http-server
  869. ;; A variant of Guile's built-in HTTP server that offloads possibly long
  870. ;; responses to a different thread.
  871. (@@ (web server http) http-open)
  872. (@@ (web server http) http-read)
  873. http-write
  874. (@@ (web server http) http-close))
  875. (define (string->compression-type string)
  876. "Return a symbol denoting the compression method expressed by STRING; return
  877. #f if STRING doesn't match any supported method."
  878. (match string
  879. ("gzip" 'gzip)
  880. ("lzip" 'lzip)
  881. ("zstd" 'zstd)
  882. (_ #f)))
  883. (define (effective-compression requested-type compressions)
  884. "Given the REQUESTED-TYPE for compression and the set of chosen COMPRESSION
  885. methods, return the applicable compression."
  886. (or (find (match-lambda
  887. (($ <compression> type)
  888. (and (eq? type requested-type)
  889. compression)))
  890. compressions)
  891. (default-compression requested-type)))
  892. (define* (make-request-handler store
  893. #:key
  894. cache pool
  895. narinfo-ttl
  896. (nar-path "nar")
  897. (compressions (list %no-compression)))
  898. (define compression-type?
  899. string->compression-type)
  900. (define nar-path?
  901. (let ((expected (split-and-decode-uri-path nar-path)))
  902. (cut equal? expected <>)))
  903. (lambda (request body)
  904. (format #t "~a ~a~%"
  905. (request-method request)
  906. (uri-path (request-uri request)))
  907. (if (get-request? request) ;reject POST, PUT, etc.
  908. (match (request-path-components request)
  909. ;; /nix-cache-info
  910. (("nix-cache-info")
  911. (render-nix-cache-info))
  912. ;; /
  913. ((or () ("index.html"))
  914. (render-home-page request))
  915. ;; guix signing-key
  916. (("signing-key.pub")
  917. (render-signing-key))
  918. ;; /<hash>.narinfo
  919. (((= extract-narinfo-hash (? string? hash)))
  920. (if cache
  921. (render-narinfo/cached store request hash
  922. #:cache cache
  923. #:pool pool
  924. #:ttl narinfo-ttl
  925. #:nar-path nar-path
  926. #:compressions compressions)
  927. (render-narinfo store request hash
  928. #:ttl narinfo-ttl
  929. #:nar-path nar-path
  930. #:compressions compressions)))
  931. ;; /nar/file/NAME/sha256/HASH
  932. (("file" name "sha256" hash)
  933. (guard (c ((invalid-base32-character? c)
  934. (not-found request)))
  935. (let ((hash (nix-base32-string->bytevector hash)))
  936. (render-content-addressed-file store request
  937. name 'sha256 hash))))
  938. ;; /log/OUTPUT
  939. (("log" name)
  940. (render-log-file store request name))
  941. ;; Use different URLs depending on the compression type. This
  942. ;; guarantees that /nar URLs remain valid even when 'guix publish'
  943. ;; is restarted with different compression parameters.
  944. ;; /nar/gzip/<store-item>
  945. ((components ... (? compression-type? type) store-item)
  946. (if (nar-path? components)
  947. (let* ((compression-type (string->compression-type type))
  948. (compression (effective-compression compression-type
  949. compressions)))
  950. (if cache
  951. (render-nar/cached store cache request store-item
  952. #:ttl narinfo-ttl
  953. #:compression compression)
  954. (render-nar store request store-item
  955. #:compression compression)))
  956. (not-found request)))
  957. ;; /nar/<store-item>
  958. ((components ... store-item)
  959. (if (nar-path? components)
  960. (if cache
  961. (render-nar/cached store cache request store-item
  962. #:ttl narinfo-ttl
  963. #:compression %no-compression)
  964. (render-nar store request store-item
  965. #:compression %no-compression))
  966. (not-found request)))
  967. (x (not-found request)))
  968. (not-found request))))
  969. (define (service-name)
  970. "Return the Avahi service name of the server."
  971. (string-append "guix-publish-" (gethostname)))
  972. (define publish-service-type
  973. ;; Return the Avahi service type of the server.
  974. "_guix_publish._tcp")
  975. (define* (run-publish-server socket store
  976. #:key
  977. advertise? port
  978. (compressions (list %no-compression))
  979. (nar-path "nar") narinfo-ttl
  980. cache pool)
  981. (when advertise?
  982. (let ((name (service-name)))
  983. ;; XXX: Use a callback from Guile-Avahi here, as Avahi can pick a
  984. ;; different name to avoid name clashes.
  985. (info (G_ "Advertising ~a~%.") name)
  986. (avahi-publish-service-thread name
  987. #:type publish-service-type
  988. #:port port)))
  989. (run-server (make-request-handler store
  990. #:cache cache
  991. #:pool pool
  992. #:nar-path nar-path
  993. #:narinfo-ttl narinfo-ttl
  994. #:compressions compressions)
  995. concurrent-http-server
  996. `(#:socket ,socket)))
  997. (define (open-server-socket address)
  998. "Return a TCP socket bound to ADDRESS, a socket address."
  999. (let ((sock (socket (sockaddr:fam address) SOCK_STREAM 0)))
  1000. (configure-socket sock #:options (cons (list SO_REUSEADDR 1)
  1001. %default-socket-options))
  1002. (bind sock address)
  1003. sock))
  1004. (define (gather-user-privileges user)
  1005. "Switch to the identity of USER, a user name."
  1006. (catch 'misc-error
  1007. (lambda ()
  1008. (let ((user (getpw user)))
  1009. (setgroups #())
  1010. (setgid (passwd:gid user))
  1011. (setuid (passwd:uid user))))
  1012. (lambda (key proc message args . rest)
  1013. (leave (G_ "user '~a' not found: ~a~%")
  1014. user (apply format #f message args)))))
  1015. ;;;
  1016. ;;; Entry point.
  1017. ;;;
  1018. (define-command (guix-publish . args)
  1019. (category packaging)
  1020. (synopsis "publish build results over HTTP")
  1021. (with-error-handling
  1022. (let* ((opts (parse-command-line args %options (list %default-options)
  1023. #:build-options? #f
  1024. #:argument-handler
  1025. (lambda (arg result)
  1026. (leave (G_ "~A: extraneous argument~%") arg))))
  1027. (advertise? (assoc-ref opts 'advertise?))
  1028. (user (assoc-ref opts 'user))
  1029. (port (assoc-ref opts 'port))
  1030. (ttl (assoc-ref opts 'narinfo-ttl))
  1031. (compressions (match (filter-map (match-lambda
  1032. (('compression . compression)
  1033. compression)
  1034. (_ #f))
  1035. opts)
  1036. (()
  1037. ;; Default to fast & low compression.
  1038. (list %default-gzip-compression))
  1039. (lst (reverse lst))))
  1040. (address (let ((addr (assoc-ref opts 'address)))
  1041. (make-socket-address (sockaddr:fam addr)
  1042. (sockaddr:addr addr)
  1043. port)))
  1044. (socket (open-server-socket address))
  1045. (nar-path (assoc-ref opts 'nar-path))
  1046. (repl-port (assoc-ref opts 'repl))
  1047. (cache (assoc-ref opts 'cache))
  1048. (workers (assoc-ref opts 'workers))
  1049. ;; Read the key right away so that (1) we fail early on if we can't
  1050. ;; access them, and (2) we can then drop privileges.
  1051. (public-key (read-file-sexp (assoc-ref opts 'public-key-file)))
  1052. (private-key (read-file-sexp (assoc-ref opts 'private-key-file))))
  1053. (when user
  1054. ;; Now that we've read the key material and opened the socket, we can
  1055. ;; drop privileges.
  1056. (gather-user-privileges user))
  1057. (when (zero? (getuid))
  1058. (warning (G_ "server running as root; \
  1059. consider using the '--user' option!~%")))
  1060. (parameterize ((%public-key public-key)
  1061. (%private-key private-key)
  1062. (cache-bypass-threshold
  1063. (or (assoc-ref opts 'cache-bypass-threshold)
  1064. (cache-bypass-threshold))))
  1065. (info (G_ "publishing ~a on ~a, port ~d~%")
  1066. %store-directory
  1067. (inet-ntop (sockaddr:fam address) (sockaddr:addr address))
  1068. (sockaddr:port address))
  1069. (for-each (lambda (compression)
  1070. (info (G_ "using '~a' compression method, level ~a~%")
  1071. (compression-type compression)
  1072. (compression-level compression)))
  1073. compressions)
  1074. (when repl-port
  1075. (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port)))
  1076. ;; Set the name of the main thread.
  1077. (set-thread-name "guix publish")
  1078. (with-store store
  1079. (run-publish-server socket store
  1080. #:advertise? advertise?
  1081. #:port port
  1082. #:cache cache
  1083. #:pool (and cache (make-pool workers
  1084. #:thread-name
  1085. "publish worker"))
  1086. #:nar-path nar-path
  1087. #:compressions compressions
  1088. #:narinfo-ttl ttl))))))
  1089. ;;; Local Variables:
  1090. ;;; eval: (put 'single-baker 'scheme-indent-function 1)
  1091. ;;; End: