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