~ chicken-core (master) /egg-download.scm


  1;;;; egg download
  2;
  3; Copyright (c) 2017-2022, The CHICKEN Team
  4; All rights reserved.
  5;
  6; Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
  7; conditions are met:
  8;
  9;   Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 10;     disclaimer.
 11;   Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 12;     disclaimer in the documentation and/or other materials provided with the distribution.
 13;   Neither the name of the author nor the names of its contributors may be used to endorse or promote
 14;     products derived from this software without specific prior written permission.
 15;
 16; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
 17; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 18; AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 19; CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 20; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 21; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 22; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 23; OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 24; POSSIBILITY OF SUCH DAMAGE.
 25
 26
 27(define +default-tcp-connect-timeout+ 30000) ; 30 seconds
 28(define +default-tcp-read/write-timeout+ 30000) ; 30 seconds
 29(define +url-regex+ "(http://)?([^/:]+)(:([^:/]+))?(/.*)?")
 30(define +max-redirects+ 3)
 31
 32(tcp-connect-timeout +default-tcp-connect-timeout+)
 33(tcp-read-timeout +default-tcp-read/write-timeout+)
 34(tcp-write-timeout +default-tcp-read/write-timeout+)
 35
 36(define user-agent (conc "chicken-install " (chicken-version)))
 37
 38(define (deconstruct-url url)
 39  (let ((m (irregex-match +url-regex+ url)))
 40    (values
 41     (if m (irregex-match-substring m 2) url)
 42     (if (and m (irregex-match-substring m 3))
 43         (let ((port (irregex-match-substring m 4)))
 44           (or (string->number port)
 45	       (error "not a valid port" port)))
 46	 80)
 47     (or (and m (irregex-match-substring m 5))
 48         "/"))))
 49
 50(define (http-fetch host port locn dest proxy-host proxy-port proxy-user-pass)
 51  (let-values (((in out _)
 52    	         (http-connect host port locn proxy-host proxy-port
 53                               proxy-user-pass)))
 54    (http-retrieve-files in out dest)))
 55
 56(define (http-query host port locn proxy-host proxy-port proxy-user-pass)
 57  (let-values (((in out len)
 58    	         (http-connect host port locn proxy-host proxy-port
 59                               proxy-user-pass)))
 60    (close-output-port out)
 61    (http-retrieve-response in len)))
 62
 63(define (http-connect host port locn proxy-host proxy-port proxy-user-pass)
 64  (let next-req ((redirects 0)
 65		 (host host)
 66		 (port port)
 67		 (locn locn)
 68		 (req (make-HTTP-GET/1.1
 69		       locn user-agent host
 70		       port: port accept: "*/*"
 71		       proxy-host: proxy-host proxy-port: proxy-port)))
 72
 73    (when (= redirects +max-redirects+)
 74      (network-failure "too many redirects" redirects))
 75
 76    (d "connecting to host ~s, port ~a ~a...~%" host port
 77       (if proxy-host
 78           (sprintf "(via ~a:~a) " proxy-host proxy-port)
 79           ""))
 80
 81    (let-values (((in out)
 82                  (tcp-connect (or proxy-host host) (or proxy-port port) 'binary)))
 83      (d "requesting ~s ...~%" locn)
 84      (display req out)
 85      (flush-output out)
 86      (d "reading response ...~%")
 87      (let* ((chunked #f)
 88             (datalen #f)
 89             (h1 (read-line in))
 90             (response-match (match-http-response h1)))
 91
 92	(define (process-headers)
 93	  (let ((ln (read-line in)))
 94	    (unless (equal? ln "")
 95	      (cond ((match-chunked-transfer-encoding ln)
 96                     (set! chunked #t))
 97                    ((match-content-length ln) =>
 98                     (lambda (sz) (set! datalen sz)))
 99		    ((match-location ln) =>
100                     (lambda (new-locn)
101		       (set!-values (host port locn)
102				    (deconstruct-url new-locn)))))
103	      (d "~a~%" ln)
104	      (process-headers) ) ) )
105
106	(d "~a~%" h1)
107
108	(cond
109	 ((response-match-code? response-match 407)
110	  (close-input-port in)
111	  (close-output-port out)
112
113	  (d "retrying with proxy auth ~a~%" locn)
114	  (next-req redirects host port locn
115		    (make-HTTP-GET/1.1
116		     locn user-agent host port: port
117		     accept: "*/*"
118		     proxy-host: proxy-host proxy-port: proxy-port
119		     proxy-user-pass: proxy-user-pass)))
120
121	 ((or (response-match-code? response-match 301)
122	      (response-match-code? response-match 302))
123	  (process-headers)
124	  (close-input-port in)
125	  (close-output-port out)
126
127	  (d "redirected to ~a~%" locn)
128	  (next-req (add1 redirects) host port locn
129		    (make-HTTP-GET/1.1
130		     locn user-agent host
131		     port: port accept: "*/*"
132		     proxy-host: proxy-host proxy-port: proxy-port)))
133
134	 ((response-match-code? response-match 200)
135	  (process-headers)
136	  (when chunked
137	    (d "reading chunks ")
138	    (let ((data (read-chunks in)))
139	      (close-input-port in)
140	      (set! in (open-input-bytevector data))) )
141	  (values in out datalen))
142	 (else (network-failure "invalid response from server" h1)))))))
143
144(define (http-retrieve-files in out dest)
145  (d "reading files ...~%")
146  (let ((version #f)
147        (ws (list->string '(#\tab #\newline #\x0b #\space #\x0c #\x0d #\xa0))))
148    (define (skip)
149      (let ((ln (read-line in)))
150        (cond ((or (eof-object? ln)
151   	           (irregex-match " *#!eof *" ln))
152	        (open-input-string ""))
153	       ((irregex-match " *#\\|-+ +([^| ]*) *\\|#.*" ln) =>
154	         (lambda (m)
155		   (let ((v (irregex-match-substring m 1)))
156		     (cond ((or (string=? "" v) (string=? "#f" v)))
157			   ((and version (not (string=? v version)))
158			    (warning "file versions are not identical"
159                              ln version)
160			    (set! version #f))
161			   (else
162			    (set! version v)))
163		     (open-input-string ln))))
164	       ((irregex-match "^[ ]*\\(error .*\\)[ ]*$" ln)
165		 (open-input-string ln)) ; get-files deals with errors
166	       ((irregex-match `(* (,ws)) ln)
167		 (skip)) ; Blank line.
168	       (else
169		 (error "unrecognized file-information - possibly corrupt transmission"
170			ln)))))
171    (let get-files ((files '()))
172      (let* ((ins (skip))
173             (name (read ins)))
174        (cond ((and (pair? name) (eq? 'error (car name)))
175            	   (server-error (cadr name) (cddr name)))
176	      ((or (eof-object? name) (not name))
177	        (close-input-port in)
178	        (close-output-port out)
179	        version)
180	      ((not (string? name))
181	        (error "invalid file name - possibly corrupt transmission"
182                       name) )
183	      ((string-suffix? "/" name)
184	        (d "  ~a~%" name)
185	        (create-directory (make-pathname dest name))
186	        (get-files files) )
187	      (else
188	        (let* ((size (read ins))
189	               (_ (d "  ~a (~a bytes)~%" name size))
190	      	       (data (read-bytevector size in)) )
191		  (with-output-to-file (make-pathname dest name)
192                    (lambda () (write-bytevector data))
193                    #:binary ) )
194		(get-files (cons name files)) ) ) ) ) ))
195
196(define (http-retrieve-response in len)
197  (d "reading response ...~%")
198  (let ((data (read-string len in)))
199    (close-input-port in)
200    data))
201
202(define (server-error msg args)
203  (abort
204     (make-composite-condition
205      (make-property-condition
206       'exn
207       'message (string-append "[Server] " msg)
208       'arguments args)
209      (make-property-condition 'setup-download-error))))
210
211(define (read-chunks in)
212  (let get-chunks ((data '()))
213    (let* ((line (read-line in))
214           (size (and (not (eof-object? line))
215                      (string->number line 16))))
216      (cond ((not size)
217	       (error "invalid response from server - please try again"))
218            ((zero? size)
219               (d "~%")
220               (apply bytevector-append (reverse data)))
221	    (else
222	       (let ((chunk (read-bytevector size in)))
223		 (d ".")
224		 (read-line in)
225		 (get-chunks (cons chunk data)) ) ) ) ) ))
226
227(define (match-http-response rsp)
228  (and (string? rsp)
229       (irregex-match "HTTP/[0-9.]+\\s+([0-9]+)\\s+.*" rsp)) )
230
231(define (response-match-code? mrsp code)
232  (and mrsp (string=? (number->string code)
233                      (irregex-match-substring mrsp 1))) )
234
235(define (match-chunked-transfer-encoding ln)
236  (irregex-match "[Tt]ransfer-[Ee]ncoding:\\s*chunked.*" ln) )
237
238(define (match-location ln)
239  (let ((m (irregex-match "[Ll]ocation:\\s*(.+)\\s*" ln)))
240    (and m (irregex-match-substring m 1))))
241
242(define (match-content-length ln)
243  (let ((m (irregex-match "[Cc]ontent-[Ll]ength:\\s*([0-9]+).*" ln)))
244    (and m (string->number (irregex-match-substring m 1)))))
245
246(define (make-HTTP-GET/1.1 location user-agent host
247                           #!key
248                           (port 80)
249                           (connection "close")
250                           (accept "*")
251                           (content-length 0)
252                     	     proxy-host proxy-port proxy-user-pass)
253  (conc
254     "GET "
255     (if proxy-host
256	 (string-append "http://" host location)
257	 location)
258     " HTTP/1.1" "\r\n"
259     "Connection: " connection "\r\n"
260     "User-Agent: " user-agent "\r\n"
261     "Accept: " accept "\r\n"
262     "Host: " host #\: port "\r\n"
263     (if proxy-user-pass
264         (string-append "Proxy-Authorization: Basic " proxy-user-pass "\r\n")
265         "")
266     "Content-length: " content-length "\r\n"
267     "\r\n") )
268
269(define (network-failure msg . args)
270  (signal
271     (make-composite-condition
272      (make-property-condition
273       'exn
274       'message msg
275       'arguments args)
276      (make-property-condition 'http-fetch))) )
277
278
279;; entry points
280
281(define (list-versions egg url)
282  (receive (host port locn) (deconstruct-url url)
283    (let ((locn (conc locn
284                      "?name=" egg
285                      "&release=" major-version
286                      "&mode=default"
287                      "&listversions=1")))
288      (let ((data	(http-query host port locn proxy-host
289                              proxy-port proxy-user-pass)))
290        (string-split data)))))
291
292(define (try-list-versions name url #!key
293                           proxy-host proxy-port proxy-user-pass)
294  (d "listing versions for ~a: ~a~%" name url)
295  (condition-case (list-versions name url)
296    ((exn net)
297       (print "TCP connect timeout")
298       #f)
299    ((exn http-fetch)
300       (print "HTTP protocol error")
301       #f)
302    (e (exn setup-download-error)
303	 (print "Server error:")
304	 (print-error-message e)
305	 #f)
306    (e () (abort e) )))
307
308(define (download-egg egg url #!key version destination tests
309                      proxy-host proxy-port proxy-user-pass)
310  (receive (host port locn) (deconstruct-url url)
311    (let* ((locn (conc locn
312                       "?name=" egg
313                       "&release=" major-version
314                       (if version (string-append "&version=" version) "")
315                       "&mode=default"
316                       (if tests "&tests=yes" "")))
317	   (eggdir destination))
318        (let ((fversion	(http-fetch host port locn eggdir proxy-host
319                                    proxy-port proxy-user-pass)))
320	  ;; If we get here then version of egg exists
321	  (values eggdir (or fversion version "")) )) ) )
322
323(define (try-download name url #!key version destination tests
324                      proxy-host proxy-port proxy-user-pass)
325  (d "downloading ~a: ~a~%" name url)
326  (condition-case
327     (download-egg
328         name url
329         version: version
330         destination: destination
331         tests: tests
332 	 proxy-host: proxy-host
333	 proxy-port: proxy-port
334	 proxy-user-pass: proxy-user-pass)
335    ((exn net)
336       (print "TCP connect timeout")
337       (values #f "") )
338    ((exn http-fetch)
339       (print "HTTP protocol error")
340       (values #f "") )
341    (e (exn setup-download-error)
342	 (print "Server error:")
343	 (print-error-message e)
344	 (values #f ""))
345    (e () (abort e) )))
Trap