#!/usr/bin/env python import base64 import cgi import datetime import hashlib import importlib import io import json import mimetypes import os import random import subprocess import tempfile import urllib from urllib.parse import urlencode, urljoin from flask import Blueprint, redirect, current_app, url_for, request, render_template, session, flash, send_file, abort, jsonify, Markup, Response from flask_login import login_user, logout_user, current_user, login_required import jwt from PIL import Image, ExifTags import requests from werkzeug.utils import secure_filename 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' if not os.path.isdir(current_app.config['THUMB_DIRECTORY']): os.mkdir(current_app.config['THUMB_DIRECTORY']) 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) @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) if isinstance(path, Response): return path if not path or not os.path.exists(path): abort(404) return send_file(path, 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]) try: db.session.query(UserSession).filter_by(id = session_id).delete() except: raise 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: access_data = jwt.decode(token['access_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) refresh_data = jwt.decode(token['refresh_token'], key = current_app.config['JWT_PUBLIC_KEY'], audience = current_app.config['OAUTH_CLIENT_ID']) 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: 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']) 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'