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