source: project/release/4/http-client/trunk/http-client.scm @ 20960

Last change on this file since 20960 was 20960, checked in by sjamaan, 10 years ago

Tag http-client 0.2

File size: 27.8 KB
Line 
1;;
2;; Convenient HTTP client library
3;;
4; Copyright (c) 2008-2010, Peter Bex
5; Parts copyright (c) 2000-2004, Felix L. Winkelmann
6; All rights reserved.
7;
8; Redistribution and use in source and binary forms, with or without
9; modification, are permitted provided that the following conditions
10; are met:
11;
12; 1. Redistributions of source code must retain the above copyright
13;    notice, this list of conditions and the following disclaimer.
14; 2. Redistributions in binary form must reproduce the above copyright
15;    notice, this list of conditions and the following disclaimer in the
16;    documentation and/or other materials provided with the distribution.
17; 3. Neither the name of the author nor the names of its
18;    contributors may be used to endorse or promote products derived
19;    from this software without specific prior written permission.
20;
21; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24; FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25; COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
26; INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
27; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
30; STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32; OF THE POSSIBILITY OF SUCH DAMAGE.
33
34(module http-client
35  (max-retry-attempts max-redirect-depth retry-request? client-software
36   close-connection! close-all-connections!
37   call-with-input-request with-input-from-request call-with-response
38   store-cookie! delete-cookie! get-cookies-for-uri
39   determine-username/password determine-proxy determine-proxy-from-environment)
40
41(import chicken scheme lolevel)
42(use srfi-1 srfi-13 srfi-18 srfi-69
43     ports extras tcp data-structures
44     openssl intarweb uri-common message-digest md5)
45
46;; Major TODOs:
47;; * Find a better approach for storing cookies and server connections,
48;;    which will scale to applications with hundreds of connections
49;; * Implement md5-sess handling for digest auth
50;; * Use nonce count in digest auth (is this even needed? I think it's only
51;;    needed if there are webservers out there that send the same nonce
52;;    repeatedly. This client doesn't do request pipelining so we don't
53;;    generate requests with the same nonce if the server doesn't)
54;; * Find a way to do automated testing to increase robustness & reliability
55;; * Test and document SSL support
56
57(define-record http-connection base-uri inport outport proxy)
58
59(define max-retry-attempts (make-parameter 1))
60(define max-redirect-depth (make-parameter 5))
61
62(define retry-request? (make-parameter idempotent?))
63
64(define (determine-proxy-from-environment uri)
65  (let* ((proxy-variable (conc (uri-scheme uri) "_proxy"))
66         (no-proxy (or (get-environment-variable "no_proxy")
67                       (get-environment-variable "NO_PROXY")))
68         (no-proxy (and no-proxy (map (lambda (s)
69                                        (string-split s ":"))
70                                      (string-split no-proxy ","))))
71         (host-excluded? (lambda (entry)
72                           (let ((host (car entry))
73                                 (port (and (pair? (cdr entry))
74                                            (string->number (cadr entry)))))
75                             (and (or (string=? host "*")
76                                      (string-ci=? host (uri-host uri)))
77                                  (or (not port)
78                                      (= (uri-port uri) port)))))))
79    (cond
80     ((and no-proxy (any host-excluded? no-proxy)) #f)
81     ((or (get-environment-variable proxy-variable)
82          (get-environment-variable (string-upcase proxy-variable))
83          (get-environment-variable "all_proxy")
84          (get-environment-variable "ALL_PROXY")) =>
85          (lambda (proxy)               ; TODO: make this just absolute-uri
86            (and-let* ((proxy-uri (uri-reference proxy))
87                       ((absolute-uri? proxy-uri)))
88              proxy-uri)))
89     (else #f))))
90
91(define determine-proxy (make-parameter determine-proxy-from-environment))
92
93(define determine-proxy-username/password
94  (make-parameter (lambda (uri realm)
95                    (values (uri-username uri) (uri-password uri)))))
96
97;; Maybe only pass uri and realm to this?
98(define determine-username/password
99  (make-parameter (lambda (uri realm)
100                    (values (uri-username uri) (uri-password uri)))))
101
102(define client-software
103  (make-parameter (list (list "Chicken Scheme HTTP-client" "0.2" #f))))
104
105;; TODO: find a smarter storage mechanism
106(define cookie-jar (list))
107
108(define connections
109  (make-parameter (make-hash-table
110                   (lambda (a b)
111                     (and (equal? (uri-port a) (uri-port b))
112                          (equal? (uri-host a) (uri-host b))))
113                   (lambda (uri . maybe-bound)
114                     (apply string-hash
115                            (sprintf "~S ~S" (uri-host uri) (uri-port uri))
116                            maybe-bound)))))
117
118(define connections-owner
119  (make-parameter (current-thread)))
120
121(define (ensure-local-connections)
122  (unless (eq? (connections-owner) (current-thread))
123    (connections (make-hash-table equal?))
124    (connections-owner (current-thread))))
125
126(cond-expand
127  ((not has-port-closed)
128   (define (port-closed? p)
129     (##sys#check-port p 'port-closed?)
130     (##sys#slot p 8)))
131  (else))
132
133(define (get-connection uri)
134  (ensure-local-connections)
135  (and-let* ((con (hash-table-ref/default (connections) uri #f)))
136    (if (or (port-closed? (http-connection-inport con))
137            (port-closed? (http-connection-outport con)))
138        (begin (close-connection! uri) #f)
139        con)))
140
141(define (add-connection! uri con)
142  (ensure-local-connections)
143  (hash-table-set! (connections) uri con))
144
145(define (close-connection! uri-or-con)
146  (ensure-local-connections)
147  (and-let* ((con (if (http-connection? uri-or-con)
148                      uri-or-con
149                      (hash-table-ref/default (connections) uri-or-con #f))))
150    (close-input-port (http-connection-inport con))
151    (close-output-port (http-connection-outport con))
152    (hash-table-delete! (connections) (http-connection-base-uri con))))
153
154(define (close-all-connections!)
155  (ensure-local-connections)
156  (hash-table-walk
157   (connections)
158   (lambda (uri con)
159     (hash-table-delete! (connections) uri)
160     (close-input-port (http-connection-inport con))
161     (close-output-port (http-connection-outport con)))))
162
163(define (ensure-connection! uri)
164  (or (get-connection uri)
165      (let* ((proxy ((determine-proxy) uri))
166             (remote-end (or proxy uri)))
167       (receive (in out)
168         (case (uri-scheme remote-end)
169           ((#f http) (tcp-connect (uri-host remote-end) (uri-port remote-end)))
170           ((https) (ssl-connect (uri-host remote-end) (uri-port remote-end)))
171           (else (error "Unknown URI scheme" (uri-scheme remote-end))))
172         (let ((con (make-http-connection uri in out proxy)))
173           (add-connection! uri con)
174           con)))))
175
176(define (make-delimited-input-port port len)
177  (if (not len)
178      port ;; no need to delimit anything
179      (let ((pos 0))
180        (make-input-port (lambda () ; read
181                           (if (= pos len)
182                               #!eof
183                               (let ((char (read-char port)))
184                                 (set! pos (add1 pos))
185                                 char)))
186                         (lambda () ; char-ready?
187                           (if (= pos len)
188                               #f
189                               (char-ready? port)))
190                         (lambda () ; close
191                           (close-input-port port))))))
192
193(define (read-response-data response)
194  (let ((len (header-value 'content-length (response-headers response))))
195    ;; If the header is not available, this will read until EOF
196    (read-string len (response-port response))))
197
198(define (add-headers req)
199  (let* ((uri (request-uri req))
200         (cookies (get-cookies-for-uri (request-uri req)))
201         (h `(,@(if (not (null? cookies)) `((cookie . ,cookies)) '())
202              (host ,(cons (uri-host uri) (and (not (uri-default-port? uri))
203                                               (uri-port uri))))
204              ,@(if (and (client-software) (not (null? (client-software))))
205                    `((user-agent ,(client-software)))
206                    '()))))
207    (update-request req
208                    headers: (headers h (request-headers req)))))
209
210(define (http-client-error loc msg specific . rest)
211  (raise (make-composite-condition
212          (make-property-condition 'exn 'location loc 'message msg)
213          (make-property-condition 'http)
214          (apply make-property-condition specific rest))))
215
216;; RFC 2965, section 3.3.3
217(define (cookie-eq? a b)
218  (let ((ref (lambda (cookie attr) (get-param attr cookie ""))))
219    (and (string-ci=? (car (get-value a)) (car (get-value b)))
220         (string-ci=? (ref a 'domain)     (ref b 'domain))
221         (string=?    (ref a 'path)       (ref b 'path)))))
222
223(define (store-cookie! cookie)
224  (let loop ((jar cookie-jar))
225    (cond
226     ((null? jar) (set! cookie-jar (cons cookie cookie-jar)) cookie-jar)
227     ((cookie-eq? cookie (car jar))
228      (set-car! jar cookie)
229      cookie-jar)
230     (else (loop (cdr jar))))))
231
232(define (delete-cookie! cookie)
233  (set! cookie-jar (delete! cookie cookie-jar cookie-eq?)))
234
235(define (domain-match? uri pattern)
236  (let ((target (uri-host uri)))
237    (or (string-ci=? target pattern)
238        (and (string-prefix? "." pattern)
239             (string-suffix-ci? pattern target)))))
240
241;; Things might be simpler if we used path list representation...
242(define (path-match? uri path)
243  (and (uri-path-absolute? uri)
244       (let loop ((path (cdr (string-split path "/" #t)))
245                  (uri-path (cdr (uri-path uri))))
246         (or (null? path)               ; done
247             (and (not (null? uri-path))
248                  (or (and (string-null? (car path)) (null? (cdr path)))
249
250                      (and (string=? (car path) (car uri-path))
251                           (loop (cdr path) (cdr uri-path)))))))))
252
253;; We store slightly more info about a cookie than a cookie header
254;; accepts, so filter it.
255(define (cookie-info->cookie info)
256  (vector (get-value info)
257          (filter (lambda (p)
258                    (member (car p) '(domain path version)))
259                  (get-params info))))
260
261(define (get-cookies-for-uri uri)
262  (let ((uri (if (string? uri) (uri-reference uri) uri)))
263    (map cookie-info->cookie
264         (sort!
265          (filter (lambda (c)
266                    (and (domain-match? uri (get-param 'domain c))
267                         (member (uri-port uri)
268                                 (get-param 'port c (list (uri-port uri))))
269                         (path-match? uri (get-param 'path c))
270                         (if (get-param 'secure c)
271                             (member (uri-scheme uri) '(https shttp))
272                             #t)))
273                  cookie-jar)
274          (lambda (a b)
275            (< (string-length (get-param 'path a))
276               (string-length (get-param 'path b))))))))
277
278(define (process-set-cookie! con uri r)
279  (let ((prefix-contains-dots?
280         (lambda (host pattern)
281           (string-index host #\. 0 (string-contains-ci host pattern)))))
282    (for-each (lambda (c)
283                (and-let* (((path-match? uri (get-param 'path c)))
284                           ;; really, path should be in separated form!
285                           (path (get-param 'path c (string-join (cdr (uri-path uri)) "/")))
286                           ;; domain must start with dot. Add to intarweb!
287                           (dn (get-param 'domain c (uri-host uri)))
288                           (idx (string-index dn #\.))
289                           ((domain-match? uri dn))
290                           ((not (prefix-contains-dots? (uri-host uri) dn))))
291                  ;; Store only the cookie values we understand and need
292                  (store-cookie! (vector (get-value c)
293                                         `((version . ,(get-param 'version c))
294                                           (path . ,path)
295                                           (domain . ,dn)
296                                           (secure . ,(get-param 'secure c)))))))
297              (header-contents 'set-cookie (response-headers r) '()))
298    (for-each (lambda (c)
299                (and-let* (((get-param 'version c)) ; required for set-cookie2
300                           (path (get-param 'path c (string-join (cdr (uri-path uri)) "/")))
301                           ((path-match? uri (get-param 'path c)))
302                           (dn (get-param 'domain c (uri-host uri)))
303                           ((or (string-ci=? dn ".local")
304                                (and (not (string-null? dn))
305                                     (string-index dn #\. 1))))
306                           ((domain-match? uri dn))
307                           ((not (prefix-contains-dots? (uri-host uri) dn)))
308                           ;; This is a little bit too messy for my tastes...
309                           ;; Can't use #f because that would shortcut and-let*
310                           (ports-value (get-param 'port c 'any))
311                           (ports (if (eq? ports-value #t)
312                                      (list (uri-port uri))
313                                      ports-value))
314                           ((or (eq? ports 'any)
315                                (member (uri-port uri) ports))))
316                  ;; Store only the cookie values we understand and need
317                  (store-cookie! (vector (get-value c)
318                                         `((version . ,(get-param 'version c))
319                                           (path . ,path)
320                                           (domain . ,dn)
321                                           (port . ,(if (eq? ports 'any)
322                                                        #f
323                                                        ports))
324                                           (secure . ,(get-param 'secure c)))))))
325              (header-contents 'set-cookie2 (response-headers r) '()))))
326
327(define (call-with-output-digest primitive proc)
328  (let* ((ctx-info (message-digest-primitive-context-info primitive))
329         (ctx (if (procedure? ctx-info) (ctx-info) (allocate ctx-info)))
330         (update-digest (message-digest-primitive-update primitive))
331         (update (lambda (str) (update-digest ctx str (string-length str))))
332         (outport (make-output-port update void)))
333    (handle-exceptions exn
334      (unless (procedure? ctx-info) (free ctx))
335      (let ((result (make-string
336                     (message-digest-primitive-digest-length primitive))))
337        ((message-digest-primitive-init primitive) ctx)
338        (proc outport)
339        ((message-digest-primitive-final primitive) ctx result)
340        (unless (procedure? ctx-info) (free ctx))
341        (byte-string->hexadecimal result)))))
342
343(define (authenticate-request request response writer proxy-uri)
344  (and-let* ((type (if (= (response-code response) 401) 'auth 'proxy))
345             (resp-header (if (eq? type 'auth)
346                              'www-authenticate
347                              'proxy-authenticate))
348             (req-header (if (eq? type 'auth)
349                             'authorization
350                             'proxy-authorization))
351             (authenticate (if (eq? type 'auth)
352                               (determine-username/password)
353                               (determine-proxy-username/password)))
354             (authtype (header-value resp-header (response-headers response)))
355             (realm (header-param 'realm resp-header (response-headers response)))
356             (auth-uri (if (eq? type 'auth) (request-uri request) proxy-uri)))
357    (receive (username password)
358      (authenticate auth-uri realm)
359      (and username password
360           ;; TODO: Maybe we should implement a way to make it ask
361           ;; the question only once. This would be faster, but
362           ;; maybe less secure. (we should at least use domain info)
363           (case authtype
364             ((basic)
365              (update-request
366               request
367               headers: (headers
368                         `((,req-header
369                            #(basic ((username . ,username)
370                                     (password . ,password)))))
371                         (request-headers request))))
372             ((digest)
373              (let* ((hashconc
374                      (lambda args
375                        (md5-digest (string-join (map ->string args) ":"))))
376                     (authless-uri (update-uri (request-uri request)
377                                               username: #f password: #f))
378                     ;; TODO: domain handling
379                     (h (response-headers response))
380                     (nonce (header-param 'nonce resp-header h))
381                     (opaque (header-param 'opaque resp-header h))
382                     (stale (header-param 'stale resp-header h))
383                     ;; TODO: "md5-sess" algorithm handling
384                     (algorithm (header-param 'algorithm resp-header h))
385                     (qops (header-param 'qop resp-header h '()))
386                     (qop (cond ; Pick the strongest of the offered options
387                           ((member 'auth-int qops) 'auth-int)
388                           ((member 'auth qops) 'auth)
389                           (else #f)))
390                     (cnonce (and qop (hashconc (current-seconds) realm)))
391                     (nc (and qop 1)) ;; TODO
392                     (ha1 (hashconc username realm password))
393                     (ha2 (if (eq? qop 'auth-int)
394                              (hashconc (request-method request)
395                                        (uri->string authless-uri)
396                                        ;; Generate digest from writer's output
397                                        (call-with-output-digest
398                                         (md5-primitive)
399                                         (lambda (p)
400                                           (writer
401                                            (update-request request port: p)))))
402                              (hashconc (request-method request)
403                                        (uri->string authless-uri))))
404                     (digest
405                      (case qop
406                        ((auth-int auth)
407                         (let ((hex-nc (string-pad (number->string nc 16) 8 #\0)))
408                           (hashconc ha1 nonce hex-nc cnonce qop ha2)))
409                        (else
410                         (hashconc ha1 nonce ha2)))))
411                (update-request request
412                                headers: (headers
413                                          `((,req-header
414                                             #(digest ((username . ,username)
415                                                       (uri . ,authless-uri)
416                                                       (realm . ,realm)
417                                                       (nonce . ,nonce)
418                                                       (cnonce . ,cnonce)
419                                                       (qop . ,qop)
420                                                       (nc . ,nc)
421                                                       (response . ,digest)
422                                                       (opaque . ,opaque)))))
423                                          (request-headers request)))))
424             (else (http-client-error 'authenticate-request
425                                      "Unknown authentication type"
426                                      'unknown-authtype 'authtype authtype)))))))
427
428(define (call-with-response req writer reader)
429  (let loop ((attempts 0)
430             (redirects 0)
431             (req req))
432    (condition-case
433      (let* ((con (ensure-connection! (request-uri req)))
434             (req (add-headers (update-request
435                                req port: (http-connection-outport con))))
436             ;; No outgoing URIs should ever contain credentials or fragments
437             (req-uri (update-uri (request-uri req)
438                                  fragment: #f username: #f password: #f))
439             ;; RFC1945, 5.1.2: "The absoluteURI form is only allowed
440             ;; when the request is being made to a proxy."
441             ;; RFC2616 is a little more regular (hosts MUST accept
442             ;; absoluteURI), but it says "HTTP/1.1 clients will only
443             ;; generate them in requests to proxies." (also 5.1.2)
444             (req-uri (if (http-connection-proxy con)
445                          req-uri
446                          (update-uri req-uri host: #f port: #f scheme: #f
447                                      path: (or (uri-path req-uri) '(/ "")))))
448             (request (write-request (update-request req uri: req-uri)))
449             ;; Writer should be prepared to be called several times
450             ;; Maybe try and figure out a good way to use the
451             ;; "Expect: 100-continue" header to prevent too much writing?
452             ;; Unfortunately RFC2616 says it's unreliable (8.2.3)...
453             (_ (begin (writer request) (flush-output (request-port req))))
454             (response (read-response (http-connection-inport con)))
455             (cleanup! (lambda (clear-response-data?)
456                         (when clear-response-data?
457                           (read-response-data response))
458                         (unless (and (keep-alive? request)
459                                      (keep-alive? response))
460                           (close-connection! con)))))
461        (process-set-cookie! con (request-uri req) response)
462        (case (response-code response)
463          ;; TODO: According to spec, we should provide the user with a choice
464          ;; when it's not a GET or HEAD request...
465          ((301 302 303 307)
466           (cleanup! #t)
467           ;; Maybe we should switch to GET on 302 too?  It's not compliant,
468           ;; but very widespread and there's enough software that depends
469           ;; on that behaviour, which might break horribly otherwise...
470           (when (= (response-code response) 303)
471             (request-method-set! request 'GET)) ; Switch to GET
472           (let ((new-uri (header-value 'location (response-headers response))))
473             (if (or (not (max-redirect-depth)) ; unlimited?
474                     (<= redirects (max-redirect-depth)))
475                 (loop attempts
476                       (add1 redirects)
477                       (update-request req uri: (uri-relative-to
478                                                 new-uri (request-uri req))))
479                 (http-client-error 'send-request
480                                    "Maximum number of redirects exceeded"
481                                    'redirect-depth-exceeded
482                                    'uri new-uri))))
483          ;; TODO: Test this
484          ((305)                        ; Use proxy (for this request only)
485           (cleanup! #t)
486           (let ((old-determine-proxy (determine-proxy))
487                 (proxy-uri (header-value 'location (response-headers response))))
488             (parameterize ((determine-proxy
489                             (lambda _
490                               ;; Reset determine-proxy so the proxy is really
491                               ;; used for only this one request.
492                               ;; Yes, this is a bit of a hack :)
493                               (determine-proxy old-determine-proxy)
494                               proxy-uri)))
495               (loop attempts redirects req))))
496          ((401 407)          ; Unauthorized, Proxy Authentication Required
497           (or (and-let* (((or (not (max-retry-attempts)) ; unlimited?
498                               (<= attempts (max-retry-attempts))))
499                          (new-req (authenticate-request
500                                    req response writer
501                                    (http-connection-proxy con))))
502                 (cleanup! #t)
503                 (loop (add1 attempts) redirects new-req))
504               ;; pass it on, we can't throw an error here
505               (let ((data (reader response)))
506                 (values data (request-uri request) response))))
507          (else (let ((data (reader response)))
508                  (cleanup! #f)
509                  (values data (request-uri req) response)))))
510        (exn (exn i/o net)
511             (close-connection! (request-uri req))
512             (if (and (or (not (max-retry-attempts)) ; unlimited?
513                          (<= attempts (max-retry-attempts)))
514                      ((retry-request?) req))
515                 (loop (add1 attempts) redirects req)
516                 (raise exn)))
517        (exn ()
518             ;; Never leave the port in an unknown/inconsistent state
519             ;; (the error could have occurred while reading, so there
520             ;;  might be data left in the buffer)
521             (close-connection! (request-uri req))
522             (raise exn)))))
523
524(define (call-with-input-request uri-or-request writer reader)
525  ;; "writer" is an alist to be encoded as form?
526  (let* ((postdata (or (and (string? writer) writer)
527                       (and (list? writer)
528                            (form-urlencode writer separator: "&"))))
529         (write-data! (if writer
530                          (if postdata
531                              (lambda (p)
532                                (display postdata p))
533                              writer)
534                          (lambda x (void))))
535         (uri (cond ((uri? uri-or-request) uri-or-request)
536                    ((string? uri-or-request) (uri-reference uri-or-request))
537                    (else (request-uri uri-or-request))))
538         (req (if (request? uri-or-request)
539                  uri-or-request
540                  (make-request uri: uri)))
541         (req (if postdata
542                  (update-request req
543                   headers: (headers
544                             `((content-length ,(string-length postdata))
545                               ,@(if (list? writer)
546                                     `((content-type
547                                        application/x-www-form-urlencoded))
548                                     `()))
549                             (request-headers req)))
550                  req)))
551    (call-with-response
552     req
553     (lambda (request)
554       (let ((port (request-port request)))
555         (write-data! port)))
556     (lambda (response)
557       (let ((port (make-delimited-input-port
558                    (response-port response)
559                    (header-value 'content-length (response-headers response)))))
560         (if (= 200 (response-class response)) ; Everything cool?
561             (reader port)
562             (http-client-error
563              'call-with-input-request
564              ;; Message
565              (sprintf (case (response-class response)
566                         ((400) "Client error: ~A ~A")
567                         ((500) "Server error: ~A ~A")
568                         (else "Unexpected server response: ~A ~A"))
569                       (response-code response) (response-reason response))
570              ;; Specific type
571              (case (response-class response)
572                ((400) 'client-error)
573                ((500) 'server-error)
574                (else 'unexpected-server-response))
575              'response response
576              'body (read-string #f port))))))))
577
578(define (with-input-from-request uri-or-request writer reader)
579  (call-with-input-request uri-or-request
580                           (if (procedure? writer)
581                               (lambda (p) (with-output-to-port p writer))
582                               writer) ;; Assume it's an alist
583                           (lambda (p) (with-input-from-port p reader))))
584
585)
Note: See TracBrowser for help on using the repository browser.