source: trunk/base/src/package1.0/portarchivefetch.tcl @ 68996

Last change on this file since 68996 was 68996, checked in by jmr@…, 10 years ago

Added integrity checking for fetched archives via signed digests. New pubkeys.conf file allows configuring keys to trust. The private counterpart of the installed public key will of course need to live on our binary building server.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 11.6 KB
Line 
1# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4
2# $Id: portarchivefetch.tcl 68996 2010-06-19 23:21:02Z jmr@macports.org $
3#
4# Copyright (c) 2002 - 2003 Apple Inc.
5# Copyright (c) 2004-2010 The MacPorts Project
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# 1. Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13# 2. Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in the
15#    documentation and/or other materials provided with the distribution.
16# 3. Neither the name of Apple Inc. nor the names of its contributors
17#    may be used to endorse or promote products derived from this software
18#    without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31#
32
33package provide portarchivefetch 1.0
34package require fetch_common 1.0
35package require portutil 1.0
36package require Pextlib 1.0
37
38set org.macports.archivefetch [target_new org.macports.archivefetch portarchivefetch::archivefetch_main]
39target_init ${org.macports.archivefetch} portarchivefetch::archivefetch_init
40target_provides ${org.macports.archivefetch} archivefetch
41target_requires ${org.macports.archivefetch} main
42target_prerun ${org.macports.archivefetch} portarchivefetch::archivefetch_start
43
44namespace eval portarchivefetch {
45    variable archivefetch_urls {}
46}
47
48options archive_sites archivefetch.user archivefetch.password \
49    archivefetch.use_epsv archivefetch.ignore_sslcert \
50    archive_sites.mirror_subdir archivefetch.pubkeys
51
52# user name & password
53default archivefetch.user ""
54default archivefetch.password ""
55# Use EPSV for FTP transfers
56default archivefetch.use_epsv no
57# Ignore SSL certificate
58default archivefetch.ignore_sslcert no
59default archivefetch.pubkeys {$archivefetch_pubkeys}
60
61default archive_sites macports_archives
62default archive_sites.listfile {"archive_sites.tcl"}
63default archive_sites.listpath {"port1.0/fetch"}
64
65set_ui_prefix
66
67# Checks possible archive files to assemble url lists for later fetching
68proc portarchivefetch::checkarchivefiles {urls} {
69    global all_archive_files archivefetch.fulldestpath \
70           portarchivepath name version revision portvariants archive_sites
71    upvar $urls fetch_urls
72
73    # Define archive directory, file, and path
74    if {[llength [get_canonical_archs]] > 1} {
75        set archivefetch.fulldestpath [file join ${portarchivepath} [option os.platform] "universal"]
76    } else {
77        set archivefetch.fulldestpath [file join ${portarchivepath} [option os.platform] [get_canonical_archs]]
78    }
79
80    set unsupported 0
81    set found 0
82    foreach archive.type [option portarchivetype] {
83        if {[catch {archiveTypeIsSupported ${archive.type}} errmsg] == 0} {
84            set archstring [join [get_canonical_archs] -]
85            set archive.file "${name}-${version}_${revision}${portvariants}.${archstring}.${archive.type}"
86            set archive.path [file join ${archivefetch.fulldestpath} ${archive.file}]
87            if {[file exists ${archive.path}]} {
88                set found 1
89                break
90            } else {
91                lappend all_archive_files ${archive.file}
92                if {[info exists archive_sites]} {
93                    lappend fetch_urls archive_sites ${archive.file}
94                }
95            }
96        } else {
97            ui_debug "Skipping [string toupper ${archive.type}] archive: $errmsg"
98            incr unsupported
99        }
100    }
101    if {$found} {
102        ui_debug "Found [string toupper ${archive.type}] archive: ${archive.path}"
103        set all_archive_files {}
104        set fetch_urls {}
105    } elseif {[llength [option portarchivetype]] == $unsupported} {
106        return -code error "Unable to fetch archive ($name) since specified archive types not supported"
107    }
108}
109
110# returns full path to mirror list file
111proc portarchivefetch::get_full_archive_sites_path {} {
112    global archive_sites.listfile archive_sites.listpath porturl
113    return [getportresourcepath $porturl [file join ${archive_sites.listpath} ${archive_sites.listfile}]]
114}
115
116# Perform the full checksites/checkarchivefiles sequence.
117proc portarchivefetch::checkfiles {urls} {
118    upvar $urls fetch_urls
119
120    portfetch::checksites [list archive_sites [list {} {} ARCHIVE_SITE_LOCAL]] \
121                          [get_full_archive_sites_path]
122    checkarchivefiles fetch_urls
123}
124
125
126# Perform a standard fetch, assembling fetch urls from
127# the listed url variable and associated archive file
128proc portarchivefetch::fetchfiles {args} {
129    global portarchivepath archivefetch.fulldestpath UI_PREFIX
130    global archivefetch.user archivefetch.password archivefetch.use_epsv \
131           archivefetch.ignore_sslcert
132    global portverbose ports_binary_only
133    variable archivefetch_urls
134    variable ::portfetch::urlmap
135
136    if {![file isdirectory ${archivefetch.fulldestpath}]} {
137        if {[catch {file mkdir ${archivefetch.fulldestpath}} result]} {
138            elevateToRoot "archivefetch"
139            set elevated yes
140            if {[catch {file mkdir ${archivefetch.fulldestpath}} result]} {
141                return -code error [format [msgcat::mc "Unable to create archive path: %s"] $result]
142            }
143        }
144    }
145    set incoming_path [file join ${portarchivepath} incoming]
146    if {![file isdirectory $incoming_path]} {
147        if {[catch {file mkdir $incoming_path} result]} {
148            elevateToRoot "archivefetch"
149            set elevated yes
150            if {[catch {file mkdir $incoming_path} result]} {
151                return -code error [format [msgcat::mc "Unable to create archive fetch path: %s"] $result]
152            }
153        }
154    }
155    chownAsRoot ${archivefetch.fulldestpath}
156    chownAsRoot $incoming_path
157    if {[info exists elevated] && $elevated == yes} {
158        dropPrivileges
159    }
160
161    set fetch_options {}
162    if {[string length ${archivefetch.user}] || [string length ${archivefetch.password}]} {
163        lappend fetch_options -u
164        lappend fetch_options "${archivefetch.user}:${archivefetch.password}"
165    }
166    if {${archivefetch.use_epsv} != "yes"} {
167        lappend fetch_options "--disable-epsv"
168    }
169    if {${archivefetch.ignore_sslcert} != "no"} {
170        lappend fetch_options "--ignore-ssl-cert"
171    }
172    if {$portverbose == "yes"} {
173        lappend fetch_options "-v"
174    }
175    set sorted no
176
177    foreach {url_var archive} $archivefetch_urls {
178        if {![file isfile ${archivefetch.fulldestpath}/${archive}]} {
179            ui_info "$UI_PREFIX [format [msgcat::mc "%s doesn't seem to exist in %s"] $archive ${archivefetch.fulldestpath}]"
180            if {![file writable ${archivefetch.fulldestpath}]} {
181                return -code error [format [msgcat::mc "%s must be writable"] ${archivefetch.fulldestpath}]
182            }
183            if {![file writable $incoming_path]} {
184                return -code error [format [msgcat::mc "%s must be writable"] $incoming_path]
185            }
186            if {!$sorted} {
187                portfetch::sortsites archivefetch_urls {} archive_sites
188                set sorted yes
189            }
190            if {![info exists urlmap($url_var)]} {
191                ui_error [format [msgcat::mc "No defined site for tag: %s, using archive_sites"] $url_var]
192                set urlmap($url_var) $urlmap(archive_sites)
193            }
194            unset -nocomplain fetched
195            foreach site $urlmap($url_var) {
196                ui_msg "$UI_PREFIX [format [msgcat::mc "Attempting to fetch %s from %s"] $archive $site]"
197                set file_url [portfetch::assemble_url $site $archive]
198                set effectiveURL ""
199                if {![catch {eval curl fetch --effective-url effectiveURL $fetch_options {$file_url} {"${incoming_path}/${archive}.TMP"}} result]} {
200                    # Successful fetch
201                    set fetched 1
202                    break
203                } else {
204                    ui_debug "[msgcat::mc "Fetching archive failed:"]: $result"
205                    file delete -force "${incoming_path}/${archive}.TMP"
206                }
207            }
208            if {[info exists fetched]} {
209                # there should be an rmd160 digest of the archive signed with one of the trusted keys
210                set signature "${incoming_path}/${archive}.rmd160"
211                ui_msg "$UI_PREFIX [format [msgcat::mc "Attempting to fetch %s from %s"] ${archive}.rmd160 $site]"
212                # reusing $file_url from the last iteration of the loop above
213                if {[catch {eval curl fetch --effective-url effectiveURL $fetch_options {${file_url}.rmd160} {$signature}} result]} {
214                    ui_debug "$::errorInfo"
215                    return -code error "Failed to fetch signature for archive: $result"
216                }
217                set verified 0
218                foreach pubkey [option archivefetch.pubkeys] {
219                    set openssl [findBinary openssl $portutil::autoconf::openssl_path]
220                    if {![catch {exec $openssl dgst -ripemd160 -verify $pubkey -signature $signature "${incoming_path}/${archive}.TMP"} result]} {
221                        set verified 1
222                        break
223                    } else {
224                        ui_debug "failed verification with key $pubkey"
225                        ui_debug "openssl output: $result"
226                    }
227                }
228                if {!$verified} {
229                    return -code error "Failed to verify signature for archive!"
230                }
231                if {[catch {file rename -force "${incoming_path}/${archive}.TMP" "${archivefetch.fulldestpath}/${archive}"} result]} {
232                    ui_debug "$::errorInfo"
233                    return -code error "Failed to move downloaded archive into place: $result"
234                }
235                file delete -force $signature
236                return 0
237            }
238        } else {
239            return 0
240        }
241    }
242    if {[info exists ports_binary_only] && $ports_binary_only == "yes"} {
243        return -code error "archivefetch failed for [option name] @[option version]_[option revision][option portvariants]"
244    } else {
245        return 0
246    }
247}
248
249# Initialize archivefetch target and call checkfiles.
250proc portarchivefetch::archivefetch_init {args} {
251    variable archivefetch_urls
252
253    if {[option portarchivemode] != "yes"} {
254        return -code error "Archive mode is not enabled!"
255    }
256
257    portarchivefetch::checkfiles archivefetch_urls
258}
259
260proc portarchivefetch::archivefetch_start {args} {
261    global UI_PREFIX name
262
263    ui_msg "$UI_PREFIX [format [msgcat::mc "Fetching archive for %s"] $name]"
264}
265
266# Main archive fetch routine
267# just calls the standard fetchfiles procedure
268proc portarchivefetch::archivefetch_main {args} {
269    global all_archive_files
270    if {[info exists all_archive_files] && [llength $all_archive_files] > 0} {
271        # Fetch the files
272        return [portarchivefetch::fetchfiles]
273    }
274}
Note: See TracBrowser for help on using the repository browser.