diff --git a/rtv/__main__.py b/rtv/__main__.py index e13acac..6d94366 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -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()) diff --git a/rtv/config.py b/rtv/config.py index 27010bd..5fe3e0e 100644 --- a/rtv/config.py +++ b/rtv/config.py @@ -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') diff --git a/rtv/docs.py b/rtv/docs.py index 79513f9..da1a273 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -103,3 +103,27 @@ SUBMISSION_EDIT_FILE = """{content} # # Editing {name} """ + +OAUTH_ACCESS_DENIED = """\ +
Reddit Terminal Viewer was + denied access and will continue to operate in unauthenticated mode + you can close this window.
+""" + +OAUTH_ERROR = """\ +{error}
+""" + +OAUTH_INVALID = """\ +This page is supposed to be a Reddit OAuth callback. + You can't just come here hands in your pocket!
+""" + +OAUTH_SUCCESS = """\ +Reddit Terminal Viewer + will now log in, you can close this window.
+""" \ No newline at end of file diff --git a/rtv/oauth.py b/rtv/oauth.py index e18fdd7..8147fb3 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -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: @@ -138,10 +159,4 @@ 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() \ No newline at end of file + self.config.delete_refresh_token() \ No newline at end of file diff --git a/rtv/templates/index.html b/rtv/templates/index.html index 8c3e275..20a0987 100644 --- a/rtv/templates/index.html +++ b/rtv/templates/index.html @@ -25,20 +25,7 @@ - {% if error == 'access_denied' %} -Reddit Terminal Viewer was denied access and will continue to operate in unauthenticated mode, you can close this window. - {% elif error is not None %} -
{{ error }}
- {% elif (state is None or code is None) %} -This page is supposed to be a Reddit OAuth callback. You can't just come here hands in your pocket!
- {% else %} -Reddit Terminal Viewer will now log in, you can close this window.
- {% end %} - +${message}