Skip to content

Commit

Permalink
add auth handler, fix CORS (#1595)
Browse files Browse the repository at this point in the history
* flake

* Fixing super method call

* add auth handler, fix CORS

* add assetHandler and NotFoundHandler

* add network to AssetHandler

* fix unused var

* add detailed query string to shows, fix mime_type

* flake8

* fix import

* Flake8 fixes

* Detailed show returns episodes grouped by season
  • Loading branch information
OmgImAlexis authored Nov 25, 2016
1 parent ecdc557 commit d1f3534
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 22 deletions.
43 changes: 43 additions & 0 deletions medusa/server/api/v2/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# coding=utf-8
"""Request handler for assets."""
import glob
import mimetypes
import os

from .base import BaseRequestHandler
from .... import app


class AssetHandler(BaseRequestHandler):
"""Asset request handler."""

def get(self, asset_group=None, query=None, *args, **kwargs):
"""Get an asset."""
if asset_group and query:
if asset_group == 'show':
asset_type = self.get_argument('type', default='banner')
return self._serve_asset(path=os.path.join(app.CACHE_DIR, 'images/'), filename=query + '.' + asset_type)
if asset_group == 'network':
return self._serve_asset(path=os.path.join(app.PROG_DIR, 'static/images/network/'), filename=query)

def _serve_asset(self, path=None, filename=None):
"""Serve the asset from the provided path."""
if path and filename:
for infile in glob.glob(os.path.join(path, filename.lower() + '.*')):
path = infile
mime_type, _ = mimetypes.guess_type(path)
if mime_type:
self.set_status(200)
self.set_header('Content-type', mime_type)
try:
with open(path, 'rb') as f:
while 1:
data = f.read(16384)
if not data:
break
self.write(data)
self.finish()
except IOError:
self.api_finish(status=404, error='Asset or Asset Type Does Not Exist')
else:
self.api_finish(status=404, error='Asset or Asset Type Does Not Exist')
43 changes: 43 additions & 0 deletions medusa/server/api/v2/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# coding=utf-8
"""Request handler for authentication."""

import base64

from .base import BaseRequestHandler
from .... import app, helpers, logger, notifiers


class LoginHandler(BaseRequestHandler):
"""Login request handler."""

def set_default_headers(self):
"""Set default CORS headers."""
super(LoginHandler, self).set_default_headers()
self.set_header('Access-Control-Allow-Methods', 'POST, OPTIONS')

def prepare(self):
"""Prepare."""
pass

def post(self, *args, **kwargs):
"""Submit login."""
username = app.WEB_USERNAME
password = app.WEB_PASSWORD

if self.request.headers.get('Authorization'):
auth_decoded = base64.decodestring(self.request.headers.get('Authorization')[6:])
decoded_username, decoded_password = auth_decoded.split(':', 2)

if app.NOTIFY_ON_LOGIN and not helpers.is_ip_private(self.request.remote_ip):
notifiers.notify_login(self.request.remote_ip)

if username != decoded_username or password != decoded_password:
logger.log('User attempted a failed login to the Medusa API from IP: {ip}'.format(ip=self.request.remote_ip), logger.WARNING)
self.api_finish(status=401, error='Invalid credentials')
else:
logger.log('User logged into the Medusa API', logger.INFO)
self.api_finish(data={
'apiKey': app.API_KEY
})
else:
self.api_finish(status=401, error='No Credentials Provided')
39 changes: 24 additions & 15 deletions medusa/server/api/v2/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# coding=utf-8
"""Base module for request handlers."""

import base64
import json
import operator

Expand All @@ -17,20 +16,21 @@ class BaseRequestHandler(RequestHandler):
"""A base class used for shared RequestHandler methods."""

def prepare(self):
"""Prepare request headers with authorization keys."""
web_username = app.WEB_USERNAME
web_password = app.WEB_PASSWORD
api_key = self.get_argument('api_key', default='')
api_username = ''
api_password = ''

if self.request.headers.get('Authorization'):
auth_header = self.request.headers.get('Authorization')
auth_decoded = base64.decodestring(auth_header[6:])
api_username, api_password = auth_decoded.split(':', 2)

if (web_username != api_username and web_password != api_password) and (app.API_KEY != api_key):
self.api_finish(status=401, error='Invalid API key')
"""Check if API key is provided and valid."""
if self.request.method != 'OPTIONS':
if app.API_KEY not in (self.get_argument('api_key', default=''), self.request.headers.get('X-Api-Key')):
self.api_finish(status=401, error='Invalid API key')

def options(self, *args, **kwargs):
"""Options."""
self.set_status(204)
self.finish()

def set_default_headers(self):
"""Set default CORS headers."""
self.set_header('Access-Control-Allow-Origin', '*')
self.set_header('Access-Control-Allow-Headers', 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token, X-Api-Key')
self.set_header('Access-Control-Allow-Methods', 'GET, OPTIONS')

def api_finish(self, status=None, error=None, data=None, headers=None, **kwargs):
"""End the api request writing error or data to http response."""
Expand All @@ -45,6 +45,7 @@ def api_finish(self, status=None, error=None, data=None, headers=None, **kwargs)
else:
self.set_status(status or 200)
if data is not None:
self.set_header('Content-Type', 'application/json; charset=UTF-8')
self.finish(json.JSONEncoder(default=json_string_encoder).encode(data))
elif kwargs:
self.finish(kwargs)
Expand Down Expand Up @@ -103,6 +104,14 @@ def _parse_date(value, fmt='%Y-%m-%d'):
return BaseRequestHandler._parse(value, lambda d: datetime.strptime(d, fmt))


class NotFoundHandler(BaseRequestHandler):
"""A class used for the API v2 404 page."""

def get(self, *args, **kwargs):
"""Get."""
self.api_finish(status=404)


def json_string_encoder(o):
"""Convert properties to string."""
if isinstance(o, Language):
Expand Down
7 changes: 6 additions & 1 deletion medusa/server/api/v2/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def __bool__(self):
class ShowHandler(BaseRequestHandler):
"""Shows request handler."""

def set_default_headers(self):
"""Set default CORS headers."""
super(ShowHandler, self).set_default_headers()
self.set_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')

def get(self, show_indexer, show_id, season, episode, absolute_episode, air_date, query):
"""Query show information.
Expand Down Expand Up @@ -62,7 +67,7 @@ def get(self, show_indexer, show_id, season, episode, absolute_episode, air_date

return self._handle_detailed_show(tv_show, query)

data = [s.to_json(detailed=False) for s in app.showList if self._match(s, arg_paused)]
data = [s.to_json(detailed=self.get_argument('detailed', default=False)) for s in app.showList if self._match(s, arg_paused)]
return self._paginate(data, 'title')

@staticmethod
Expand Down
15 changes: 12 additions & 3 deletions medusa/server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,34 @@
from tornado.web import Application, RedirectHandler, StaticFileHandler, url
from tornroutes import route
from .api.v1.core import ApiHandler
from .api.v2.config import ConfigHandler
from .api.v2.log import LogHandler
from .api.v2.show import ShowHandler
from .web import CalendarHandler, KeyHandler, LoginHandler, LogoutHandler
from .. import app, logger
from ..helpers import create_https_certificates, generateApiKey


def get_apiv2_handlers(base):
"""Return api v2 handlers."""
from .api.v2.config import ConfigHandler
from .api.v2.log import LogHandler
from .api.v2.show import ShowHandler
from .api.v2.auth import LoginHandler
from .api.v2.asset import AssetHandler
from .api.v2.base import NotFoundHandler

show_id = r'(?P<show_indexer>[a-z]+)(?P<show_id>\d+)'
ep_id = r'(?:(?:s(?P<season>\d{1,2})(?:e(?P<episode>\d{1,2}))?)|(?:e(?P<absolute_episode>\d{1,3}))|(?P<air_date>\d{4}\-\d{2}\-\d{2}))'
query = r'(?P<query>[\w]+)'
query_extended = r'(?P<query>[\w \(\)%]+)' # This also accepts the space char, () and %
log_level = r'(?P<log_level>[a-zA-Z]+)'
asset_group = r'(?P<asset_group>[a-zA-Z0-9]+)'

return [
(r'{base}/show(?:/{show_id}(?:/{ep_id})?(?:/{query})?)?/?'.format(base=base, show_id=show_id, ep_id=ep_id, query=query), ShowHandler),
(r'{base}/config(?:/{query})?/?'.format(base=base, query=query), ConfigHandler),
(r'{base}/log(?:/{log_level})?/?'.format(base=base, log_level=log_level), LogHandler),
(r'{base}/auth/login(/?)'.format(base=base), LoginHandler),
(r'{base}/asset(?:/{asset_group})(?:/{query})?/?'.format(base=base, asset_group=asset_group, query=query_extended), AssetHandler),
(r'{base}(/?.*)'.format(base=base), NotFoundHandler)
]


Expand Down
8 changes: 5 additions & 3 deletions medusa/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import time
import traceback
from collections import OrderedDict, namedtuple
from itertools import groupby

from imdb import imdb
from imdb._exceptions import IMDbDataAccessError, IMDbParserError
Expand Down Expand Up @@ -1744,7 +1745,7 @@ def to_json(self, detailed=True):
('requiredWords', [v for v in (self.rls_require_words or '').split(',') if v]),
])),
])),
('episodes', OrderedDict([])),
('seasons', OrderedDict([])),
]))

if 'rating' in self.imdb_info and 'votes' in self.imdb_info:
Expand All @@ -1758,8 +1759,9 @@ def to_json(self, detailed=True):
result['cache']['banner'] = cache.banner_path(self.indexerid)

episodes = self.get_all_episodes()
result['seasons'] = sorted(list(set([e.season for e in episodes])))
result['episodes'] = sorted(list(set([e.identifier for e in episodes])))
result['seasons'] = OrderedDict((k, list(v)) for k, v in groupby([ep.to_json() for ep in episodes],
lambda item: item['season']))
result['episodeCount'] = len(episodes)
last_episode = episodes[-1] if episodes else None
if self.status == 'Ended' and last_episode and last_episode.airdate:
result['year']['end'] = last_episode.airdate.year
Expand Down
Empty file modified start.py
100644 → 100755
Empty file.

0 comments on commit d1f3534

Please sign in to comment.