• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
75        @param url: URL path to send the request to.
76        @param request_data: json data to send in POST request.
77                If None, a GET request is sent with no data.
78        @param retry_count: max request retry count.
79        @param retry_delay: retry_delay (in seconds) between retries.
80        @param headers: optional dictionary of http request headers
81        @param timeout_seconds: int number of seconds for curl to wait
82                to complete the request.
83        @param tolerate_failure: True iff we should allow curl failures.
84        @return The string content of the page requested at url.
85
86        """
87        logging.debug('Requesting %s', url)
88        args = []
89        if request_data is not None:
90            headers['Content-Type'] = 'application/json; charset=utf8'
91            args.append('--data')
92            args.append(request_data)
93        for header in headers.iteritems():
94            args.append('--header')
95            args.append(': '.join(header))
96        # TODO(wiley do cert checking
97        args.append('--insecure')
98        args.append('--max-time')
99        args.append('%d' % timeout_seconds)
100        # Write the HTTP code to stdout
101        args.append('-w')
102        args.append('%{http_code}')
103        output_file = '/tmp/privetd_http_output'
104        args.append('-o')
105        args.append(output_file)
106        while retry_count >= 0:
107            result = self._run('curl %s' % url, args=args,
108                               ignore_status=True)
109            retry_count -= 1
110            raw_response = ''
111            success = result.exit_status == 0
112            http_code = result.stdout or 'timeout'
113            if success:
114                raw_response = self._run('cat %s' % output_file).stdout
115                logging.debug('Got raw response: %s', raw_response)
116            if success and http_code == '200':
117                return raw_response
118            if retry_count < 0:
119                if tolerate_failure:
120                    return None
121                raise error.TestFail('Failed requesting %s (code=%s)' %
122                                     (url, http_code))
123            logging.warn('Failed to connect to host. Retrying...')
124            time.sleep(retry_delay)
125
126
127    def send_privet_request(self, path_fragment, request_data=None,
128                            auth_token='Privet anonymous',
129                            tolerate_failure=False):
130        """Sends a privet request over HTTPS.
131
132        @param path_fragment: URL path fragment to be appended to /privet/ URL.
133        @param request_data: json data to send in POST request.
134                             If None, a GET request is sent with no data.
135        @param auth_token: authorization token to be added as 'Authorization'
136                           http header using 'Privet' as the auth realm.
137        @param tolerate_failure: True iff we should allow curl failures.
138
139        """
140        if isinstance(request_data, dict):
141                request_data = json.dumps(request_data)
142        headers = {'Authorization': auth_token}
143        url = self._build_privet_url(path_fragment, use_https=True)
144        data = self._http_request(url, request_data=request_data,
145                                  headers=headers,
146                                  tolerate_failure=tolerate_failure)
147        if data is None and tolerate_failure:
148            return None
149        try:
150            json_data = json.loads(data)
151            data = json.dumps(json_data)  # Drop newlines, pretty format.
152        finally:
153            logging.info('Received /privet/%s response: %s',
154                         path_fragment, data)
155        return json_data
156
157
158    def ping_server(self, use_https=False):
159        """Ping the privetd webserver.
160
161        Reuses port numbers from the last restart request.  The server
162        must have been restarted with enable_ping=True for this to work.
163
164        @param use_https: set to True to use 'https' protocol instead of 'http'.
165
166        """
167        url = self._build_privet_url(URL_PING, use_https=use_https);
168        content = self._http_request(url, retry_delay=5, retry_count=5)
169        if content != 'Hello, world!':
170            raise error.TestFail('Unexpected response from web server: %s.' %
171                                 content)
172
173
174    def privet_auth(self):
175        """Go through pairing and insecure auth.
176
177        @return resulting auth token.
178
179        """
180        data = {'pairing': 'pinCode', 'crypto': 'none'}
181        pairing = self.send_privet_request(URL_PAIRING_START, request_data=data)
182
183        data = {'sessionId': pairing['sessionId'],
184                'clientCommitment': pairing['deviceCommitment']
185        }
186        self.send_privet_request(URL_PAIRING_CONFIRM, request_data=data)
187
188        data = {'authCode': pairing['deviceCommitment'],
189                'mode': 'pairing',
190                'requestedScope': 'owner'
191        }
192        auth = self.send_privet_request(URL_AUTH, request_data=data)
193        auth_token = '%s %s' % (auth['tokenType'], auth['accessToken'])
194        return auth_token
195
196
197    def setup_add_wifi_credentials(self, ssid, passphrase, data={}):
198        """Add WiFi credentials to the data provided to setup_start().
199
200        @param ssid: string ssid of network to connect to.
201        @param passphrase: string passphrase for network.
202        @param data: optional dict of information to append to.
203
204        """
205        data['wifi'] = {'ssid': ssid, 'passphrase': passphrase}
206        return data
207
208
209    def setup_start(self, data, auth_token):
210        """Provide privetd with credentials for various services.
211
212        @param data: dict of information to give to privetd.  Should be
213                formed by one or more calls to setup_add_*() above.
214        @param auth_token: string auth token returned from privet_auth()
215                above.
216
217        """
218        # We don't return the response here, because in general, we may not
219        # get one.  In many cases, we'll tear down the AP so quickly that
220        # the webserver won't have time to respond.
221        self.send_privet_request(URL_SETUP_START, request_data=data,
222                                 auth_token=auth_token, tolerate_failure=True)
223
224
225    def wifi_setup_was_successful(self, ssid, auth_token):
226        """Detect whether privetd thinks bootstrapping has succeeded.
227
228        @param ssid: string network we expect to connect to.
229        @param auth_token: string auth token returned from prviet_auth()
230                above.
231        @return True iff setup/status reports success in connecting to
232                the given network.
233
234        """
235        response = self.send_privet_request(URL_SETUP_STATUS,
236                                            auth_token=auth_token)
237        return (response['wifi']['status'] == 'success' and
238                response['wifi']['ssid'] == ssid)
239