def oauth_register_POST(): valid = Validation(request) client_name = valid.require("client-name") redirect_uri = valid.require("redirect-uri") valid.expect(not redirect_uri or valid_url(redirect_uri), "Must be a valid HTTP or HTTPS URI", field="redirect-uri") if not valid.ok: return render_template("oauth-register.html", client_name=client_name, redirect_uri=redirect_uri, valid=valid) client = OAuthClient(current_user, client_name, redirect_uri) secret = client.gen_client_secret() session["client_id"] = client.client_id session["client_secret"] = secret session["client_event"] = "registered" db.session.add(client) audit_log("register oauth client", "Registered OAuth client {}".format(client.client_id)) db.session.commit() return redirect("/oauth/registered")
def forgot_POST(): valid = Validation(request) email = valid.require("email", friendly_name="Email") if not valid.ok: return render_template("forgot.html", **valid.kwargs) user = User.query.filter(User.email == email).first() valid.expect(user, "No account found with this email address.") if not valid.ok: return render_template("forgot.html", **valid.kwargs) factors = (UserAuthFactor.query.filter( UserAuthFactor.user_id == user.id)).all() valid.expect( not any(f for f in factors if f.factor_type in [FactorType.totp, FactorType.u2f]), "This account has two-factor authentication enabled, contact support.") if not valid.ok: return render_template("forgot.html", **valid.kwargs) rh = user.gen_reset_hash() db.session.commit() send_email( 'reset_pw', user.email, 'Reset your password on {}'.format(site_name), headers={ "From": f"{cfg('sr.ht', 'owner-name')} <*****@*****.**>", "To": "{} <{}>".format(user.username, user.email), "Reply-To": f"{cfg('sr.ht', 'owner-name')} <{cfg('sr.ht', 'owner-email')}>", }, user=user) audit_log("password reset requested", user=user) return render_template("forgot.html", done=True)
def logout(): if current_user: audit_log("logged out") logout_user() db.session.commit() metrics.meta_logouts.inc() return redirect("/login")
def ssh_keys_POST(): user = User.query.get(current_user.id) valid = Validation(request) ssh_key = valid.require("ssh-key") if valid.ok: try: parsed_key = ssh.SSHKey(ssh_key) valid.expect(parsed_key.bits, "This is not a valid SSH key", "ssh-key") except: valid.error("This is not a valid SSH key", "ssh-key") if valid.ok: fingerprint = parsed_key.hash_md5()[4:] valid.expect(SSHKey.query\ .filter(SSHKey.fingerprint == fingerprint) \ .count() == 0, "We already have this SSH key on file.", "ssh-key") if not valid.ok: return render_template("keys.html", current_user=user, ssh_key=ssh_key, valid=valid) key = SSHKey(user, ssh_key, fingerprint, parsed_key.comment) db.session.add(key) audit_log("ssh key added", 'Added SSH key {}'.format(fingerprint)) db.session.commit() return redirect("/keys")
def client_delete_POST(client_id): client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first() if not client or client.user_id != current_user.id: abort(404) audit_log("deleted oauth client", "Deleted OAuth client {}".format(client_id)) db.session.delete(client) db.session.commit() return redirect("/oauth")
def revoke_tokens_POST(client_id): client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first() if not client or client.user_id != current_user.id: abort(404) OAuthToken.query.filter(OAuthToken.client_id == client.id).delete() audit_log("revoked oauth tokens", "Revoked all OAuth tokens for {}".format(client_id)) db.session.commit() return redirect("/oauth")
def pgp_keys_delete(key_id): user = User.query.get(current_user.id) key = PGPKey.query.get(int(key_id)) if not key or key.user_id != user.id: abort(404) audit_log("pgp key deleted", 'Deleted PGP key {}'.format(key.key_id)) db.session.delete(key) db.session.commit() return redirect("/keys")
def ssh_keys_delete(key_id): user = User.query.get(current_user.id) key = SSHKey.query.get(int(key_id)) if not key or key.user_id != user.id: abort(404) audit_log("ssh key deleted", 'Deleted SSH key {}'.format(key.fingerprint)) db.session.delete(key) db.session.commit() return redirect("/keys")
def personal_token_POST(): oauth_token = OAuthToken(current_user, None) token = oauth_token.gen_token() oauth_token._scopes = "*" audit_log("issued oauth token", "issued personal access token {}...".format( oauth_token.token_partial)) db.session.add(oauth_token) db.session.commit() return render_template("oauth-personal-token.html", token=token)
def oauth_exchange_POST(): valid = Validation(request) client_id = valid.require('client_id') client_secret = valid.require('client_secret') exchange = valid.require('exchange') if not valid.ok: return valid.response client = (OAuthClient.query.filter( OAuthClient.client_id == client_id)).one_or_none() valid.expect(client, 'Unknown client ID') if not valid.ok: return valid.response client_secret_hash = hashlib.sha512(client_secret.encode()).hexdigest() valid.expect(client_secret_hash == client.client_secret_hash, 'Invalid client secret') if not valid.ok: return valid.response stash = redis.get(exchange) valid.expect(stash, 'Exchange token expired') if not valid.ok: return valid.response stash = json.loads(stash.decode()) redis.delete(exchange) user = stash.get('user_id') scopes = stash.get('scopes') user = User.query.filter(User.id == user).first() valid.expect( user, "Unknown user ID stored for " "this exchange token (this isn't supposed to happen") if not valid.ok: return valid.response previous = (OAuthToken.query.filter(OAuthToken.user_id == user.id).filter( OAuthToken.client_id == client.id)).one_or_none() if not previous: oauth_token = OAuthToken(user, client) else: oauth_token = previous previous.expires = datetime.utcnow() + timedelta(days=365) oauth_token.scopes = [OAuthScope(s) for s in scopes.split(",")] token = oauth_token.gen_token() if not client.preauthorized: audit_log("oauth token issued", "issued oauth token {} to client {}".format( oauth_token.token_partial, client.client_id), user=user) if not previous: db.session.add(oauth_token) db.session.commit() return {"token": token, "expires": oauth_token.expires}
def reset_secret(client_id): client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first() if not client or client.user_id != current_user.id: abort(404) secret = client.gen_client_secret() session["client_id"] = client.client_id session["client_secret"] = secret session["client_event"] = "reset-secret" audit_log("reset client secret", "Reset OAuth client secret for {}".format(client.client_id)) db.session.commit() return redirect("/oauth/registered")
def security_totp_disable_POST(): factor = UserAuthFactor.query \ .filter(UserAuthFactor.user_id == current_user.id)\ .filter(UserAuthFactor.factor_type == FactorType.totp)\ .one_or_none() if not factor: return redirect("/security") db.session.delete(factor) audit_log("disabled two factor auth", 'Disabled TOTP') db.session.commit() metrics.meta_totp_disabled.inc() return redirect("/security")
def revoke_token_POST(token_id): token = OAuthToken.query.filter(OAuthToken.id == token_id).first() if not token or token.user_id != current_user.id: abort(404) if token.client: audit_log("revoked oauth token", "revoked access from {}".format(token.client.client_name)) else: audit_log("revoked personal access token", "revoked {}...".format(token.token_partial)) token.expires = datetime.utcnow() db.session.commit() return redirect("/oauth")
def totp_challenge_POST(): user_id = session.get('authorized_user') factors = session.get('extra_factors') return_to = session.get('return_to') or '/' if not user_id or not factors: return redirect("/login") valid = Validation(request) code = valid.require('code') if not valid.ok: return render_template("totp-challenge.html", return_to=return_to, valid=valid) code = code.replace(" ", "") try: code = int(code) except: valid.error("This TOTP code is invalid (expected a number)", field="code") if not valid.ok: return render_template("totp-challenge.html", return_to=return_to, valid=valid) factor = UserAuthFactor.query.get(factors[0]) secret = factor.secret.decode('utf-8') valid.expect(totp(secret, code), 'The code you entered is incorrect.', field='code') if not valid.ok: return render_template("totp-challenge.html", valid=valid, return_to=return_to) factors = factors[1:] if len(factors) != 0: return get_challenge(factors[0]) del session['authorized_user'] del session['extra_factors'] del session['return_to'] user = User.query.get(user_id) login_user(user, remember=True) audit_log("logged in") db.session.commit() metrics.meta_logins_success.inc() return redirect(return_to)
def pgp_keys_POST(): user = User.query.get(current_user.id) valid = Validation(request) pgp_key = valid.require("pgp-key") valid.expect( not pgp_key or len(pgp_key) < 32768, Markup("Maximum encoded key length is 32768 bytes. " "Try <br /><code>gpg --armor --export-options export-minimal " "--export <fingerprint></code><br /> to export a " "smaller key."), field="pgp-key") if valid.ok: try: key = pgpy.PGPKey() key.parse(pgp_key.replace('\r', '').encode('utf-8')) except: valid.error("This is not a valid PGP key", field="pgp-key") valid.expect(any(key.userids), "This key has no user IDs", field="pgp-key") try: prepare_email("test", user.email, "test", encrypt_key=pgp_key) except: valid.error( "We were unable to encrypt a test message with this key", field="pgp-key") if valid.ok: valid.expect(PGPKey.query\ .filter(PGPKey.user_id == user.id) \ .filter(PGPKey.key_id == key.fingerprint)\ .count() == 0, "This is a duplicate key", field="pgp-key") if not valid.ok: return render_template("keys.html", current_user=user, pgp_key=pgp_key, valid=valid) pgp = PGPKey(user, pgp_key, key.fingerprint, key.userids[0].email) db.session.add(pgp) audit_log("pgp key added", 'Added PGP key {}'.format(key.fingerprint)) db.session.commit() return redirect("/keys")
def new_payment_POST(): valid = Validation(request) term = valid.require("term") token = valid.require("stripe-token") if not valid.ok: return "Invalid form submission", 400 if not current_user.stripe_customer: new_customer = True try: customer = stripe.Customer.create( description="~" + current_user.username, email=current_user.email, card=token) current_user.stripe_customer = customer.id current_user.payment_due = datetime.utcnow() + timedelta(seconds=-1) except stripe.error.CardError as e: details = e.json_body["error"]["message"] return render_template("new-payment.html", amount=current_user.payment_cents, error=details) else: new_customer = False try: customer = stripe.Customer.retrieve(current_user.stripe_customer) source = customer.sources.create(source=token) customer.default_source = source.stripe_id customer.save() except stripe.error.CardError as e: details = e.json_body["error"]["message"] return render_template("new-payment.html", amount=current_user.payment_cents, error=details) audit_log("billing", "New payment method handed") current_user.payment_interval = PaymentInterval(term) success, details = charge_user(current_user) if not success: return render_template("new-payment.html", amount=current_user.payment_cents, error=details) db.session.commit() if new_customer: return redirect(onboarding_redirect) session["message"] = "Your payment method was updated." return redirect(url_for("billing.billing_GET"))
def charge_user(user): if user.user_type == UserType.active_free: return ChargeResult.account_current, "Your account is exempt from payment." if user.payment_due >= datetime.utcnow(): return ChargeResult.account_current, "Your account is current." desc = f"{cfg('sr.ht', 'site-name')} {user.payment_interval.value} payment" if user.payment_cents == 0: # They cancelled their payment and their current term is up user.user_type = UserType.active_non_paying return ChargeResult.cancelled, "Your paid service has been cancelled." try: amount = user.payment_cents if user.payment_interval == PaymentInterval.yearly: amount = amount * 10 # Apply yearly discount # TODO: Multiple currencies charge = stripe.Charge.create(amount=amount, currency="usd", customer=user.stripe_customer, description=desc) audit_log("billing", details="charged ${:.2f}".format(amount / 100)) except stripe.error.CardError as e: details = e.json_body["error"]["message"] user.user_type = UserType.active_delinquent return ChargeResult.failed, details invoice = Invoice() invoice.cents = amount invoice.user_id = user.id try: invoice.source = f"{charge.source.brand} ending in {charge.source.last4}" except: # Not a credit card? dunno how this works invoice.source = charge.source.stripe_id db.session.add(invoice) if user.payment_interval == PaymentInterval.monthly: invoice.valid_thru = datetime.utcnow() + timedelta(days=30) user.payment_due = invoice.valid_thru else: invoice.valid_thru = datetime.utcnow() + timedelta(days=365) user.payment_due = invoice.valid_thru user.user_type = UserType.active_paying return ChargeResult.success, "Your card was successfully charged. Thank you!"
def reset_POST(token): user = User.query.filter(User.reset_hash == token).first() if not user: abort(404) if user.reset_expiry < datetime.utcnow(): abort(404) valid = Validation(request) password = valid.require("password", friendly_name="Password") if not valid.ok: return render_template("reset.html", valid=valid) valid.expect(8 <= len(password) <= 512, "Password must be between 8 and 512 characters.", "password") if not valid.ok: return render_template("reset.html", valid=valid) user.password = bcrypt.hashpw(password.encode('utf-8'), salt=bcrypt.gensalt()).decode('utf-8') audit_log("password reset", user=user) db.session.commit() login_user(user, remember=True) metrics.meta_pw_resets.inc() return redirect("/")
def privacy_POST(): valid = Validation(request) key_id = valid.require("pgp-key") key_id = key_id if key_id != "null" else None key = None if key_id: key = PGPKey.query.get(int(key_id)) valid.expect(key.user_id == current_user.id, "Invalid PGP key") if not valid.ok: return redirect("/privacy") user = User.query.get(current_user.id) user.pgp_key = key audit_log("changed pgp key", "Set default PGP key to {}".format(key.key_id if key else None)) db.session.commit() return redirect("/privacy")
def login_POST(): if current_user: return redirect("/") valid = Validation(request) username = valid.require("username", friendly_name="Username") password = valid.require("password", friendly_name="Password") return_to = valid.optional("return_to", "/") if not valid.ok: return render_template("login.html", valid=valid), 400 user = User.query.filter(User.username == username.lower()).one_or_none() valid.expect(user is not None, "Username or password incorrect") if valid.ok: valid.expect( bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')), "Username or password incorrect") if not valid.ok: metrics.meta_logins_failed.inc() return render_template("login.html", username=username, valid=valid) factors = UserAuthFactor.query \ .filter(UserAuthFactor.user_id == user.id).all() if any(factors): session['extra_factors'] = [f.id for f in factors] session['authorized_user'] = user.id session['return_to'] = return_to return get_challenge(factors[0]) login_user(user, remember=True) audit_log("logged in") db.session.commit() metrics.meta_logins_success.inc() return redirect(return_to)
def security_totp_enable_POST(): valid = Validation(request) secret = valid.require("secret") code = valid.require("code") if not valid.ok: return render_template("totp-enable.html", qrcode=totp_get_qrcode(secret), otpauth_uri=otpauth_uri(secret), secret=secret, valid=valid), 400 code = code.replace(" ", "") try: code = int(code) except: valid.error( "This TOTP code is invalid (expected a number)", field="code") if not valid.ok: return render_template("totp-enable.html", qrcode=totp_get_qrcode(secret), otpauth_uri=otpauth_uri(secret), secret=secret, valid=valid), 400 valid.expect(totp(secret, code), "The code you entered is incorrect.", field="code") if not valid.ok: return render_template("totp-enable.html", qrcode=totp_get_qrcode(secret), otpauth_uri=otpauth_uri(secret), secret=secret, valid=valid), 400 factor = UserAuthFactor(current_user, FactorType.totp) factor.secret = secret.encode('utf-8') db.session.add(factor) audit_log("enabled two factor auth", 'Enabled TOTP') db.session.commit() metrics.meta_totp_enabled.inc() return redirect("/security")
def confirm_account(token): if current_user: return redirect(onboarding_redirect) user = User.query.filter(User.confirmation_hash == token).one_or_none() if not user: return render_template("already-confirmed.html", redir=onboarding_redirect) if user.new_email: user.confirmation_hash = None audit_log("email updated", "{} became {}".format(user.email, user.new_email)) user.email = user.new_email user.new_email = None db.session.commit() elif user.user_type == UserType.unconfirmed: user.confirmation_hash = None user.user_type = UserType.active_non_paying audit_log("account created") db.session.commit() login_user(user, remember=True) if cfg("meta.sr.ht::billing", "enabled") == "yes": return redirect(url_for("billing.billing_initial_GET")) metrics.meta_confirmations.inc() return redirect(onboarding_redirect)
def profile_POST(): valid = Validation(request) user = User.query.filter(User.id == current_user.id).one() email = valid.optional("email", user.email) email = email.strip() url = valid.optional("url", user.url) location = valid.optional("location", user.location) bio = valid.optional("bio", user.bio) valid.expect(not url or 0 <= len(url) <= 256, "URL must fewer than 256 characters.", "url") valid.expect(not url or valid_url(url), "URL must be a valid http or https URL", "url") valid.expect(not location or 0 <= len(location) <= 256, "Location must fewer than 256 characters.", "location") valid.expect(not bio or 0 <= len(bio) <= 4096, "Bio must fewer than 4096 characters.", "bio") if not valid.ok: return render_template("profile.html", email=email, url=url, location=location, bio=bio, valid=valid), 400 user.url = url user.location = location user.bio = bio new_email = user.email != email if new_email: valid.expect( len(email) <= 256, "Email must be no more than 256 characters.", "email") prev = User.query.filter(User.email == email).first() valid.expect(not prev, "This email address is already in use.", "email") if not valid.ok: return render_template("profile.html", email=email, url=url, location=location, bio=bio, valid=valid), 400 user.new_email = email user.gen_confirmation_hash() send_email('update_email_old', user.email, 'Your {} email address is changing'.format(site_name), new_email=email) send_email('update_email_new', user.new_email, 'Confirm your {} email address change'.format(site_name), new_email=email) audit_log("updated profile") db.session.commit() UserWebhook.deliver(UserWebhook.Events.profile_update, user.to_dict(), UserWebhook.Subscription.user_id == user.id) return render_template("profile.html", new_email=new_email)
def cancel_POST(): current_user.payment_cents = 0 db.session.commit() audit_log("billing", "Plan cancelled (will not renew)") return redirect(url_for("billing.billing_GET"))