From ef7d0f93c37886357ee77382baab8a2547b1bcd4 Mon Sep 17 00:00:00 2001 From: cekk Date: Sun, 29 Dec 2019 15:20:40 +0100 Subject: [PATCH] improved security --- Makefile | 5 +- app.py | 77 +++++++++++++++++++++--------- flask.cfg | 1 + lambda_function/lambda_function.py | 29 +++++++---- lambda_function/requirements.txt | 3 +- 5 files changed, 82 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index cbf923c..2711da2 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,11 @@ buildout: dev: pipenv run python app.py +dev-ssl: + pipenv run python app.py --cert /Users/cekk/letsencrypt/config/live/cekk.changeip.co/fullchain.pem --key /Users/cekk/letsencrypt/config/live/cekk.changeip.co/privkey.pem + prod: - pipenv run gunicorn -c gunicorn_config.py wsgi:app + bin/gunicorn -c gunicorn_config.py wsgi:app deploy_lambda: pipenv run pip install --target ./lambda_function/package -r lambda_function/requirements.txt diff --git a/app.py b/app.py index 96574f9..916f8d6 100644 --- a/app.py +++ b/app.py @@ -1,20 +1,35 @@ -from flask import Flask, render_template, send_from_directory -from flask_restful import Resource, Api, reqparse +# -*- coding: utf-8 -*- +from flask import Blueprint +from flask import Flask +from flask import Flask +from flask import render_template +from flask import request +from flask import send_from_directory from flask_mqtt import Mqtt +from flask_restful import Resource, Api, reqparse, abort from flask_socketio import SocketIO from utils import createSocketMessage - +from flask_jwt_extended import ( + JWTManager, + jwt_required, + create_access_token, + get_jwt_identity, +) +from functools import wraps +import logging import os import re -app = Flask( - __name__, instance_relative_config=True, static_folder="static/build" -) +app = Flask(__name__, instance_relative_config=True, static_folder="static/build") app.config.from_pyfile("config.py") -api = Api(app) +api_bp = Blueprint("api", __name__) +api = Api(api_bp) +app.register_blueprint(api_bp) + socketio = SocketIO(app) mqtt = Mqtt(app) +jwt = JWTManager(app) parser = reqparse.RequestParser() parser.add_argument("command", type=str, help="") @@ -25,9 +40,7 @@ def handle_connect(client, userdata, flags, rc): # mqtt.subscribe("shellies/command") for blind in app.config.get("BLINDS", []): mqtt.subscribe("shellies/{id}/online".format(id=blind.get("id", ""))) - mqtt.subscribe( - "shellies/{id}/roller/0/pos".format(id=blind.get("id", "")) - ) + mqtt.subscribe("shellies/{id}/roller/0/pos".format(id=blind.get("id", ""))) mqtt.subscribe("shellies/{id}/roller/0".format(id=blind.get("id", ""))) @@ -47,17 +60,33 @@ def connect_handler(): mqtt.publish("shellies/command", "announce") -@app.route("/", defaults={"path": ""}) -@app.route("/") -def serve(path): - if path != "" and os.path.exists(app.static_folder + "/" + path): - return send_from_directory(app.static_folder, path) - else: - return send_from_directory(app.static_folder, "index.html") +# @app.route("/", defaults={"path": ""}) +# @app.route("/") +# def serve(path): +# if path != "" and os.path.exists(app.static_folder + "/" + path): +# return send_from_directory(app.static_folder, path) +# else: +# app.logger.info("AAAA") +# app.logger.debug("BBB") +# app.logger.warning("CCC") +# app.logger.error("DDD") +# return send_from_directory(app.static_folder, "index.html") + + +class ProtectedResource(Resource): + method_decorators = [jwt_required] + + def check_skill_id(*args, **kwargs): + if not app.config["SKILL_ID"]: + abort(401) + skill_id = get_jwt_identity() + if skill_id != app.config["SKILL_ID"]: + abort(401) -class Blinds(Resource): +class Blinds(ProtectedResource): def get(self): + self.check_skill_id() return app.config.get("BLINDS", []) @@ -75,16 +104,15 @@ def get(self): return "", 204 -class Action(Resource): +class Action(ProtectedResource): def publish(self, id, action): mqtt.publish("shellies/{id}/roller/0/command".format(id=id), action) def get(self, id, action): + self.check_skill_id() if action not in ["close", "open", "stop"]: return ( - { - "message": 'Valid actions are "close", "open", "stop" or "rc"' - }, + {"message": 'Valid actions are "close", "open", "stop" or "rc"'}, 400, ) if id == "all": @@ -109,4 +137,7 @@ def get(self, id, action): api.add_resource(Action, "/roller//") if __name__ == "__main__": - socketio.run(app, debug=True, host="0.0.0.0", threaded=False) + gunicorn_logger = logging.getLogger("gunicorn.error") + app.logger.handlers = gunicorn_logger.handlers + app.logger.setLevel(gunicorn_logger.level) + socketio.run(app, debug=True, host="0.0.0.0", port=4000) diff --git a/flask.cfg b/flask.cfg index e7fe1b5..85bcde9 100644 --- a/flask.cfg +++ b/flask.cfg @@ -11,3 +11,4 @@ eggs = gunicorn flask-socketio gevent + flask_jwt_extended diff --git a/lambda_function/lambda_function.py b/lambda_function/lambda_function.py index a1d1426..c6766e4 100644 --- a/lambda_function/lambda_function.py +++ b/lambda_function/lambda_function.py @@ -3,10 +3,13 @@ import boto3 import json -import requests +import jwt import os +import requests api_endpoint = os.environ["API_ENDPOINT"] +skill_id = os.environ["SKILL_ID"] +token_secret = os.environ["TOKEN_SECRET"] def lambda_handler(request, context): @@ -61,7 +64,7 @@ def lambda_handler(request, context): if namespace == "Alexa.Discovery": if name == "Discover": - res = requests.get("{}/blinds".format(api_endpoint)) + res = call_api(endpoint="blinds") adr = AlexaResponse(namespace="Alexa.Discovery", name="Discover.Response") capability_alexa = adr.create_payload_endpoint_capability() @@ -119,18 +122,17 @@ def lambda_handler(request, context): # Note: This sample always returns a success response for either a request to TurnOff or TurnOn device_id = request["directive"]["endpoint"]["endpointId"] action = "close" if name == "TurnOff" else "open" - print(">>>>> NAME: {}".format(name)) - print(request) - res = requests.get( - "{api_endpoint}/roller/{device_id}/{action}".format( - api_endpoint=api_endpoint, device_id=device_id, action=action + call_api( + endpoint="roller/{device_id}/{action}".format( + device_id=device_id, action=action ) ) correlation_token = request["directive"]["header"]["correlationToken"] apcr = AlexaResponse(correlation_token=correlation_token) + state_value = "OFF" if name == "TurnOff" else "ON" apcr.add_context_property( - namespace="Alexa.ToggleController", name="toggleState", value="ciccio", + namespace="Alexa.ToggleController", name="toggleState", value=state_value, ) return send_response(apcr.get()) @@ -141,3 +143,14 @@ def send_response(response): print(json.dumps(response)) return response + +def call_api(endpoint): + auth_token = jwt.encode({"identity": skill_id}, token_secret, algorithm="HS256") + hed = {"Authorization": b"Bearer " + auth_token} + url = "{api_endpoint}/{endpoint}".format( + api_endpoint=api_endpoint, endpoint=endpoint + ) + print("lambda_handler CALL API -----") + print(url) + requests.get(url, headers=hed) + diff --git a/lambda_function/requirements.txt b/lambda_function/requirements.txt index 663bd1f..32469f6 100644 --- a/lambda_function/requirements.txt +++ b/lambda_function/requirements.txt @@ -1 +1,2 @@ -requests \ No newline at end of file +requests +pyjwt \ No newline at end of file