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

View File

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

View File

@@ -103,3 +103,27 @@ SUBMISSION_EDIT_FILE = """{content}
# #
# Editing {name} # 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 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import time import time
import uuid import uuid
import string
import codecs
import logging
import threading
from concurrent.futures import ThreadPoolExecutor from urllib.parse import urlparse, parse_qs
from tornado import gen, ioloop, web, httpserver from http.server import BaseHTTPRequestHandler, HTTPServer
from .config import TEMPLATE from . import docs
from .config import TEMPLATES
class OAuthHandler(web.RequestHandler): _logger = logging.getLogger(__name__)
"""
Intercepts the redirect that Reddit sends the user to after they verify or
deny the application access.
The GET should supply 3 request params: INDEX = os.path.join(TEMPLATES, 'index.html')
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.
"""
def initialize(self, display=None, params=None):
self.display = display
self.params = params
def get(self): class OAuthHandler(BaseHTTPRequestHandler):
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)
self.render('index.html', **self.params) params = {'state': None, 'code': None, 'error': None}
complete = self.params['state'] and self.params['code'] def do_GET(self):
if complete or self.params['error']:
# Stop IOLoop if using a background browser such as firefox parsed_path = urlparse(self.path)
if self.display: if parsed_path.path != '/':
ioloop.IOLoop.current().stop() 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): class OAuthHelper(object):
params = OAuthHandler.params
def __init__(self, reddit, term, config): def __init__(self, reddit, term, config):
self.term = term self.term = term
self.reddit = reddit self.reddit = reddit
self.config = config self.config = config
self.http_server = None address = ('', self.config['oauth_redirect_port'])
self.params = {'state': None, 'code': None, 'error': None} self.server = HTTPServer(address, OAuthHandler)
# 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)
self.reddit.set_oauth_app_info( self.reddit.set_oauth_app_info(
self.config['oauth_client_id'], self.config['oauth_client_id'],
@@ -79,14 +108,6 @@ class OAuthHelper(object):
self.config.refresh_token) self.config.refresh_token)
return 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 state = uuid.uuid4().hex
authorize_url = self.reddit.get_authorize_url( authorize_url = self.reddit.get_authorize_url(
state, scope=self.config['oauth_scope'], refreshable=True) state, scope=self.config['oauth_scope'], refreshable=True)
@@ -97,7 +118,7 @@ class OAuthHelper(object):
# point we continue and check the callback params. # point we continue and check the callback params.
with self.term.loader('Opening browser for authorization'): with self.term.loader('Opening browser for authorization'):
self.term.open_browser(authorize_url) self.term.open_browser(authorize_url)
io.start() self.server.serve_forever()
if self.term.loader.exception: if self.term.loader.exception:
return return
else: else:
@@ -139,9 +160,3 @@ class OAuthHelper(object):
def clear_oauth_data(self): def clear_oauth_data(self):
self.reddit.clear_authentication() self.reddit.clear_authentication()
self.config.delete_refresh_token() 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> </style>
</head> </head>
<body> <body>
{% if error == 'access_denied' %} ${message}
<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 %}
<div id="footer">View the <a href="http://www.github.com/michael-lazar/rtv">Documentation</a></div> <div id="footer">View the <a href="http://www.github.com/michael-lazar/rtv">Documentation</a></div>
</body> </body>
</html> </html>

View File

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