first commit

This commit is contained in:
2020-04-11 15:20:25 +01:00
commit d321726d62
18 changed files with 1035 additions and 0 deletions

View File

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import requests
class Error(Exception):
pass
class Jellyfin:
class UserExistsError(Error):
pass
class UserNotFoundError(Error):
pass
class AuthenticationError(Error):
pass
class AuthenticationRequiredError(Error):
pass
def __init__(self, server, client, version, device, deviceId):
self.server = server
self.client = client
self.version = version
self.device = device
self.deviceId = deviceId
self.useragent = f"{self.client}/{self.version}"
self.auth = "MediaBrowser "
self.auth += f"Client={self.client}, "
self.auth += f"Device={self.device}, "
self.auth += f"DeviceId={self.deviceId}, "
self.auth += f"Version={self.version}"
self.header = {
"Accept": "application/json",
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Application": f"{self.client}/{self.version}",
"Accept-Charset": "UTF-8,*",
"Accept-encoding": "gzip",
"User-Agent": self.useragent,
"X-Emby-Authorization": self.auth
}
def getUsers(self, username="all"):
response = requests.get(self.server+"/emby/Users/Public").json()
if username == "all":
return response
else:
match = False
for user in response:
if user['Name'] == username:
match = True
return user
if not match:
raise self.UserNotFoundError
def authenticate(self, username, password):
self.username = username
self.password = password
response = requests.post(self.server+"/emby/Users/AuthenticateByName",
headers=self.header,
params={'Username': self.username,
'Pw': self.password})
if response.status_code == 200:
json = response.json()
self.userId = json['User']['Id']
self.accessToken = json['AccessToken']
self.auth += f", Token={self.accessToken}"
self.header['X-Emby-Authorization'] = self.auth
return True
else:
raise self.AuthenticationError
def setPolicy(self, userId, policy):
return requests.post(self.server+"/Users/"+userId+"/Policy",
headers=self.header,
params=policy)
def newUser(self, username, password):
for user in self.getUsers():
if user['Name'] == username:
raise self.UserExistsError
response = requests.post(self.server+"/emby/Users/New",
headers=self.header,
params={'Name': username,
'Password': password})
if response.status_code == 401:
raise self.AuthenticationRequiredError
return response
# template user's policies should be copied to each new account.

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# from flask import g
from flask_httpauth import HTTPBasicAuth
from itsdangerous import (TimedJSONWebSignatureSerializer
as Serializer, BadSignature, SignatureExpired)
from passlib.apps import custom_app_context as pwd_context
import uuid
from __main__ import config, app, g
class Account():
def __init__(self, username, password):
self.username = username
self.password_hash = pwd_context.hash(password)
self.id = str(uuid.uuid4())
def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)
def generate_token(self, expiration=1200):
s = Serializer(app.config['SECRET_KEY'], expires_in=expiration)
return s.dumps({ 'id': self.id })
@staticmethod
def verify_token(token, account):
s = Serializer(app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
return None
except BadSignature:
return None
if data['id'] == account.id:
return account
auth = HTTPBasicAuth()
adminAccount = Account(config['ui']['username'], config['ui']['password'])
@auth.verify_password
def verify_password(username, password):
user = adminAccount.verify_token(username, adminAccount)
if not user:
if username == adminAccount.username and adminAccount.verify_password(password):
g.user = adminAccount
print(g)
return True
else:
return False
g.user = adminAccount
return True

41
jellyfin_accounts/web.py Normal file
View File

@@ -0,0 +1,41 @@
from pathlib import Path
from flask import Flask, send_from_directory, render_template
from __main__ import config, app, g
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html',
contactMessage=config['ui']['contact_message']), 404
@app.route('/', methods=['GET', 'POST'])
def admin():
# return app.send_static_file('admin.html')
return render_template('admin.html',
contactMessage='')
@app.route('/<path:path>')
def static_proxy(path):
if 'form.html' not in path and 'admin.html' not in path:
return app.send_static_file(path)
return render_template('404.html',
contactMessage=config['ui']['contact_message']), 404
from jellyfin_accounts.web_api import checkInvite
@app.route('/invite/<path:path>')
def inviteProxy(path):
if checkInvite(path):
return render_template('form.html',
contactMessage=config['ui']['contact_message'],
helpMessage=config['ui']['help_message'],
successMessage=config['ui']['success_message'],
jfLink=config['jellyfin']['server'])
elif 'admin.html' not in path and 'admin.html' not in path:
return app.send_static_file(path)
else:
return render_template('invalidCode.html',
contactMessage=config['ui']['contact_message'])

