blob: 1cd4b6ae90034ce5ea96addd4c97b8803c4c6d2e [file] [log] [blame]
Andrew Lassalle165843c2019-11-05 13:30:34 -08001#!/usr/bin/env python
Alex Deymo6751bbe2017-03-21 11:20:02 -07002#
3# Copyright (C) 2017 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""Send an A/B update to an Android device over adb."""
19
Andrew Lassalle165843c2019-11-05 13:30:34 -080020from __future__ import absolute_import
21
Alex Deymo6751bbe2017-03-21 11:20:02 -070022import argparse
Kelvin Zhangaba70ab2020-08-04 10:32:59 -040023import binascii
Sen Jiang3b15b592017-09-26 18:21:04 -070024import hashlib
Alex Deymo6751bbe2017-03-21 11:20:02 -070025import logging
26import os
27import socket
28import subprocess
29import sys
Kelvin Zhangaba70ab2020-08-04 10:32:59 -040030import struct
Alex Deymo6751bbe2017-03-21 11:20:02 -070031import threading
Sen Jiang144f9f82017-09-26 15:49:45 -070032import xml.etree.ElementTree
Alex Deymo6751bbe2017-03-21 11:20:02 -070033import zipfile
34
Andrew Lassalle165843c2019-11-05 13:30:34 -080035from six.moves import BaseHTTPServer
36
Sen Jianga1784b72017-08-09 17:42:36 -070037import update_payload.payload
38
Alex Deymo6751bbe2017-03-21 11:20:02 -070039
40# The path used to store the OTA package when applying the package from a file.
41OTA_PACKAGE_PATH = '/data/ota_package'
42
Sen Jianga1784b72017-08-09 17:42:36 -070043# The path to the payload public key on the device.
44PAYLOAD_KEY_PATH = '/etc/update_engine/update-payload-key.pub.pem'
45
46# The port on the device that update_engine should connect to.
47DEVICE_PORT = 1234
Alex Deymo6751bbe2017-03-21 11:20:02 -070048
Andrew Lassalle165843c2019-11-05 13:30:34 -080049
Alex Deymo6751bbe2017-03-21 11:20:02 -070050def CopyFileObjLength(fsrc, fdst, buffer_size=128 * 1024, copy_length=None):
51 """Copy from a file object to another.
52
53 This function is similar to shutil.copyfileobj except that it allows to copy
54 less than the full source file.
55
56 Args:
57 fsrc: source file object where to read from.
58 fdst: destination file object where to write to.
59 buffer_size: size of the copy buffer in memory.
60 copy_length: maximum number of bytes to copy, or None to copy everything.
61
62 Returns:
63 the number of bytes copied.
64 """
65 copied = 0
66 while True:
67 chunk_size = buffer_size
68 if copy_length is not None:
69 chunk_size = min(chunk_size, copy_length - copied)
70 if not chunk_size:
71 break
72 buf = fsrc.read(chunk_size)
73 if not buf:
74 break
75 fdst.write(buf)
76 copied += len(buf)
77 return copied
78
79
80class AndroidOTAPackage(object):
81 """Android update payload using the .zip format.
82
83 Android OTA packages traditionally used a .zip file to store the payload. When
84 applying A/B updates over the network, a payload binary is stored RAW inside
85 this .zip file which is used by update_engine to apply the payload. To do
86 this, an offset and size inside the .zip file are provided.
87 """
88
89 # Android OTA package file paths.
90 OTA_PAYLOAD_BIN = 'payload.bin'
91 OTA_PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
Tianjie Xu3f9be772019-11-02 18:31:50 -070092 SECONDARY_OTA_PAYLOAD_BIN = 'secondary/payload.bin'
93 SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
Kelvin Zhangaba70ab2020-08-04 10:32:59 -040094 PAYLOAD_MAGIC_HEADER = b'CrAU'
Alex Deymo6751bbe2017-03-21 11:20:02 -070095
Tianjie Xu3f9be772019-11-02 18:31:50 -070096 def __init__(self, otafilename, secondary_payload=False):
Alex Deymo6751bbe2017-03-21 11:20:02 -070097 self.otafilename = otafilename
98
99 otazip = zipfile.ZipFile(otafilename, 'r')
Tianjie Xu3f9be772019-11-02 18:31:50 -0700100 payload_entry = (self.SECONDARY_OTA_PAYLOAD_BIN if secondary_payload else
101 self.OTA_PAYLOAD_BIN)
102 payload_info = otazip.getinfo(payload_entry)
Kelvin Zhangaba70ab2020-08-04 10:32:59 -0400103
104 if payload_info.compress_type != 0:
105 logging.error(
106 "Expected layload to be uncompressed, got compression method %d",
107 payload_info.compress_type)
108 # Don't use len(payload_info.extra). Because that returns size of extra
109 # fields in central directory. We need to look at local file directory,
110 # as these two might have different sizes.
111 with open(otafilename, "rb") as fp:
112 fp.seek(payload_info.header_offset)
113 data = fp.read(zipfile.sizeFileHeader)
114 fheader = struct.unpack(zipfile.structFileHeader, data)
115 # Last two fields of local file header are filename length and
116 # extra length
117 filename_len = fheader[-2]
118 extra_len = fheader[-1]
119 self.offset = payload_info.header_offset
120 self.offset += zipfile.sizeFileHeader
121 self.offset += filename_len + extra_len
122 self.size = payload_info.file_size
123 fp.seek(self.offset)
124 payload_header = fp.read(4)
125 if payload_header != self.PAYLOAD_MAGIC_HEADER:
126 logging.warning(
127 "Invalid header, expeted %s, got %s."
128 "Either the offset is not correct, or payload is corrupted",
129 binascii.hexlify(self.PAYLOAD_MAGIC_HEADER),
130 payload_header)
Tianjie Xu3f9be772019-11-02 18:31:50 -0700131
132 property_entry = (self.SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT if
133 secondary_payload else self.OTA_PAYLOAD_PROPERTIES_TXT)
134 self.properties = otazip.read(property_entry)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700135
136
137class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler):
138 """A HTTPServer that supports single-range requests.
139
140 Attributes:
141 serving_payload: path to the only payload file we are serving.
Sen Jiang3b15b592017-09-26 18:21:04 -0700142 serving_range: the start offset and size tuple of the payload.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700143 """
144
145 @staticmethod
Sen Jiang10485592017-08-15 18:20:24 -0700146 def _parse_range(range_str, file_size):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700147 """Parse an HTTP range string.
148
149 Args:
150 range_str: HTTP Range header in the request, not including "Header:".
151 file_size: total size of the serving file.
152
153 Returns:
154 A tuple (start_range, end_range) with the range of bytes requested.
155 """
156 start_range = 0
157 end_range = file_size
158
159 if range_str:
160 range_str = range_str.split('=', 1)[1]
161 s, e = range_str.split('-', 1)
162 if s:
163 start_range = int(s)
164 if e:
165 end_range = int(e) + 1
166 elif e:
167 if int(e) < file_size:
168 start_range = file_size - int(e)
169 return start_range, end_range
170
Alex Deymo6751bbe2017-03-21 11:20:02 -0700171 def do_GET(self): # pylint: disable=invalid-name
172 """Reply with the requested payload file."""
173 if self.path != '/payload':
174 self.send_error(404, 'Unknown request')
175 return
176
177 if not self.serving_payload:
178 self.send_error(500, 'No serving payload set')
179 return
180
181 try:
182 f = open(self.serving_payload, 'rb')
183 except IOError:
184 self.send_error(404, 'File not found')
185 return
186 # Handle the range request.
187 if 'Range' in self.headers:
188 self.send_response(206)
189 else:
190 self.send_response(200)
191
Sen Jiang3b15b592017-09-26 18:21:04 -0700192 serving_start, serving_size = self.serving_range
Sen Jiang10485592017-08-15 18:20:24 -0700193 start_range, end_range = self._parse_range(self.headers.get('range'),
Sen Jiang3b15b592017-09-26 18:21:04 -0700194 serving_size)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700195 logging.info('Serving request for %s from %s [%d, %d) length: %d',
Sen Jiang3b15b592017-09-26 18:21:04 -0700196 self.path, self.serving_payload, serving_start + start_range,
197 serving_start + end_range, end_range - start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700198
199 self.send_header('Accept-Ranges', 'bytes')
200 self.send_header('Content-Range',
201 'bytes ' + str(start_range) + '-' + str(end_range - 1) +
202 '/' + str(end_range - start_range))
203 self.send_header('Content-Length', end_range - start_range)
204
Sen Jiang3b15b592017-09-26 18:21:04 -0700205 stat = os.fstat(f.fileno())
Alex Deymo6751bbe2017-03-21 11:20:02 -0700206 self.send_header('Last-Modified', self.date_time_string(stat.st_mtime))
207 self.send_header('Content-type', 'application/octet-stream')
208 self.end_headers()
209
Sen Jiang3b15b592017-09-26 18:21:04 -0700210 f.seek(serving_start + start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700211 CopyFileObjLength(f, self.wfile, copy_length=end_range - start_range)
212
Sen Jianga1784b72017-08-09 17:42:36 -0700213 def do_POST(self): # pylint: disable=invalid-name
214 """Reply with the omaha response xml."""
215 if self.path != '/update':
216 self.send_error(404, 'Unknown request')
217 return
218
219 if not self.serving_payload:
220 self.send_error(500, 'No serving payload set')
221 return
222
223 try:
224 f = open(self.serving_payload, 'rb')
225 except IOError:
226 self.send_error(404, 'File not found')
227 return
228
Sen Jiang144f9f82017-09-26 15:49:45 -0700229 content_length = int(self.headers.getheader('Content-Length'))
230 request_xml = self.rfile.read(content_length)
231 xml_root = xml.etree.ElementTree.fromstring(request_xml)
232 appid = None
233 for app in xml_root.iter('app'):
234 if 'appid' in app.attrib:
235 appid = app.attrib['appid']
236 break
237 if not appid:
238 self.send_error(400, 'No appid in Omaha request')
239 return
240
Sen Jianga1784b72017-08-09 17:42:36 -0700241 self.send_response(200)
242 self.send_header("Content-type", "text/xml")
243 self.end_headers()
244
Sen Jiang3b15b592017-09-26 18:21:04 -0700245 serving_start, serving_size = self.serving_range
246 sha256 = hashlib.sha256()
247 f.seek(serving_start)
248 bytes_to_hash = serving_size
249 while bytes_to_hash:
250 buf = f.read(min(bytes_to_hash, 1024 * 1024))
251 if not buf:
252 self.send_error(500, 'Payload too small')
253 return
254 sha256.update(buf)
255 bytes_to_hash -= len(buf)
256
257 payload = update_payload.Payload(f, payload_file_offset=serving_start)
Sen Jianga1784b72017-08-09 17:42:36 -0700258 payload.Init()
259
Sen Jiang144f9f82017-09-26 15:49:45 -0700260 response_xml = '''
Sen Jianga1784b72017-08-09 17:42:36 -0700261 <?xml version="1.0" encoding="UTF-8"?>
262 <response protocol="3.0">
Sen Jiang144f9f82017-09-26 15:49:45 -0700263 <app appid="{appid}">
Sen Jianga1784b72017-08-09 17:42:36 -0700264 <updatecheck status="ok">
265 <urls>
Sen Jiang144f9f82017-09-26 15:49:45 -0700266 <url codebase="http://127.0.0.1:{port}/"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700267 </urls>
268 <manifest version="0.0.0.1">
269 <actions>
270 <action event="install" run="payload"/>
Sen Jiang144f9f82017-09-26 15:49:45 -0700271 <action event="postinstall" MetadataSize="{metadata_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700272 </actions>
273 <packages>
Sen Jiang144f9f82017-09-26 15:49:45 -0700274 <package hash_sha256="{payload_hash}" name="payload" size="{payload_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700275 </packages>
276 </manifest>
277 </updatecheck>
278 </app>
279 </response>
Sen Jiang144f9f82017-09-26 15:49:45 -0700280 '''.format(appid=appid, port=DEVICE_PORT,
Sen Jiang3b15b592017-09-26 18:21:04 -0700281 metadata_size=payload.metadata_size,
282 payload_hash=sha256.hexdigest(),
283 payload_size=serving_size)
Sen Jiang144f9f82017-09-26 15:49:45 -0700284 self.wfile.write(response_xml.strip())
Sen Jianga1784b72017-08-09 17:42:36 -0700285 return
286
287
Alex Deymo6751bbe2017-03-21 11:20:02 -0700288class ServerThread(threading.Thread):
289 """A thread for serving HTTP requests."""
290
Sen Jiang3b15b592017-09-26 18:21:04 -0700291 def __init__(self, ota_filename, serving_range):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700292 threading.Thread.__init__(self)
Sen Jiang3b15b592017-09-26 18:21:04 -0700293 # serving_payload and serving_range are class attributes and the
294 # UpdateHandler class is instantiated with every request.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700295 UpdateHandler.serving_payload = ota_filename
Sen Jiang3b15b592017-09-26 18:21:04 -0700296 UpdateHandler.serving_range = serving_range
Alex Deymo6751bbe2017-03-21 11:20:02 -0700297 self._httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), UpdateHandler)
298 self.port = self._httpd.server_port
299
300 def run(self):
301 try:
302 self._httpd.serve_forever()
303 except (KeyboardInterrupt, socket.error):
304 pass
305 logging.info('Server Terminated')
306
307 def StopServer(self):
308 self._httpd.socket.close()
309
310
Sen Jiang3b15b592017-09-26 18:21:04 -0700311def StartServer(ota_filename, serving_range):
312 t = ServerThread(ota_filename, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700313 t.start()
314 return t
315
316
Tianjie Xu3f9be772019-11-02 18:31:50 -0700317def AndroidUpdateCommand(ota_filename, secondary, payload_url, extra_headers):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700318 """Return the command to run to start the update in the Android device."""
Tianjie Xu3f9be772019-11-02 18:31:50 -0700319 ota = AndroidOTAPackage(ota_filename, secondary)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700320 headers = ota.properties
321 headers += 'USER_AGENT=Dalvik (something, something)\n'
Alex Deymo6751bbe2017-03-21 11:20:02 -0700322 headers += 'NETWORK_ID=0\n'
Sen Jiang6fbfd7d2017-10-31 16:16:56 -0700323 headers += extra_headers
Alex Deymo6751bbe2017-03-21 11:20:02 -0700324
325 return ['update_engine_client', '--update', '--follow',
326 '--payload=%s' % payload_url, '--offset=%d' % ota.offset,
327 '--size=%d' % ota.size, '--headers="%s"' % headers]
328
329
Sen Jianga1784b72017-08-09 17:42:36 -0700330def OmahaUpdateCommand(omaha_url):
331 """Return the command to run to start the update in a device using Omaha."""
332 return ['update_engine_client', '--update', '--follow',
333 '--omaha_url=%s' % omaha_url]
334
335
Alex Deymo6751bbe2017-03-21 11:20:02 -0700336class AdbHost(object):
337 """Represents a device connected via ADB."""
338
339 def __init__(self, device_serial=None):
340 """Construct an instance.
341
342 Args:
343 device_serial: options string serial number of attached device.
344 """
345 self._device_serial = device_serial
346 self._command_prefix = ['adb']
347 if self._device_serial:
348 self._command_prefix += ['-s', self._device_serial]
349
350 def adb(self, command):
351 """Run an ADB command like "adb push".
352
353 Args:
354 command: list of strings containing command and arguments to run
355
356 Returns:
357 the program's return code.
358
359 Raises:
360 subprocess.CalledProcessError on command exit != 0.
361 """
362 command = self._command_prefix + command
363 logging.info('Running: %s', ' '.join(str(x) for x in command))
364 p = subprocess.Popen(command, universal_newlines=True)
365 p.wait()
366 return p.returncode
367
Sen Jianga1784b72017-08-09 17:42:36 -0700368 def adb_output(self, command):
369 """Run an ADB command like "adb push" and return the output.
370
371 Args:
372 command: list of strings containing command and arguments to run
373
374 Returns:
375 the program's output as a string.
376
377 Raises:
378 subprocess.CalledProcessError on command exit != 0.
379 """
380 command = self._command_prefix + command
381 logging.info('Running: %s', ' '.join(str(x) for x in command))
382 return subprocess.check_output(command, universal_newlines=True)
383
Alex Deymo6751bbe2017-03-21 11:20:02 -0700384
385def main():
386 parser = argparse.ArgumentParser(description='Android A/B OTA helper.')
Sen Jiang3b15b592017-09-26 18:21:04 -0700387 parser.add_argument('otafile', metavar='PAYLOAD', type=str,
388 help='the OTA package file (a .zip file) or raw payload \
389 if device uses Omaha.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700390 parser.add_argument('--file', action='store_true',
391 help='Push the file to the device before updating.')
392 parser.add_argument('--no-push', action='store_true',
393 help='Skip the "push" command when using --file')
394 parser.add_argument('-s', type=str, default='', metavar='DEVICE',
395 help='The specific device to use.')
396 parser.add_argument('--no-verbose', action='store_true',
397 help='Less verbose output')
Sen Jianga1784b72017-08-09 17:42:36 -0700398 parser.add_argument('--public-key', type=str, default='',
399 help='Override the public key used to verify payload.')
Sen Jiang6fbfd7d2017-10-31 16:16:56 -0700400 parser.add_argument('--extra-headers', type=str, default='',
401 help='Extra headers to pass to the device.')
Tianjie Xu3f9be772019-11-02 18:31:50 -0700402 parser.add_argument('--secondary', action='store_true',
403 help='Update with the secondary payload in the package.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700404 args = parser.parse_args()
405 logging.basicConfig(
406 level=logging.WARNING if args.no_verbose else logging.INFO)
407
408 dut = AdbHost(args.s)
409
410 server_thread = None
411 # List of commands to execute on exit.
412 finalize_cmds = []
413 # Commands to execute when canceling an update.
414 cancel_cmd = ['shell', 'su', '0', 'update_engine_client', '--cancel']
415 # List of commands to perform the update.
416 cmds = []
417
Sen Jianga1784b72017-08-09 17:42:36 -0700418 help_cmd = ['shell', 'su', '0', 'update_engine_client', '--help']
419 use_omaha = 'omaha' in dut.adb_output(help_cmd)
420
Alex Deymo6751bbe2017-03-21 11:20:02 -0700421 if args.file:
422 # Update via pushing a file to /data.
423 device_ota_file = os.path.join(OTA_PACKAGE_PATH, 'debug.zip')
424 payload_url = 'file://' + device_ota_file
425 if not args.no_push:
Tao Baoabb45a52017-10-25 11:13:03 -0700426 data_local_tmp_file = '/data/local/tmp/debug.zip'
427 cmds.append(['push', args.otafile, data_local_tmp_file])
428 cmds.append(['shell', 'su', '0', 'mv', data_local_tmp_file,
429 device_ota_file])
430 cmds.append(['shell', 'su', '0', 'chcon',
431 'u:object_r:ota_package_file:s0', device_ota_file])
Alex Deymo6751bbe2017-03-21 11:20:02 -0700432 cmds.append(['shell', 'su', '0', 'chown', 'system:cache', device_ota_file])
433 cmds.append(['shell', 'su', '0', 'chmod', '0660', device_ota_file])
434 else:
435 # Update via sending the payload over the network with an "adb reverse"
436 # command.
Sen Jianga1784b72017-08-09 17:42:36 -0700437 payload_url = 'http://127.0.0.1:%d/payload' % DEVICE_PORT
Sen Jiang3b15b592017-09-26 18:21:04 -0700438 if use_omaha and zipfile.is_zipfile(args.otafile):
Tianjie Xu3f9be772019-11-02 18:31:50 -0700439 ota = AndroidOTAPackage(args.otafile, args.secondary)
Sen Jiang3b15b592017-09-26 18:21:04 -0700440 serving_range = (ota.offset, ota.size)
441 else:
442 serving_range = (0, os.stat(args.otafile).st_size)
443 server_thread = StartServer(args.otafile, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700444 cmds.append(
Sen Jianga1784b72017-08-09 17:42:36 -0700445 ['reverse', 'tcp:%d' % DEVICE_PORT, 'tcp:%d' % server_thread.port])
446 finalize_cmds.append(['reverse', '--remove', 'tcp:%d' % DEVICE_PORT])
447
448 if args.public_key:
449 payload_key_dir = os.path.dirname(PAYLOAD_KEY_PATH)
450 cmds.append(
451 ['shell', 'su', '0', 'mount', '-t', 'tmpfs', 'tmpfs', payload_key_dir])
452 # Allow adb push to payload_key_dir
453 cmds.append(['shell', 'su', '0', 'chcon', 'u:object_r:shell_data_file:s0',
454 payload_key_dir])
455 cmds.append(['push', args.public_key, PAYLOAD_KEY_PATH])
456 # Allow update_engine to read it.
457 cmds.append(['shell', 'su', '0', 'chcon', '-R', 'u:object_r:system_file:s0',
458 payload_key_dir])
459 finalize_cmds.append(['shell', 'su', '0', 'umount', payload_key_dir])
Alex Deymo6751bbe2017-03-21 11:20:02 -0700460
461 try:
462 # The main update command using the configured payload_url.
Sen Jianga1784b72017-08-09 17:42:36 -0700463 if use_omaha:
464 update_cmd = \
465 OmahaUpdateCommand('http://127.0.0.1:%d/update' % DEVICE_PORT)
466 else:
Tianjie Xu3f9be772019-11-02 18:31:50 -0700467 update_cmd = AndroidUpdateCommand(args.otafile, args.secondary,
468 payload_url, args.extra_headers)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700469 cmds.append(['shell', 'su', '0'] + update_cmd)
470
471 for cmd in cmds:
472 dut.adb(cmd)
473 except KeyboardInterrupt:
474 dut.adb(cancel_cmd)
475 finally:
476 if server_thread:
477 server_thread.StopServer()
478 for cmd in finalize_cmds:
479 dut.adb(cmd)
480
481 return 0
482
Andrew Lassalle165843c2019-11-05 13:30:34 -0800483
Alex Deymo6751bbe2017-03-21 11:20:02 -0700484if __name__ == '__main__':
485 sys.exit(main())