Merge pull request #1472 from lexi-lambda/pkg-git-credentials

Add support for git-backed packages that require authentication
This commit is contained in:
Alexis King 2016-10-06 18:24:24 -07:00 committed by GitHub
commit d9750064b9
13 changed files with 347 additions and 102 deletions

View File

@ -36,7 +36,9 @@ for information on command-line arguments and flags.
[(git) 9418] [(git) 9418]
[(http) 80] [(http) 80]
[(https) 443])] [(https) 443])]
[#:strict-links? strict-links? any/c #f]) [#:strict-links? strict-links? any/c #f]
[#:username username (or/c string? #f) (current-git-username)]
[#:password password (or/c string? #f) (current-git-password)])
string?]{ string?]{
Contacts the server at @racket[hostname] and @racket[port] Contacts the server at @racket[hostname] and @racket[port]
@ -96,6 +98,27 @@ If @racket[strict-links?] is true, then the checkout fails with an
error if it would produce a symbolic link that refers to an absolute path error if it would produce a symbolic link that refers to an absolute path
or to a relative path that contains up-directory elements. or to a relative path that contains up-directory elements.
If both @racket[username] and @racket[password] are non-@racket[#f]
@emph{and} @racket[transport] is @racket['http] or @racket['https], then
the provided credentials are passed to the remote server using HTTP Basic
Authentication.
@history[#:added "6.1.1.1" @history[#:added "6.1.1.1"
#:changed "6.3" @elem{Added the @racket[initial-error] argument.} #:changed "6.3" @elem{Added the @racket[initial-error] argument.}
#:changed "6.2.900.17" @elem{Added the @racket[strict-links?] argument.}]} #:changed "6.2.900.17" @elem{Added the @racket[strict-links?] argument.}
#:changed "6.6.0.5" @elem{Added the @racket[username] and @racket[password] arguments.}
#:changed "6.6.0.5" @elem{Changed to raise @racket[exn:fail:git] exceptions
instead of @racket[exn:fail].}]}
@deftogether[(@defparam[current-git-username username (or/c string? #f)]
@defparam[current-git-password password (or/c string? #f)])]{
Parameters used by @racket[git-checkout] as the default values of the
@racket[_username] and @racket[_password] arguments to control
authentication with the remote server.
@history[#:added "6.6.0.5"]}
@defstruct[(exn:fail:git exn:fail) () #:transparent]{
Raised by @racket[git-checkout] due to errors parsing or communicating with the git protocol.
@history[#:added "6.6.0.5"]}

View File

@ -944,6 +944,10 @@ for @nonterm{key}.
documentation; an empty string, which is the default, disables documentation; an empty string, which is the default, disables
the URL so that the local filesystem is used. This key can be the URL so that the local filesystem is used. This key can be
set only in @exec{installation} scope.} set only in @exec{installation} scope.}
@item{@exec{git-checkout-credentials} --- A list of git credentials in the form
@nonterm{username}@litchar{:}@nonterm{password} that are tried when downloading
packages with git sources using the HTTP or HTTPS protocols. The credentials are
currently stored @bold{unencrypted} on the filesystem.}
@item{@exec{trash-max-packages} --- A limit on the number of package implementations @item{@exec{trash-max-packages} --- A limit on the number of package implementations
that are kept in a trash folder when the package is removed or updated.} that are kept in a trash folder when the package is removed or updated.}
@item{@exec{trash-max-seconds} --- A limit on the time since a package is removed or @item{@exec{trash-max-seconds} --- A limit on the time since a package is removed or
@ -955,7 +959,8 @@ for @nonterm{key}.
] ]
@history[#:changed "6.1.1.6" @elem{Added @exec{trash-max-packages} and @exec{trash-max-seconds}.} @history[#:changed "6.1.1.6" @elem{Added @exec{trash-max-packages} and @exec{trash-max-seconds}.}
#:changed "6.3" @elem{Added @exec{network-retries}.}]} #:changed "6.3" @elem{Added @exec{network-retries}.}
#:changed "6.6.0.5" @elem{Added @exec{git-checkout-credentials}.}]}
@subcommand{@command/toc{catalog-show} @nonterm{option} ... @nonterm{package-name} ... @subcommand{@command/toc{catalog-show} @nonterm{option} ... @nonterm{package-name} ...

View File

@ -0,0 +1,82 @@
#lang racket/base
(provide serve-git-http-proxy!)
(require net/base64
net/uri-codec
net/url
racket/match
racket/port
racket/runtime-path
racket/string
racket/system
web-server/http
web-server/servlet-env)
(define (url-path-string url)
(string-join (map path/param-path (url-path url))
"/" #:before-first "/"))
(define (url-query-string url)
(alist->form-urlencoded (url-query url)))
(define (verify-authorization header-value)
; strip #"Basic " off of the header value
(define encoded-value (subbytes header-value 6))
(equal? (string-split (bytes->string/utf-8 (base64-decode encoded-value)) ":")
'("user" "password")))
(define (serve-git-http-proxy req)
; check if the right Authorization header is provided
(define authorization (headers-assq* #"Authorization" (request-headers/raw req)))
(cond
[(and authorization (verify-authorization (header-value authorization)))
(parameterize ([current-environment-variables (environment-variables-copy
(current-environment-variables))])
; git-http-backend depends on these environment variables to find the git repo
(putenv "GIT_PROJECT_ROOT" (path->string (find-system-path 'temp-dir)))
(putenv "GIT_HTTP_EXPORT_ALL" "")
; set standard CGI environment variables
(environment-variables-set! (current-environment-variables)
#"REQUEST_METHOD" (request-method req))
(putenv "PATH_INFO" (url-path-string (request-uri req)))
(putenv "QUERY_STRING" (url-query-string (request-uri req)))
(let ([content-type (headers-assq* #"Content-Type" (request-headers/raw req))])
(when content-type
(environment-variables-set! (current-environment-variables)
#"CONTENT_TYPE" (header-value content-type))))
; run git-http-backend
(match-define (list git-response-port git-body-port _ _ _)
(process*/ports #f #f (current-error-port)
(find-executable-path "git") "http-backend"))
; pass POST body to git-http-backend
(when (request-post-data/raw req)
(write-bytes (request-post-data/raw req) git-body-port))
(close-output-port git-body-port)
; convert CGI headers to ones the web server can understand
(define headers
(for/list ([line (in-lines git-response-port)]
#:break (zero? (string-length line)))
(apply header (map string->bytes/utf-8 (string-split line ": ")))))
; produce a response
(response 200 #"OK" (current-seconds) #f headers
(λ (out)
(copy-port git-response-port out)
(close-input-port git-response-port))))]
; if authorization fails, return a WWW-Authenticate header
[else (response/full 401 #"Authorization Required" (current-seconds)
#"text/plain; charset=utf-8"
(list (header #"WWW-Authenticate" #"Basic"))
(list #"Repository not found."))]))
(define (serve-git-http-proxy! #:port port)
(serve/servlet serve-git-http-proxy
#:port port
#:command-line? #t
#:servlet-regexp #rx""))

View File

@ -0,0 +1,3 @@
#lang info
(define collection 'multi)

View File

@ -14,6 +14,7 @@
(initialize-catalogs) (initialize-catalogs)
$ "raco pkg config --set catalogs http://localhost:9990" $ "raco pkg config --set catalogs http://localhost:9990"
$ "raco pkg config --set git-checkout-credentials user:password"
$ "racket -l racket/base -l pkg/lib -e '(pkg-config-catalogs)'" $ "racket -l racket/base -l pkg/lib -e '(pkg-config-catalogs)'"
=stdout> "'(\"http://localhost:9990\")\n" =stdout> "'(\"http://localhost:9990\")\n"

View File

@ -251,4 +251,14 @@
$ "racket -e '(require pkg/lib)' -e '(path->pkg (build-path (pkg-directory \"pkg-test1\") \"pkg-test2\"))'" $ "racket -e '(require pkg/lib)' -e '(path->pkg (build-path (pkg-directory \"pkg-test1\") \"pkg-test2\"))'"
=stdout> "\"pkg-test1\"\n" =stdout> "\"pkg-test1\"\n"
$ "raco pkg remove pkg-test2-snd pkg-test1" $ "raco pkg remove pkg-test2-snd pkg-test1"
$ "racket -e '(require pkg-test1)'" =exit> 1))))) $ "racket -e '(require pkg-test1)'" =exit> 1))
(with-fake-root
(shelly-case
"git package that requires authentication"
$ "raco pkg config --set catalogs http://localhost:9990"
$ "raco pkg install pkg-git" =exit> 1
$ "raco pkg config --set git-checkout-credentials user:bad-password"
$ "raco pkg install pkg-git" =exit> 1
$ "raco pkg config --set git-checkout-credentials user:password"
$ "raco pkg install pkg-git")))))

View File

@ -10,8 +10,10 @@
racket/list racket/list
racket/format racket/format
racket/port racket/port
racket/string
setup/dirs setup/dirs
"shelly.rkt") "shelly.rkt"
"git-http-proxy.rkt")
(define-runtime-path test-source-directory ".") (define-runtime-path test-source-directory ".")
@ -167,8 +169,11 @@
(with-thread (with-thread
(λ () (start-pkg-server *index-ht-2* 9991)) (λ () (start-pkg-server *index-ht-2* 9991))
(λ () (λ ()
(with-thread (λ () (start-file-server)) (with-thread
t)))))])) (λ () (start-file-server))
(λ ()
(with-thread (λ () (serve-git-http-proxy! #:port 9996))
t)))))))]))
(define-syntax-rule (with-servers e ...) (define-syntax-rule (with-servers e ...)
(with-servers* (λ () e ...))) (with-servers* (λ () e ...)))
@ -246,7 +251,28 @@
'source 'source
"http://localhost:9997/pkg-test2.zip" "http://localhost:9997/pkg-test2.zip"
'dependencies 'dependencies
'("pkg-test1")))) '("pkg-test1")))
(initialize-catalogs/git))
(define (initialize-catalogs/git)
(define pkg-git.git (make-temporary-file "pkg-git-~a.git"))
(delete-file pkg-git.git)
(parameterize ([current-directory (build-path test-source-directory "test-pkgs")])
(copy-directory/files (build-path test-source-directory "test-pkgs" "pkg-git") pkg-git.git))
(define checksum
(parameterize ([current-directory pkg-git.git])
(system "git init")
(system "git add -A")
(system "git commit -m 'initial commit'")
(string-trim
(with-output-to-string
(λ () (system "git rev-parse HEAD"))))))
(match-define-values [_ pkg-git.git-filename _] (split-path pkg-git.git))
(hash-set! *index-ht-1* "pkg-git"
(hasheq 'checksum checksum
'source (~a "http://localhost:9996/" (path->string pkg-git.git-filename)))))
(define (set-file path content) (define (set-file path content)
(make-parent-directory* path) (make-parent-directory* path)

View File

@ -7,6 +7,7 @@
file/gunzip file/gunzip
file/private/check-path file/private/check-path
openssl/sha1 openssl/sha1
net/base64
net/url net/url
net/head net/head
net/http-client net/http-client
@ -16,10 +17,22 @@
;; http://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/ ;; http://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/
;; provided many helpful hints for this implementation. ;; provided many helpful hints for this implementation.
(provide git-checkout) (provide git-checkout
current-git-username
current-git-password
(struct-out exn:fail:git))
(define-logger git-checkout) (define-logger git-checkout)
(define current-git-username (make-parameter #f))
(define current-git-password (make-parameter #f))
(struct exn:fail:git exn:fail () #:transparent)
(define (raise-git-error name fmt . vals)
(raise (exn:fail:git (apply format (string-append "~s: " fmt) name vals)
(current-continuation-marks))))
;; Like `git clone`, but producing just the checkout ;; Like `git clone`, but producing just the checkout
(define (git-checkout host (define (git-checkout host
repo repo
@ -35,7 +48,9 @@
#:clean-tmp-dir? [clean-tmp-dir? (not given-tmp-dir)] #:clean-tmp-dir? [clean-tmp-dir? (not given-tmp-dir)]
#:verify-server? [verify? #t] #:verify-server? [verify? #t]
#:port [given-port #f] #:port [given-port #f]
#:strict-links? [strict-links? #f]) #:strict-links? [strict-links? #f]
#:username [username (current-git-username)]
#:password [password (current-git-password)])
(let retry-loop ([given-depth given-depth]) (let retry-loop ([given-depth given-depth])
(define tmp-dir (or given-tmp-dir (define tmp-dir (or given-tmp-dir
(make-temporary-file "git~a" 'directory))) (make-temporary-file "git~a" 'directory)))
@ -51,7 +66,7 @@
(status "Contacting ~a" host) (status "Contacting ~a" host)
(define-values (i o dumb-protocol?) (define-values (i o dumb-protocol?)
(initial-connect transport host verify? port repo status)) (initial-connect transport host verify? port repo status username password))
((let/ec esc ((let/ec esc
(dynamic-wind (dynamic-wind
void void
@ -66,7 +81,7 @@
;; smart protocol provides packets: ;; smart protocol provides packets:
(read-pkts i))) (read-pkts i)))
(unless (pair? pkts) (unless (pair? pkts)
(error 'git-checkout "no initial pkts from the server")) (raise-git-error 'git-checkout "no initial pkts from the server"))
;; Parse server's initial reply ;; Parse server's initial reply
(define server-capabilities (parse-server-capabilities (car pkts))) (define server-capabilities (parse-server-capabilities (car pkts)))
@ -106,7 +121,7 @@
;; Tell the server that we're ready for the objects ;; Tell the server that we're ready for the objects
(write-pkt o "done\n") (write-pkt o "done\n")
(set!-values (i o) (done-step transport host verify? port repo i o)) (set!-values (i o) (done-step transport host verify? port repo username password i o))
(when depth (when depth
;; If we wrote `deepen`, then the server replies with `shallow`s. ;; If we wrote `deepen`, then the server replies with `shallow`s.
@ -120,12 +135,12 @@
[(regexp-match? #rx"^shallow " r) [(regexp-match? #rx"^shallow " r)
(loop)] (loop)]
[else [else
(error 'git-checkout "expected shallow, got ~s" r)]))) (raise-git-error 'git-checkout "expected shallow, got ~s" r)])))
;; Tell the server that we're ready for the objects ;; Tell the server that we're ready for the objects
(define nak (read-pkt i)) (define nak (read-pkt i))
(unless (equal? #"NAK\n" nak) (unless (equal? #"NAK\n" nak)
(error 'git-checkout "expected NAK, got ~s" nak))) (raise-git-error 'git-checkout "expected NAK, got ~s" nak)))
(make-directory* tmp-dir) (make-directory* tmp-dir)
(define tmp (make-tmp-info tmp-dir #:fresh? #t)) (define tmp (make-tmp-info tmp-dir #:fresh? #t))
@ -189,18 +204,23 @@
;; ---------------------------------------- ;; ----------------------------------------
;; Transports: git, http, and https ;; Transports: git, http, and https
(define http-request-headers (define (http-request-headers username password)
;; bitbucket.org seems to require a "git" value for "User-Agent", ;; bitbucket.org seems to require a "git" value for "User-Agent",
;; otherwise it returns a "broken link" web page ;; otherwise it returns a "broken link" web page
'("User-Agent: git/1.9")) (define base-headers '("User-Agent: git/1.9"))
;; include an Authorization header if credentials are provided
(if (and username password)
(cons (~a "Authorization: Basic " (base64-encode (string->bytes/utf-8 (~a username ":" password)) #""))
base-headers)
base-headers))
;; initial-connect: transport-sym string bool natural string status-proc ;; initial-connect: transport-sym string bool natural string status-proc string string
;; -> (values input-port output-port boolean) ;; -> (values input-port output-port boolean)
;; Contacts the server and returns an output port for writing ;; Contacts the server and returns an output port for writing
;; the request (ignored if not needed for the the transport) ;; the request (ignored if not needed for the the transport)
;; and an input port from reading the available references. The ;; and an input port from reading the available references. The
;; boolean result indicates whether the protocol is "dumb". ;; boolean result indicates whether the protocol is "dumb".
(define (initial-connect transport host verify? port repo status) (define (initial-connect transport host verify? port repo status username password)
(case transport (case transport
[(git) [(git)
(define-values (i o) (tcp-or-tunnel-connect "git" host port)) (define-values (i o) (tcp-or-tunnel-connect "git" host port))
@ -212,7 +232,7 @@
(define-values (i headers) (define-values (i headers)
(parameterize ([current-https-protocol (ssl-context verify?)]) (parameterize ([current-https-protocol (ssl-context verify?)])
(get-pure-port/headers (string->url url-str) (get-pure-port/headers (string->url url-str)
http-request-headers (http-request-headers username password)
#:redirections 5))) #:redirections 5)))
(define ok? #f) (define ok? #f)
(dynamic-wind (dynamic-wind
@ -224,14 +244,14 @@
"application/x-git-upload-pack-advertisement") "application/x-git-upload-pack-advertisement")
;; "smart" protocol ;; "smart" protocol
(unless (regexp-match-peek #px#"^[0-9a-f]{4}#" i) (unless (regexp-match-peek #px#"^[0-9a-f]{4}#" i)
(error 'git-checkout (~a "error reading repository content;\n" (raise-git-error 'git-checkout (~a "error reading repository content;\n"
" response is not consistent with the Git protocol\n" " response is not consistent with the Git protocol\n"
" initial portion: ~s") " initial portion: ~s")
(read-bytes 640 i))) (read-bytes 640 i)))
(define pkt (read-pkt i)) (define pkt (read-pkt i))
(define term-pkt (read-pkt i)) (define term-pkt (read-pkt i))
(unless (eof-object? term-pkt) (unless (eof-object? term-pkt)
(error 'git-checkout (~a "expected a null packet, received something else\n" (raise-git-error 'git-checkout (~a "expected a null packet, received something else\n"
" packet: ~s") " packet: ~s")
term-pkt)) term-pkt))
#f] #f]
@ -243,7 +263,7 @@
(lambda () (lambda ()
(unless ok? (close-input-port i))))] (unless ok? (close-input-port i))))]
[else [else
(error 'git-checkout "unrecognized transport\n given: ~e" transport)])) (raise-git-error 'git-checkout "unrecognized transport\n given: ~e" transport)]))
;; want-step: transport-sym string natural string input-port output-port ;; want-step: transport-sym string natural string input-port output-port
;; -> (values input-port output-port) ;; -> (values input-port output-port)
@ -256,12 +276,12 @@
(close-input-port i) (close-input-port i)
(values (open-input-bytes #"") (open-output-bytes))])) (values (open-input-bytes #"") (open-output-bytes))]))
;; done-step: transport-sym string bool natural string input-port output-port ;; done-step: transport-sym string bool natural string string string input-port output-port
;; -> (values input-port output-port) ;; -> (values input-port output-port)
;; Replaces the connection, if appropriate to the transport, after ;; Replaces the connection, if appropriate to the transport, after
;; writing the wanted references and before reading the server's ;; writing the wanted references and before reading the server's
;; response. ;; response.
(define (done-step transport host verify? port repo i o) (define (done-step transport host verify? port repo username password i o)
(case transport (case transport
[(git) (values i o)] [(git) (values i o)]
[(http https) [(http https)
@ -274,7 +294,7 @@
"/git-upload-pack")) "/git-upload-pack"))
s s
(append (append
http-request-headers (http-request-headers username password)
(list "Content-Type: application/x-git-upload-pack-request"))))) (list "Content-Type: application/x-git-upload-pack-request")))))
(values i (open-output-nowhere))])) (values i (open-output-nowhere))]))
@ -305,8 +325,7 @@
(define m (regexp-match #px#"^([0-9a-fA-F]{40})[ \t]([^\0\n]+)[\0\n]" pkt)) (define m (regexp-match #px#"^([0-9a-fA-F]{40})[ \t]([^\0\n]+)[\0\n]" pkt))
(unless m (unless m
(when initial-error (initial-error)) (when initial-error (initial-error))
(error 'git-checkout "could not parse ref pkt\n pkt: ~s" (raise-git-error 'git-checkout "could not parse ref pkt\n pkt: ~s" pkt))
pkt))
(define name (caddr m)) (define name (caddr m))
(define id (bytes->string/utf-8 (cadr m))) (define id (bytes->string/utf-8 (cadr m)))
(cond (cond
@ -351,7 +370,7 @@
(for/list ([ref (in-list refs)]) (for/list ([ref (in-list refs)])
(cadr ref))] (cadr ref))]
[else [else
(error 'git "could not find requested reference\n reference: ~a" ref)])) (raise-git-error 'git "could not find requested reference\n reference: ~a" ref)]))
(values ref-commit want-commits)) (values ref-commit want-commits))
@ -389,16 +408,16 @@
[else [else
(unless (and (bytes? len-bstr) (unless (and (bytes? len-bstr)
(= 4 (bytes-length len-bstr))) (= 4 (bytes-length len-bstr)))
(error 'git-checkout "error getting pkt length")) (raise-git-error 'git-checkout "error getting pkt length"))
(define len (string->number (bytes->string/utf-8 len-bstr #\?) 16)) (define len (string->number (bytes->string/utf-8 len-bstr #\?) 16))
(unless len (unless len
(error 'git-checkout "error getting pkt length\n length string: ~e" len-bstr)) (raise-git-error 'git-checkout "error getting pkt length\n length string: ~e" len-bstr))
(cond (cond
[(= len 0) eof] ; flush pkt [(= len 0) eof] ; flush pkt
[else [else
(define payload-len (- len 4)) (define payload-len (- len 4))
(unless (payload-len . >= . 0) (unless (payload-len . >= . 0)
(error 'git-checkout "pkt length makes no sense\n length: ~a" len)) (raise-git-error 'git-checkout "pkt length makes no sense\n length: ~a" len))
(read-bytes-exactly 'payload payload-len i)])])) (read-bytes-exactly 'payload payload-len i)])]))
;; read a list of pkts until an empty packet is found ;; read a list of pkts until an empty packet is found
@ -427,10 +446,10 @@
(when (and (eof-object? pack-bstr) (when (and (eof-object? pack-bstr)
initial-eof-handler) initial-eof-handler)
(initial-eof-handler)) (initial-eof-handler))
(error 'git-checkout "header error\n bytes: ~s" pack-bstr)) (raise-git-error 'git-checkout "header error\n bytes: ~s" pack-bstr))
(define vers (read-bytes 4 i)) (define vers (read-bytes 4 i))
(unless (equal? vers #"\0\0\0\2") (unless (equal? vers #"\0\0\0\2")
(error 'git-checkout "only version 2 supported")) (raise-git-error 'git-checkout "only version 2 supported"))
(define count-bstr (read-bytes-exactly 'count 4 i)) (define count-bstr (read-bytes-exactly 'count 4 i))
(define count (integer-bytes->integer count-bstr #t #t)) (define count (integer-bytes->integer count-bstr #t #t))
(define obj-stream-poses (make-hash)) ; for OBJ_OFS_DELTA references (define obj-stream-poses (make-hash)) ; for OBJ_OFS_DELTA references
@ -458,7 +477,7 @@
(define obj-stream-pos (file-position i)) (define obj-stream-pos (file-position i))
(define c (read-byte-only 'type-and-size i)) (define c (read-byte-only 'type-and-size i))
(define type (bitwise-and (arithmetic-shift c -4) #x7)) (define type (bitwise-and (arithmetic-shift c -4) #x7))
(when (zero? type) (error 'git-checkout "bad packfile type")) (when (zero? type) (raise-git-error 'git-checkout "bad packfile type"))
(define init-len (bitwise-and c #xF)) (define init-len (bitwise-and c #xF))
(define len (define len
(if (msb-set? c) (if (msb-set? c)
@ -472,7 +491,7 @@
[(ofs-delta) [(ofs-delta)
(define delta (read-offset-integer i)) (define delta (read-offset-integer i))
(hash-ref obj-stream-poses (- obj-stream-pos delta) (hash-ref obj-stream-poses (- obj-stream-pos delta)
(lambda () (error 'git-checkout "OBJ_OFS_DELTA object not found")))] (lambda () (raise-git-error 'git-checkout "OBJ_OFS_DELTA object not found")))]
[else #f])) [else #f]))
(define obj (define obj
(save-object (lambda (o) (zlib-inflate i o)) len type-sym type-info tmp)) (save-object (lambda (o) (zlib-inflate i o)) len type-sym type-info tmp))
@ -592,9 +611,9 @@
(cond (cond
[(= 1 (length matches)) (car matches)] [(= 1 (length matches)) (car matches)]
[(null? matches) [(null? matches)
(error 'git-checkout "no commit found matching id: ~a" ref)] (raise-git-error 'git-checkout "no commit found matching id: ~a" ref)]
[else [else
(error 'git-checkout "found multiple commits matching id: ~a" ref)])) (raise-git-error 'git-checkout "found multiple commits matching id: ~a" ref)]))
(define (id-ref->regexp ref) (define (id-ref->regexp ref)
(regexp (~a "^" (regexp-quote (string-downcase ref))))) (regexp (~a "^" (regexp-quote (string-downcase ref)))))
@ -624,7 +643,7 @@
(lambda (i) (lambda (i)
(define m (regexp-try-match #px"^object ([0-9a-fA-F]{40})" i)) (define m (regexp-try-match #px"^object ([0-9a-fA-F]{40})" i))
(unless m (unless m
(error 'git-checkout "cannot extract commit from tag file for ~s" (raise-git-error 'git-checkout "cannot extract commit from tag file for ~s"
(bytes->hex-string obj-id))) (bytes->hex-string obj-id)))
(cadr m)))) (cadr m))))
(define commit-id (hex-string->bytes (bytes->string/utf-8 commit-id-bstr))) (define commit-id (hex-string->bytes (bytes->string/utf-8 commit-id-bstr)))
@ -632,7 +651,7 @@
[(tree) [(tree)
(extract-tree obj-id obj-ids tmp dest-dir strict-links?)] (extract-tree obj-id obj-ids tmp dest-dir strict-links?)]
[else [else
(error 'git-checkout "cannot extract tree from ~a: ~s" (raise-git-error 'git-checkout "cannot extract tree from ~a: ~s"
(object-type obj) (object-type obj)
(bytes->hex-string obj-id))])) (bytes->hex-string obj-id))]))
@ -642,7 +661,7 @@
(define (extract-commit-info i obj-id) (define (extract-commit-info i obj-id)
(define m (regexp-try-match #px"^tree ([0-9a-fA-F]{40})" i)) (define m (regexp-try-match #px"^tree ([0-9a-fA-F]{40})" i))
(unless m (unless m
(error 'git-checkout (raise-git-error 'git-checkout
(~a "cannot extract tree from commit file for ~s\n" (~a "cannot extract tree from commit file for ~s\n"
" content starts: ~s") " content starts: ~s")
(bytes->hex-string obj-id) (bytes->hex-string obj-id)
@ -693,7 +712,7 @@
;; submodule; just make a directory placeholder ;; submodule; just make a directory placeholder
(make-directory* (build-path dest-dir fn))] (make-directory* (build-path dest-dir fn))]
[else [else
(error 'extract-tree "unknown mode: ~s" mode)]) (raise-git-error 'extract-tree "unknown mode: ~s" mode)])
(loop)))))) (loop))))))
;; extract-tree-entry: input-port -> bytes-or-#f bytes-or-#f path-or-#f ;; extract-tree-entry: input-port -> bytes-or-#f bytes-or-#f path-or-#f
@ -758,7 +777,7 @@
(for/list ([l (in-lines i)] (for/list ([l (in-lines i)]
#:unless (equal? l "")) #:unless (equal? l ""))
(define m (regexp-match #rx"^P (.*)" l)) (define m (regexp-match #rx"^P (.*)" l))
(unless m (error 'git-checkout "error parsing packfile list line\n line: ~e" l)) (unless m (raise-git-error 'git-checkout "error parsing packfile list line\n line: ~e" l))
(cadr m))) (cadr m)))
;; read-dumb-packfile : string (hashof string object) tmp conn strung status ;; read-dumb-packfile : string (hashof string object) tmp conn strung status
@ -818,7 +837,7 @@
;; Parse the object description: ;; Parse the object description:
(define header-m (regexp-try-match #rx#"^[^\0]*\0" i)) (define header-m (regexp-try-match #rx#"^[^\0]*\0" i))
(unless header-m (unless header-m
(error 'git-checkout "bad initial line for object content")) (raise-git-error 'git-checkout "bad initial line for object content"))
(define header (car header-m)) (define header (car header-m))
(define header-len (bytes-length header)) (define header-len (bytes-length header))
(define type-sym (string->symbol (define type-sym (string->symbol
@ -827,7 +846,7 @@
(bytes->string/utf-8 (cadr (or (regexp-match #rx"[^ ]* ([0-9]+)" header) (bytes->string/utf-8 (cadr (or (regexp-match #rx"[^ ]* ([0-9]+)" header)
'(#"" #"")))))) '(#"" #""))))))
(unless (memq type-sym valid-types) (unless (memq type-sym valid-types)
(error 'git-checkout "bad type: ~e" type-sym)) (raise-git-error 'git-checkout "bad type: ~e" type-sym))
(define obj (define obj
(save-object (lambda (o) (copy-port i o)) (save-object (lambda (o) (copy-port i o))
@ -882,7 +901,7 @@
(define status (let ([m (regexp-match #rx"^[^ ]* ([0-9]+)" status-line)]) (define status (let ([m (regexp-match #rx"^[^ ]* ([0-9]+)" status-line)])
(and m (string->number (bytes->string/utf-8 (cadr m)))))) (and m (string->number (bytes->string/utf-8 (cadr m))))))
(unless (memv status '(200)) (unless (memv status '(200))
(error 'git-checkout "~a\n server respone: ~a" (raise-git-error 'git-checkout "~a\n server respone: ~a"
msg msg
status-line))) status-line)))
@ -914,7 +933,7 @@
(define (call-with-output-object tmp filename len proc) (define (call-with-output-object tmp filename len proc)
(define (check-len got-len) (define (check-len got-len)
(unless (= len got-len) (unless (= len got-len)
(error 'git-checkout "size mismatch\n expected: ~a\n received: ~a" (raise-git-error 'git-checkout "size mismatch\n expected: ~a\n received: ~a"
len len
got-len))) got-len)))
(cond (cond
@ -981,7 +1000,7 @@
(define bstr (read-bytes len i)) (define bstr (read-bytes len i))
(unless (and (bytes? bstr) (unless (and (bytes? bstr)
(= (bytes-length bstr) len)) (= (bytes-length bstr) len))
(error 'git-checkout (~a "error getting bytes for ~a\n" (raise-git-error 'git-checkout (~a "error getting bytes for ~a\n"
" expected length: ~a\n" " expected length: ~a\n"
" got length: ~a") " got length: ~a")
what what
@ -994,8 +1013,7 @@
(define (read-byte-only what i) (define (read-byte-only what i)
(define c (read-byte i)) (define c (read-byte i))
(unless (byte? c) (unless (byte? c)
(error 'git-checkout "expected to get a byte for ~a, got enf-of-file" (raise-git-error 'git-checkout "expected to get a byte for ~a, got enf-of-file" what))
what))
c) c)
;; copy-port-n : input-port output-port natural -> void ;; copy-port-n : input-port output-port natural -> void
@ -1005,7 +1023,7 @@
(define bstr (read-bytes n i)) (define bstr (read-bytes n i))
(unless (and (bytes? bstr) (unless (and (bytes? bstr)
(= (bytes-length bstr) n)) (= (bytes-length bstr) n))
(error 'git-checkout "not enough bytes during copy")) (raise-git-error 'git-checkout "not enough bytes during copy"))
(write-bytes bstr o)] (write-bytes bstr o)]
[else [else
(copy-port-n i o 4096) (copy-port-n i o 4096)
@ -1068,7 +1086,7 @@
(define cmf (read-byte-only 'zlib-cmf i)) (define cmf (read-byte-only 'zlib-cmf i))
(define flg (read-byte-only 'zlib-flag i)) (define flg (read-byte-only 'zlib-flag i))
(unless (= 8 (bitwise-and cmf #xF)) (unless (= 8 (bitwise-and cmf #xF))
(error 'git-checkout "compression is not `deflate`")) (raise-git-error 'git-checkout "compression is not `deflate`"))
(when (bitwise-bit-set? flg 5) (when (bitwise-bit-set? flg 5)
;; read dictid ;; read dictid
(read-bytes-exactly 'dictid 4 i)) (read-bytes-exactly 'dictid 4 i))
@ -1109,6 +1127,10 @@
(set! ref branch/tag/commit)] (set! ref branch/tag/commit)]
[("--tmp") dir "Write temporary files to <dir>" [("--tmp") dir "Write temporary files to <dir>"
(set! tmp-dir dir)] (set! tmp-dir dir)]
[("-u" "--username") username "Username used to authenticate over HTTP(S)"
(current-git-username username)]
[("-p" "--password") password "Password used to authenticate over HTTP(S)"
(current-git-password password)]
[("--quiet") "Suppress status printouts" [("--quiet") "Suppress status printouts"
(set! status-printf void)] (set! status-printf void)]
#:args (host repo dest) #:args (host repo dest)

View File

@ -0,0 +1,24 @@
#lang racket/base
(require net/git-checkout
racket/list
"config.rkt")
(provide call-with-git-checkout-credentials)
(define (call-with-git-checkout-credentials thunk)
(let loop ([credentials-list (cons #f (get-git-checkout-credentials))])
(define credentials (first credentials-list))
(with-handlers ([exn:fail:git? (λ (exn)
(if (empty? (rest credentials-list))
(raise exn)
(loop (rest credentials-list))))])
(define c (make-custodian))
(parameterize ([current-custodian c]
[current-git-username (and credentials (hash-ref credentials 'username))]
[current-git-password (and credentials (hash-ref credentials 'password))])
(dynamic-wind
void
thunk
(lambda ()
(custodian-shutdown-all c)))))))

View File

@ -4,6 +4,7 @@
racket/path racket/path
racket/match racket/match
racket/format racket/format
racket/string
net/url net/url
"../path.rkt" "../path.rkt"
"dirs.rkt" "dirs.rkt"
@ -37,6 +38,10 @@
(or (current-pkg-network-retries) (or (current-pkg-network-retries)
(read-pkg-cfg/def 'network-retries))) (read-pkg-cfg/def 'network-retries)))
(define (get-git-checkout-credentials)
(or (current-pkg-git-checkout-credentials)
(read-pkg-cfg/def 'git-checkout-credentials)))
(define (read-pkg-cfg/def k) (define (read-pkg-cfg/def k)
;; Lock is held for the current scope, but if ;; Lock is held for the current scope, but if
;; the key is not found in the current scope, ;; the key is not found in the current scope,
@ -56,6 +61,7 @@
['trash-max-packages 512] ['trash-max-packages 512]
['trash-max-seconds (* 60 60 24 2)] ; 2 days ['trash-max-seconds (* 60 60 24 2)] ; 2 days
['network-retries 5] ['network-retries 5]
['git-checkout-credentials '()]
[_ #f])) [_ #f]))
(define c (read-pkg-file-hash (pkg-config-file))) (define c (read-pkg-file-hash (pkg-config-file)))
(define v (hash-ref c k 'none)) (define v (hash-ref c k 'none))
@ -197,6 +203,35 @@
" current package scope: ~a") " current package scope: ~a")
(current-pkg-scope))) (current-pkg-scope)))
(update-pkg-cfg! 'doc-open-url (if (equal? val "") #f val))] (update-pkg-cfg! 'doc-open-url (if (equal? val "") #f val))]
[(list* "git-checkout-credentials" vals)
(define (credentials-format-error msg val)
(pkg-error (~a msg "\n"
" given: ~a\n"
" expected: value in the form <username>:<password>")
val))
(update-pkg-cfg! 'git-checkout-credentials
(for/list ([val (in-list vals)])
(match (string-split val ":" #:trim? #f)
[(list "" _)
(credentials-format-error
"invalid empty username in git checkout credentials"
val)]
[(list _ "")
(credentials-format-error
"invalid empty password in git checkout credentials"
val)]
[(list username password)
`#hasheq((username . ,username)
(password . ,password))]
[(list* _ _ _)
(credentials-format-error
"too many elements for git checkout credentials"
val)]
[(list _)
(credentials-format-error
"not enough elements for git checkout credentials"
val)])))
(displayln "WARNING: checkout credentials are stored UNENCRYPTED" (current-error-port))]
[(list* key args) [(list* key args)
(pkg-error "unsupported config key\n key: ~a" key)])] (pkg-error "unsupported config key\n key: ~a" key)])]
[else [else
@ -220,6 +255,9 @@
(printf "~a~a\n" indent (read-pkg-cfg/def (string->symbol key)))] (printf "~a~a\n" indent (read-pkg-cfg/def (string->symbol key)))]
["doc-open-url" ["doc-open-url"
(printf "~a~a\n" indent (or (read-pkg-cfg/def 'doc-open-url) ""))] (printf "~a~a\n" indent (or (read-pkg-cfg/def 'doc-open-url) ""))]
["git-checkout-credentials"
(for ([creds (in-list (read-pkg-cfg/def 'git-checkout-credentials))])
(printf "~a~a:~a\n" indent (hash-ref creds 'username) (hash-ref creds 'password)))]
[_ [_
(pkg-error "unsupported config key\n key: ~e" key)])] (pkg-error "unsupported config key\n key: ~e" key)])]
[(list) [(list)
@ -237,6 +275,7 @@
"download-cache-dir" "download-cache-dir"
"download-cache-max-files" "download-cache-max-files"
"download-cache-max-bytes" "download-cache-max-bytes"
"git-checkout-credentials"
"trash-max-packages" "trash-max-packages"
"trash-max-seconds" "trash-max-seconds"
"network-retries"))]) "network-retries"))])

View File

@ -11,6 +11,7 @@
"path.rkt" "path.rkt"
"print.rkt" "print.rkt"
"config.rkt" "config.rkt"
"checkout-credentials.rkt"
"network.rkt") "network.rkt")
(provide download-file! (provide download-file!
@ -101,6 +102,8 @@
(define (download!) (define (download!)
(when download-printf (when download-printf
(download-printf "Downloading repository ~a\n" (url->string url))) (download-printf "Downloading repository ~a\n" (url->string url)))
(call-with-git-checkout-credentials
(lambda ()
(call-with-network-retries (call-with-network-retries
(lambda () (lambda ()
(git-checkout host #:port port repo (git-checkout host #:port port repo
@ -109,10 +112,11 @@
#:status-printf (lambda (fmt . args) #:status-printf (lambda (fmt . args)
(define (strip-ending-newline s) (define (strip-ending-newline s)
(regexp-replace #rx"\n$" s "")) (regexp-replace #rx"\n$" s ""))
(log-pkg-debug (strip-ending-newline (apply format fmt args)))) (log-pkg-debug (strip-ending-newline
(apply format fmt args))))
#:transport transport #:transport transport
#:strict-links? #t #:strict-links? #t
#:depth 1))) #:depth 1)))))
(set! unpacked? #t) (set! unpacked? #t)
;; package directory as ".tgz" so it can be cached: ;; package directory as ".tgz" so it can be cached:
(parameterize ([current-directory dest-dir]) (parameterize ([current-directory dest-dir])

View File

@ -34,3 +34,6 @@
(define current-pkg-network-retries (define current-pkg-network-retries
(make-parameter #f)) (make-parameter #f))
(define current-pkg-git-checkout-credentials
(make-parameter #f))

View File

@ -31,6 +31,7 @@
"orig-pkg.rkt" "orig-pkg.rkt"
"git.rkt" "git.rkt"
"prefetch.rkt" "prefetch.rkt"
"checkout-credentials.rkt"
"network.rkt") "network.rkt")
(provide (struct-out install-info) (provide (struct-out install-info)
@ -735,6 +736,8 @@
(define-values (transport host port repo branch path) (define-values (transport host port repo branch path)
(split-git-or-hub-url pkg-url #:type type)) (split-git-or-hub-url pkg-url #:type type))
(download-printf "Querying Git references for ~a at ~a\n" pkg-name pkg-url-str) (download-printf "Querying Git references for ~a at ~a\n" pkg-name pkg-url-str)
(call-with-git-checkout-credentials
(lambda ()
(call-with-network-retries (call-with-network-retries
(lambda () (lambda ()
;; Supplying `#:dest-dir #f` means that we just resolve `branch` ;; Supplying `#:dest-dir #f` means that we just resolve `branch`
@ -751,7 +754,7 @@
" the given URL might not refer to a Git repository\n" " the given URL might not refer to a Git repository\n"
" given URL: ~a") " given URL: ~a")
pkg-url-str)) pkg-url-str))
#:transport transport)))] #:transport transport)))))]
[(github) [(github)
(match-define (list* user repo branch path) (match-define (list* user repo branch path)
(split-github-url pkg-url)) (split-github-url pkg-url))