Making real progress this time.
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
24
rtv/docs.py
24
rtv/docs.py
@@ -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>
|
||||||
|
"""
|
||||||
115
rtv/oauth.py
115
rtv/oauth.py
@@ -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()
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|
||||||
|
|||||||
10
setup.py
10
setup.py
@@ -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=[
|
||||||
|
|||||||
Reference in New Issue
Block a user