#!/usr/bin/env python import base64 import datetime import importlib import os import random import subprocess import tempfile from urllib.parse import urlencode, urljoin from flask import Blueprint, redirect, current_app, url_for, request, render_template, session, \ flash, send_file, abort, jsonify, Response from flask_login import login_user, logout_user, current_user, login_required import jwt from PIL import Image import requests from .db import db, User, UserSession, File, NoResultFound, IntegrityError from .monkey import patch as monkey_patch from .login import login_manager, load_user from .file_storage.exceptions import StorageError storage = importlib.import_module(current_app.config.get('STORAGE_MODULE', '.file_storage.filesystem'), package='fbin') \ .Storage(current_app) monkey_patch() base62_alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' def get_or_create_user(username, jab_id): try: return db.session.query(User).filter(User.jab_id == jab_id).one() except NoResultFound: try: user = User(username, jab_id) db.session.add(user) db.session.commit() db.session.refresh(user) return user except IntegrityError: return None def get_file(file_hash, user_id=None, update_accessed=False): try: f = db.session.query(File).filter(File.hash == file_hash) if user_id: f = f.filter(File.user_id == user_id) f = f.one() except NoResultFound: return None if update_accessed: f.accessed = datetime.datetime.utcnow() db.session.add(f) db.session.commit() # Refresh after field update. db.session.refresh(f) return f def get_files(user): try: db.session.add(user) files = user.files except NoResultFound: return [] return files def delete_file(file): db.session.delete(file) db.session.commit() storage.delete_file(file) app = Blueprint('fbin', __name__) @app.route('/') def index(): return redirect(url_for('.upload')) @app.route('/u') @app.route('/upload', methods=['GET', 'POST']) def upload(api=False, user=None): def error(message): if api: return jsonify({ 'status': False, 'message': message, }) elif old_api: return 'ERROR {}'.format(message) else: flash(message, 'warning') return render_template('upload.html', **context) context = { 'title': 'Upload', } old_api = bool(request.form.get('api')) if request.method == 'GET': if api or old_api: # API calls shouldn't use GET. abort(405) return render_template('upload.html', **context) if not user and current_user.is_authenticated: user = current_user.user if not user and not current_app.config.get('ALLOW_ANONYMOUS_UPLOADS'): abort(403) uploaded_file = request.files.get('file') if not uploaded_file or not uploaded_file.filename: return error('No valid file or filename was provided.') file_hash = ''.join(random.choice(base62_alphabet) for x in range(5)) try: new_file = storage.store_file(uploaded_file, file_hash, user, request.remote_addr) except StorageError as e: return error(str(e)) if api: return jsonify({ 'status': True, 'hash': new_file.hash, 'urls': { 'base': url_for('fbin.file', hash='', _external=True), 'full': url_for('fbin.file', hash=new_file.hash, filename=new_file.filename, _external=True), 'ext': url_for('fbin.file', hash=new_file.hash, ext=new_file.ext, _external=True), 'hash': url_for('fbin.file', hash=new_file.hash, _external=True), }, }) elif old_api: return 'OK {hash}'.format(hash=new_file.hash) else: context = { 'file': new_file, } return redirect(url_for('.uploaded', hash=new_file.hash)) @app.route('/uploaded/') def uploaded(hash): f = get_file(hash, update_accessed=False) if not f: abort(404) if f.user_id and (not current_user.is_authenticated or f.user_id != current_user.get_user_id()): abort(404) context = { 'title': 'Uploaded', 'subtitle': f.filename, 'file': f, } return render_template('uploaded.html', **context) def _get_mimetype(f, path): if isinstance(path, Response): mimetype = path.content_type else: mimetype = f.get_mime_type() # Serve blacklisted mimetypes as either text/plain or application/octet-stream if mimetype in current_app.config['MIMETYPE_BLACKLIST'] and (f.user is None or f.user.username not in current_app.config['MIMETYPE_USER_WHITELIST']): if mimetype.startswith('text/'): mimetype = 'text/plain' else: mimetype = 'application/octet-stream' return mimetype @app.route('/f/') @app.route('/f/') @app.route('/f//') @app.route('/file/', endpoint='file') @app.route('/file/', endpoint='file') @app.route('/file//', endpoint='file') def _file(hash, ext=None, filename=None): f = get_file(hash) if not f or (f.blocked_reason and (f.blocked_reason['positives'] >= current_app.config['VIRUSTOTAL_MINIMUM_POSITIVES'] or any(scan['detected'] and scan['result'] in current_app.config['VIRUSTOTAL_SINGULAR_MATCHES'] for scan in f.blocked_reason['scans'].values()))): abort(404) path = storage.get_file(f) mimetype = _get_mimetype(f, path) if isinstance(path, Response): path.content_type = mimetype return path if not path or not os.path.exists(path): abort(404) return send_file(path, mimetype=mimetype, attachment_filename=f.filename) @app.route('/l') @app.route('/login') def login(): session['oauth_state'] = base64.urlsafe_b64encode(os.urandom(30)).decode() return redirect(urljoin(current_app.config['OAUTH_URL'], 'authorize') + '?' + urlencode({ 'response_type': 'code', 'client_id': current_app.config['OAUTH_CLIENT_ID'], 'redirect_uri': urljoin(request.host_url, url_for('.auth')), 'state': session['oauth_state'], })) @app.route('/account') def account(): return redirect(current_app.config['ACCOUNT_URL']) @app.route('/o') @app.route('/logout') def logout(): if not current_user.is_authenticated: return redirect(url_for('.index')) session_id = int(current_user.get_id().split(':', 1)[-1]) db.session.query(UserSession).filter_by(id=session_id).delete() logout_user() return redirect(url_for('.index')) @app.route('/auth') def auth(): if 'error' in request.args: error = request.args['error'] error_description = request.args.get('error_description') msg = 'OAuth error: {}'.format(error) if error_description: msg += ' ({})'.format(error_description) flash(msg, 'error') return redirect(url_for('.index')) session_state = session.pop('oauth_state', None) state = request.args.get('state') if session_state != state: flash('Invalid OAuth state', 'error') return redirect(url_for('.index')) code = request.args.get('code') if not code: flash('Missing OAuth code', 'error') return redirect(url_for('.index')) rs = requests.Session() response = rs.post(urljoin(current_app.config['OAUTH_URL'], 'token'), data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': current_app.config['OAUTH_CLIENT_ID'], 'client_secret': current_app.config['OAUTH_CLIENT_SECRET'], 'redirect_uri': urljoin(request.host_url, url_for('.auth')), }) token = response.json() if 'error' in token: msg = 'OAuth error: {}'.format(token['error']) flash(msg, 'error') return redirect(url_for('.index')) try: jwt.decode(token['access_token'], key=current_app.config['JWT_PUBLIC_KEY'], audience=current_app.config['OAUTH_CLIENT_ID'], algorithms=[current_app.config['OAUTH_JWT_ALGORITHM']]) jwt.decode(token['refresh_token'], key=current_app.config['JWT_PUBLIC_KEY'], audience=current_app.config['OAUTH_CLIENT_ID'], algorithms=[current_app.config['OAUTH_JWT_ALGORITHM']]) except jwt.InvalidTokenError as e: flash('Failed to verify token: {!s}'.format(e), 'error') return redirect(url_for('.index')) response = rs.get(urljoin(current_app.config['OAUTH_URL'], '/api/user'), headers={'Authorization': 'Bearer {}'.format(token['access_token'])}) user = response.json() user = get_or_create_user(user['username'], user['id']) us = UserSession(user.id, token['access_token'], token['refresh_token']) us.updated = datetime.datetime.utcnow() db.session.add(us) db.session.commit() db.session.refresh(us) user = load_user('{}:{}'.format(user.id, us.id)) if not user: flash('Failed to retrieve user instance.', 'error') else: login_user(user, remember=True) return redirect(url_for('.index')) @app.route('/m') @app.route('/files') @login_required def files(): files = get_files(current_user.user) context = { 'title': 'Files', 'files': files, 'total_size': File.pretty_size(sum(size for size in (f.size for f in files) if size is not None)), } return render_template('files.html', **context) @app.route('/files', methods=['POST']) @login_required def file_edit(): user_id = int(current_user.get_id().split(':')[0]) f = get_file(request.form.get('hash'), user_id=user_id, update_accessed=False) if not f: flash('File not found.', 'error') return redirect(url_for('.files')) if 'filename' in request.form: filename = request.form.get('filename', f.filename) f.filename = filename db.session.add(f) flash('Filename changed to "{}".'.format(f.filename), 'success') elif 'delete' in request.form: try: delete_file(f) except Exception: flash('Failed to delete file.', 'error') else: flash('File deleted.', 'success') else: flash('No action was performed.', 'warning') return redirect(url_for('.files')) @app.route('/i') @app.route('/images') @login_required def images(): files = [f for f in get_files(current_user.user) if f.is_image()] context = { 'title': 'Images', 'fullwidth': True, 'files': files, 'total_size': File.pretty_size(sum(size for size in (f.size for f in files) if size is not None)), } return render_template('images.html', **context) @app.route('/v') @app.route('/videos') @login_required def videos(): files = [f for f in get_files(current_user.user) if f.is_video()] context = { 'title': 'Videos', 'fullwidth': True, 'files': files, 'total_size': File.pretty_size(sum(size for size in (f.size for f in files) if size is not None)), } return render_template('images.html', **context) @app.route('/t/') @app.route('/thumb/') def thumb(hash): f = get_file(hash, update_accessed=False) response = storage.get_thumbnail(f) if not response: with tempfile.NamedTemporaryFile(suffix='.jpg') as ttf: # temporary thumb file if f.is_image(): try: with storage.temp_file(f) as tf: im = Image.open(tf) # Check for valid JPEG modes. if im.mode not in ('1', 'L', 'RGB', 'RGBX', 'CMYK', 'YCbCr'): im = im.convert('RGB') e = im.getexif() # Get orientation EXIF tag orientation = e.get(274) if orientation == 3: im = im.rotate(180, expand=True) elif orientation == 6: im = im.rotate(270, expand=True) elif orientation == 8: im = im.rotate(90, expand=True) im.thumbnail(current_app.config.get('THUMB_SIZE', (128, 128)), Image.ANTIALIAS) im.save(ttf) except IOError: # We can't generate a thumbnail for this file, just say it doesn't exist. abort(404) elif f.is_video(): with storage.temp_file(f) as tf: p = subprocess.run(['ffmpegthumbnailer', '-i', '-', '-o', ttf.name], stdin=tf) if p.returncode != 0: abort(404) else: abort(404) ttf.seek(0) if not os.path.getsize(ttf.name): abort(404) storage.store_thumbnail(f, ttf) response = storage.get_thumbnail(f) if isinstance(response, Response): return response return send_file(response, attachment_filename='thumb.jpg') @app.route('/h') @app.route('/help') def help(): return redirect(url_for('.api')) @app.route('/api') def api(): context = { 'title': 'API', 'subtitle': 'keys and usage', } return render_template('api.html', **context) @app.route('/generate-api-key') def generate_api_key(): if not current_user.is_authenticated: abort(403) now = datetime.datetime.utcnow() user_id = int(current_user.get_id().split(':')[0]) data = { 'iss': request.url_root, 'iat': now, 'nbf': now, 'sub': user_id, } token = jwt.encode(data, current_app.config['SECRET_KEY'], algorithm=current_app.config['API_JWT_ALGORITHM']) return token @app.route('/invalidate-api-keys') @login_required def invalidate_api_keys(): user = current_user.user db.session.add(user) user.api_key_date = datetime.datetime.utcnow() db.session.commit() flash('All API keys invalidated.', 'success') return redirect(request.referrer) login_manager.login_view = '.login'