mirror of https://github.com/evilhero/mylar
(#1834) Added login page as an optional authentication method (thnx to 2mny for the Proof of concept & help)
This commit is contained in:
parent
ad74051985
commit
a2f3fb7d67
2
Mylar.py
2
Mylar.py
|
@ -240,6 +240,8 @@ def main():
|
|||
'https_chain': mylar.CONFIG.HTTPS_CHAIN,
|
||||
'http_username': mylar.CONFIG.HTTP_USERNAME,
|
||||
'http_password': mylar.CONFIG.HTTP_PASSWORD,
|
||||
'authentication': mylar.CONFIG.AUTHENTICATION,
|
||||
'login_timeout': mylar.CONFIG.LOGIN_TIMEOUT,
|
||||
'opds_enable': mylar.CONFIG.OPDS_ENABLE,
|
||||
'opds_authentication': mylar.CONFIG.OPDS_AUTHENTICATION,
|
||||
'opds_username': mylar.CONFIG.OPDS_USERNAME,
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url(http://themes.googleusercontent.com/static/fonts/lato/v7/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
background: #808080 ;
|
||||
font-family: "Lato" ;
|
||||
}
|
||||
.wrap {
|
||||
width:250px;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
margin-top: 10%;
|
||||
}
|
||||
.avatar {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
width: 65px;
|
||||
border-radius: 100px;
|
||||
height: 65px;
|
||||
background: #808080 ;
|
||||
position: relative;
|
||||
bottom: -15px;
|
||||
}
|
||||
.avatar img {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border-radius: 100px;
|
||||
margin: auto;
|
||||
border:3px solid #fff;
|
||||
display: block;
|
||||
}
|
||||
.wrap input {
|
||||
border: none;
|
||||
background: #fff;
|
||||
font-family:Lato ;
|
||||
font-weight:700 ;
|
||||
display: block;
|
||||
height: 40px;
|
||||
outline: none;
|
||||
width: calc(100% - 24px) ;
|
||||
margin: auto;
|
||||
padding: 6px 12px 6px 12px;
|
||||
}
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #fff ;
|
||||
}
|
||||
.bar i {
|
||||
width: 95%;
|
||||
margin: auto;
|
||||
height: 1px ;
|
||||
display: block;
|
||||
background: #d1d1d1;
|
||||
}
|
||||
.wrap input[type="text"] {
|
||||
border-radius: 7px 7px 0px 0px ;
|
||||
}
|
||||
.wrap input[type="password"] {
|
||||
border-radius: 0px 0px 7px 7px ;
|
||||
}
|
||||
.forgot_link {
|
||||
color: #83afdf;
|
||||
text-decoration: none;
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
left: 193px;
|
||||
top: -36px;
|
||||
}
|
||||
.wrap button {
|
||||
width: 100%;
|
||||
border-radius: 7px;
|
||||
background: #666666;
|
||||
text-decoration: center;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
margin-top:-5px;
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
border-bottom: 3px solid #404040;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -92,6 +92,7 @@
|
|||
<small>
|
||||
<a href="shutdown"><span class="ui-button-icon-primary ui-icon ui-icon-power"></span>Shutdown</a> |
|
||||
<a href="restart"><span class="ui-button-icon-primary ui-icon ui-icon-power"></span>Restart</a> |
|
||||
<a href="/auth/logout"><span class="ui-button-icon-primary ui-icon ui-icon-power"></span>Logout</a></br>
|
||||
<a href="#" onclick="doAjaxCall('checkGithub',$(this))" data-success="Checking for update successful" data-error="Error checking for update"><span class="ui-icon ui-icon-refresh"></span>Check for new version</a>
|
||||
</small>
|
||||
</div>
|
||||
|
|
|
@ -143,16 +143,38 @@
|
|||
<input type="text" title="the FULL path to the key" name="https_key" value="${config['https_key']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>HTTP Username</label>
|
||||
<input type="text" name="http_username" value="${config['http_user']}" size="30">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>HTTP Password</label>
|
||||
<input type="password" name="http_password" value="${config['http_pass']| h}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Authentication</label>
|
||||
<select name="authentication" onchange="authoptions()" id="authentication">
|
||||
%for lpe in [0, 1, 2]:
|
||||
<%
|
||||
if config['authentication'] == lpe:
|
||||
outputselect = 'selected'
|
||||
else:
|
||||
outputselect = ''
|
||||
if lpe == 2:
|
||||
auth_value = 'Forms (Login Page)'
|
||||
elif lpe == 1:
|
||||
auth_value = 'Basic (Browser Popup)'
|
||||
else:
|
||||
auth_value = 'None'
|
||||
|
||||
%>
|
||||
<option value=${lpe} ${outputselect}>${auth_value}</option>
|
||||
%endfor
|
||||
</select>
|
||||
</div>
|
||||
<div id="auth_options">
|
||||
<div class="row">
|
||||
<label>HTTP Username</label>
|
||||
<input type="text" name="http_username" value="${config['http_user']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>HTTP Password</label>
|
||||
<input type="password" name="http_password" value="${config['http_pass']| h}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} /> <label>Launch Browser on Startup</label>
|
||||
</div>
|
||||
|
@ -1390,7 +1412,17 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function authoptions()
|
||||
{
|
||||
var answer = document.getElementById("authentication");
|
||||
if(answer[answer.selectedIndex].value == 2 || answer[answer.selectedIndex].value == 1){
|
||||
document.getElementById("auth_options").style.display = "initial";
|
||||
} else {
|
||||
document.getElementById("auth_options").style.display = "none";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function initThisPage()
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" >
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mylar</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form action="/auth/login">
|
||||
<div class="wrap">
|
||||
<div class="avatar">
|
||||
<img src="/images/mylarlogo.png" />
|
||||
</div>
|
||||
<input type="text" placeholder="username" name="username" required autofocus />
|
||||
<div class="bar">
|
||||
<i></i>
|
||||
</div>
|
||||
<input type="password" placeholder="password" name="password" required />
|
||||
<button>Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- encoding: UTF-8 -*-
|
||||
# This file is part of Mylar.
|
||||
#
|
||||
# Mylar is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Mylar is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Mylar. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
######
|
||||
# Form based authentication for CherryPy. Requires the
|
||||
# Session tool to be loaded.
|
||||
###### from cherrypy/tools on github
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib.static import serve_file
|
||||
from cgi import escape
|
||||
import urllib
|
||||
|
||||
SESSION_KEY = '_cp_username'
|
||||
|
||||
def check_credentials(username, password):
|
||||
"""Verifies credentials for username and password.
|
||||
Returns None on success or a string describing the error on failure"""
|
||||
# Adapt to your needs
|
||||
forms_user = cherrypy.request.config['auth.forms_username']
|
||||
forms_pass = cherrypy.request.config['auth.forms_password']
|
||||
if username == forms_user and password == forms_pass:
|
||||
return None
|
||||
else:
|
||||
return u"Incorrect username or password."
|
||||
|
||||
def check_auth(*args, **kwargs):
|
||||
"""A tool that looks in config for 'auth.require'. If found and it
|
||||
is not None, a login is required and the entry is evaluated as a list of
|
||||
conditions that the user must fulfill"""
|
||||
conditions = cherrypy.request.config.get('auth.require', None)
|
||||
get_parmas = urllib.quote(cherrypy.request.request_line.split()[1])
|
||||
if conditions is not None:
|
||||
username = cherrypy.session.get(SESSION_KEY)
|
||||
if username:
|
||||
cherrypy.request.login = username
|
||||
for condition in conditions:
|
||||
# A condition is just a callable that returns true or false
|
||||
if not condition():
|
||||
raise cherrypy.HTTPRedirect("/auth/login")
|
||||
else:
|
||||
# Send old page as from_page parameter
|
||||
raise cherrypy.HTTPRedirect("/auth/login")
|
||||
|
||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
|
||||
|
||||
def require(*conditions):
|
||||
"""A decorator that appends conditions to the auth.require config
|
||||
variable."""
|
||||
def decorate(f):
|
||||
if not hasattr(f, '_cp_config'):
|
||||
f._cp_config = dict()
|
||||
if 'auth.require' not in f._cp_config:
|
||||
f._cp_config['auth.require'] = []
|
||||
f._cp_config['auth.require'].extend(conditions)
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
# Conditions are callables that return True
|
||||
# if the user fulfills the conditions they define, False otherwise
|
||||
#
|
||||
# They can access the current username as cherrypy.request.login
|
||||
#
|
||||
# Define those at will however suits the application.
|
||||
|
||||
def member_of(groupname):
|
||||
def check():
|
||||
# replace with actual check if <username> is in <groupname>
|
||||
return cherrypy.request.login == 'joe' and groupname == 'admin'
|
||||
return check
|
||||
|
||||
def name_is(reqd_username):
|
||||
return lambda: reqd_username == cherrypy.request.login
|
||||
|
||||
# These might be handy
|
||||
|
||||
def any_of(*conditions):
|
||||
"""Returns True if any of the conditions match"""
|
||||
def check():
|
||||
for c in conditions:
|
||||
if c():
|
||||
return True
|
||||
return False
|
||||
return check
|
||||
|
||||
# By default all conditions are required, but this might still be
|
||||
# needed if you want to use it inside of an any_of(...) condition
|
||||
def all_of(*conditions):
|
||||
"""Returns True if all of the conditions match"""
|
||||
def check():
|
||||
for c in conditions:
|
||||
if not c():
|
||||
return False
|
||||
return True
|
||||
return check
|
||||
|
||||
# Controller to provide login and logout actions
|
||||
|
||||
class AuthController(object):
|
||||
def on_login(self, username):
|
||||
"""Called on successful login"""
|
||||
# not needed or used for Mylar currently
|
||||
|
||||
def on_logout(self, username):
|
||||
"""Called on logout"""
|
||||
# not needed or used for Mylar currently
|
||||
|
||||
def get_loginform(self, username, msg="Enter login information"):
|
||||
from mylar.webserve import serve_template
|
||||
username=escape(username, True)
|
||||
return serve_template(templatename="login.html", title="Login")
|
||||
|
||||
@cherrypy.expose
|
||||
def login(self, username=None, password=None):
|
||||
if username is None or password is None:
|
||||
return self.get_loginform("")
|
||||
|
||||
error_msg = check_credentials(username, password)
|
||||
if error_msg:
|
||||
return self.get_loginform(username, error_msg)
|
||||
else:
|
||||
cherrypy.session.regenerate()
|
||||
cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
|
||||
self.on_login(username)
|
||||
raise cherrypy.HTTPRedirect("/")
|
||||
|
||||
@cherrypy.expose
|
||||
def logout(self):
|
||||
sess = cherrypy.session
|
||||
username = sess.get(SESSION_KEY, None)
|
||||
sess[SESSION_KEY] = None
|
||||
if username:
|
||||
cherrypy.request.login = None
|
||||
self.on_logout(username)
|
||||
raise cherrypy.HTTPRedirect("/")
|
||||
|
|
@ -95,6 +95,8 @@ _CONFIG_DEFINITIONS = OrderedDict({
|
|||
'HTTPS_CHAIN' : (str, 'Interface', None),
|
||||
'HTTPS_FORCE_ON' : (bool, 'Interface', False),
|
||||
'HOST_RETURN' : (str, 'Interface', None),
|
||||
'AUTHENTICATION' : (int, 'Interface', 0),
|
||||
'LOGIN_TIMEOUT': (int, 'Interface', 43800),
|
||||
|
||||
'API_ENABLED' : (bool, 'API', False),
|
||||
'API_KEY' : (str, 'API', None),
|
||||
|
@ -727,6 +729,12 @@ class Config(object):
|
|||
if self.BLACKLISTED_PUBLISHERS is not None and type(self.BLACKLISTED_PUBLISHERS) == unicode:
|
||||
setattr(self, 'BLACKLISTED_PUBLISHERS', self.BLACKLISTED_PUBLISHERS.split(', '))
|
||||
|
||||
if all([self.AUTHENTICATION == 0, self.HTTP_USERNAME is not None, self.HTTP_PASSWORD is not None]):
|
||||
#set it to the default login prompt if nothing selected.
|
||||
self.AUTHENTICATION = 1
|
||||
elif all([self.HTTP_USERNAME is None, self.HTTP_PASSWORD is None]):
|
||||
self.AUTHENTICATION = 0
|
||||
|
||||
#comictagger - force to use included version if option is enabled.
|
||||
if self.ENABLE_META:
|
||||
mylar.CMTAGGER_PATH = mylar.PROG_DIR
|
||||
|
|
|
@ -39,6 +39,7 @@ import shutil
|
|||
import mylar
|
||||
|
||||
from mylar import logger, db, importer, mb, search, filechecker, helpers, updater, parseit, weeklypull, PostProcessor, librarysync, moveit, Failed, readinglist, notifiers, sabparse, config
|
||||
from mylar.auth import AuthController, require
|
||||
|
||||
import simplejson as simplejson
|
||||
|
||||
|
@ -66,6 +67,9 @@ class WebInterface(object):
|
|||
|
||||
# _cp_config = { 'tools.filter_reqeust_on': True }
|
||||
|
||||
# login and logout methods at /auth/login and /auth/logout
|
||||
auth = AuthController()
|
||||
|
||||
def index(self):
|
||||
if mylar.SAFESTART:
|
||||
raise cherrypy.HTTPRedirect("manageComics")
|
||||
|
@ -4233,6 +4237,7 @@ class WebInterface(object):
|
|||
"enable_https": helpers.checked(mylar.CONFIG.ENABLE_HTTPS),
|
||||
"https_cert": mylar.CONFIG.HTTPS_CERT,
|
||||
"https_key": mylar.CONFIG.HTTPS_KEY,
|
||||
"authentication": int(mylar.CONFIG.AUTHENTICATION),
|
||||
"api_enabled": helpers.checked(mylar.CONFIG.API_ENABLED),
|
||||
"api_key": mylar.CONFIG.API_KEY,
|
||||
"launch_browser": helpers.checked(mylar.CONFIG.LAUNCH_BROWSER),
|
||||
|
|
|
@ -26,6 +26,7 @@ from mylar import logger
|
|||
from mylar.webserve import WebInterface
|
||||
from mylar.helpers import create_https_certificates
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
|
@ -112,18 +113,46 @@ def initialize(options):
|
|||
'/cache': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': mylar.CONFIG.CACHE_DIR,
|
||||
'tools.auth_basic.on': False
|
||||
'tools.auth_basic.on': False,
|
||||
'tools.auth.on': False
|
||||
}
|
||||
}
|
||||
|
||||
if options['http_password'] is not None:
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'Mylar',
|
||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict(
|
||||
{options['http_username']: options['http_password']})
|
||||
})
|
||||
conf['/api'] = {'tools.auth_basic.on': False}
|
||||
if options['authentication'] == 2:
|
||||
# Set up a sessions based login page instead of using basic auth,
|
||||
# using the credentials set for basic auth. Attempting to browse to
|
||||
# a restricted page without a session token will result in a
|
||||
# redirect to the login page. A sucessful login should then redirect
|
||||
# to the originally requested page.
|
||||
#
|
||||
# Login sessions timeout after 43800 minutes (1 month) unless
|
||||
# changed in the config.
|
||||
cherrypy.tools.sessions.timeout = options['login_timeout']
|
||||
conf['/'].update({
|
||||
'tools.sessions.on': True,
|
||||
'tools.auth.on': True,
|
||||
'auth.forms_username': options['http_username'],
|
||||
'auth.forms_password': options['http_password'],
|
||||
# Set all pages to require authentication.
|
||||
# You can also set auth requirements on a per-method basis by
|
||||
# using the @require() decorator on the methods in webserve.py
|
||||
'auth.require': []
|
||||
})
|
||||
# exempt api, login page and static elements from authentication requirements
|
||||
for i in ('/api', '/auth/login', '/css', '/images', '/js', 'favicon.ico'):
|
||||
if i in conf:
|
||||
conf[i].update({'tools.auth.on': False})
|
||||
else:
|
||||
conf[i] = {'tools.auth.on': False}
|
||||
elif options['authentication'] == 1:
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'Mylar',
|
||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict(
|
||||
{options['http_username']: options['http_password']})
|
||||
})
|
||||
conf['/api'] = {'tools.auth_basic.on': False}
|
||||
|
||||
if options['opds_authentication']:
|
||||
user_list = {}
|
||||
|
|
Loading…
Reference in New Issue