Home | History | Annotate | Download | only in update_engine
      1 # Copyright 2018 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import BaseHTTPServer
      6 import base64
      7 import binascii
      8 import thread
      9 import urlparse
     10 
     11 from string import Template
     12 from xml.dom import minidom
     13 
     14 def _split_url(url):
     15     """Splits a URL into the URL base and path."""
     16     split_url = urlparse.urlsplit(url)
     17     url_base = urlparse.urlunsplit(
     18             (split_url.scheme, split_url.netloc, '', '', ''))
     19     url_path = split_url.path
     20     return url_base, url_path.lstrip('/')
     21 
     22 
     23 class NanoOmahaDevserver(object):
     24     """A simple Omaha instance that can be setup on a DUT in client tests."""
     25 
     26     def __init__(self, eol=False, failures_per_url=1, backoff=False,
     27                  num_urls=2):
     28         """
     29         Create a nano omaha devserver.
     30 
     31         @param eol: True if we should return a response with _eol flag.
     32         @param failures_per_url: how many times each url can fail.
     33         @param backoff: Whether we should wait a while before trying to
     34                         update again after a failure.
     35         @param num_urls: The number of URLs in the omaha response.
     36 
     37         """
     38         self._eol = eol
     39         self._failures_per_url = failures_per_url
     40         self._backoff = backoff
     41         self._num_urls = num_urls
     42 
     43 
     44     def create_update_response(self, appid):
     45         """
     46         Create an update response using the values from set_image_params().
     47 
     48         @param appid: the appid parsed from the request.
     49 
     50         @returns: a string of the response this server should send.
     51 
     52         """
     53         EOL_TEMPLATE = Template("""
     54           <response protocol="3.0">
     55             <daystart elapsed_seconds="44801"/>
     56             <app appid="$appid" status="ok">
     57               <ping status="ok"/>
     58               <updatecheck _eol="eol" status="noupdate"/>
     59             </app>
     60           </response>
     61         """)
     62 
     63         RESPONSE_TEMPLATE = Template("""
     64           <response protocol="3.0">
     65             <daystart elapsed_seconds="44801"/>
     66               <app appid="$appid" status="ok">
     67               <ping status="ok"/>
     68                 <updatecheck ${ROLLBACK_FLAGS}status="ok">
     69                 <urls>
     70                   $PER_URL_TAGS
     71                 </urls>
     72                 <manifest version="$build_number">
     73                   <packages>
     74                     <package hash_sha256="$sha256" name="$image_name"
     75                     size="$image_size" required="true"/>
     76                   </packages>
     77                   <actions>
     78                     <action event="postinstall"
     79                     ChromeOSVersion="$build_number"
     80                     sha256="$sha256"
     81                     needsadmin="false"
     82                     IsDeltaPayload="$is_delta"
     83                     MaxFailureCountPerUrl="$failures_per_url"
     84                     DisablePayloadBackoff="$disable_backoff"
     85                     $OPTIONAL_ACTION_FLAGS
     86                     />
     87                   </actions>
     88                 </manifest>
     89               </updatecheck>
     90             </app>
     91           </response>
     92         """)
     93         PER_URL_TEMPLATE = Template('<url codebase="$base/"/>')
     94         FLAG_TEMPLATE = Template('$key="$value"')
     95         ROLLBACK_TEMPLATE = Template("""
     96                 _firmware_version="$fw"
     97                 _firmware_version_0="$fw0"
     98                 _firmware_version_1="$fw1"
     99                 _firmware_version_2="$fw2"
    100                 _firmware_version_3="$fw3"
    101                 _firmware_version_4="$fw4"
    102                 _kernel_version="$kern"
    103                 _kernel_version_0="$kern0"
    104                 _kernel_version_1="$kern1"
    105                 _kernel_version_2="$kern2"
    106                 _kernel_version_3="$kern3"
    107                 _kernel_version_4="$kern4"
    108                 _rollback="$is_rollback"
    109                 """)
    110 
    111         # IF EOL, return a simplified response with _eol tag.
    112         if self._eol:
    113             return EOL_TEMPLATE.substitute(appid=appid)
    114 
    115         template_keys = {}
    116         template_keys['is_delta'] = str(self._is_delta).lower()
    117         template_keys['build_number'] = self._build
    118         template_keys['sha256'] = (
    119             binascii.hexlify(base64.b64decode(self._sha256)))
    120         template_keys['image_size'] = self._image_size
    121         template_keys['failures_per_url'] = self._failures_per_url
    122         template_keys['disable_backoff'] = str(not self._backoff).lower()
    123         template_keys['num_urls'] = self._num_urls
    124         template_keys['appid'] = appid
    125 
    126         (base, name) = _split_url(self._image_url)
    127         template_keys['base'] = base
    128         template_keys['image_name'] = name
    129 
    130         # For now, set all version flags to the same value.
    131         if self._is_rollback:
    132             fw_val = '5'
    133             k_val = '7'
    134             rollback_flags = ROLLBACK_TEMPLATE.substitute(
    135                 fw=fw_val, fw0=fw_val, fw1=fw_val, fw2=fw_val, fw3=fw_val,
    136                 fw4=fw_val, kern=k_val, kern0=k_val, kern1=k_val, kern2=k_val,
    137                 kern3=k_val, kern4=k_val, is_rollback='true')
    138         else:
    139             rollback_flags = ''
    140         template_keys['ROLLBACK_FLAGS'] = rollback_flags
    141 
    142         per_url = ''
    143         for i in xrange(self._num_urls):
    144             per_url += PER_URL_TEMPLATE.substitute(template_keys)
    145         template_keys['PER_URL_TAGS'] = per_url
    146 
    147         action_flags = []
    148         def add_action_flag(key, value):
    149             """Helper function for the OPTIONAL_ACTION_FLAGS parameter."""
    150             action_flags.append(
    151                     FLAG_TEMPLATE.substitute(key=key, value=value))
    152         if self._critical:
    153             add_action_flag('deadline', 'now')
    154         if self._metadata_size:
    155             add_action_flag('MetadataSize', self._metadata_size)
    156         if self._metadata_signature:
    157             add_action_flag('MetadataSignatureRsa', self._metadata_signature)
    158         if self._public_key:
    159             add_action_flag('PublicKeyRsa', self._public_key)
    160         template_keys['OPTIONAL_ACTION_FLAGS'] = (
    161                 '\n                    '.join(action_flags))
    162 
    163         return RESPONSE_TEMPLATE.substitute(template_keys)
    164 
    165 
    166     class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
    167         """Inner class for handling HTTP requests."""
    168         def do_POST(self):
    169             """Handler for POST requests."""
    170             if self.path == '/update':
    171                 # Parse the app id from the request to use in the response.
    172                 content_len = int(self.headers.getheader('content-length'))
    173                 request_string = self.rfile.read(content_len)
    174                 request_dom = minidom.parseString(request_string)
    175                 app = request_dom.firstChild.getElementsByTagName('app')[0]
    176                 appid = app.getAttribute('appid')
    177 
    178                 response = self.server._devserver.create_update_response(appid)
    179 
    180                 self.send_response(200)
    181                 self.send_header('Content-Type', 'application/xml')
    182                 self.end_headers()
    183                 self.wfile.write(response)
    184             else:
    185                 self.send_response(500)
    186 
    187     def start(self):
    188         """Starts the server."""
    189         self._httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), self.Handler)
    190         self._httpd._devserver = self
    191         # Serve HTTP requests in a dedicated thread.
    192         thread.start_new_thread(self._httpd.serve_forever, ())
    193         self._port = self._httpd.socket.getsockname()[1]
    194 
    195     def stop(self):
    196         """Stops the server."""
    197         self._httpd.shutdown()
    198 
    199     def get_port(self):
    200         """Returns the TCP port number the server is listening on."""
    201         return self._port
    202 
    203     def get_update_url(self):
    204         """Returns the update url for this server."""
    205         return 'http://127.0.0.1:%d/update' % self._port
    206 
    207     def set_image_params(self, image_url, image_size, sha256,
    208                          metadata_size=None, metadata_signature=None,
    209                          public_key=None, is_delta=False, critical=True,
    210                          is_rollback=False, build='999999.0.0'):
    211         """
    212         Sets the values to return in the Omaha response.
    213 
    214         Only the |image_url|, |image_size| and |sha256| parameters are
    215         mandatory.
    216 
    217         @param image_url: the url of the image to install.
    218         @param image_size: the size of the image to install.
    219         @param sha256: the sha256 hash of the image to install.
    220         @param metadata_size: the size of the metadata.
    221         @param metadata_signature: the signature of the metadata.
    222         @param public_key: the public key.
    223         @param is_delta: True if image is a delta, False if a full payload.
    224         @param critical: True for forced update, False for regular update.
    225         @param is_rollback: True if image is for rollback, False if not.
    226         @param build: the build number the response should claim to have.
    227 
    228         """
    229         self._image_url = image_url
    230         self._image_size = image_size
    231         self._sha256 = sha256
    232         self._metadata_size = metadata_size
    233         self._metadata_signature = metadata_signature
    234         self._public_key = public_key
    235         self._is_delta = is_delta
    236         self._critical = critical
    237         self._is_rollback = is_rollback
    238         self._build = build
    239