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 5import BaseHTTPServer 6import base64 7import binascii 8import thread 9import urlparse 10 11from string import Template 12from xml.dom import minidom 13 14def _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 23class 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