Making real progress this time.

This commit is contained in:
Michael Lazar
2016-08-08 23:05:06 -07:00
parent 0f0845b346
commit dd0d0db764
7 changed files with 96 additions and 82 deletions

View File

@@ -9,7 +9,6 @@ import warnings
import six
import praw
import tornado
import requests
from . import docs
@@ -158,7 +157,5 @@ def main():
# Ensure sockets are closed to prevent a ResourceWarning
if 'reddit' in locals():
reddit.handler.http.close()
# Explicitly close file descriptors opened by Tornado's IOLoop
tornado.ioloop.IOLoop.current().close(all_fds=True)
sys.exit(main())

View File

@@ -15,9 +15,9 @@ from .objects import KeyMap
PACKAGE = os.path.dirname(__file__)
HOME = os.path.expanduser('~')
TEMPLATE = os.path.join(PACKAGE, 'templates')
DEFAULT_CONFIG = os.path.join(TEMPLATE, 'rtv.cfg')
DEFAULT_MAILCAP = os.path.join(TEMPLATE, 'mailcap')
TEMPLATES = os.path.join(PACKAGE, 'templates')
DEFAULT_CONFIG = os.path.join(TEMPLATES, 'rtv.cfg')
DEFAULT_MAILCAP = os.path.join(TEMPLATES, 'mailcap')
XDG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
CONFIG = os.path.join(XDG_HOME, 'rtv', 'rtv.cfg')
MAILCAP = os.path.join(HOME, '.mailcap')

View File

@@ -103,3 +103,27 @@ SUBMISSION_EDIT_FILE = """{content}
#
# Editing {name}
"""
OAUTH_ACCESS_DENIED = """\
<h1 style="color: red">Access Denied</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> was
denied access and will continue to operate in unauthenticated mode
you can close this window.</p>
"""
OAUTH_ERROR = """\
<h1 style="color: red">Error</h1><hr>
<p>{error}</p>
"""
OAUTH_INVALID = """\
<h1>Wait...</h1><hr>
<p>This page is supposed to be a Reddit OAuth callback.
You can't just come here hands in your pocket!</p>
"""
OAUTH_SUCCESS = """\
<h1 style="color: green">Access Granted</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span>
will now log in, you can close this window.</p>
"""

View File

