Home | History | Annotate | Download | only in tendo
      1 # Copyright 2014 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 
      5 import json
      6 import logging
      7 import time
      8 
      9 from autotest_lib.client.common_lib import error
     10 from autotest_lib.client.common_lib import utils
     11 
     12 URL_PING = 'ping'
     13 URL_INFO = 'info'
     14 URL_AUTH = 'v3/auth'
     15 URL_PAIRING_CONFIRM = 'v3/pairing/confirm'
     16 URL_PAIRING_START = 'v3/pairing/start'
     17 URL_SETUP_START = 'v3/setup/start'
     18 URL_SETUP_STATUS = 'v3/setup/status'
     19 
     20 DEFAULT_HTTP_PORT = 80
     21 DEFAULT_HTTPS_PORT = 443
     22 
     23 class PrivetHelper(object):
     24     """Delegate class containing logic useful with privetd."""
     25 
     26 
     27     def __init__(self, host=None, hostname='localhost',
     28                  http_port=DEFAULT_HTTP_PORT, https_port=DEFAULT_HTTPS_PORT):
     29         """Construct a PrivetdHelper
     30 
     31         @param host: host object where we should run the HTTP requests from.
     32         @param hostname: string hostname of host to issue HTTP requests against.
     33         @param http_port: int HTTP port to use when making HTTP requests.
     34         @param https_port: int HTTPS port to use when making HTTPs requests.
     35 
     36         """
     37         self._host = None
     38         self._run = utils.run
     39         if host is not None:
     40             self._host = host
     41             self._run = host.run
     42         self._hostname = hostname
     43         self._http_port = http_port
     44         self._https_port = https_port
     45 
     46 
     47     def _build_privet_url(self, path_fragment, use_https=True):
     48         """Builds a request URL for privet.
     49 
     50         @param path_fragment: URL path fragment to be appended to /privet/ URL.
     51         @param use_https: set to False to use 'http' protocol instead of https.
     52 
     53         @return The full URL to be used for request.
     54 
     55         """
     56         protocol = 'http'
     57         port = self._http_port
     58         if use_https:
     59             protocol = 'https'
     60             port = self._https_port
     61         url = '%s://%s:%s/privet/%s' % (protocol, self._hostname, port,
     62                                         path_fragment)
     63         return url
     64 
     65 
     66     def _http_request(self, url, request_data=None, retry_count=0,
     67                       retry_delay=0.3, headers={},
     68                       timeout_seconds=10,
     69                       tolerate_failure=False):
     70         """Sends a GET/POST request to a web server at the given |url|.
     71 
     72         If the request fails due to error 111:Connection refused, try it again
     73         after |retry_delay| seconds and repeat this to a max |retry_count|.
     74         This is needed to make sure peerd has a chance to start up and start
     75         responding to HTTP requests.
     76 
     77         @param url: URL path to send the request to.
     78         @param request_data: json data to send in POST request.
     79                 If None, a GET request is sent with no data.
     80         @param retry_count: max request retry count.
     81         @param retry_delay: retry_delay (in seconds) between retries.
     82         @param headers: optional dictionary of http request headers
     83         @param timeout_seconds: int number of seconds for curl to wait
     84                 to complete the request.
     85         @param tolerate_failure: True iff we should allow curl failures.
     86         @return The string content of the page requested at url.
     87 
     88         """
     89         logging.debug('Requesting %s', url)
     90         args = []
     91         if request_data is not None:
     92             headers['Content-Type'] = 'application/json; charset=utf8'
     93             args.append('--data')
     94             args.append(request_data)
     95         for header in headers.iteritems():
     96             args.append('--header')
     97             args.append(': '.join(header))
     98         # TODO(wiley do cert checking
     99         args.append('--insecure')
    100         args.append('--max-time')
    101         args.append('%d' % timeout_seconds)
    102         # Write the HTTP code to stdout
    103         args.append('-w')
    104         args.append('%{http_code}')
    105         output_file = '/tmp/privetd_http_output'
    106         args.append('-o')
    107         args.append(output_file)
    108         while retry_count >= 0:
    109             result = self._run('curl %s' % url, args=args,
    110                                ignore_status=True)
    111             retry_count -= 1
    112             raw_response = ''
    113             success = result.exit_status == 0
    114             http_code = result.stdout or 'timeout'
    115             if success:
    116                 raw_response = self._run('cat %s' % output_file).stdout
    117                 logging.debug('Got raw response: %s', raw_response)
    118             if success and http_code == '200':
    119                 return raw_response
    120             if retry_count < 0:
    121                 if tolerate_failure:
    122                     return None
    123                 raise error.TestFail('Failed requesting %s (code=%s)' %
    124                                      (url, http_code))
    125             logging.warn('Failed to connect to host. Retrying...')
    126             time.sleep(retry_delay)
    127 
    128 
    129     def send_privet_request(self, path_fragment, request_data=None,
    130                             auth_token='Privet anonymous',
    131                             tolerate_failure=False):
    132         """Sends a privet request over HTTPS.
    133 
    134         @param path_fragment: URL path fragment to be appended to /privet/ URL.
    135         @param request_data: json data to send in POST request.
    136                              If None, a GET request is sent with no data.
    137         @param auth_token: authorization token to be added as 'Authorization'
    138                            http header using 'Privet' as the auth realm.
    139         @param tolerate_failure: True iff we should allow curl failures.
    140 
    141         """
    142         if isinstance(request_data, dict):
    143                 request_data = json.dumps(request_data)
    144         headers = {'Authorization': auth_token}
    145         url = self._build_privet_url(path_fragment, use_https=True)
    146         data = self._http_request(url, request_data=request_data,
    147                                   headers=headers,
    148                                   tolerate_failure=tolerate_failure)
    149         if data is None and tolerate_failure:
    150             return None
    151         try:
    152             json_data = json.loads(data)
    153             data = json.dumps(json_data)  # Drop newlines, pretty format.
    154         finally:
    155             logging.info('Received /privet/%s response: %s',
    156                          path_fragment, data)
    157         return json_data
    158 
    159 
    160     def ping_server(self, use_https=False):
    161         """Ping the privetd webserver.
    162 
    163         Reuses port numbers from the last restart request.  The server
    164         must have been restarted with enable_ping=True for this to work.
    165 
    166         @param use_https: set to True to use 'https' protocol instead of 'http'.
    167 
    168         """
    169         url = self._build_privet_url(URL_PING, use_https=use_https);
    170         content = self._http_request(url, retry_delay=5, retry_count=5)
    171         if content != 'Hello, world!':
    172             raise error.TestFail('Unexpected response from web server: %s.' %
    173                                  content)
    174 
    175 
    176     def privet_auth(self):
    177         """Go through pairing and insecure auth.
    178 
    179         @return resulting auth token.
    180 
    181         """
    182         data = {'pairing': 'pinCode', 'crypto': 'none'}
    183         pairing = self.send_privet_request(URL_PAIRING_START, request_data=data)
    184 
    185         data = {'sessionId': pairing['sessionId'],
    186                 'clientCommitment': pairing['deviceCommitment']
    187         }
    188         self.send_privet_request(URL_PAIRING_CONFIRM, request_data=data)
    189 
    190         data = {'authCode': pairing['deviceCommitment'],
    191                 'mode': 'pairing',
    192                 'requestedScope': 'owner'
    193         }
    194         auth = self.send_privet_request(URL_AUTH, request_data=data)
    195         auth_token = '%s %s' % (auth['tokenType'], auth['accessToken'])
    196         return auth_token
    197 
    198 
    199     def setup_add_wifi_credentials(self, ssid, passphrase, data={}):
    200         """Add WiFi credentials to the data provided to setup_start().
    201 
    202         @param ssid: string ssid of network to connect to.
    203         @param passphrase: string passphrase for network.
    204         @param data: optional dict of information to append to.
    205 
    206         """
    207         data['wifi'] = {'ssid': ssid, 'passphrase': passphrase}
    208         return data
    209 
    210 
    211     def setup_start(self, data, auth_token):
    212         """Provide privetd with credentials for various services.
    213 
    214         @param data: dict of information to give to privetd.  Should be
    215                 formed by one or more calls to setup_add_*() above.
    216         @param auth_token: string auth token returned from privet_auth()
    217                 above.
    218 
    219         """
    220         # We don't return the response here, because in general, we may not
    221         # get one.  In many cases, we'll tear down the AP so quickly that
    222         # the webserver won't have time to respond.
    223         self.send_privet_request(URL_SETUP_START, request_data=data,
    224                                  auth_token=auth_token, tolerate_failure=True)
    225 
    226 
    227     def wifi_setup_was_successful(self, ssid, auth_token):
    228         """Detect whether privetd thinks bootstrapping has succeeded.
    229 
    230         @param ssid: string network we expect to connect to.
    231         @param auth_token: string auth token returned from prviet_auth()
    232                 above.
    233         @return True iff setup/status reports success in connecting to
    234                 the given network.
    235 
    236         """
    237         response = self.send_privet_request(URL_SETUP_STATUS,
    238                                             auth_token=auth_token)
    239         return (response['wifi']['status'] == 'success' and
    240                 response['wifi']['ssid'] == ssid)
    241