blob: b2452ac989d20c9b38e3fef048ba8f14d2a2d026 [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
Sen Jiang3b15b592017-09-26 18:21:04 -070022import hashlib
Alex Deymo6751bbe2017-03-21 11:20:02 -070023import logging
24import os
25import socket
26import subprocess
27import sys
28import threading
Sen Jiang144f9f82017-09-26 15:49:45 -070029import xml.etree.ElementTree
Alex Deymo6751bbe2017-03-21 11:20:02 -070030import zipfile
31
Sen Jianga1784b72017-08-09 17:42:36 -070032import update_payload.payload
33
Alex Deymo6751bbe2017-03-21 11:20:02 -070034
35# The path used to store the OTA package when applying the package from a file.
36OTA_PACKAGE_PATH = '/data/ota_package'
37
Sen Jianga1784b72017-08-09 17:42:36 -070038# The path to the payload public key on the device.
39PAYLOAD_KEY_PATH = '/etc/update_engine/update-payload-key.pub.pem'
40
41# The port on the device that update_engine should connect to.
42DEVICE_PORT = 1234
Alex Deymo6751bbe2017-03-21 11:20:02 -070043
44def CopyFileObjLength(fsrc, fdst, buffer_size=128 * 1024, copy_length=None):
45 """Copy from a file object to another.
46
47 This function is similar to shutil.copyfileobj except that it allows to copy
48 less than the full source file.
49
50 Args:
51 fsrc: source file object where to read from.
52 fdst: destination file object where to write to.
53 buffer_size: size of the copy buffer in memory.
54 copy_length: maximum number of bytes to copy, or None to copy everything.
55
56 Returns:
57 the number of bytes copied.
58 """
59 copied = 0
60 while True:
61 chunk_size = buffer_size
62 if copy_length is not None:
63 chunk_size = min(chunk_size, copy_length - copied)
64 if not chunk_size:
65 break
66 buf = fsrc.read(chunk_size)
67 if not buf:
68 break
69 fdst.write(buf)
70 copied += len(buf)
71 return copied
72
73
74class AndroidOTAPackage(object):
75 """Android update payload using the .zip format.
76
77 Android OTA packages traditionally used a .zip file to store the payload. When
78 applying A/B updates over the network, a payload binary is stored RAW inside
79 this .zip file which is used by update_engine to apply the payload. To do
80 this, an offset and size inside the .zip file are provided.
81 """
82
83 # Android OTA package file paths.
84 OTA_PAYLOAD_BIN = 'payload.bin'
85 OTA_PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
86
87 def __init__(self, otafilename):
88 self.otafilename = otafilename
89
90 otazip = zipfile.ZipFile(otafilename, 'r')
91 payload_info = otazip.getinfo(self.OTA_PAYLOAD_BIN)
92 self.offset = payload_info.header_offset + len(payload_info.FileHeader())
93 self.size = payload_info.file_size
94 self.properties = otazip.read(self.OTA_PAYLOAD_PROPERTIES_TXT)
95
96
97class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler):
98 """A HTTPServer that supports single-range requests.
99
100 Attributes:
101 serving_payload: path to the only payload file we are serving.
Sen Jiang3b15b592017-09-26 18:21:04 -0700102 serving_range: the start offset and size tuple of the payload.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700103 """
104
105 @staticmethod
Sen Jiang10485592017-08-15 18:20:24 -0700106 def _parse_range(range_str, file_size):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700107 """Parse an HTTP range string.
108
109 Args:
110 range_str: HTTP Range header in the request, not including "Header:".
111 file_size: total size of the serving file.
112
113 Returns:
114 A tuple (start_range, end_range) with the range of bytes requested.
115 """
116 start_range = 0
117 end_range = file_size
118
119 if range_str:
120 range_str = range_str.split('=', 1)[1]
121 s, e = range_str.split('-', 1)
122 if s:
123 start_range = int(s)
124 if e:
125 end_range = int(e) + 1
126 elif e:
127 if int(e) < file_size:
128 start_range = file_size - int(e)
129 return start_range, end_range
130
131
132 def do_GET(self): # pylint: disable=invalid-name
133 """Reply with the requested payload file."""
134 if self.path != '/payload':
135 self.send_error(404, 'Unknown request')
136 return
137
138 if not self.serving_payload:
139 self.send_error(500, 'No serving payload set')
140 return
141
142 try:
143 f = open(self.serving_payload, 'rb')
144 except IOError:
145 self.send_error(404, 'File not found')
146 return
147 # Handle the range request.
148 if 'Range' in self.headers:
149 self.send_response(206)
150 else:
151 self.send_response(200)
152
Sen Jiang3b15b592017-09-26 18:21:04 -0700153 serving_start, serving_size = self.serving_range
Sen Jiang10485592017-08-15 18:20:24 -0700154 start_range, end_range = self._parse_range(self.headers.get('range'),
Sen Jiang3b15b592017-09-26 18:21:04 -0700155 serving_size)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700156 logging.info('Serving request for %s from %s [%d, %d) length: %d',
Sen Jiang3b15b592017-09-26 18:21:04 -0700157 self.path, self.serving_payload, serving_start + start_range,
158 serving_start + end_range, end_range - start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700159
160 self.send_header('Accept-Ranges', 'bytes')
161 self.send_header('Content-Range',
162 'bytes ' + str(start_range) + '-' + str(end_range - 1) +
163 '/' + str(end_range - start_range))
164 self.send_header('Content-Length', end_range - start_range)
165
Sen Jiang3b15b592017-09-26 18:21:04 -0700166 stat = os.fstat(f.fileno())
Alex Deymo6751bbe2017-03-21 11:20:02 -0700167 self.send_header('Last-Modified', self.date_time_string(stat.st_mtime))
168 self.send_header('Content-type', 'application/octet-stream')
169 self.end_headers()
170
Sen Jiang3b15b592017-09-26 18:21:04 -0700171 f.seek(serving_start + start_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700172 CopyFileObjLength(f, self.wfile, copy_length=end_range - start_range)
173
174
Sen Jianga1784b72017-08-09 17:42:36 -0700175 def do_POST(self): # pylint: disable=invalid-name
176 """Reply with the omaha response xml."""
177 if self.path != '/update':
178 self.send_error(404, 'Unknown request')
179 return
180
181 if not self.serving_payload:
182 self.send_error(500, 'No serving payload set')
183 return
184
185 try:
186 f = open(self.serving_payload, 'rb')
187 except IOError:
188 self.send_error(404, 'File not found')
189 return
190
Sen Jiang144f9f82017-09-26 15:49:45 -0700191 content_length = int(self.headers.getheader('Content-Length'))
192 request_xml = self.rfile.read(content_length)
193 xml_root = xml.etree.ElementTree.fromstring(request_xml)
194 appid = None
195 for app in xml_root.iter('app'):
196 if 'appid' in app.attrib:
197 appid = app.attrib['appid']
198 break
199 if not appid:
200 self.send_error(400, 'No appid in Omaha request')
201 return
202
Sen Jianga1784b72017-08-09 17:42:36 -0700203 self.send_response(200)
204 self.send_header("Content-type", "text/xml")
205 self.end_headers()
206
Sen Jiang3b15b592017-09-26 18:21:04 -0700207 serving_start, serving_size = self.serving_range
208 sha256 = hashlib.sha256()
209 f.seek(serving_start)
210 bytes_to_hash = serving_size
211 while bytes_to_hash:
212 buf = f.read(min(bytes_to_hash, 1024 * 1024))
213 if not buf:
214 self.send_error(500, 'Payload too small')
215 return
216 sha256.update(buf)
217 bytes_to_hash -= len(buf)
218
219 payload = update_payload.Payload(f, payload_file_offset=serving_start)
Sen Jianga1784b72017-08-09 17:42:36 -0700220 payload.Init()
221
Sen Jiang144f9f82017-09-26 15:49:45 -0700222 response_xml = '''
Sen Jianga1784b72017-08-09 17:42:36 -0700223 <?xml version="1.0" encoding="UTF-8"?>
224 <response protocol="3.0">
Sen Jiang144f9f82017-09-26 15:49:45 -0700225 <app appid="{appid}">
Sen Jianga1784b72017-08-09 17:42:36 -0700226 <updatecheck status="ok">
227 <urls>
Sen Jiang144f9f82017-09-26 15:49:45 -0700228 <url codebase="http://127.0.0.1:{port}/"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700229 </urls>
230 <manifest version="0.0.0.1">
231 <actions>
232 <action event="install" run="payload"/>
Sen Jiang144f9f82017-09-26 15:49:45 -0700233 <action event="postinstall" MetadataSize="{metadata_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700234 </actions>
235 <packages>
Sen Jiang144f9f82017-09-26 15:49:45 -0700236 <package hash_sha256="{payload_hash}" name="payload" size="{payload_size}"/>
Sen Jianga1784b72017-08-09 17:42:36 -0700237 </packages>
238 </manifest>
239 </updatecheck>
240 </app>
241 </response>
Sen Jiang144f9f82017-09-26 15:49:45 -0700242 '''.format(appid=appid, port=DEVICE_PORT,
Sen Jiang3b15b592017-09-26 18:21:04 -0700243 metadata_size=payload.metadata_size,
244 payload_hash=sha256.hexdigest(),
245 payload_size=serving_size)
Sen Jiang144f9f82017-09-26 15:49:45 -0700246 self.wfile.write(response_xml.strip())
Sen Jianga1784b72017-08-09 17:42:36 -0700247 return
248
249
Alex Deymo6751bbe2017-03-21 11:20:02 -0700250class ServerThread(threading.Thread):
251 """A thread for serving HTTP requests."""
252
Sen Jiang3b15b592017-09-26 18:21:04 -0700253 def __init__(self, ota_filename, serving_range):
Alex Deymo6751bbe2017-03-21 11:20:02 -0700254 threading.Thread.__init__(self)
Sen Jiang3b15b592017-09-26 18:21:04 -0700255 # serving_payload and serving_range are class attributes and the
256 # UpdateHandler class is instantiated with every request.
Alex Deymo6751bbe2017-03-21 11:20:02 -0700257 UpdateHandler.serving_payload = ota_filename
Sen Jiang3b15b592017-09-26 18:21:04 -0700258 UpdateHandler.serving_range = serving_range
Alex Deymo6751bbe2017-03-21 11:20:02 -0700259 self._httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), UpdateHandler)
260 self.port = self._httpd.server_port
261
262 def run(self):
263 try:
264 self._httpd.serve_forever()
265 except (KeyboardInterrupt, socket.error):
266 pass
267 logging.info('Server Terminated')
268
269 def StopServer(self):
270 self._httpd.socket.close()
271
272
Sen Jiang3b15b592017-09-26 18:21:04 -0700273def StartServer(ota_filename, serving_range):
274 t = ServerThread(ota_filename, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700275 t.start()
276 return t
277
278
279def AndroidUpdateCommand(ota_filename, payload_url):
280 """Return the command to run to start the update in the Android device."""
281 ota = AndroidOTAPackage(ota_filename)
282 headers = ota.properties
283 headers += 'USER_AGENT=Dalvik (something, something)\n'
284
285 # headers += 'POWERWASH=1\n'
286 headers += 'NETWORK_ID=0\n'
287
288 return ['update_engine_client', '--update', '--follow',
289 '--payload=%s' % payload_url, '--offset=%d' % ota.offset,
290 '--size=%d' % ota.size, '--headers="%s"' % headers]
291
292
Sen Jianga1784b72017-08-09 17:42:36 -0700293def OmahaUpdateCommand(omaha_url):
294 """Return the command to run to start the update in a device using Omaha."""
295 return ['update_engine_client', '--update', '--follow',
296 '--omaha_url=%s' % omaha_url]
297
298
Alex Deymo6751bbe2017-03-21 11:20:02 -0700299class AdbHost(object):
300 """Represents a device connected via ADB."""
301
302 def __init__(self, device_serial=None):
303 """Construct an instance.
304
305 Args:
306 device_serial: options string serial number of attached device.
307 """
308 self._device_serial = device_serial
309 self._command_prefix = ['adb']
310 if self._device_serial:
311 self._command_prefix += ['-s', self._device_serial]
312
313 def adb(self, command):
314 """Run an ADB command like "adb push".
315
316 Args:
317 command: list of strings containing command and arguments to run
318
319 Returns:
320 the program's return code.
321
322 Raises:
323 subprocess.CalledProcessError on command exit != 0.
324 """
325 command = self._command_prefix + command
326 logging.info('Running: %s', ' '.join(str(x) for x in command))
327 p = subprocess.Popen(command, universal_newlines=True)
328 p.wait()
329 return p.returncode
330
Sen Jianga1784b72017-08-09 17:42:36 -0700331 def adb_output(self, command):
332 """Run an ADB command like "adb push" and return the output.
333
334 Args:
335 command: list of strings containing command and arguments to run
336
337 Returns:
338 the program's output as a string.
339
340 Raises:
341 subprocess.CalledProcessError on command exit != 0.
342 """
343 command = self._command_prefix + command
344 logging.info('Running: %s', ' '.join(str(x) for x in command))
345 return subprocess.check_output(command, universal_newlines=True)
346
Alex Deymo6751bbe2017-03-21 11:20:02 -0700347
348def main():
349 parser = argparse.ArgumentParser(description='Android A/B OTA helper.')
Sen Jiang3b15b592017-09-26 18:21:04 -0700350 parser.add_argument('otafile', metavar='PAYLOAD', type=str,
351 help='the OTA package file (a .zip file) or raw payload \
352 if device uses Omaha.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700353 parser.add_argument('--file', action='store_true',
354 help='Push the file to the device before updating.')
355 parser.add_argument('--no-push', action='store_true',
356 help='Skip the "push" command when using --file')
357 parser.add_argument('-s', type=str, default='', metavar='DEVICE',
358 help='The specific device to use.')
359 parser.add_argument('--no-verbose', action='store_true',
360 help='Less verbose output')
Sen Jianga1784b72017-08-09 17:42:36 -0700361 parser.add_argument('--public-key', type=str, default='',
362 help='Override the public key used to verify payload.')
Alex Deymo6751bbe2017-03-21 11:20:02 -0700363 args = parser.parse_args()
364 logging.basicConfig(
365 level=logging.WARNING if args.no_verbose else logging.INFO)
366
367 dut = AdbHost(args.s)
368
369 server_thread = None
370 # List of commands to execute on exit.
371 finalize_cmds = []
372 # Commands to execute when canceling an update.
373 cancel_cmd = ['shell', 'su', '0', 'update_engine_client', '--cancel']
374 # List of commands to perform the update.
375 cmds = []
376
Sen Jianga1784b72017-08-09 17:42:36 -0700377 help_cmd = ['shell', 'su', '0', 'update_engine_client', '--help']
378 use_omaha = 'omaha' in dut.adb_output(help_cmd)
379
Alex Deymo6751bbe2017-03-21 11:20:02 -0700380 if args.file:
381 # Update via pushing a file to /data.
382 device_ota_file = os.path.join(OTA_PACKAGE_PATH, 'debug.zip')
383 payload_url = 'file://' + device_ota_file
384 if not args.no_push:
385 cmds.append(['push', args.otafile, device_ota_file])
386 cmds.append(['shell', 'su', '0', 'chown', 'system:cache', device_ota_file])
387 cmds.append(['shell', 'su', '0', 'chmod', '0660', device_ota_file])
388 else:
389 # Update via sending the payload over the network with an "adb reverse"
390 # command.
Sen Jianga1784b72017-08-09 17:42:36 -0700391 payload_url = 'http://127.0.0.1:%d/payload' % DEVICE_PORT
Sen Jiang3b15b592017-09-26 18:21:04 -0700392 if use_omaha and zipfile.is_zipfile(args.otafile):
393 ota = AndroidOTAPackage(args.otafile)
394 serving_range = (ota.offset, ota.size)
395 else:
396 serving_range = (0, os.stat(args.otafile).st_size)
397 server_thread = StartServer(args.otafile, serving_range)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700398 cmds.append(
Sen Jianga1784b72017-08-09 17:42:36 -0700399 ['reverse', 'tcp:%d' % DEVICE_PORT, 'tcp:%d' % server_thread.port])
400 finalize_cmds.append(['reverse', '--remove', 'tcp:%d' % DEVICE_PORT])
401
402 if args.public_key:
403 payload_key_dir = os.path.dirname(PAYLOAD_KEY_PATH)
404 cmds.append(
405 ['shell', 'su', '0', 'mount', '-t', 'tmpfs', 'tmpfs', payload_key_dir])
406 # Allow adb push to payload_key_dir
407 cmds.append(['shell', 'su', '0', 'chcon', 'u:object_r:shell_data_file:s0',
408 payload_key_dir])
409 cmds.append(['push', args.public_key, PAYLOAD_KEY_PATH])
410 # Allow update_engine to read it.
411 cmds.append(['shell', 'su', '0', 'chcon', '-R', 'u:object_r:system_file:s0',
412 payload_key_dir])
413 finalize_cmds.append(['shell', 'su', '0', 'umount', payload_key_dir])
Alex Deymo6751bbe2017-03-21 11:20:02 -0700414
415 try:
416 # The main update command using the configured payload_url.
Sen Jianga1784b72017-08-09 17:42:36 -0700417 if use_omaha:
418 update_cmd = \
419 OmahaUpdateCommand('http://127.0.0.1:%d/update' % DEVICE_PORT)
420 else:
421 update_cmd = AndroidUpdateCommand(args.otafile, payload_url)
Alex Deymo6751bbe2017-03-21 11:20:02 -0700422 cmds.append(['shell', 'su', '0'] + update_cmd)
423
424 for cmd in cmds:
425 dut.adb(cmd)
426 except KeyboardInterrupt:
427 dut.adb(cancel_cmd)
428 finally:
429 if server_thread:
430 server_thread.StopServer()
431 for cmd in finalize_cmds:
432 dut.adb(cmd)
433
434 return 0
435
436if __name__ == '__main__':
437 sys.exit(main())