@@ -1,62 +1,91 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import time
import uuid
import string
import codecs
import logging
import threading
from concurrent.futures import ThreadPoolExecutor
from tornado import gen, ioloop, web, httpserver
from urllib.parse import urlparse, parse_qs
from http.server import BaseHTTPRequestHandler, HTTPServer
from .config import TEMPLATE
from . import docs
from .config import TEMPLATES
class OAuthHandler(web.RequestHandler):
"""
Intercepts the redirect that Reddit sends the user to after they verify or
deny the application access.
_logger = logging.getLogger(__name__)
The GET should supply 3 request params:
state: Unique id that was supplied by us at the beginning of the
process to verify that the session matches.
code: Code that we can use to generate the refresh token.
error: If an error occurred, it will be placed here.
"""
INDEX = os.path.join(TEMPLATES, 'index.html')
def initialize(self, display=None, params=None):
self.display = display
self.params = params
def get(self):
self.params['state'] = self.get_argument('state', default=None)
self.params['code'] = self.get_argument('code', default=None)
self.params['error'] = self.get_argument('error', default=None)
class OAuthHandler(BaseHTTPRequestHandler):
self.render('index.html', **self.params)
params = {'state': None, 'code': None, 'error': None}
complete = self.params['state'] and self.params['code']
if complete or self.params['error']:
# Stop IOLoop if using a background browser such as firefox
if self.display:
ioloop.IOLoop.current().stop()
def do_GET(self):
parsed_path = urlparse(self.path)
if parsed_path.path != '/':
self.send_error(404)
qs = parse_qs(parsed_path.query)
self.params['state'] = qs['state'][0] if 'state' in qs else None
self.params['code'] = qs['code'][0] if 'code' in qs else None
self.params['error'] = qs['error'][0] if 'error' in qs else None
body = self.build_body()
# send_response also sets the Server and Date headers
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=UTF-8')
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
# Shutdown the server after serving the request
# http://stackoverflow.com/a/22533929
thread = threading.Thread(target=self.server.shutdown)
thread.daemon = True
thread.start()
def build_body(self, template_file=INDEX):
if self.params['error'] == 'access_denied':
message = docs.OAUTH_ACCESS_DENIED
elif self.params['error'] is not None:
message = docs.OAUTH_ERROR.format(error=self.params['error'])
elif self.params['state'] is None or self.params['code'] is None:
message = docs.OAUTH_INVALID
else:
message = docs.OAUTH_SUCCESS
with codecs.open(template_file, 'r', 'utf-8') as fp:
index_text = fp.read()
body = string.Template(index_text).substitute(message=message)
body = codecs.encode(body, 'utf-8')
return body
def log_message(self, format, *args):
_logger.debug(format, *args)
class OAuthHelper(object):
params = OAuthHandler.params
def __init__(self, reddit, term, config):
self.term = term
self.reddit = reddit
self.config = config
self.http_server = None
self.params = {'state': None, 'code': None, 'error': None}
# Initialize Tornado webapp
# Pass a mutable params object so the request handler can modify it
kwargs = {'display': self.term.display, 'params': self.params}
routes = [('/', OAuthHandler, kwargs)]
self.callback_app = web.Application(
routes, template_path=TEMPLATE)
address = ('', self.config['oauth_redirect_port'])
self.server = HTTPServer(address, OAuthHandler)
self.reddit.set_oauth_app_info(
self.config['oauth_client_id'],
@@ -79,14 +108,6 @@ class OAuthHelper(object):
self.config.refresh_token)
return
# https://github.com/tornadoweb/tornado/issues/1420
io = ioloop.IOLoop.current()
# Start the authorization callback server
if self.http_server is None:
self.http_server = httpserver.HTTPServer(self.callback_app)
self.http_server.listen(self.config['oauth_redirect_port'])
state = uuid.uuid4().hex
authorize_url = self.reddit.get_authorize_url(
state, scope=self.config['oauth_scope'], refreshable=True)
@@ -97,7 +118,7 @@ class OAuthHelper(object):
# point we continue and check the callback params.
with self.term.loader('Opening browser for authorization'):
self.term.open_browser(authorize_url)
io.start()
self.server.serve_forever()
if self.term.loader.exception:
return
else:
@@ -139,9 +160,3 @@ class OAuthHelper(object):
def clear_oauth_data(self):
self.reddit.clear_authentication()
self.config.delete_refresh_token()
@gen.coroutine
def _async_open_browser(self, url):
with ThreadPoolExecutor(max_workers=1) as executor:
yield executor.submit(self.term.open_browser, url)
ioloop.IOLoop.current().stop()

View File

@@ -25,20 +25,7 @@
</style>
</head>
<body>
{% if error == 'access_denied' %}
<h1 style="color: red">Access Denied</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> was denied access and will continue to operate in unauthenticated mode, you can close this window.
{% elif error is not None %}
<h1 style="color: red">Error</h1><hr>
<p>{{ error }}</p>
{% elif (state is None or code is None) %}
<h1>Wait...</h1><hr>
<p>This page is supposed to be a Reddit OAuth callback. You can't just come here hands in your pocket!</p>
{% else %}
<h1 style="color: green">Access Granted</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> will now log in, you can close this window.</p>
{% end %}
${message}
<div id="footer">View the <a href="http://www.github.com/michael-lazar/rtv">Documentation</a></div>
</body>
</html>

View File

@@ -3,12 +3,9 @@ universal = 1
[metadata]
requires-dist =
tornado
praw>=3.5,<4
six
requests
kitchen
beautifulsoup4
mailcap-fix
futures; python_version=="2.6" or python_version=="2.7"

View File

@@ -3,12 +3,8 @@ import setuptools
from version import __version__ as version
requirements = ['tornado', 'praw==3.5.0', 'six', 'requests', 'kitchen',
'beautifulsoup4', 'mailcap-fix']
# Python 2: add required concurrent.futures backport from Python 3.2
if sys.version_info.major <= 2:
requirements.append('futures')
requirements = ['praw==3.5.0', 'six', 'requests', 'kitchen', 'beautifulsoup4',
'mailcap-fix']
setuptools.setup(
name='rtv',
@@ -23,8 +19,6 @@ setuptools.setup(
packages=['rtv'],
package_data={'rtv': ['templates/*']},
data_files=[("share/man/man1", ["rtv.1"])],
extras_require={
':python_version=="2.6" or python_version=="2.7"': ['futures']},
install_requires=requirements,
entry_points={'console_scripts': ['rtv=rtv.__main__:main']},
classifiers=[