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
5import json
6import logging
7import time
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib import utils
11
12URL_PING = 'ping'
13URL_INFO = 'info'
14URL_AUTH = 'v3/auth'
15URL_PAIRING_CONFIRM = 'v3/pairing/confirm'
16URL_PAIRING_START = 'v3/pairing/start'
17URL_SETUP_START = 'v3/setup/start'
18URL_SETUP_STATUS = 'v3/setup/status'
19
20DEFAULT_HTTP_PORT = 80
21DEFAULT_HTTPS_PORT = 443
22
23class 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