commit
12ff54d2cd
|
@ -21,7 +21,8 @@ def add_resources():
|
||||||
DeviceListResource,
|
DeviceListResource,
|
||||||
DeviceTypeResource,
|
DeviceTypeResource,
|
||||||
DeviceTypeListResource,
|
DeviceTypeListResource,
|
||||||
DeviceConfigurationResource)
|
DeviceConfigurationResource,
|
||||||
|
DeviceSecretResource)
|
||||||
from .resources.dashboard import (DashboardResource,
|
from .resources.dashboard import (DashboardResource,
|
||||||
DashboardListResource,
|
DashboardListResource,
|
||||||
DashboardWidgetResource,
|
DashboardWidgetResource,
|
||||||
|
@ -47,6 +48,8 @@ def add_resources():
|
||||||
api.add_resource(DeviceTypeListResource, '/v1/devices/types')
|
api.add_resource(DeviceTypeListResource, '/v1/devices/types')
|
||||||
api.add_resource(DeviceConfigurationResource,
|
api.add_resource(DeviceConfigurationResource,
|
||||||
'/v1/devices/<int:device_id>/configuration')
|
'/v1/devices/<int:device_id>/configuration')
|
||||||
|
api.add_resource(DeviceSecretResource,
|
||||||
|
'/v1/devices/<int:device_id>/secret')
|
||||||
api.add_resource(DashboardResource,
|
api.add_resource(DashboardResource,
|
||||||
'/v1/dashboards/<int:dashboard_id>')
|
'/v1/dashboards/<int:dashboard_id>')
|
||||||
api.add_resource(DashboardListResource, '/v1/dashboards')
|
api.add_resource(DashboardListResource, '/v1/dashboards')
|
||||||
|
|
|
@ -34,6 +34,11 @@ class RecordingsSchema(BaseResourceSchema):
|
||||||
record_value = fields.String()
|
record_value = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSecretSchema(BaseResourceSchema):
|
||||||
|
device_secret = fields.String(dump_only=True)
|
||||||
|
secret_algorithm = fields.String()
|
||||||
|
|
||||||
|
|
||||||
def validate_device_ownership(device_id):
|
def validate_device_ownership(device_id):
|
||||||
if not devices.can_user_access_device(g.current_account.id, device_id):
|
if not devices.can_user_access_device(g.current_account.id, device_id):
|
||||||
abort(403, message='You are not allowed to access this device',
|
abort(403, message='You are not allowed to access this device',
|
||||||
|
@ -127,3 +132,10 @@ class DeviceConfigurationResource(ProtectedResource):
|
||||||
def get(self, device_id):
|
def get(self, device_id):
|
||||||
validate_device_ownership(device_id)
|
validate_device_ownership(device_id)
|
||||||
return devices.get_device_configuration(device_id), 200
|
return devices.get_device_configuration(device_id), 200
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSecretResource(ProtectedResource):
|
||||||
|
@swag_from('swagger/get_device_secret_spec.yaml')
|
||||||
|
def get(self, device_id):
|
||||||
|
validate_device_ownership(device_id)
|
||||||
|
return DeviceSecretSchema().dump(devices.get_device(device_id)), 200
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
Gets a device secret info
|
||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Device
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: device_id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
description: Id of the device
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- content
|
||||||
|
properties:
|
||||||
|
content:
|
||||||
|
$ref: '#/definitions/DeviceSecretInfo'
|
|
@ -1,8 +1,27 @@
|
||||||
import sys
|
import sys
|
||||||
|
import hmac
|
||||||
|
import urllib.parse
|
||||||
from .models import Device, Recording, DeviceAssociation, DeviceType
|
from .models import Device, Recording, DeviceAssociation, DeviceType
|
||||||
from app.core import app
|
from app.core import app
|
||||||
|
|
||||||
|
|
||||||
|
# Private helpers
|
||||||
|
def generate_hmac_for_message(device_id, raw_json):
|
||||||
|
device = Device.get(id=device_id)
|
||||||
|
raw_json_bytes = urllib.parse.urlencode(raw_json).encode('utf-8')
|
||||||
|
return hmac.new(
|
||||||
|
bytes(device.device_secret, 'utf-8'),
|
||||||
|
raw_json_bytes,
|
||||||
|
device.secret_algorithm).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_hmac_in_message(device_id, raw_json):
|
||||||
|
hmac_value = raw_json.pop('hmac', None)
|
||||||
|
calculated_hmac = generate_hmac_for_message(device_id, raw_json)
|
||||||
|
if not hmac.compare_digest(hmac_value, calculated_hmac):
|
||||||
|
raise ValueError("Bad hmac")
|
||||||
|
|
||||||
|
|
||||||
# Public interface
|
# Public interface
|
||||||
def create_device(name, account_id, device_type=1):
|
def create_device(name, account_id, device_type=1):
|
||||||
"""
|
"""
|
||||||
|
@ -51,6 +70,9 @@ def set_device_configuration(device_id, configuration_json):
|
||||||
device = Device.get(id=device_id)
|
device = Device.get(id=device_id)
|
||||||
device.configuration = configuration_json
|
device.configuration = configuration_json
|
||||||
device.save()
|
device.save()
|
||||||
|
configuration_json['hmac'] = generate_hmac_for_message(
|
||||||
|
device_id,
|
||||||
|
configuration_json)
|
||||||
send_config.delay(device_id, str(configuration_json))
|
send_config.delay(device_id, str(configuration_json))
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
@ -178,7 +200,7 @@ def get_device_types():
|
||||||
|
|
||||||
def parse_raw_json_recording(device_id, json_msg):
|
def parse_raw_json_recording(device_id, json_msg):
|
||||||
"""
|
"""
|
||||||
Parses raw json recording and creates Recrding object
|
Parses raw json recording and creates Recording object
|
||||||
|
|
||||||
:param device_id: Id of device
|
:param device_id: Id of device
|
||||||
:type device_id: int
|
:type device_id: int
|
||||||
|
@ -198,7 +220,8 @@ def parse_raw_json_recording(device_id, json_msg):
|
||||||
+ str(error_instance))
|
+ str(error_instance))
|
||||||
|
|
||||||
|
|
||||||
def create_recording_and_return(device_id, raw_json):
|
def create_recording_and_return(device_id, raw_json,
|
||||||
|
authenticated=False):
|
||||||
"""
|
"""
|
||||||
Tries to create recording with given parameters and returns it.
|
Tries to create recording with given parameters and returns it.
|
||||||
Raises error on failure
|
Raises error on failure
|
||||||
|
@ -212,6 +235,9 @@ def create_recording_and_return(device_id, raw_json):
|
||||||
if not Device.exists(id=device_id):
|
if not Device.exists(id=device_id):
|
||||||
raise ValueError("Device does not exist!")
|
raise ValueError("Device does not exist!")
|
||||||
|
|
||||||
|
if not authenticated:
|
||||||
|
validate_hmac_in_message(device_id, raw_json)
|
||||||
|
|
||||||
recording = parse_raw_json_recording(device_id, raw_json)
|
recording = parse_raw_json_recording(device_id, raw_json)
|
||||||
recording.save()
|
recording.save()
|
||||||
return recording
|
return recording
|
||||||
|
@ -230,6 +256,7 @@ def create_recording(device_id, raw_json):
|
||||||
if not Device.exists(id=device_id):
|
if not Device.exists(id=device_id):
|
||||||
raise ValueError("Device does not exist!")
|
raise ValueError("Device does not exist!")
|
||||||
|
|
||||||
|
validate_hmac_in_message(device_id, raw_json)
|
||||||
recording = parse_raw_json_recording(device_id, raw_json)
|
recording = parse_raw_json_recording(device_id, raw_json)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
recording.save()
|
recording.save()
|
||||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||||
import datetime as datetime_module
|
import datetime as datetime_module
|
||||||
from app.core import db
|
from app.core import db
|
||||||
from sqlalchemy.dialects.postgresql import JSON
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
|
||||||
class Recording(db.Model):
|
class Recording(db.Model):
|
||||||
|
@ -126,6 +127,8 @@ class Device(db.Model):
|
||||||
name = db.Column(db.String, nullable=False)
|
name = db.Column(db.String, nullable=False)
|
||||||
device_type_id = db.Column(db.Integer, db.ForeignKey('device_types.id'))
|
device_type_id = db.Column(db.Integer, db.ForeignKey('device_types.id'))
|
||||||
device_type = db.relationship("DeviceType", foreign_keys=[device_type_id])
|
device_type = db.relationship("DeviceType", foreign_keys=[device_type_id])
|
||||||
|
device_secret = db.Column(db.String, nullable=False)
|
||||||
|
secret_algorithm = db.Column(db.String, nullable=False)
|
||||||
configuration = db.Column(JSON, nullable=True)
|
configuration = db.Column(JSON, nullable=True)
|
||||||
|
|
||||||
users = db.relationship("DeviceAssociation",
|
users = db.relationship("DeviceAssociation",
|
||||||
|
@ -137,6 +140,8 @@ class Device(db.Model):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.device_type_id = device_type
|
self.device_type_id = device_type
|
||||||
|
self.secret_algorithm = 'sha512'
|
||||||
|
self.device_secret = token_urlsafe(32)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,8 +3,7 @@ from app.celery_builder import task_builder
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
|
|
||||||
@task_builder.task()
|
def connect_and_send_mqtt_message(topic, message):
|
||||||
def send_config(device_id, config):
|
|
||||||
from flask_mqtt import Mqtt, MQTT_ERR_SUCCESS
|
from flask_mqtt import Mqtt, MQTT_ERR_SUCCESS
|
||||||
mqtt = Mqtt(app)
|
mqtt = Mqtt(app)
|
||||||
|
|
||||||
|
@ -15,14 +14,12 @@ def send_config(device_id, config):
|
||||||
@mqtt.on_connect()
|
@mqtt.on_connect()
|
||||||
def handle_connect(client, userdata, flags, rc):
|
def handle_connect(client, userdata, flags, rc):
|
||||||
print('MQTT worker client connected')
|
print('MQTT worker client connected')
|
||||||
print("Sending configuration to device: " + str(device_id))
|
|
||||||
print("Configuration: " + str(config))
|
|
||||||
topic = 'device/' + str(device_id) + '/config'
|
|
||||||
print("Targeting topic: " + topic)
|
print("Targeting topic: " + topic)
|
||||||
|
print("Sending message: " + message)
|
||||||
try:
|
try:
|
||||||
(result, mid) = mqtt.publish(topic, config, 2)
|
(result, mid) = mqtt.publish(topic, message, 2)
|
||||||
if (result == MQTT_ERR_SUCCESS):
|
if (result == MQTT_ERR_SUCCESS):
|
||||||
print("Success!!!")
|
print("Successfully sent a message")
|
||||||
print("Result: " + str(result))
|
print("Result: " + str(result))
|
||||||
print("Message id: " + str(mid))
|
print("Message id: " + str(mid))
|
||||||
mqtt.client.disconnect()
|
mqtt.client.disconnect()
|
||||||
|
@ -33,3 +30,11 @@ def send_config(device_id, config):
|
||||||
print("Instance: " + str(error_instance))
|
print("Instance: " + str(error_instance))
|
||||||
mqtt.client.disconnect()
|
mqtt.client.disconnect()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@task_builder.task()
|
||||||
|
def send_config(device_id, config):
|
||||||
|
print("Sending configuration to device: " + str(device_id))
|
||||||
|
print("Configuration: " + str(config))
|
||||||
|
topic = 'device/' + str(device_id) + '/config'
|
||||||
|
connect_and_send_mqtt_message(topic, config)
|
||||||
|
|
|
@ -94,6 +94,16 @@ definitions:
|
||||||
description: Type of chart
|
description: Type of chart
|
||||||
example: line
|
example: line
|
||||||
|
|
||||||
|
secret:
|
||||||
|
type: string
|
||||||
|
description: Secret key
|
||||||
|
example: Ranom-Key123
|
||||||
|
|
||||||
|
hashalgorithm:
|
||||||
|
type: string
|
||||||
|
description: Hashing algorithm used
|
||||||
|
example: sha512
|
||||||
|
|
||||||
Credentials:
|
Credentials:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
@ -209,6 +219,17 @@ definitions:
|
||||||
configuration:
|
configuration:
|
||||||
$ref: '#/definitions/configuration'
|
$ref: '#/definitions/configuration'
|
||||||
|
|
||||||
|
DeviceSecretInfo:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- device_secret
|
||||||
|
- secret_algorithm
|
||||||
|
properties:
|
||||||
|
device_secret:
|
||||||
|
$ref: '#/definitions/secret'
|
||||||
|
secret_algorithm:
|
||||||
|
$ref: '#/definitions/hashalgorithm'
|
||||||
|
|
||||||
DeviceCreation:
|
DeviceCreation:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
# App configuration
|
# App configuration
|
||||||
DEBUG = os.environ['DEBUG']
|
DEBUG = os.environ['DEBUG']
|
||||||
APP_VERSION = '0.3.2'
|
APP_VERSION = '0.3.3'
|
||||||
|
|
||||||
# Define the application directory
|
# Define the application directory
|
||||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: dad1f9b4eec2
|
||||||
|
Revises: b5eb4a04c77e
|
||||||
|
Create Date: 2018-10-29 20:48:49.588509
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'dad1f9b4eec2'
|
||||||
|
down_revision = 'b5eb4a04c77e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('devices', sa.Column('device_secret', sa.String(),
|
||||||
|
nullable=True))
|
||||||
|
op.add_column('devices', sa.Column('secret_algorithm', sa.String(),
|
||||||
|
nullable=True))
|
||||||
|
|
||||||
|
devices = sa.sql.table('devices', sa.column('id', sa.Integer),
|
||||||
|
sa.sql.column('device_secret'),
|
||||||
|
sa.sql.column('secret_algorithm'))
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
res = conn.execute("select id from devices")
|
||||||
|
results = res.fetchall()
|
||||||
|
for result in results:
|
||||||
|
op.execute(devices.update().where(devices.c.id ==
|
||||||
|
op.inline_literal(result[0])).
|
||||||
|
values({'device_secret': token_urlsafe(32),
|
||||||
|
'secret_algorithm': 'sha512'})
|
||||||
|
)
|
||||||
|
|
||||||
|
# Forbid nulls
|
||||||
|
op.alter_column('devices', 'device_secret', nullable=False)
|
||||||
|
op.alter_column('devices', 'secret_algorithm', nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('devices', 'secret_algorithm')
|
||||||
|
op.drop_column('devices', 'device_secret')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in New Issue