Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please accept my updates into the master branch of xDripAPS #6

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ Note: ensure you enter http:// (NOT https://). <api_secret> is the plain-text AP

If using xDrip+ you also need to navigate to Settings > Cloud Upload > MongoDB and ensure that the "Skip LAN uploads" option is NOT selected. If you don't have this setting, update to a recent version of the xDrip+ app. (This option was added to a nightly build around December 2016).

Please note xDripAPS does *not* support token based authentication. That means your API_SECRET environment variable will need to be an ordinary password.

xDripAPS has been improved to allow compatibility with OpenAPS rigs which are setup to use a token for their API_SECRET. This compatibility works by users configuring a second API_SECRET_xDripAPS variable setting a password for xDrip to use during upload.

Make sure that this API_SECRET_xDripAPS variable is set to the SHA1 hashed version of whatever you type into your password component of the <password>@<host>:<port> in the baseURL of the xDrip Nightscout uploader.



Expand Down Expand Up @@ -102,3 +107,11 @@ If using xDrip+ you also need to navigate to Settings > Cloud Upload > MongoDB a
openaps device add xdrip process 'bash -c "curl -s http://localhost:5000/api/v1/entries?count=288"'
openaps report add monitor/glucose.json text xdrip shell
```


**Additions by Andy Armstrong making version 2**
1/Added checking for API_SECRET and won't start unless this, or API_SECRET_xDripAPS environment variable is set
2/Added support for second environment variable API_SECRET_xDripAPS to allow for compatibility to run xDripAPS when OpenAPS users are using token based authentication with their API_SECRET value for Nightscout, and would like to be able to use the basic environment variable based password for their xDrip+ integration with xDripAPS.
3/Added error output when api-secret header isn't sent instead of just letting the program return a 500
4/Changed header from Api_Secret to api-secret as xDrip+ doesn't send Api_Secret anymore and therefore this update makes the loop work again when using the latest version of xDrip.
5/Added a persisted log file for xDripAPS. Each log will be 2 megabytes and there is a 2 file rollover scheme, meaning at maximum there will be a current working log, and 2 historical log files, to be able to track a sensible amount of historical logs to assist with problem determination.
138 changes: 109 additions & 29 deletions xDripAPS.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import json
import os
import sqlite3
import sys
import logging
from flask import Flask, request
from flask_restful import Resource, Api
from logging.handlers import RotatingFileHandler
from datetime import datetime

# Maximum number of rows to retain - older rows will be deleted to minimise disk usage
MAX_ROWS = 336



# SQLite3 .db filename
DB_FILE = os.environ['HOME']+ "/.xDripAPS_data/xDripAPS.db"

app_log = logging.getLogger('root')
app = Flask(__name__)
api = Api(app)

#Booleans to help us know which authorisation we use runtime will pick one of these during setup
api_secret = True
api_secret_xDripAPS = True

def create_schema():
xLog("Creating database at:"+DB_FILE)
if not os.path.exists(os.environ['HOME']+ "/.xDripAPS_data/"):
xLog(".xDripAPS_data folder didn't exist so creating it")
os.makedirs(os.environ['HOME']+ "/.xDripAPS_data/")

conn = sqlite3.connect(DB_FILE)
qry = """CREATE TABLE entries
(device text,
Expand All @@ -32,6 +47,13 @@ def create_schema():
conn.close()

def startup_checks():

xLog("Performing xDripAPS startup checks")

#We are referencing our global variables here
global api_secret
global api_secret_xDripAPS

# Does .db file exist?
if os.path.isfile(DB_FILE):
# Check for corruption
Expand All @@ -40,38 +62,83 @@ def startup_checks():
c.execute("PRAGMA integrity_check")
status = str(c.fetchone()[0])
if status == "ok":
print "Startup checks OK"
xLog("Startup checks OK")
conn.close()
else:
print "Startup checks FAIL"
xLog("Startup checks FAIL")
# Delete corrupt database
print "Deleting corrupt SQLite database file (" + DB_FILE + ")..."
xLog("Deleting corrupt SQLite database file (" + DB_FILE + ")...")
conn.close()
os.remove(DB_FILE)
# re-create database
print "Re-cresting database..."
xLog("Re-cresting database...")
create_schema()
else:
# Database doesn't exist, so create it
xLog("Database doesn't exist yet, so creating it")
create_schema()

xLog("Checking that environment variables are setup for authorisation")
try:
os.environ['API_SECRET']
xLog("API_SECRET is set.")
except:
api_secret = False
xLog("API_SECRET is not set.")
try:
os.environ['API_SECRET_xDripAPS']
xLog("API_SECRET_xDripAPS is set.")
except:
api_secret_xDripAPS = False
xLog("API_SECRET_xDripAPS is not set.")

if api_secret == False and api_secret_xDripAPS == False:
xLog("Neither API_SECRET or API_SECRET_xDripAPS is set. Please set one and run again!")
sys.exit(1)

def setup_logging():

log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(funcName)s %(message)s')
logFile = '/var/log/openaps/xDripAPS-{:%d-%m-%Y}.log'.format(datetime.now())

if not os.path.exists("/var/log/openaps"):
print "/var/log/openaps directory doesn't exist! You should create this before continuing. We won't create this for you, as we expected something else, like an openaps rig to have created this for you already!"
sys.exit(1)

my_handler = RotatingFileHandler(logFile, mode='a', maxBytes=2*1024*1024,
backupCount=2, encoding=None, delay=0)
my_handler.setFormatter(log_formatter)
my_handler.setLevel(logging.INFO)

app_log = logging.getLogger('root')
app_log.setLevel(logging.INFO)
app_log.addHandler(my_handler)

xLog("xDrip APS log setup complete, find the log here:"+logFile)

def xLog(message):
global app_log
print message
app_log.info(message)
return

class Entries(Resource):

def get(self):

# Connect to database
conn = sqlite3.connect(DB_FILE)
# Connect to database
conn = sqlite3.connect(DB_FILE)

# Housekeeping first
qry = "DELETE FROM entries WHERE ROWID IN "
qry += "(SELECT ROWID FROM entries ORDER BY ROWID DESC LIMIT -1 OFFSET " + str(MAX_ROWS) + ")"
conn.execute(qry)
conn.commit()
# Housekeeping first
qry = "DELETE FROM entries WHERE ROWID IN "
qry += "(SELECT ROWID FROM entries ORDER BY ROWID DESC LIMIT -1 OFFSET " + str(MAX_ROWS) + ")"
conn.execute(qry)
conn.commit()

# Get count parameter
count = request.args.get('count')
# Get count parameter
count = request.args.get('count')

# Perform query and return JSON data
# Perform query and return JSON data
qry = "SELECT ROWID as _id, device, date, dateString, sgv, direction, type, filtered, "
qry += "unfiltered, rssi, noise "
qry += "FROM entries ORDER BY date DESC"
Expand All @@ -84,7 +151,7 @@ def get(self):

for row in cursor:
result_as_dict = {
# '_id' : row[0],
# '_id' : row[0],
'device' : row[1],
'date' : row[2],
'dateString' : row[3],
Expand All @@ -95,27 +162,39 @@ def get(self):
'unfiltered' : row[8],
'rssi' : row[9],
'noise' : row[10],
'glucose' : row[4]}
'glucose' : row[4]}
results_as_dict.append(result_as_dict)

conn.close()
return results_as_dict
return results_as_dict

def post(self):

# Get hashed API secret from request
request_secret_hashed = request.headers['Api_Secret']
print 'request_secret_hashed : ' + request_secret_hashed

# Get API_SECRET environment variable
env_secret_hashed = os.environ['API_SECRET']

try:
request_secret_hashed = request.headers['Api_Secret']
xLog('request_secret_hashed : ' + request_secret_hashed)
except:
xLog("Client didn't pass in Api-Secret header")
return 'Client didnt pass in Api-Secret header',500

if api_secret:
# Get API_SECRET environment variable
env_secret_hashed = os.environ['API_SECRET']
xLog("We will authenticate using environment variable API_SECRET")

elif api_secret_xDripAPS:
#get API_SECRET_xDripAPS environment variable if needed
env_secret_hashed = os.environ['API_SECRET_xDripAPS']
xLog("We will authenticate using environment variable API_SECRET_xDripAPS")

# Authentication check
if request_secret_hashed != env_secret_hashed:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a bug here -
You compare hash value with non-hash value
request secret is hashed while env_secret_hashed is actually the plain text and not hashed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ArmyAndy
There is a bug here, you compare hash value vs. non-hash value
request_secret_hashed is really hashed, while env_secret_hashed is just the plain text value. Should be hashed before comparison

print 'Authentication failure!'
print 'API Secret passed in request does not match API_SECRET environment variable'
xLog('Authentication failure!')
xLog('API Secret passed in request does not match your API_SECRET or API_SECRET_xDripAPS environment variable')
return 'Authentication failed!', 401

xLog("Authentication successful")
# Get JSON data
json_data = request.get_json(force=True)

Expand Down Expand Up @@ -161,15 +240,15 @@ class Test(Resource):
def get(self):
# Get hashed API secret from request
request_secret_hashed = request.headers['Api_Secret']
print 'request_secret_hashed : ' + request_secret_hashed
xLog('request_secret_hashed : ' + request_secret_hashed)

# Get API_SECRET environment variable
env_secret_hashed = os.environ['API_SECRET']

# Authentication check
if request_secret_hashed != env_secret_hashed:
print 'Authentication failure!'
print 'API Secret passed in request does not match API_SECRET environment variable'
xLog('Authentication failure!')
xLog('API Secret passed in request does not match API_SECRET environment variable')
return 'Authentication failed!', 401

return {"status": 'ok'}
Expand All @@ -178,5 +257,6 @@ def get(self):
api.add_resource(Test, '/api/v1/experiments/test')

if __name__ == '__main__':
setup_logging()
startup_checks()
app.run(host='0.0.0.0')