]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - usr.sbin/portsnap/portsnap/portsnap.sh
Upgrade to OpenSSH 7.8p1.
[FreeBSD/FreeBSD.git] / usr.sbin / portsnap / portsnap / portsnap.sh
1 #!/bin/sh
2
3 #-
4 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
5 #
6 # Copyright 2004-2005 Colin Percival
7 # All rights reserved
8 #
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted providing that the following conditions 
11 # are met:
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 #
18 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
27 # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 # POSSIBILITY OF SUCH DAMAGE.
29
30 # $FreeBSD$
31
32 #### Usage function -- called from command-line handling code.
33
34 # Usage instructions.  Options not listed:
35 # --debug       -- don't filter output from utilities
36 # --no-stats    -- don't show progress statistics while fetching files
37 usage() {
38         cat <<EOF
39 usage: `basename $0` [options] command ... [path]
40
41 Options:
42   -d workdir   -- Store working files in workdir
43                   (default: /var/db/portsnap/)
44   -f conffile  -- Read configuration options from conffile
45                   (default: /etc/portsnap.conf)
46   -I           -- Update INDEX only. (update command only)
47   -k KEY       -- Trust an RSA key with SHA256 hash of KEY
48   -l descfile  -- Merge the specified local describes file into the INDEX.
49   -p portsdir  -- Location of uncompressed ports tree
50                   (default: /usr/ports/)
51   -s server    -- Server from which to fetch updates.
52                   (default: portsnap.FreeBSD.org)
53   --interactive -- interactive: override auto-detection of calling process
54                   (use this when calling portsnap from an interactive, non-
55                   terminal application AND NEVER ELSE).
56   path         -- Extract only parts of the tree starting with the given
57                   string.  (extract command only)
58 Commands:
59   fetch        -- Fetch a compressed snapshot of the ports tree,
60                   or update an existing snapshot.
61   cron         -- Sleep rand(3600) seconds, and then fetch updates.
62   extract      -- Extract snapshot of ports tree, replacing existing
63                   files and directories.
64   update       -- Update ports tree to match current snapshot, replacing
65                   files and directories which have changed.
66   auto         -- Fetch updates, and either extract a new ports tree or
67                   update an existing tree.
68 EOF
69         exit 0
70 }
71
72 #### Parameter handling functions.
73
74 # Initialize parameters to null, just in case they're
75 # set in the environment.
76 init_params() {
77         KEYPRINT=""
78         EXTRACTPATH=""
79         WORKDIR=""
80         PORTSDIR=""
81         CONFFILE=""
82         COMMAND=""
83         COMMANDS=""
84         QUIETREDIR=""
85         QUIETFLAG=""
86         STATSREDIR=""
87         XARGST=""
88         NDEBUG=""
89         DDSTATS=""
90         INDEXONLY=""
91         SERVERNAME=""
92         REFUSE=""
93         LOCALDESC=""
94         INTERACTIVE=""
95 }
96
97 # Parse the command line
98 parse_cmdline() {
99         while [ $# -gt 0 ]; do
100                 case "$1" in
101                 -d)
102                         if [ $# -eq 1 ]; then usage; fi
103                         if [ ! -z "${WORKDIR}" ]; then usage; fi
104                         shift; WORKDIR="$1"
105                         ;;
106                 --debug)
107                         QUIETREDIR="/dev/stderr"
108                         STATSREDIR="/dev/stderr"
109                         QUIETFLAG=" "
110                         NDEBUG=" "
111                         XARGST="-t"
112                         DDSTATS=".."
113                         ;;
114                 --interactive)
115                         INTERACTIVE="YES"
116                         ;;
117                 -f)
118                         if [ $# -eq 1 ]; then usage; fi
119                         if [ ! -z "${CONFFILE}" ]; then usage; fi
120                         shift; CONFFILE="$1"
121                         ;;
122                 -h | --help | help)
123                         usage
124                         ;;
125                 -I)
126                         INDEXONLY="YES"
127                         ;;
128                 -k)
129                         if [ $# -eq 1 ]; then usage; fi
130                         if [ ! -z "${KEYPRINT}" ]; then usage; fi
131                         shift; KEYPRINT="$1"
132                         ;;
133                 -l)
134                         if [ $# -eq 1 ]; then usage; fi
135                         if [ ! -z "${LOCALDESC}" ]; then usage; fi
136                         shift; LOCALDESC="$1"
137                         ;;
138                 --no-stats)
139                         if [ -z "${STATSREDIR}" ]; then
140                                 STATSREDIR="/dev/null"
141                                 DDSTATS=".. "
142                         fi
143                         ;;
144                 -p)
145                         if [ $# -eq 1 ]; then usage; fi
146                         if [ ! -z "${PORTSDIR}" ]; then usage; fi
147                         shift; PORTSDIR="$1"
148                         ;;
149                 -s)
150                         if [ $# -eq 1 ]; then usage; fi
151                         if [ ! -z "${SERVERNAME}" ]; then usage; fi
152                         shift; SERVERNAME="$1"
153                         ;;
154                 cron | extract | fetch | update | auto)
155                         COMMANDS="${COMMANDS} $1"
156                         ;;
157                 up)
158                         COMMANDS="${COMMANDS} update"
159                         ;;
160                 alfred)
161                         COMMANDS="${COMMANDS} auto"
162                         ;;
163                 *)
164                         if [ $# -gt 1 ]; then usage; fi
165                         if echo ${COMMANDS} | grep -vq extract; then
166                                 usage
167                         fi
168                         EXTRACTPATH="$1"
169                         ;;
170                 esac
171                 shift
172         done
173
174         if [ -z "${COMMANDS}" ]; then
175                 usage
176         fi
177 }
178
179 # If CONFFILE was specified at the command-line, make
180 # sure that it exists and is readable.
181 sanity_conffile() {
182         if [ ! -z "${CONFFILE}" ] && [ ! -r "${CONFFILE}" ]; then
183                 echo -n "File does not exist "
184                 echo -n "or is not readable: "
185                 echo ${CONFFILE}
186                 exit 1
187         fi
188 }
189
190 # If a configuration file hasn't been specified, use
191 # the default value (/etc/portsnap.conf)
192 default_conffile() {
193         if [ -z "${CONFFILE}" ]; then
194                 CONFFILE="/etc/portsnap.conf"
195         fi
196 }
197
198 # Read {KEYPRINT, SERVERNAME, WORKDIR, PORTSDIR} from the configuration
199 # file if they haven't already been set.  If the configuration
200 # file doesn't exist, do nothing.
201 # Also read REFUSE (which cannot be set via the command line) if it is
202 # present in the configuration file.
203 parse_conffile() {
204         if [ -r "${CONFFILE}" ]; then
205                 for X in KEYPRINT WORKDIR PORTSDIR SERVERNAME; do
206                         eval _=\$${X}
207                         if [ -z "${_}" ]; then
208                                 eval ${X}=`grep "^${X}=" "${CONFFILE}" |
209                                     cut -f 2- -d '=' | tail -1`
210                         fi
211                 done
212
213                 if grep -qE "^REFUSE[[:space:]]" ${CONFFILE}; then
214                         REFUSE="^(`
215                                 grep -E "^REFUSE[[:space:]]" "${CONFFILE}" |
216                                     cut -c 7- | xargs echo | tr ' ' '|'
217                                 `)"
218                 fi
219
220                 if grep -qE "^INDEX[[:space:]]" ${CONFFILE}; then
221                         INDEXPAIRS="`
222                                 grep -E "^INDEX[[:space:]]" "${CONFFILE}" |
223                                     cut -c 7- | tr ' ' '|' | xargs echo`"
224                 fi
225         fi
226 }
227
228 # If parameters have not been set, use default values
229 default_params() {
230         _QUIETREDIR="/dev/null"
231         _QUIETFLAG="-q"
232         _STATSREDIR="/dev/stdout"
233         _WORKDIR="/var/db/portsnap"
234         _PORTSDIR="/usr/ports"
235         _NDEBUG="-n"
236         _LOCALDESC="/dev/null"
237         for X in QUIETREDIR QUIETFLAG STATSREDIR WORKDIR PORTSDIR       \
238             NDEBUG LOCALDESC; do
239                 eval _=\$${X}
240                 eval __=\$_${X}
241                 if [ -z "${_}" ]; then
242                         eval ${X}=${__}
243                 fi
244         done
245         if [ -z "${INTERACTIVE}" ]; then
246                 if [ -t 0 ]; then
247                         INTERACTIVE="YES"
248                 else
249                         INTERACTIVE="NO"
250                 fi
251         fi
252 }
253
254 # Perform sanity checks and set some final parameters
255 # in preparation for fetching files.  Also chdir into
256 # the working directory.
257 fetch_check_params() {
258         export HTTP_USER_AGENT="portsnap (${COMMAND}, `uname -r`)"
259
260         _SERVERNAME_z=\
261 "SERVERNAME must be given via command line or configuration file."
262         _KEYPRINT_z="Key must be given via -k option or configuration file."
263         _KEYPRINT_bad="Invalid key fingerprint: "
264         _WORKDIR_bad="Directory does not exist or is not writable: "
265
266         if [ -z "${SERVERNAME}" ]; then
267                 echo -n "`basename $0`: "
268                 echo "${_SERVERNAME_z}"
269                 exit 1
270         fi
271         if [ -z "${KEYPRINT}" ]; then
272                 echo -n "`basename $0`: "
273                 echo "${_KEYPRINT_z}"
274                 exit 1
275         fi
276         if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
277                 echo -n "`basename $0`: "
278                 echo -n "${_KEYPRINT_bad}"
279                 echo ${KEYPRINT}
280                 exit 1
281         fi
282         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
283                 echo -n "`basename $0`: "
284                 echo -n "${_WORKDIR_bad}"
285                 echo ${WORKDIR}
286                 exit 1
287         fi
288         cd ${WORKDIR} || exit 1
289
290         BSPATCH=/usr/bin/bspatch
291         SHA256=/sbin/sha256
292         PHTTPGET=/usr/libexec/phttpget
293 }
294
295 # Perform sanity checks and set some final parameters
296 # in preparation for extracting or updating ${PORTSDIR}
297 # Complain if ${PORTSDIR} exists but is not writable,
298 # but don't complain if ${PORTSDIR} doesn't exist.
299 extract_check_params() {
300         _WORKDIR_bad="Directory does not exist: "
301         _PORTSDIR_bad="Directory is not writable: "
302
303         if ! [ -d "${WORKDIR}" ]; then
304                 echo -n "`basename $0`: "
305                 echo -n "${_WORKDIR_bad}"
306                 echo ${WORKDIR}
307                 exit 1
308         fi
309         if [ -d "${PORTSDIR}" ] && ! [ -w "${PORTSDIR}" ]; then
310                 echo -n "`basename $0`: "
311                 echo -n "${_PORTSDIR_bad}"
312                 echo ${PORTSDIR}
313                 exit 1
314         fi
315
316         if ! [ -d "${WORKDIR}/files" -a -r "${WORKDIR}/tag"     \
317             -a -r "${WORKDIR}/INDEX" -a -r "${WORKDIR}/tINDEX" ]; then
318                 echo "No snapshot available.  Try running"
319                 echo "# `basename $0` fetch"
320                 exit 1
321         fi
322
323         MKINDEX=/usr/libexec/make_index
324 }
325
326 # Perform sanity checks and set some final parameters
327 # in preparation for updating ${PORTSDIR}
328 update_check_params() {
329         extract_check_params
330
331         if ! [ -r ${PORTSDIR}/.portsnap.INDEX ]; then
332                 echo "${PORTSDIR} was not created by portsnap."
333                 echo -n "You must run '`basename $0` extract' before "
334                 echo "running '`basename $0` update'."
335                 exit 1
336         fi
337
338 }
339
340 #### Core functionality -- the actual work gets done here
341
342 # Use an SRV query to pick a server.  If the SRV query doesn't provide
343 # a useful answer, use the server name specified by the user.
344 # Put another way... look up _http._tcp.${SERVERNAME} and pick a server
345 # from that; or if no servers are returned, use ${SERVERNAME}.
346 # This allows a user to specify "portsnap.freebsd.org" (in which case
347 # portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org"
348 # (in which case portsnap will use that particular server, since there
349 # won't be an SRV entry for that name).
350 #
351 # We ignore the Port field, since we are always going to use port 80.
352
353 # Fetch the mirror list, but do not pick a mirror yet.  Returns 1 if
354 # no mirrors are available for any reason.
355 fetch_pick_server_init() {
356         : > serverlist_tried
357
358 # Check that host(1) exists (i.e., that the system wasn't built with the
359 # WITHOUT_BIND set) and don't try to find a mirror if it doesn't exist.
360         if ! which -s host; then
361                 : > serverlist_full
362                 return 1
363         fi
364
365         echo -n "Looking up ${SERVERNAME} mirrors... "
366
367 # Issue the SRV query and pull out the Priority, Weight, and Target fields.
368 # BIND 9 prints "$name has SRV record ..." while BIND 8 prints
369 # "$name server selection ..."; we allow either format.
370         MLIST="_http._tcp.${SERVERNAME}"
371         host -t srv "${MLIST}" |
372             sed -nE "s/${MLIST} (has SRV record|server selection) //Ip" |
373             cut -f 1,2,4 -d ' ' |
374             sed -e 's/\.$//' |
375             sort > serverlist_full
376
377 # If no records, give up -- we'll just use the server name we were given.
378         if [ `wc -l < serverlist_full` -eq 0 ]; then
379                 echo "none found."
380                 return 1
381         fi
382
383 # Report how many mirrors we found.
384         echo `wc -l < serverlist_full` "mirrors found."
385
386 # Generate a random seed for use in picking mirrors.  If HTTP_PROXY
387 # is set, this will be used to generate the seed; otherwise, the seed
388 # will be random.
389         if [ -n "${HTTP_PROXY}${http_proxy}" ]; then
390                 RANDVALUE=`sha256 -qs "${HTTP_PROXY}${http_proxy}" |
391                     tr -d 'a-f' |
392                     cut -c 1-9`
393         else
394                 RANDVALUE=`jot -r 1 0 999999999`
395         fi
396 }
397
398 # Pick a mirror.  Returns 1 if we have run out of mirrors to try.
399 fetch_pick_server() {
400 # Generate a list of not-yet-tried mirrors
401         sort serverlist_tried |
402             comm -23 serverlist_full - > serverlist
403
404 # Have we run out of mirrors?
405         if [ `wc -l < serverlist` -eq 0 ]; then
406                 echo "No mirrors remaining, giving up."
407                 return 1
408         fi
409
410 # Find the highest priority level (lowest numeric value).
411         SRV_PRIORITY=`cut -f 1 -d ' ' serverlist | sort -n | head -1`
412
413 # Add up the weights of the response lines at that priority level.
414         SRV_WSUM=0;
415         while read X; do
416                 case "$X" in
417                 ${SRV_PRIORITY}\ *)
418                         SRV_W=`echo $X | cut -f 2 -d ' '`
419                         SRV_WSUM=$(($SRV_WSUM + $SRV_W))
420                         ;;
421                 esac
422         done < serverlist
423
424 # If all the weights are 0, pretend that they are all 1 instead.
425         if [ ${SRV_WSUM} -eq 0 ]; then
426                 SRV_WSUM=`grep -E "^${SRV_PRIORITY} " serverlist | wc -l`
427                 SRV_W_ADD=1
428         else
429                 SRV_W_ADD=0
430         fi
431
432 # Pick a value between 0 and the sum of the weights - 1
433         SRV_RND=`expr ${RANDVALUE} % ${SRV_WSUM}`
434
435 # Read through the list of mirrors and set SERVERNAME.  Write the line
436 # corresponding to the mirror we selected into serverlist_tried so that
437 # we won't try it again.
438         while read X; do
439                 case "$X" in
440                 ${SRV_PRIORITY}\ *)
441                         SRV_W=`echo $X | cut -f 2 -d ' '`
442                         SRV_W=$(($SRV_W + $SRV_W_ADD))
443                         if [ $SRV_RND -lt $SRV_W ]; then
444                                 SERVERNAME=`echo $X | cut -f 3 -d ' '`
445                                 echo "$X" >> serverlist_tried
446                                 break
447                         else
448                                 SRV_RND=$(($SRV_RND - $SRV_W))
449                         fi
450                         ;;
451                 esac
452         done < serverlist
453 }
454
455 # Check that we have a public key with an appropriate hash, or
456 # fetch the key if it doesn't exist.  Returns 1 if the key has
457 # not yet been fetched.
458 fetch_key() {
459         if [ -r pub.ssl ] && [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
460                 return 0
461         fi
462
463         echo -n "Fetching public key from ${SERVERNAME}... "
464         rm -f pub.ssl
465         fetch ${QUIETFLAG} http://${SERVERNAME}/pub.ssl \
466             2>${QUIETREDIR} || true
467         if ! [ -r pub.ssl ]; then
468                 echo "failed."
469                 return 1
470         fi
471         if ! [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
472                 echo "key has incorrect hash."
473                 rm -f pub.ssl
474                 return 1
475         fi
476         echo "done."
477 }
478
479 # Fetch a snapshot tag
480 fetch_tag() {
481         rm -f snapshot.ssl tag.new
482
483         echo ${NDEBUG} "Fetching snapshot tag from ${SERVERNAME}... "
484         fetch ${QUIETFLAG} http://${SERVERNAME}/$1.ssl          \
485             2>${QUIETREDIR} || true
486         if ! [ -r $1.ssl ]; then
487                 echo "failed."
488                 return 1
489         fi
490
491         openssl rsautl -pubin -inkey pub.ssl -verify            \
492             < $1.ssl > tag.new 2>${QUIETREDIR} || true
493         rm $1.ssl
494
495         if ! [ `wc -l < tag.new` = 1 ] ||
496             ! grep -qE "^portsnap\|[0-9]{10}\|[0-9a-f]{64}" tag.new; then
497                 echo "invalid snapshot tag."
498                 return 1
499         fi
500
501         echo "done."
502
503         SNAPSHOTDATE=`cut -f 2 -d '|' < tag.new`
504         SNAPSHOTHASH=`cut -f 3 -d '|' < tag.new`
505 }
506
507 # Sanity-check the date on a snapshot tag
508 fetch_snapshot_tagsanity() {
509         if [ `date "+%s"` -gt `expr ${SNAPSHOTDATE} + 31536000` ]; then
510                 echo "Snapshot appears to be more than a year old!"
511                 echo "(Is the system clock correct?)"
512                 echo "Cowardly refusing to proceed any further."
513                 return 1
514         fi
515         if [ `date "+%s"` -lt `expr ${SNAPSHOTDATE} - 86400` ]; then
516                 echo -n "Snapshot appears to have been created more than "
517                 echo "one day into the future!"
518                 echo "(Is the system clock correct?)"
519                 echo "Cowardly refusing to proceed any further."
520                 return 1
521         fi
522 }
523
524 # Sanity-check the date on a snapshot update tag
525 fetch_update_tagsanity() {
526         fetch_snapshot_tagsanity || return 1
527
528         if [ ${OLDSNAPSHOTDATE} -gt ${SNAPSHOTDATE} ]; then
529                 echo -n "Latest snapshot on server is "
530                 echo "older than what we already have!"
531                 echo -n "Cowardly refusing to downgrade from "
532                 date -r ${OLDSNAPSHOTDATE}
533                 echo "to `date -r ${SNAPSHOTDATE}`."
534                 return 1
535         fi
536 }
537
538 # Compare old and new tags; return 1 if update is unnecessary
539 fetch_update_neededp() {
540         if [ ${OLDSNAPSHOTDATE} -eq ${SNAPSHOTDATE} ]; then
541                 echo -n "Latest snapshot on server matches "
542                 echo "what we already have."
543                 echo "No updates needed."
544                 rm tag.new
545                 return 1
546         fi
547         if [ ${OLDSNAPSHOTHASH} = ${SNAPSHOTHASH} ]; then
548                 echo -n "Ports tree hasn't changed since "
549                 echo "last snapshot."
550                 echo "No updates needed."
551                 rm tag.new
552                 return 1
553         fi
554
555         return 0
556 }
557
558 # Fetch snapshot metadata file
559 fetch_metadata() {
560         rm -f ${SNAPSHOTHASH} tINDEX.new
561
562         echo ${NDEBUG} "Fetching snapshot metadata... "
563         fetch ${QUIETFLAG} http://${SERVERNAME}/t/${SNAPSHOTHASH} \
564             2>${QUIETREDIR} || return
565         if [ "`${SHA256} -q ${SNAPSHOTHASH}`" != ${SNAPSHOTHASH} ]; then
566                 echo "snapshot metadata corrupt."
567                 return 1
568         fi
569         mv ${SNAPSHOTHASH} tINDEX.new
570         echo "done."
571 }
572
573 # Warn user about bogus metadata
574 fetch_metadata_freakout() {
575         echo
576         echo "Portsnap metadata is correctly signed, but contains"
577         echo "at least one line which appears bogus."
578         echo "Cowardly refusing to proceed any further."
579 }
580
581 # Sanity-check a snapshot metadata file
582 fetch_metadata_sanity() {
583         if grep -qvE "^[0-9A-Z.]+\|[0-9a-f]{64}$" tINDEX.new; then
584                 fetch_metadata_freakout
585                 return 1
586         fi
587         if [ `look INDEX tINDEX.new | wc -l` != 1 ]; then
588                 echo
589                 echo "Portsnap metadata appears bogus."
590                 echo "Cowardly refusing to proceed any further."
591                 return 1
592         fi
593 }
594
595 # Take a list of ${oldhash}|${newhash} and output a list of needed patches
596 fetch_make_patchlist() {
597         local IFS='|'
598         echo "" 1>${QUIETREDIR}
599         grep -vE "^([0-9a-f]{64})\|\1$" |
600                 while read X Y; do
601                         printf "Processing: $X $Y ...\r" 1>${QUIETREDIR}
602                         if [ -f "files/${Y}.gz" -o ! -f "files/${X}.gz" ]; then continue; fi
603                         echo "${X}|${Y}"
604                 done
605         echo "" 1>${QUIETREDIR}
606 }
607
608 # Print user-friendly progress statistics
609 fetch_progress() {
610         LNC=0
611         while read x; do
612                 LNC=$(($LNC + 1))
613                 if [ $(($LNC % 10)) = 0 ]; then
614                         echo -n $LNC
615                 elif [ $(($LNC % 2)) = 0 ]; then
616                         echo -n .
617                 fi
618         done
619         echo -n " "
620 }
621
622 pct_fmt()
623 {
624         if [ $TOTAL -gt 0 ]; then
625                 printf "                                     \r"
626                 printf "($1/$2) %02.2f%% " `echo "scale=4;$LNC / $TOTAL * 100"|bc`
627         fi
628 }
629
630 fetch_progress_percent() {
631         TOTAL=$1
632         LNC=0
633         pct_fmt $LNC $TOTAL
634         while read x; do
635                 LNC=$(($LNC + 1))
636                 if [ $(($LNC % 100)) = 0 ]; then
637                      pct_fmt $LNC $TOTAL
638                 elif [ $(($LNC % 10)) = 0 ]; then
639                         echo -n .
640                 fi
641         done
642         pct_fmt $LNC $TOTAL
643         echo " done. "
644 }
645
646 # Sanity-check an index file
647 fetch_index_sanity() {
648         if grep -qvE "^[-_+./@0-9A-Za-z]+\|[0-9a-f]{64}$" INDEX.new ||
649             fgrep -q "./" INDEX.new; then
650                 fetch_metadata_freakout
651                 return 1
652         fi
653 }
654
655 # Verify a list of files
656 fetch_snapshot_verify() {
657         while read F; do
658                 if [ "`gunzip -c < snap/${F}.gz | ${SHA256} -q`" != ${F} ]; then
659                         echo "snapshot corrupt."
660                         return 1
661                 fi
662         done
663         return 0
664 }
665
666 # Fetch a snapshot tarball, extract, and verify.
667 fetch_snapshot() {
668         while ! fetch_tag snapshot; do
669                 fetch_pick_server || return 1
670         done
671         fetch_snapshot_tagsanity || return 1
672         fetch_metadata || return 1
673         fetch_metadata_sanity || return 1
674
675         rm -rf snap/
676
677 # Don't ask fetch(1) to be quiet -- downloading a snapshot of ~ 35MB will
678 # probably take a while, so the progrees reports that fetch(1) generates
679 # will be useful for keeping the users' attention from drifting.
680         echo "Fetching snapshot generated at `date -r ${SNAPSHOTDATE}`:"
681         fetch -r http://${SERVERNAME}/s/${SNAPSHOTHASH}.tgz || return 1
682
683         echo -n "Extracting snapshot... "
684         tar -xz --numeric-owner -f ${SNAPSHOTHASH}.tgz snap/ || return 1
685         rm ${SNAPSHOTHASH}.tgz
686         echo "done."
687
688         echo -n "Verifying snapshot integrity... "
689 # Verify the metadata files
690         cut -f 2 -d '|' tINDEX.new | fetch_snapshot_verify || return 1
691 # Extract the index
692         rm -f INDEX.new
693         gunzip -c < snap/`look INDEX tINDEX.new |
694             cut -f 2 -d '|'`.gz > INDEX.new
695         fetch_index_sanity || return 1
696 # Verify the snapshot contents
697         cut -f 2 -d '|' INDEX.new | fetch_snapshot_verify || return 1
698         cut -f 2 -d '|' tINDEX.new INDEX.new | sort -u |
699             lam -s 'snap/' - -s '.gz' > files.expected
700         find snap -mindepth 1 | sort > files.snap
701         if ! cmp -s files.expected files.snap; then
702                 echo "unexpected files in snapshot."
703                 return 1
704         fi
705         rm files.expected files.snap
706         echo "done."
707
708 # Move files into their proper locations
709         rm -f tag INDEX tINDEX
710         rm -rf files
711         mv tag.new tag
712         mv tINDEX.new tINDEX
713         mv INDEX.new INDEX
714         mv snap/ files/
715
716         return 0
717 }
718
719 # Update a compressed snapshot
720 fetch_update() {
721         rm -f patchlist diff OLD NEW filelist INDEX.new
722
723         OLDSNAPSHOTDATE=`cut -f 2 -d '|' < tag`
724         OLDSNAPSHOTHASH=`cut -f 3 -d '|' < tag`
725
726         while ! fetch_tag latest; do
727                 fetch_pick_server || return 1
728         done
729         fetch_update_tagsanity || return 1
730         fetch_update_neededp || return 0
731         fetch_metadata || return 1
732         fetch_metadata_sanity || return 1
733
734         echo -n "Updating from `date -r ${OLDSNAPSHOTDATE}` "
735         echo "to `date -r ${SNAPSHOTDATE}`."
736
737 # Generate a list of wanted metadata patches
738         join -t '|' -o 1.2,2.2 tINDEX tINDEX.new |
739             fetch_make_patchlist > patchlist
740
741 # Attempt to fetch metadata patches
742         echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
743         echo ${NDEBUG} "metadata patches.${DDSTATS}"
744         tr '|' '-' < patchlist |
745             lam -s "tp/" - -s ".gz" |
746             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
747             2>${STATSREDIR} | fetch_progress
748         echo "done."
749
750 # Attempt to apply metadata patches
751         echo -n "Applying metadata patches... "
752         local oldifs="$IFS" IFS='|'
753         while read X Y; do
754                 if [ ! -f "${X}-${Y}.gz" ]; then continue; fi
755                 gunzip -c < ${X}-${Y}.gz > diff
756                 gunzip -c < files/${X}.gz > OLD
757                 cut -c 2- diff | join -t '|' -v 2 - OLD > ptmp
758                 grep '^\+' diff | cut -c 2- |
759                     sort -k 1,1 -t '|' -m - ptmp > NEW
760                 if [ `${SHA256} -q NEW` = ${Y} ]; then
761                         mv NEW files/${Y}
762                         gzip -n files/${Y}
763                 fi
764                 rm -f diff OLD NEW ${X}-${Y}.gz ptmp
765         done < patchlist 2>${QUIETREDIR}
766         IFS="$oldifs"
767         echo "done."
768
769 # Update metadata without patches
770         join -t '|' -v 2 tINDEX tINDEX.new |
771             cut -f 2 -d '|' /dev/stdin patchlist |
772                 while read Y; do
773                         if [ ! -f "files/${Y}.gz" ]; then
774                                 echo ${Y};
775                         fi
776                 done > filelist
777         echo -n "Fetching `wc -l < filelist | tr -d ' '` "
778         echo ${NDEBUG} "metadata files... "
779         lam -s "f/" - -s ".gz" < filelist |
780             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
781             2>${QUIETREDIR}
782
783         while read Y; do
784                 echo -n "Verifying ${Y}... " 1>${QUIETREDIR}
785                 if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
786                         mv ${Y}.gz files/${Y}.gz
787                 else
788                         echo "metadata is corrupt."
789                         return 1
790                 fi
791                 echo "ok." 1>${QUIETREDIR}
792         done < filelist
793         echo "done."
794
795 # Extract the index
796         echo -n "Extracting index... " 1>${QUIETREDIR}
797         gunzip -c < files/`look INDEX tINDEX.new |
798             cut -f 2 -d '|'`.gz > INDEX.new
799         fetch_index_sanity || return 1
800
801 # If we have decided to refuse certain updates, construct a hybrid index which
802 # is equal to the old index for parts of the tree which we don't want to
803 # update, and equal to the new index for parts of the tree which gets updates.
804 # This means that we should always have a "complete snapshot" of the ports
805 # tree -- with the caveat that it isn't actually a snapshot.
806         if [ ! -z "${REFUSE}" ]; then
807                 echo "Refusing to download updates for ${REFUSE}"       \
808                     >${QUIETREDIR}
809
810                 grep -Ev "${REFUSE}" INDEX.new > INDEX.tmp
811                 grep -E "${REFUSE}" INDEX |
812                     sort -m -k 1,1 -t '|' - INDEX.tmp > INDEX.new
813                 rm -f INDEX.tmp
814         fi
815
816 # Generate a list of wanted ports patches
817         echo -n "Generating list of wanted patches..." 1>${QUIETREDIR}
818         join -t '|' -o 1.2,2.2 INDEX INDEX.new |
819             fetch_make_patchlist > patchlist
820         echo " done." 1>${QUIETREDIR}
821
822 # Attempt to fetch ports patches
823         patchcnt=`wc -l < patchlist | tr -d ' '`      
824         echo -n "Fetching $patchcnt "
825         echo ${NDEBUG} "patches.${DDSTATS}"
826         echo " "
827         tr '|' '-' < patchlist | lam -s "bp/" - |
828             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
829             2>${STATSREDIR} | fetch_progress_percent $patchcnt
830         echo "done."
831
832 # Attempt to apply ports patches
833         PATCHCNT=`wc -l patchlist`
834         echo "Applying patches... "
835         local oldifs="$IFS" IFS='|'
836         I=0
837         while read X Y; do
838                 I=$(($I + 1))
839                 F="${X}-${Y}"
840                 if [ ! -f "${F}" ]; then
841                         printf "  Skipping ${F} (${I} of ${PATCHCNT}).\r"
842                         continue;
843                 fi
844                 echo "  Processing ${F}..." 1>${QUIETREDIR}
845                 gunzip -c < files/${X}.gz > OLD
846                 ${BSPATCH} OLD NEW ${X}-${Y}
847                 if [ `${SHA256} -q NEW` = ${Y} ]; then
848                         mv NEW files/${Y}
849                         gzip -n files/${Y}
850                 fi
851                 rm -f diff OLD NEW ${X}-${Y}
852         done < patchlist 2>${QUIETREDIR}
853         IFS="$oldifs"
854         echo "done."
855
856 # Update ports without patches
857         join -t '|' -v 2 INDEX INDEX.new |
858             cut -f 2 -d '|' /dev/stdin patchlist |
859                 while read Y; do
860                         if [ ! -f "files/${Y}.gz" ]; then
861                                 echo ${Y};
862                         fi
863                 done > filelist
864         echo -n "Fetching `wc -l < filelist | tr -d ' '` "
865         echo ${NDEBUG} "new ports or files... "
866         lam -s "f/" - -s ".gz" < filelist |
867             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
868             2>${QUIETREDIR}
869
870         I=0
871         while read Y; do
872                 I=$(($I + 1))
873                 printf "   Processing ${Y} (${I} of ${PATCHCNT}).\r" 1>${QUIETREDIR}
874                 if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
875                         mv ${Y}.gz files/${Y}.gz
876                 else
877                         echo "snapshot is corrupt."
878                         return 1
879                 fi
880         done < filelist
881         echo "done."
882
883 # Remove files which are no longer needed
884         cut -f 2 -d '|' tINDEX INDEX | sort -u > oldfiles
885         cut -f 2 -d '|' tINDEX.new INDEX.new | sort -u | comm -13 - oldfiles |
886             lam -s "files/" - -s ".gz" | xargs rm -f
887         rm patchlist filelist oldfiles
888
889 # We're done!
890         mv INDEX.new INDEX
891         mv tINDEX.new tINDEX
892         mv tag.new tag
893
894         return 0
895 }
896
897 # Do the actual work involved in "fetch" / "cron".
898 fetch_run() {
899         fetch_pick_server_init && fetch_pick_server
900
901         while ! fetch_key; do
902                 fetch_pick_server || return 1
903         done
904
905         if ! [ -d files -a -r tag -a -r INDEX -a -r tINDEX ]; then
906                 fetch_snapshot || return 1
907         fi
908         fetch_update || return 1
909 }
910
911 # Build a ports INDEX file
912 extract_make_index() {
913         if ! look $1 ${WORKDIR}/tINDEX > /dev/null; then
914                 echo -n "$1 not provided by portsnap server; "
915                 echo "$2 not being generated."
916         else
917         gunzip -c < "${WORKDIR}/files/`look $1 ${WORKDIR}/tINDEX |
918             cut -f 2 -d '|'`.gz" |
919             cat - ${LOCALDESC} |
920             ${MKINDEX} /dev/stdin > ${PORTSDIR}/$2
921         fi
922 }
923
924 # Create INDEX, INDEX-5, INDEX-6
925 extract_indices() {
926         echo -n "Building new INDEX files... "
927         for PAIR in ${INDEXPAIRS}; do
928                 INDEXFILE=`echo ${PAIR} | cut -f 1 -d '|'`
929                 DESCRIBEFILE=`echo ${PAIR} | cut -f 2 -d '|'`
930                 extract_make_index ${DESCRIBEFILE} ${INDEXFILE} || return 1
931         done
932         echo "done."
933 }
934
935 # Create .portsnap.INDEX; if we are REFUSEing to touch certain directories,
936 # merge the values from any exiting .portsnap.INDEX file.
937 extract_metadata() {
938         if [ -z "${REFUSE}" ]; then
939                 sort ${WORKDIR}/INDEX > ${PORTSDIR}/.portsnap.INDEX
940         elif [ -f ${PORTSDIR}/.portsnap.INDEX ]; then
941                 grep -E "${REFUSE}" ${PORTSDIR}/.portsnap.INDEX \
942                     > ${PORTSDIR}/.portsnap.INDEX.tmp
943                 grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort |
944                     sort -m - ${PORTSDIR}/.portsnap.INDEX.tmp   \
945                     > ${PORTSDIR}/.portsnap.INDEX
946                 rm -f ${PORTSDIR}/.portsnap.INDEX.tmp
947         else
948                 grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort \
949                     > ${PORTSDIR}/.portsnap.INDEX
950         fi
951 }
952
953 # Do the actual work involved in "extract"
954 extract_run() {
955         local oldifs="$IFS" IFS='|'
956         mkdir -p ${PORTSDIR} || return 1
957
958         if !
959                 if ! [ -z "${EXTRACTPATH}" ]; then
960                         grep "^${EXTRACTPATH}" ${WORKDIR}/INDEX
961                 elif ! [ -z "${REFUSE}" ]; then
962                         grep -vE "${REFUSE}" ${WORKDIR}/INDEX
963                 else
964                         cat ${WORKDIR}/INDEX
965                 fi | while read FILE HASH; do
966                 echo ${PORTSDIR}/${FILE}
967                 if ! [ -s "${WORKDIR}/files/${HASH}.gz" ]; then
968                         echo "files/${HASH}.gz not found -- snapshot corrupt."
969                         return 1
970                 fi
971                 case ${FILE} in
972                 */)
973                         rm -rf ${PORTSDIR}/${FILE%/}
974                         mkdir -p ${PORTSDIR}/${FILE}
975                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
976                             -C ${PORTSDIR}/${FILE}
977                         ;;
978                 *)
979                         rm -f ${PORTSDIR}/${FILE}
980                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
981                             -C ${PORTSDIR} ${FILE}
982                         ;;
983                 esac
984         done; then
985                 return 1
986         fi
987         if [ ! -z "${EXTRACTPATH}" ]; then
988                 return 0;
989         fi
990
991         IFS="$oldifs"
992
993         extract_metadata
994         extract_indices
995 }
996
997 update_run_extract() {
998         local IFS='|'
999
1000 # Install new files
1001         echo "Extracting new files:"
1002         if !
1003                 if ! [ -z "${REFUSE}" ]; then
1004                         grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort
1005                 else
1006                         sort ${WORKDIR}/INDEX
1007                 fi |
1008             comm -13 ${PORTSDIR}/.portsnap.INDEX - |
1009             while read FILE HASH; do
1010                 echo ${PORTSDIR}/${FILE}
1011                 if ! [ -s "${WORKDIR}/files/${HASH}.gz" ]; then
1012                         echo "files/${HASH}.gz not found -- snapshot corrupt."
1013                         return 1
1014                 fi
1015                 case ${FILE} in
1016                 */)
1017                         mkdir -p ${PORTSDIR}/${FILE}
1018                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
1019                             -C ${PORTSDIR}/${FILE}
1020                         ;;
1021                 *)
1022                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
1023                             -C ${PORTSDIR} ${FILE}
1024                         ;;
1025                 esac
1026         done; then
1027                 return 1
1028         fi
1029 }
1030
1031 # Do the actual work involved in "update"
1032 update_run() {
1033         if ! [ -z "${INDEXONLY}" ]; then
1034                 extract_indices >/dev/null || return 1
1035                 return 0
1036         fi
1037
1038         if sort ${WORKDIR}/INDEX |
1039             cmp -s ${PORTSDIR}/.portsnap.INDEX -; then
1040                 echo "Ports tree is already up to date."
1041                 return 0
1042         fi
1043
1044 # If we are REFUSEing to touch certain directories, don't remove files
1045 # from those directories (even if they are out of date)
1046         echo -n "Removing old files and directories... "
1047         if ! [ -z "${REFUSE}" ]; then 
1048                 sort ${WORKDIR}/INDEX |
1049                     comm -23 ${PORTSDIR}/.portsnap.INDEX - | cut -f 1 -d '|' |
1050                     grep -vE "${REFUSE}" |
1051                     lam -s "${PORTSDIR}/" - |
1052                     sed -e 's|/$||' | xargs rm -rf
1053         else
1054                 sort ${WORKDIR}/INDEX |
1055                     comm -23 ${PORTSDIR}/.portsnap.INDEX - | cut -f 1 -d '|' |
1056                     lam -s "${PORTSDIR}/" - |
1057                     sed -e 's|/$||' | xargs rm -rf
1058         fi
1059         echo "done."
1060
1061         update_run_extract || return 1
1062         extract_metadata
1063         extract_indices
1064 }
1065
1066 #### Main functions -- call parameter-handling and core functions
1067
1068 # Using the command line, configuration file, and defaults,
1069 # set all the parameters which are needed later.
1070 get_params() {
1071         init_params
1072         parse_cmdline $@
1073         sanity_conffile
1074         default_conffile
1075         parse_conffile
1076         default_params
1077 }
1078
1079 # Fetch command.  Make sure that we're being called
1080 # interactively, then run fetch_check_params and fetch_run
1081 cmd_fetch() {
1082         if [ "${INTERACTIVE}" != "YES" ]; then
1083                 echo -n "`basename $0` fetch should not "
1084                 echo "be run non-interactively."
1085                 echo "Run `basename $0` cron instead"
1086                 exit 1
1087         fi
1088         fetch_check_params
1089         fetch_run || exit 1
1090 }
1091
1092 # Cron command.  Make sure the parameters are sensible; wait
1093 # rand(3600) seconds; then fetch updates.  While fetching updates,
1094 # send output to a temporary file; only print that file if the
1095 # fetching failed.
1096 cmd_cron() {
1097         fetch_check_params
1098         sleep `jot -r 1 0 3600`
1099
1100         TMPFILE=`mktemp /tmp/portsnap.XXXXXX` || exit 1
1101         if ! fetch_run >> ${TMPFILE}; then
1102                 cat ${TMPFILE}
1103                 rm ${TMPFILE}
1104                 exit 1
1105         fi
1106
1107         rm ${TMPFILE}
1108 }
1109
1110 # Extract command.  Make sure the parameters are sensible,
1111 # then extract the ports tree (or part thereof).
1112 cmd_extract() {
1113         extract_check_params
1114         extract_run || exit 1
1115 }
1116
1117 # Update command.  Make sure the parameters are sensible,
1118 # then update the ports tree.
1119 cmd_update() {
1120         update_check_params
1121         update_run || exit 1
1122 }
1123
1124 # Auto command.  Run 'fetch' or 'cron' depending on
1125 # whether stdin is a terminal; then run 'update' or
1126 # 'extract' depending on whether ${PORTSDIR} exists.
1127 cmd_auto() {
1128         if [ "${INTERACTIVE}" = "YES" ]; then
1129                 cmd_fetch
1130         else
1131                 cmd_cron
1132         fi
1133         if [ -r ${PORTSDIR}/.portsnap.INDEX ]; then
1134                 cmd_update
1135         else
1136                 cmd_extract
1137         fi
1138 }
1139
1140 #### Entry point
1141
1142 # Make sure we find utilities from the base system
1143 export PATH=/sbin:/bin:/usr/sbin:/usr/bin:${PATH}
1144
1145 # Set LC_ALL in order to avoid problems with character ranges like [A-Z].
1146 export LC_ALL=C
1147
1148 get_params $@
1149 for COMMAND in ${COMMANDS}; do
1150         cmd_${COMMAND}
1151 done