From 2322ab7c8356ee8d06adb293a78f4ee20400b21c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 28 Sep 2023 04:32:53 -0400 Subject: [PATCH] WIP: building up VAPID and subscription --- .gitignore | 1 + Dockerfile | 41 +++++++++++++++++++++ app.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ build.sh | 3 ++ init_db.py | 9 +++++ models.py | 16 ++++++++ requirements.txt | 4 ++ 7 files changed, 169 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100755 build.sh create mode 100644 init_db.py create mode 100644 models.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d9b832d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# Use an official Python runtime as a parent image +FROM python:3.8-alpine3.18 as builder + +RUN apk update && apk upgrade +RUN apk add --no-cache --virtual .build-deps build-base git +RUN apk add bash libffi-dev tzdata --upgrade --no-cache + +ENV TZ America/New_York + +# Set the working directory in the container to /app +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY app.py /app +COPY requirements.txt /app +COPY models.py /app +COPY init_db.py /app/init_db.py + + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt +RUN python /app/init_db.py + +RUN apk del .build-deps + +# ---- Production Stage ---- +FROM python:3.8-alpine3.18 as production + +# Create a user to run our application +RUN adduser -D myuser + +# Copy the dependencies and installed packages from the builder image +WORKDIR /app +COPY --from=builder /app /app +COPY --from=builder /usr/local /usr/local + +# Switch to the created user +USER myuser + +# Start gunicorn with the appropriate options +CMD ["gunicorn", "-b", "0.0.0.0:5000", "--workers=3", "app:app"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..e78ffab --- /dev/null +++ b/app.py @@ -0,0 +1,95 @@ +from flask import Flask, request, jsonify +from models import db, VAPIDKey, Subscription +from py_vapid import Vapid +from pywebpush import webpush, WebPushException + +import json +import os + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///webpush.db' +db.init_app(app) + +def generate_and_save_vapid_keys(): + vapid = Vapid() + vapid.generate_keys() + private_key = vapid.get_private_key().to_pem().decode('utf-8').strip() + public_key = vapid.get_public_key().to_pem().decode('utf-8').strip() + + key = VAPIDKey(public_key=public_key, private_key=private_key) + db.session.add(key) + db.session.commit() + + +@app.before_first_request +def initialize(): + if not VAPIDKey.query.first(): + generate_and_save_vapid_keys() + +def send_push_notification(subscription_info, message, vapid_key): + try: + webpush( + subscription_info=subscription_info, + data=json.dumps(message), + vapid_private_key=vapid_key.private_key, + vapid_claims={ + "sub": "mailto:your-email@example.com" + } + ) + except WebPushException as e: + print(f"Failed to send notification: {e}") + + +@app.route('/web-push/vapid', methods=['GET']) +def get_vapid(): + key = VAPIDKey.query.first() + if key: + return jsonify(public_key=key.public_key) + return jsonify(error='No VAPID keys found'), 404 + + +@app.route('/web-push/subscribe', methods=['POST']) +def subscribe(): + content = request.json + vapid_key = VAPIDKey.query.first() + + if not vapid_key: + return jsonify(success=False, error="No VAPID keys available"), 500 + + subscription = Subscription(endpoint=content['endpoint'], + p256dh=content['keys']['p256dh'], + auth=content['keys']['auth'], + vapid_key_id=vapid_key.id) + db.session.add(subscription) + db.session.commit() + + subscription_info = { + "endpoint": subscription.endpoint, + "keys": { + "p256dh": subscription.p256dh, + "auth": subscription.auth + } + } + + message = {"title": "Subscription Successful", "body": "Thank you for subscribing!"} + send_push_notification(subscription_info, message, vapid_key) + + return jsonify(success=True) + + +@app.route('/web-push/unsubscribe', methods=['DELETE']) +def unsubscribe(): + content = request.json + endpoint = content['endpoint'] + subscription = Subscription.query.filter_by(endpoint=endpoint).first() + + if subscription: + db.session.delete(subscription) + db.session.commit() + return jsonify(success=True, message="Subscription deleted successfully") + else: + return jsonify(success=False, error="Subscription not found"), 404 + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5a76bba --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build . -t endorser-push-server:1.0 --no-cache diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..e0ad264 --- /dev/null +++ b/init_db.py @@ -0,0 +1,9 @@ +from models import db +from flask import Flask + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///webpush.db' +db.init_app(app) + +with app.app_context(): + db.create_all() diff --git a/models.py b/models.py new file mode 100644 index 0000000..eee5197 --- /dev/null +++ b/models.py @@ -0,0 +1,16 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class VAPIDKey(db.Model): + id = db.Column(db.Integer, primary_key=True) + public_key = db.Column(db.String(255), nullable=False) + private_key = db.Column(db.String(255), nullable=False) + subscriptions = db.relationship('Subscription', backref='vapid_key', lazy=True) + +class Subscription(db.Model): + id = db.Column(db.Integer, primary_key=True) + endpoint = db.Column(db.String(500), nullable=False) + p256dh = db.Column(db.String(255), nullable=False) + auth = db.Column(db.String(255), nullable=False) + vapid_key_id = db.Column(db.Integer, db.ForeignKey('vapid_key.id'), nullable=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a60a779 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +flask_sqlalchemy +py_vapid +pywebpush