• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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