blob: 371a89a875c1b59696f8dad9971f059fdba4e246 [file] [log] [blame]
markdr5c9082d2017-10-20 13:58:06 -07001#!/usr/bin/python2
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
20import argparse
21import BaseHTTPServer
Kelvin Zhangb58ee542020-08-04 10:32:59 -040022import binascii
Sen Jiang3b15b592017-09-26 18:21:04 -070023import hashlib
Alex Deymo6751bbe2017-03-21 11:20:02 -070024import logging
25import os
26import socket
27import subprocess
28import sys
Kelvin Zhangb58ee542020-08-04 10:32:59 -040029import struct
Alex Deymo6751bbe2017-03-21 11:20:02 -070030import threading
Sen Jiang144f9f82017-09-26 15:49:45 -070031import xml.etree.ElementTree
Alex Deymo6751bbe2017-03-21 11:20:02 -070032import zipfile
33
Sen Jianga1784b72017-08-09 17:42:36 -070034import update_payload.payload
35
Alex Deymo6751bbe2017-03-21 11:20:02 -070036
37# The path used to store the OTA package when applying the package from a file.
38OTA_PACKAGE_PATH = '/data/ota_package'
39
Sen Jianga1784b72017-08-09 17:42:36 -070040# The path to the payload public key on the device.
41PAYLOAD_KEY_PATH = '/etc/update_engine/update-payload-key.pub.pem'
42
43# The port on the device that update_engine should connect to.
44DEVICE_PORT = 1234
Alex Deymo6751bbe2017-03-21 11:20:02 -070045
46def CopyFileObjLength(fsrc, fdst, buffer_size=128 * 1024, copy_length=None):
47 """Copy from a file object to another.
48
49 This function is similar to shutil.copyfileobj except that it allows to copy
50 less than the full source file.
51
52 Args:
53 fsrc: source file object where to read from.
54 fdst: destination file object where to write to.
55 buffer_size: size of the copy buffer in memory.
56 copy_length: maximum number of bytes to copy, or None to copy everything.
57
58 Returns:
59 the number of bytes copied.
60 """
61 copied = 0
62 while True:
63 chunk_size = buffer_size
64 if copy_length is not None:
65 chunk_size = min(chunk_size, copy_length - copied)
66 if not chunk_size:
67 break
68 buf = fsrc.read(chunk_size)
69 if not buf:
70 break
71 fdst.write(buf)
72 copied += len(buf)
73 return copied
74
75
76class AndroidOTAPackage(object):
77 """Android update payload using the .zip format.
78
79 Android OTA packages traditionally used a .zip file to store the payload. When
80 applying A/B updates over the network, a payload binary is stored RAW inside
81 this .zip file which is used by update_engine to apply the payload. To do
82 this, an offset and size inside the .zip file are provided.
83 """
84
85 # Android OTA package file paths.
86 OTA_PAYLOAD_BIN = 'payload.bin'
87 OTA_PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
Tianjie Xu3f9be772019-11-02 18:31:50 -070088 SECONDARY_OTA_PAYLOAD_BIN = 'secondary/payload.bin'
89 SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
Kelvin Zhangb58ee542020-08-04 10:32:59 -040090 PAYLOAD_MAGIC_HEADER = b'CrAU'
Alex Deymo6751bbe2017-03-21 11:20:02 -070091
Tianjie Xu3f9be772019-11-02 18:31:50 -070092 def __init__(self, otafilename, secondary_payload=False):
Alex Deymo6751bbe2017-03-21 11:20:02 -070093 self.otafilename = otafilename
94
95 otazip = zipfile.ZipFile(otafilename, 'r')
Tianjie Xu3f9be772019-11-02 18:31:50 -070096 payload_entry = (self.SECONDARY_OTA_PAYLOAD_BIN if secondary_payload else
97 self.OTA_PAYLOAD_BIN)
98 payload_info = otazip.getinfo(payload_entry)
Kelvin Zhangb58ee542020-08-04 10:32:59 -040099
100 if payload_info.compress_type != 0:
101 logging.error(
102 "Expected layload to be uncompressed, got compression method %d",
103 payload_info.compress_type)
104 # Don't use len(payload_info.extra). Because that returns size of extra
105 # fields in central directory. We need to look at local file directory,
106 # as these two might have different sizes.
107 with open(otafilename, "rb") as fp:
108 fp.seek(payload_info.header_offset)
109 data = fp.read(zipfile.sizeFileHeader)
110 fheader = struct.unpack(zipfile.structFileHeader, data)
111 # Last two fields of local file header are filename length and
112 # extra length
113 filename_len = fheader[-2]
114 extra_len = fheader[-1]
115 self.offset = payload_info.header_offset
116 self.offset += zipfile.sizeFileHeader
117 self.offset += filename_len + extra_len
118 self.size = payload_info.file_size
119 fp.seek(self.offset)
120 payload_header = fp.read(4)
121 if payload_header != self.PAYLOAD_MAGIC_HEADER:
122 logging.warning(
123 "Invalid header, expeted %s, got %s."
124 "Either the offset is not correct, or payload is corrupted",
125 binascii.hexlify(self.PAYLOAD_MAGIC_HEADER),
126 payload_header)
Tianjie Xu3f9be772019-11-02 18:31:50 -0700127
128 property_entry = (self.SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT if
129 secondary_payload else self.OTA_PAYLOAD_PROPERTIES_TXT)
130 self.properties = otazip.read(property_entry)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700131
132
133class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler):
134 """A HTTPServer that supports single-range requests.
135
136 Attributes:
137 serving_payload: path to the only payload file we are serving.
Sen Jiang3b15b592017-09-26 18:21:04 -0700138 serving_range: the start offset and size tuple of the payload.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700139 """
140
141 @staticmethod
Sen Jiang10485592017-08-15 18:20:24 -0700142 def _parse_range(range_str, file_size):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700143 """Parse an HTTP range string.
144
145 Args:
146 range_str: HTTP Range header in the request, not including "Header:".
147 file_size: total size of the serving file.
148
149 Returns:
150 A tuple (start_range, end_range) with the range of bytes requested.
151 """
152 start_range = 0
153 end_range = file_size
154
155 if range_str:
156 range_str = range_str.split('=', 1)[1]
157 s, e = range_str.split('-', 1)
158 if s:
159 start_range = int(s)
160 if e:
161 end_range = int(e) + 1
162 elif e:
163 if int(e) < file_size:
164 start_range = file_size - int(e)
165 return start_range, end_range
166
167
168 def do_GET(self): # pylint: disable=invalid-name
169 """Reply with the requested payload file."""
170 if self.path != '/payload':
171 self.send_error(404, 'Unknown request')
172 return
173
174 if not self.serving_payload:
175 self.send_error(500, 'No serving payload set')
176 return
177
178 try:
179 f = open(self.serving_payload, 'rb')
180 except IOError:
181 self.send_error(404, 'File not found')
182 return
183 # Handle the range request.
184 if 'Range' in self.headers:
185 self.send_response(206)
186 else:
187 self.send_response(200)
188
Sen Jiang3b15b592017-09-26 18:21:04 -0700189 serving_start, serving_size = self.serving_range
Sen Jiang10485592017-08-15 18:20:24 -0700190 start_range, end_range = self._parse_range(self.headers.get('range'),
Sen Jiang3b15b592017-09-26 18:21:04 -0700191 serving_size)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700192 logging.info('Serving request for %s from %s [%d, %d) length: %d',
Sen Jiang3b15b592017-09-26 18:21:04 -0700193 self.path, self.serving_payload, serving_start + start_range,
194 serving_start + end_range, end_range - start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700195
196 self.send_header('Accept-Ranges', 'bytes')
197 self.send_header('Content-Range',
198 'bytes ' + str(start_range) + '-' + str(end_range - 1) +
199 '/' + str(end_range - start_range))
200 self.send_header('Content-Length', end_range - start_range)
201
Sen Jiang3b15b592017-09-26 18:21:04 -0700202 stat = os.fstat(f.fileno())
Alex Deymo6751bbe2017-03-21 11:20:02 -0700203 self.send_header('Last-Modified', self.date_time_string(stat.st_mtime))
204 self.send_header('Content-type', 'application/octet-stream')
205 self.end_headers()
206
Sen Jiang3b15b592017-09-26 18:21:04 -0700207 f.seek(serving_start + start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700208 CopyFileObjLength(f, self.wfile, copy_length=end_range - start_range)
209
210
Sen Jianga1784b72017-08-09 17:42:36 -0700211 def do_POST(self): # pylint: disable=invalid-name
212 """Reply with the omaha response xml."""
213 if self.path != '/update':
214 self.send_error(404, 'Unknown request')
215 return
216
217 if not self.serving_payload:
218 self.send_error(500, 'No serving payload set')
219 return
220
221 try:
222 f = open(self.serving_payload, 'rb')
223 except IOError:
224 self.send_error(404, 'File not found')
225 return
226
Sen Jiang144f9f82017-09-26 15:49:45 -0700227 content_length = int(self.headers.getheader('Content-Length'))
228 request_xml = self.rfile.read(content_length)
229 xml_root = xml.etree.ElementTree.fromstring(request_xml)
230 appid = None
231 for app in xml_root.iter('app'):
232 if 'appid' in app.attrib:
233 appid = app.attrib['appid']
234 break
235 if not appid:
236 self.send_error(400, 'No appid in Omaha request')
237 return
238
Sen Jianga1784b72017-08-09 17:42:36 -0700239 self.send_response(200)
240 self.send_header("Content-type", "text/xml")
241 self.end_headers()
242
Sen Jiang3b15b592017-09-26 18:21:04 -0700243 serving_start, serving_size = self.serving_range
244 sha256 = hashlib.sha256()
245 f.seek(serving_start)
246 bytes_to_hash = serving_size
247 while bytes_to_hash:
248 buf = f.read(min(bytes_to_hash, 1024 * 1024))
249 if not buf:
250 self.send_error(500, 'Payload too small')
251 return
252 sha256.update(buf)
253 bytes_to_hash -= len(buf)
254
255 payload = update_payload.Payload(f, payload_file_offset=serving_start)
Sen Jianga1784b72017-08-09 17:42:36 -0700256 payload.Init()
257
Sen Jiang144f9f82017-09-26 15:49:45 -0700258 response_xml = '''
Sen Jianga1784b72017-08-09 17:42:36 -0700259 <?xml version="1.0" encoding="UTF-8"?>
260 <response protocol="3.0">
Sen Jiang144f9f82017-09-26 15:49:45 -0700261 <app appid="{appid}">
Sen Jianga1784b72017-08-09 17:42:36 -0700262 <updatecheck status="ok">
263 <urls>
Sen Jiang144f9f82017-09-26 15:49:45 -0700264 <url codebase="http://127.0.0.1:{port}/"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700265 </urls>
266 <manifest version="0.0.0.1">
267 <actions>
268 <action event="install" run="payload"/>
Sen Jiang144f9f82017-09-26 15:49:45 -0700269 <action event="postinstall" MetadataSize="{metadata_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700270 </actions>
271 <packages>
Sen Jiang144f9f82017-09-26 15:49:45 -0700272 <package hash_sha256="{payload_hash}" name="payload" size="{payload_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700273 </packages>
274 </manifest>
275 </updatecheck>
276 </app>
277 </response>
Sen Jiang144f9f82017-09-26 15:49:45 -0700278 '''.format(appid=appid, port=DEVICE_PORT,
Sen Jiang3b15b592017-09-26 18:21:04 -0700279 metadata_size=payload.metadata_size,
280 payload_hash=sha256.hexdigest(),
281 payload_size=serving_size)
Sen Jiang144f9f82017-09-26 15:49:45 -0700282 self.wfile.write(response_xml.strip())
Sen Jianga1784b72017-08-09 17:42:36 -0700283 return
284
285
Alex Deymo6751bbe2017-03-21 11:20:02 -0700286class ServerThread(threading.Thread):
287 """A thread for serving HTTP requests."""
288
Sen Jiang3b15b592017-09-26 18:21:04 -0700289 def __init__(self, ota_filename, serving_range):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700290 threading.Thread.__init__(self)
Sen Jiang3b15b592017-09-26 18:21:04 -0700291 # serving_payload and serving_range are class attributes and the
292 # UpdateHandler class is instantiated with every request.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700293 UpdateHandler.serving_payload = ota_filename
Sen Jiang3b15b592017-09-26 18:21:04 -0700294 UpdateHandler.serving_range = serving_range
Alex Deymo6751bbe2017-03-21 11:20:02 -0700295 self._httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), UpdateHandler)
296 self.port = self._httpd.server_port
297
298 def run(self):
299 try:
300 self._httpd.serve_forever()
301 except (KeyboardInterrupt, socket.error):
302 pass
303 logging.info('Server Terminated')
304
305 def StopServer(self):
306 self._httpd.socket.close()
307
308
Sen Jiang3b15b592017-09-26 18:21:04 -0700309def StartServer(ota_filename, serving_range):
310 t = ServerThread(ota_filename, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700311 t.start()
312 return t
313
314
Tianjie Xu3f9be772019-11-02 18:31:50 -0700315def AndroidUpdateCommand(ota_filename, secondary, payload_url, extra_headers):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700316 """Return the command to run to start the update in the Android device."""
Tianjie Xu3f9be772019-11-02 18:31:50 -0700317 ota = AndroidOTAPackage(ota_filename, secondary)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700318 headers = ota.properties
319 headers += 'USER_AGENT=Dalvik (something, something)\n'
Alex Deymo6751bbe2017-03-21 11:20:02 -0700320 headers += 'NETWORK_ID=0\n'
Sen Jiang6fbfd7d2017-10-31 16:16:56 -0700321 headers += extra_headers
Alex Deymo6751bbe2017-03-21 11:20:02 -0700322
323 return ['update_engine_client', '--update', '--follow',
324 '--payload=%s' % payload_url, '--offset=%d' % ota.offset,
325 '--size=%d' % ota.size, '--headers="%s"' % headers]
326
327
Sen Jianga1784b72017-08-09 17:42:36 -0700328def OmahaUpdateCommand(omaha_url):
329 """Return the command to run to start the update in a device using Omaha."""
330 return ['update_engine_client', '--update', '--follow',
331 '--omaha_url=%s' % omaha_url]
332
333
Alex Deymo6751bbe2017-03-21 11:20:02 -0700334class AdbHost(object):
335 """Represents a device connected via ADB."""
336
337 def __init__(self, device_serial=None):
338 """Construct an instance.
339
340 Args:
341 device_serial: options string serial number of attached device.
342 """
343 self._device_serial = device_serial
344 self._command_prefix = ['adb']
345 if self._device_serial:
346 self._command_prefix += ['-s', self._device_serial]
347
348 def adb(self, command):
349 """Run an ADB command like "adb push".
350
351 Args:
352 command: list of strings containing command and arguments to run
353
354 Returns:
355 the program's return code.
356
357 Raises:
358 subprocess.CalledProcessError on command exit != 0.
359 """
360 command = self._command_prefix + command
361 logging.info('Running: %s', ' '.join(str(x) for x in command))
362 p = subprocess.Popen(command, universal_newlines=True)
363 p.wait()
364 return p.returncode
365
Sen Jianga1784b72017-08-09 17:42:36 -0700366 def adb_output(self, command):
367 """Run an ADB command like "adb push" and return the output.
368
369 Args:
370 command: list of strings containing command and arguments to run
371
372 Returns:
373 the program's output as a string.
374
375 Raises:
376 subprocess.CalledProcessError on command exit != 0.
377 """
378 command = self._command_prefix + command
379 logging.info('Running: %s', ' '.join(str(x) for x in command))
380 return subprocess.check_output(command, universal_newlines=True)
381
Alex Deymo6751bbe2017-03-21 11:20:02 -0700382
383def main():
384 parser = argparse.ArgumentParser(description='Android A/B OTA helper.')
Sen Jiang3b15b592017-09-26 18:21:04 -0700385 parser.add_argument('otafile', metavar='PAYLOAD', type=str,
386 help='the OTA package file (a .zip file) or raw payload \
387 if device uses Omaha.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700388 parser.add_argument('--file', action='store_true',
389 help='Push the file to the device before updating.')
390 parser.add_argument('--no-push', action='store_true',
391 help='Skip the "push" command when using --file')
392 parser.add_argument('-s', type=str, default='', metavar='DEVICE',
393 help='The specific device to use.')
394 parser.add_argument('--no-verbose', action='store_true',
395 help='Less verbose output')
Sen Jianga1784b72017-08-09 17:42:36 -0700396 parser.add_argument('--public-key', type=str, default='',
397 help='Override the public key used to verify payload.')
Sen Jiang6fbfd7d2017-10-31 16:16:56 -0700398 parser.add_argument('--extra-headers', type=str, default='',
399 help='Extra headers to pass to the device.')
Tianjie Xu3f9be772019-11-02 18:31:50 -0700400 parser.add_argument('--secondary', action='store_true',
401 help='Update with the secondary payload in the package.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700402 args = parser.parse_args()
403 logging.basicConfig(
404 level=logging.WARNING if args.no_verbose else logging.INFO)
405
406 dut = AdbHost(args.s)
407
408 server_thread = None
409 # List of commands to execute on exit.
410 finalize_cmds = []
411 # Commands to execute when canceling an update.
412 cancel_cmd = ['shell', 'su', '0', 'update_engine_client', '--cancel']
413 # List of commands to perform the update.
414 cmds = []
415
Sen Jianga1784b72017-08-09 17:42:36 -0700416 help_cmd = ['shell', 'su', '0', 'update_engine_client', '--help']
417 use_omaha = 'omaha' in dut.adb_output(help_cmd)
418
Alex Deymo6751bbe2017-03-21 11:20:02 -0700419 if args.file:
420 # Update via pushing a file to /data.
421 device_ota_file = os.path.join(OTA_PACKAGE_PATH, 'debug.zip')
422 payload_url = 'file://' + device_ota_file
423 if not args.no_push:
Tao Baoabb45a52017-10-25 11:13:03 -0700424 data_local_tmp_file = '/data/local/tmp/debug.zip'
425 cmds.append(['push', args.otafile, data_local_tmp_file])
426 cmds.append(['shell', 'su', '0', 'mv', data_local_tmp_file,
427 device_ota_file])
428 cmds.append(['shell', 'su', '0', 'chcon',
429 'u:object_r:ota_package_file:s0', device_ota_file])
Alex Deymo6751bbe2017-03-21 11:20:02 -0700430 cmds.append(['shell', 'su', '0', 'chown', 'system:cache', device_ota_file])
431 cmds.append(['shell', 'su', '0', 'chmod', '0660', device_ota_file])
432 else:
433 # Update via sending the payload over the network with an "adb reverse"
434 # command.
Sen Jianga1784b72017-08-09 17:42:36 -0700435 payload_url = 'http://127.0.0.1:%d/payload' % DEVICE_PORT
Sen Jiang3b15b592017-09-26 18:21:04 -0700436 if use_omaha and zipfile.is_zipfile(args.otafile):
Tianjie Xu3f9be772019-11-02 18:31:50 -0700437 ota = AndroidOTAPackage(args.otafile, args.secondary)
Sen Jiang3b15b592017-09-26 18:21:04 -0700438 serving_range = (ota.offset, ota.size)
439 else:
440 serving_range = (0, os.stat(args.otafile).st_size)
441 server_thread = StartServer(args.otafile, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700442 cmds.append(
Sen Jianga1784b72017-08-09 17:42:36 -0700443 ['reverse', 'tcp:%d' % DEVICE_PORT, 'tcp:%d' % server_thread.port])
444 finalize_cmds.append(['reverse', '--remove', 'tcp:%d' % DEVICE_PORT])
445
446 if args.public_key:
447 payload_key_dir = os.path.dirname(PAYLOAD_KEY_PATH)
448 cmds.append(
449 ['shell', 'su', '0', 'mount', '-t', 'tmpfs', 'tmpfs', payload_key_dir])
450 # Allow adb push to payload_key_dir
451 cmds.append(['shell', 'su', '0', 'chcon', 'u:object_r:shell_data_file:s0',
452 payload_key_dir])
453 cmds.append(['push', args.public_key, PAYLOAD_KEY_PATH])
454 # Allow update_engine to read it.
455 cmds.append(['shell', 'su', '0', 'chcon', '-R', 'u:object_r:system_file:s0',
456 payload_key_dir])
457 finalize_cmds.append(['shell', 'su', '0', 'umount', payload_key_dir])
Alex Deymo6751bbe2017-03-21 11:20:02 -0700458
459 try:
460 # The main update command using the configured payload_url.
Sen Jianga1784b72017-08-09 17:42:36 -0700461 if use_omaha:
462 update_cmd = \
463 OmahaUpdateCommand('http://127.0.0.1:%d/update' % DEVICE_PORT)
464 else:
Tianjie Xu3f9be772019-11-02 18:31:50 -0700465 update_cmd = AndroidUpdateCommand(args.otafile, args.secondary,
466 payload_url, args.extra_headers)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700467 cmds.append(['shell', 'su', '0'] + update_cmd)
468
469 for cmd in cmds:
470 dut.adb(cmd)
471 except KeyboardInterrupt:
472 dut.adb(cancel_cmd)
473 finally:
474 if server_thread:
475 server_thread.StopServer()
476 for cmd in finalize_cmds:
477 dut.adb(cmd)
478
479 return 0
480
481if __name__ == '__main__':
482 sys.exit(main())