View File

@@ -0,0 +1,150 @@
from flask import request, jsonify
from jellyfin_accounts.jf_api import Jellyfin
import json
import datetime
import secrets
from __main__ import config, app, g
from jellyfin_accounts.login import auth
def resp(success=True, code=500):
if success:
r = jsonify({'success': True})
r.status_code = 200
else:
r = jsonify({'success': False})
r.status_code = code
return r
def checkInvite(code, delete=False):
current_time = datetime.datetime.now()
try:
with open(config['files']['invites'], 'r') as f:
invites = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
valid = False
for index, i in enumerate(invites['invites']):
expiry = datetime.datetime.strptime(i['valid_till'],
'%Y-%m-%dT%H:%M:%S.%f')
if current_time >= expiry:
del invites['invites'][index]
else:
if i['code'] == code:
valid = True
if delete:
del invites['invites'][index]
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return valid
jf = Jellyfin(config['jellyfin']['server'],
config['jellyfin']['client'],
config['jellyfin']['version'],
config['jellyfin']['device'],
config['jellyfin']['device_id'])
jf.authenticate(config['jellyfin']['username'],
config['jellyfin']['password'])
@app.route('/newUser', methods=['GET', 'POST'])
def newUser():
data = request.get_json()
if checkInvite(data['code'], delete=True):
user = jf.newUser(data['username'], data['password'])
if user.status_code == 200:
try:
with open(config['files']['user_template'], 'r') as f:
default_policy = json.load(f)
jf.setPolicy(user.json()['Id'], default_policy)
except:
pass
if config['ui']['emails_enabled'] == 'true':
try:
with open(config['files']['emails'], 'r') as f:
emails = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
emails = {}
emails[data['username']] = data['email']
with open(config['files']['emails'], 'w') as f:
f.write(json.dumps(emails, indent=4))
return resp()
else:
return resp(False)
else:
return resp(False, code=401)
@app.route('/generateInvite', methods=['GET', 'POST'])
@auth.login_required
def generateInvite():
current_time = datetime.datetime.now()
data = request.get_json()
delta = datetime.timedelta(hours=int(data['hours']),
minutes=int(data['minutes']))
invite = {'code': secrets.token_urlsafe(16)}
invite['valid_till'] = (current_time +
delta).strftime('%Y-%m-%dT%H:%M:%S.%f')
try:
with open(config['files']['invites'], 'r') as f:
invites = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
invites['invites'].append(invite)
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return resp()
@app.route('/getInvites', methods=['GET'])
@auth.login_required
def getInvites():
current_time = datetime.datetime.now()
try:
with open(config['files']['invites'], 'r') as f:
invites = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
response = {'invites': []}
for index, i in enumerate(invites['invites']):
expiry = datetime.datetime.strptime(i['valid_till'], '%Y-%m-%dT%H:%M:%S.%f')
if current_time >= expiry:
del invites['invites'][index]
else:
valid_for = expiry - current_time
response['invites'].append({
'code': i['code'],
'hours': valid_for.seconds//3600,
'minutes': (valid_for.seconds//60) % 60})
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return jsonify(response)
@app.route('/deleteInvite', methods=['POST'])
@auth.login_required
def deleteInvite():
code = request.get_json()['code']
try:
with open(config['files']['invites'], 'r') as f:
invites = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
invites = {'invites': []}
for index, i in enumerate(invites['invites']):
if i['code'] == code:
del invites['invites'][index]
with open(config['files']['invites'], 'w') as f:
f.write(json.dumps(invites, indent=4, default=str))
return resp()
@app.route('/getToken')
@auth.login_required
def get_token():
token = g.user.generate_token()
return jsonify({'token': token.decode('ascii')})