1# Copyright 2014 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Command-line tools for authenticating via OAuth 2.0
16
17Do the OAuth 2.0 Web Server dance for a command line application. Stores the
18generated credentials in a common file that is used by other example apps in
19the same directory.
20"""
21
22from __future__ import print_function
23
24__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing']
26
27import logging
28import socket
29import sys
30
31from six.moves import BaseHTTPServer
32from six.moves import urllib
33from six.moves import input
34
35from oauth2client import client
36from oauth2client import util
37
38
39_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
40
41To make this sample run you will need to populate the client_secrets.json file
42found at:
43
44   %s
45
46with information from the APIs Console <https://code.google.com/apis/console>.
47
48"""
49
50def _CreateArgumentParser():
51  try:
52    import argparse
53  except ImportError:
54    return None
55  parser = argparse.ArgumentParser(add_help=False)
56  parser.add_argument('--auth_host_name', default='localhost',
57                      help='Hostname when running a local web server.')
58  parser.add_argument('--noauth_local_webserver', action='store_true',
59                      default=False, help='Do not run a local web server.')
60  parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
61                      nargs='*', help='Port web server should listen on.')
62  parser.add_argument('--logging_level', default='ERROR',
63                      choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
64                      help='Set the logging level of detail.')
65  return parser
66
67# argparser is an ArgumentParser that contains command-line options expected
68# by tools.run(). Pass it in as part of the 'parents' argument to your own
69# ArgumentParser.
70argparser = _CreateArgumentParser()
71
72
73class ClientRedirectServer(BaseHTTPServer.HTTPServer):
74  """A server to handle OAuth 2.0 redirects back to localhost.
75
76  Waits for a single request and parses the query parameters
77  into query_params and then stops serving.
78  """
79  query_params = {}
80
81
82class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
83  """A handler for OAuth 2.0 redirects back to localhost.
84
85  Waits for a single request and parses the query parameters
86  into the servers query_params and then stops serving.
87  """
88
89  def do_GET(self):
90    """Handle a GET request.
91
92    Parses the query parameters and prints a message
93    if the flow has completed. Note that we can't detect
94    if an error occurred.
95    """
96    self.send_response(200)
97    self.send_header("Content-type", "text/html")
98    self.end_headers()
99    query = self.path.split('?', 1)[-1]
100    query = dict(urllib.parse.parse_qsl(query))
101    self.server.query_params = query
102    self.wfile.write(b"<html><head><title>Authentication Status</title></head>")
103    self.wfile.write(b"<body><p>The authentication flow has completed.</p>")
104    self.wfile.write(b"</body></html>")
105
106  def log_message(self, format, *args):
107    """Do not log messages to stdout while running as command line program."""
108
109
110@util.positional(3)
111def run_flow(flow, storage, flags, http=None):
112  """Core code for a command-line application.
113
114  The ``run()`` function is called from your application and runs
115  through all the steps to obtain credentials. It takes a ``Flow``
116  argument and attempts to open an authorization server page in the
117  user's default web browser. The server asks the user to grant your
118  application access to the user's data. If the user grants access,
119  the ``run()`` function returns new credentials. The new credentials
120  are also stored in the ``storage`` argument, which updates the file
121  associated with the ``Storage`` object.
122
123  It presumes it is run from a command-line application and supports the
124  following flags:
125
126    ``--auth_host_name`` (string, default: ``localhost``)
127       Host name to use when running a local web server to handle
128       redirects during OAuth authorization.
129
130    ``--auth_host_port`` (integer, default: ``[8080, 8090]``)
131       Port to use when running a local web server to handle redirects
132       during OAuth authorization. Repeat this option to specify a list
133       of values.
134
135    ``--[no]auth_local_webserver`` (boolean, default: ``True``)
136       Run a local web server to handle redirects during OAuth authorization.
137
138
139
140
141  The tools module defines an ``ArgumentParser`` the already contains the flag
142  definitions that ``run()`` requires. You can pass that ``ArgumentParser`` to your
143  ``ArgumentParser`` constructor::
144
145      parser = argparse.ArgumentParser(description=__doc__,
146          formatter_class=argparse.RawDescriptionHelpFormatter,
147          parents=[tools.argparser])
148      flags = parser.parse_args(argv)
149
150  Args:
151    flow: Flow, an OAuth 2.0 Flow to step through.
152    storage: Storage, a ``Storage`` to store the credential in.
153    flags: ``argparse.Namespace``, The command-line flags. This is the
154        object returned from calling ``parse_args()`` on
155        ``argparse.ArgumentParser`` as described above.
156    http: An instance of ``httplib2.Http.request`` or something that
157        acts like it.
158
159  Returns:
160    Credentials, the obtained credential.
161  """
162  logging.getLogger().setLevel(getattr(logging, flags.logging_level))
163  if not flags.noauth_local_webserver:
164    success = False
165    port_number = 0
166    for port in flags.auth_host_port:
167      port_number = port
168      try:
169        httpd = ClientRedirectServer((flags.auth_host_name, port),
170                                     ClientRedirectHandler)
171      except socket.error:
172        pass
173      else:
174        success = True
175        break
176    flags.noauth_local_webserver = not success
177    if not success:
178      print('Failed to start a local webserver listening on either port 8080')
179      print('or port 9090. Please check your firewall settings and locally')
180      print('running programs that may be blocking or using those ports.')
181      print()
182      print('Falling back to --noauth_local_webserver and continuing with')
183      print('authorization.')
184      print()
185
186  if not flags.noauth_local_webserver:
187    oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
188  else:
189    oauth_callback = client.OOB_CALLBACK_URN
190  flow.redirect_uri = oauth_callback
191  authorize_url = flow.step1_get_authorize_url()
192
193  if not flags.noauth_local_webserver:
194    import webbrowser
195    webbrowser.open(authorize_url, new=1, autoraise=True)
196    print('Your browser has been opened to visit:')
197    print()
198    print('    ' + authorize_url)
199    print()
200    print('If your browser is on a different machine then exit and re-run this')
201    print('application with the command-line parameter ')
202    print()
203    print('  --noauth_local_webserver')
204    print()
205  else:
206    print('Go to the following link in your browser:')
207    print()
208    print('    ' + authorize_url)
209    print()
210
211  code = None
212  if not flags.noauth_local_webserver:
213    httpd.handle_request()
214    if 'error' in httpd.query_params:
215      sys.exit('Authentication request was rejected.')
216    if 'code' in httpd.query_params:
217      code = httpd.query_params['code']
218    else:
219      print('Failed to find "code" in the query parameters of the redirect.')
220      sys.exit('Try running with --noauth_local_webserver.')
221  else:
222    code = input('Enter verification code: ').strip()
223
224  try:
225    credential = flow.step2_exchange(code, http=http)
226  except client.FlowExchangeError as e:
227    sys.exit('Authentication has failed: %s' % e)
228
229  storage.put(credential)
230  credential.set_store(storage)
231  print('Authentication successful.')
232
233  return credential
234
235
236def message_if_missing(filename):
237  """Helpful message to display if the CLIENT_SECRETS file is missing."""
238
239  return _CLIENT_SECRETS_MESSAGE % filename
240
241try:
242  from oauth2client.old_run import run
243  from oauth2client.old_run import FLAGS
244except ImportError:
245  def run(*args, **kwargs):
246    raise NotImplementedError(
247        'The gflags library must be installed to use tools.run(). '
248        'Please install gflags or preferrably switch to using '
249        'tools.run_flow().')
250