publish.scm 55 KB

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