use TLSv1 instead of SSLv3
[bootmanager.git] / source / BootServerRequest.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2003 Intel Corporation
4 # All rights reserved.
5 #
6 # Copyright (c) 2004-2006 The Trustees of Princeton University
7 # All rights reserved.
8
9 from __future__ import print_function
10
11 import os, sys
12 import re
13 import string
14 import urllib
15 import tempfile
16
17 # try to load pycurl
18 try:
19     import pycurl
20     PYCURL_LOADED = 1
21 except:
22     PYCURL_LOADED = 0
23
24
25 # if there is no cStringIO, fall back to the original
26 try:
27     from cStringIO import StringIO
28 except:
29     from StringIO import StringIO
30
31
32
33 class BootServerRequest:
34
35     # all possible places to check the cdrom mount point.
36     # /mnt/cdrom is typically after the machine has come up,
37     # and /usr is when the boot cd is running
38     CDROM_MOUNT_PATH = ("/mnt/cdrom/", "/usr/")
39     BOOTSERVER_CERTS = {}
40     MONITORSERVER_CERTS = {}
41     BOOTCD_VERSION = ""
42     HTTP_SUCCESS = 200
43     HAS_BOOTCD = 0
44     USE_PROXY = 0
45     PROXY = 0
46
47     # in seconds, how maximum time allowed for connect
48     DEFAULT_CURL_CONNECT_TIMEOUT = 30
49     # in seconds, maximum time allowed for any transfer
50     DEFAULT_CURL_MAX_TRANSFER_TIME = 3600
51     # location of curl executable, if pycurl isn't available
52     # and the DownloadFile method is called (backup, only
53     # really need for the boot cd environment where pycurl
54     # doesn't exist
55     CURL_CMD = 'curl'
56
57     # use TLSv1 and not SSLv3 anymore
58     if PYCURL_LOADED:
59         CURL_SSL_VERSION = pycurl.SSLVERSION_TLSv1
60     else:
61         # used to be '3' for SSLv3
62         # xxx really not sure what this means when pycurl is not loaded
63         CURL_SSL_VERSION = 1
64
65     def __init__(self, vars, verbose=0):
66
67         self.VERBOSE = verbose
68         self.VARS = vars
69             
70         # see if we have a boot cd mounted by checking for the version file
71         # if HAS_BOOTCD == 0 then either the machine doesn't have
72         # a boot cd, or something else is mounted
73         self.HAS_BOOTCD = 0
74
75         for path in self.CDROM_MOUNT_PATH:
76             self.Message("Checking existance of boot cd on {}".format(path))
77
78             os.system("/bin/mount {} > /dev/null 2>&1".format(path))
79                 
80             version_file = self.VARS['BOOTCD_VERSION_FILE'].format(path=path)
81             self.Message("Looking for version file {}".format(version_file))
82
83             if os.access(version_file, os.R_OK) == 0:
84                 self.Message("No boot cd found.");
85             else:
86                 self.Message("Found boot cd.")
87                 self.HAS_BOOTCD = 1
88                 break
89
90         if self.HAS_BOOTCD:
91
92             # check the version of the boot cd, and locate the certs
93             self.Message("Getting boot cd version.")
94         
95             versionRegExp = re.compile(r"PlanetLab BootCD v(\S+)")
96                 
97             bootcd_version_f = file(version_file, "r")
98             line = string.strip(bootcd_version_f.readline())
99             bootcd_version_f.close()
100             
101             match = versionRegExp.findall(line)
102             if match:
103                 (self.BOOTCD_VERSION) = match[0]
104             
105             # right now, all the versions of the bootcd are supported,
106             # so no need to check it
107             
108             self.Message("Getting server from configuration")
109             
110             bootservers = [ self.VARS['BOOT_SERVER'] ]
111             for bootserver in bootservers:
112                 bootserver = string.strip(bootserver)
113                 cacert_path = "{}/{}/{}".format(
114                     self.VARS['SERVER_CERT_DIR'].format(path=path),
115                     bootserver,
116                     self.VARS['CACERT_NAME'])
117                 if os.access(cacert_path, os.R_OK):
118                     self.BOOTSERVER_CERTS[bootserver] = cacert_path
119
120             monitorservers = [ self.VARS['MONITOR_SERVER'] ]
121             for monitorserver in monitorservers:
122                 monitorserver = string.strip(monitorserver)
123                 cacert_path = "{}/{}/{}".format(
124                     self.VARS['SERVER_CERT_DIR'].format(path=path),
125                     monitorserver,
126                     self.VARS['CACERT_NAME'])
127                 if os.access(cacert_path, os.R_OK):
128                     self.MONITORSERVER_CERTS[monitorserver] = cacert_path
129
130             self.Message("Set of servers to contact: {}".format(self.BOOTSERVER_CERTS))
131             self.Message("Set of servers to upload to: {}".format(self.MONITORSERVER_CERTS))
132         else:
133             self.Message("Using default boot server address.")
134             self.BOOTSERVER_CERTS[self.VARS['DEFAULT_BOOT_SERVER']] = ""
135             self.MONITORSERVER_CERTS[self.VARS['DEFAULT_BOOT_SERVER']] = ""
136
137
138     def CheckProxy(self):
139         # see if we have any proxy info from the machine
140         self.USE_PROXY = 0
141         self.Message("Checking existance of proxy config file...")
142         
143         if os.access(self.VARS['PROXY_FILE'], os.R_OK) and \
144                os.path.isfile(self.VARS['PROXY_FILE']):
145             self.PROXY = string.strip(file(self.VARS['PROXY_FILE'], 'r').readline())
146             self.USE_PROXY = 1
147             self.Message("Using proxy {}.".format(self.PROXY))
148         else:
149             self.Message("Not using any proxy.")
150
151
152
153     def Message(self, Msg):
154         if(self.VERBOSE):
155             print(Msg)
156
157     def Error(self, Msg):
158         sys.stderr.write(Msg + "\n")
159
160     def Warning(self, Msg):
161         self.Error(Msg)
162
163     def MakeRequest(self, PartialPath, GetVars,
164                      PostVars, DoSSL, DoCertCheck,
165                      ConnectTimeout = DEFAULT_CURL_CONNECT_TIMEOUT,
166                      MaxTransferTime = DEFAULT_CURL_MAX_TRANSFER_TIME,
167                      FormData = None):
168
169         fd, buffer_name = tempfile.mkstemp("MakeRequest-XXXXXX")
170         os.close(fd)
171         buffer = open(buffer_name, "w+b")
172
173         # the file "buffer_name" will be deleted by DownloadFile()
174
175         ok = self.DownloadFile(PartialPath, GetVars, PostVars,
176                                DoSSL, DoCertCheck, buffer_name,
177                                ConnectTimeout,
178                                MaxTransferTime,
179                                FormData)
180
181         # check the ok code, return the string only if it was successfull
182         if ok:
183             buffer.seek(0)
184             ret = buffer.read()
185         else:
186             ret = None
187
188         buffer.close()
189         try:
190             # just in case it is not deleted by DownloadFile()
191             os.unlink(buffer_name)
192         except OSError:
193             pass
194             
195         return ret
196
197     def DownloadFile(self, PartialPath, GetVars, PostVars,
198                      DoSSL, DoCertCheck, DestFilePath,
199                      ConnectTimeout = DEFAULT_CURL_CONNECT_TIMEOUT,
200                      MaxTransferTime = DEFAULT_CURL_MAX_TRANSFER_TIME,
201                      FormData = None):
202
203         self.Message("Attempting to retrieve {}".format(PartialPath))
204
205         # we can't do ssl and check the cert if we don't have a bootcd
206         if DoSSL and DoCertCheck and not self.HAS_BOOTCD:
207             self.Error("No boot cd exists (needed to use -c and -s.\n")
208             return 0
209
210         if DoSSL and not PYCURL_LOADED:
211             self.Warning("Using SSL without pycurl will by default " \
212                           "check at least standard certs.")
213
214         # ConnectTimeout has to be greater than 0
215         if ConnectTimeout <= 0:
216             self.Error("Connect timeout must be greater than zero.\n")
217             return 0
218
219
220         self.CheckProxy()
221
222         dopostdata = 0
223
224         # setup the post and get vars for the request
225         if PostVars:
226             dopostdata = 1
227             postdata = urllib.urlencode(PostVars)
228             self.Message("Posting data:\n{}\n".format(postdata))
229             
230         getstr = ""
231         if GetVars:
232             getstr = "?" + urllib.urlencode(GetVars)
233             self.Message("Get data:\n{}\n".format(getstr))
234
235         # now, attempt to make the request, starting at the first
236         # server in the list
237         if FormData:
238             cert_list = self.MONITORSERVER_CERTS
239         else:
240             cert_list = self.BOOTSERVER_CERTS
241         
242         for server in cert_list:
243             self.Message("Contacting server {}.".format(server))
244                         
245             certpath = cert_list[server]
246
247             
248             # output what we are going to be doing
249             self.Message("Connect timeout is {} seconds".format(ConnectTimeout))
250             self.Message("Max transfer time is {} seconds".format(MaxTransferTime))
251
252             if DoSSL:
253                 url = "https://{}/{}{}".format(server, PartialPath, getstr)
254                 
255                 if DoCertCheck and PYCURL_LOADED:
256                     self.Message("Using SSL version {} and verifying peer."
257                                  .format(self.CURL_SSL_VERSION))
258                 else:
259                     self.Message("Using SSL version {}."
260                                  .format(self.CURL_SSL_VERSION))
261             else:
262                 url = "http://{}/{}{}".format(server, PartialPath, getstr)
263                 
264             self.Message("URL: {}".format(url))
265             
266             # setup a new pycurl instance, or a curl command line string
267             # if we don't have pycurl
268             
269             if PYCURL_LOADED:
270                 curl = pycurl.Curl()
271
272                 # don't want curl sending any signals
273                 curl.setopt(pycurl.NOSIGNAL, 1)
274             
275                 curl.setopt(pycurl.CONNECTTIMEOUT, ConnectTimeout)
276                 curl.setopt(pycurl.TIMEOUT, MaxTransferTime)
277
278                 # do not follow location when attempting to download a file
279                 curl.setopt(pycurl.FOLLOWLOCATION, 0)
280
281                 if self.USE_PROXY:
282                     curl.setopt(pycurl.PROXY, self.PROXY)
283
284                 if DoSSL:
285                     curl.setopt(pycurl.SSLVERSION, self.CURL_SSL_VERSION)
286                 
287                     if DoCertCheck:
288                         curl.setopt(pycurl.CAINFO, certpath)
289                         curl.setopt(pycurl.SSL_VERIFYPEER, 2)
290                         
291                     else:
292                         curl.setopt(pycurl.SSL_VERIFYPEER, 0)
293                 
294                 if dopostdata:
295                     curl.setopt(pycurl.POSTFIELDS, postdata)
296
297                 # setup multipart/form-data upload
298                 if FormData:
299                     curl.setopt(pycurl.HTTPPOST, FormData)
300
301                 curl.setopt(pycurl.URL, url)
302             else:
303
304                 cmdline = "{} " \
305                           "--connect-timeout {} " \
306                           "--max-time {} " \
307                           "--header Pragma: " \
308                           "--output {} " \
309                           "--fail "\
310                           .format(self.CURL_CMD, ConnectTimeout,
311                                   MaxTransferTime, DestFilePath)
312
313                 if dopostdata:
314                     cmdline = cmdline + "--data '" + postdata + "' "
315
316                 if FormData:
317                     cmdline = cmdline + "".join(["--form '" + field + "' " for field in FormData])
318
319                 if not self.VERBOSE:
320                     cmdline = cmdline + "--silent "
321                     
322                 if self.USE_PROXY:
323                     cmdline = cmdline + "--proxy {} ".format(self.PROXY)
324
325                 if DoSSL:
326                     cmdline = cmdline + "--sslv{} ".format(self.CURL_SSL_VERSION)
327                     if DoCertCheck:
328                         cmdline = cmdline + "--cacert {} ".format(certpath)
329                  
330                 cmdline = cmdline + url
331
332                 self.Message("curl command: {}".format(cmdline))
333                 
334                 
335             if PYCURL_LOADED:
336                 try:
337                     # setup the output file
338                     outfile = open(DestFilePath,"wb")
339                     
340                     self.Message("Opened output file {}".format(DestFilePath))
341                 
342                     curl.setopt(pycurl.WRITEDATA, outfile)
343                 
344                     self.Message("Fetching...")
345                     curl.perform()
346                     self.Message("Done.")
347                 
348                     http_result = curl.getinfo(pycurl.HTTP_CODE)
349                     curl.close()
350                 
351                     outfile.close()
352                     self.Message("Results saved in {}".format(DestFilePath))
353
354                     # check the code, return 1 if successfull
355                     if http_result == self.HTTP_SUCCESS:
356                         self.Message("Successfull!")
357                         return 1
358                     else:
359                         self.Message("Failure, resultant http code: {}"
360                                      .format(http_result))
361
362                 except pycurl.error as err:
363                     errno, errstr = err
364                     self.Error("connect to {} failed; curl error {}: '{}'\n"
365                                .format(server, errno, errstr))
366         
367                 if not outfile.closed:
368                     try:
369                         os.unlink(DestFilePath)
370                         outfile.close()
371                     except OSError:
372                         pass
373
374             else:
375                 self.Message("Fetching...")
376                 rc = os.system(cmdline)
377                 self.Message("Done.")
378                 
379                 if rc != 0:
380                     try:
381                         os.unlink(DestFilePath)
382                     except OSError:
383                         pass
384                     self.Message("Failure, resultant curl code: {}".format(rc))
385                     self.Message("Removed {}".format(DestFilePath))
386                 else:
387                     self.Message("Successfull!")
388                     return 1
389             
390         self.Error("Unable to successfully contact any boot servers.\n")
391         return 0
392
393
394
395
396 def usage():
397     print(
398     """
399 Usage: BootServerRequest.py [options] <partialpath>
400 Options:
401  -c/--checkcert        Check SSL certs. Ignored if -s/--ssl missing.
402  -h/--help             This help text
403  -o/--output <file>    Write result to file
404  -s/--ssl              Make the request over HTTPS
405  -v                    Makes the operation more talkative
406 """);  
407
408
409
410 if __name__ == "__main__":
411     import getopt
412     
413     # check out our command line options
414     try:
415         opt_list, arg_list = getopt.getopt(sys.argv[1:],
416                                            "o:vhsc",
417                                            [ "output=", "verbose", \
418                                              "help","ssl","checkcert"])
419
420         ssl = 0
421         checkcert = 0
422         output_file = None
423         verbose = 0
424         
425         for opt, arg in opt_list:
426             if opt in ("-h","--help"):
427                 usage(0)
428                 sys.exit()
429             
430             if opt in ("-c","--checkcert"):
431                 checkcert = 1
432             
433             if opt in ("-s","--ssl"):
434                 ssl = 1
435
436             if opt in ("-o","--output"):
437                 output_file = arg
438
439             if opt == "-v":
440                 verbose = 1
441     
442         if len(arg_list) != 1:
443             raise Exception
444
445         partialpath = arg_list[0]
446         if string.lower(partialpath[:4]) == "http":
447             raise Exception
448
449     except:
450         usage()
451         sys.exit(2)
452
453     # got the command line args straightened out
454     requestor = BootServerRequest(verbose)
455         
456     if output_file:
457         requestor.DownloadFile(partialpath, None, None, ssl,
458                                 checkcert, output_file)
459     else:
460         result = requestor.MakeRequest(partialpath, None, None, ssl, checkcert)
461         if result:
462             print(result)
463         else:
464             sys.exit(1)