Skip to content

Commit

Permalink
Merge branch 'authentication' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
F483 committed Aug 20, 2015
2 parents 92f863e + 7f1c744 commit b65402f
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 7 deletions.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include requirements.txt
include test_requirements.txt
include develop_requirements.txt
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ help:
@echo "Some usefull development shortcuts."
@echo " clean Remove all generated files."
@echo " setup Setup development environment."
@echo " shell Open ipython from the development environment."
@echo " test Run tests and analysis tools."
@echo " wheel Build package wheel and save in '$(WHEEL_DIR)'."
@echo " wheels Build dependencie wheels and save in '$(WHEEL_DIR)'."
Expand All @@ -28,12 +29,12 @@ clean:
find | grep -i ".*\.pyc$$" | xargs -r -L1 rm


virtualenvs: clean
virtualenv: clean
virtualenv -p /usr/bin/python$(PYTHON_VERSION) env
$(PIP) install wheel


wheels: virtualenvs
wheels: virtualenv
$(PIP) wheel --wheel-dir=$(WHEEL_DIR) -r requirements.txt
$(PIP) wheel --wheel-dir=$(WHEEL_DIR) -r test_requirements.txt
$(PIP) wheel --wheel-dir=$(WHEEL_DIR) -r develop_requirements.txt
Expand All @@ -44,12 +45,16 @@ wheel: test
mv dist/*.whl $(WHEEL_DIR)


setup: virtualenvs
setup: virtualenv
$(PIP) install $(USE_WHEEL) -r requirements.txt
$(PIP) install $(USE_WHEEL) -r test_requirements.txt
$(PIP) install $(USE_WHEEL) -r develop_requirements.txt


shell: setup
env/bin/ipython


test: setup
$(PY) setup.py test

Expand Down
37 changes: 35 additions & 2 deletions dataserv/Farmer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import json
import hashlib
from dataserv.run import db
import binascii
from email.utils import parsedate
from dataserv.run import db, app
from datetime import datetime
from datetime import timedelta
from sqlalchemy import DateTime
from btctxstore import BtcTxStore
from dataserv.Validator import is_btc_address


Expand All @@ -17,7 +21,6 @@ class Farmer(db.Model):
btc_addr = db.Column(db.String(35), unique=True)
last_seen = db.Column(DateTime, default=datetime.utcnow)
height = db.Column(db.Integer, default=0)
last_message_hash = db.Column(db.String(64), unique=True) # sha256sum

def __init__(self, btc_addr, last_seen=None):
"""
Expand All @@ -32,6 +35,36 @@ def __init__(self, btc_addr, last_seen=None):
def __repr__(self):
return '<Farmer BTC Address: %r>' % self.btc_addr

def get_server_address(self):
return app.config["ADDRESS"]

def get_server_authentication_timeout(self):
return app.config["AUTHENTICATION_TIMEOUT"]

def authenticate(self, header_authorization, header_date):
if app.config["SKIP_AUTHENTICATION"]:
return True
if not header_authorization:
raise ValueError("Header authorization required!")
if not header_date:
raise ValueError("Header date required!")

# verify date
date = datetime(*parsedate(header_date)[:6])
timeout = self.get_server_authentication_timeout()
delta = datetime.now() - date
if delta >= timedelta(seconds=timeout):
raise ValueError("Header date to old!")

# verify signature
message = self.get_server_address() + " " + header_date
if not BtcTxStore().verify_signature_unicode(self.btc_addr,
header_authorization,
message):
raise ValueError("Invalid header_authorization!")
return True


def is_btc_address(self):
"""Check if the address is a valid Bitcoin public key."""
return is_btc_address(self.btc_addr)
Expand Down
17 changes: 16 additions & 1 deletion dataserv/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os.path
import datetime
from random import randint
from flask import make_response, jsonify
from flask import make_response, jsonify, request
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

# Import modules
Expand Down Expand Up @@ -47,8 +47,14 @@ def index():

@app.route('/api/register/<btc_addr>', methods=["GET"])
def register(btc_addr):

date = request.headers.get('Date')
authorization = request.headers.get('Authorization')

# create Farmer object to represent user
user = Farmer(btc_addr)
user.authenticate(request.headers.get('Authorization'),
request.headers.get('Date'))

# error template
error_msg = "Registration Failed: {0}"
Expand All @@ -69,6 +75,8 @@ def register(btc_addr):
def ping(btc_addr):
# create Farmer object to represent user
user = Farmer(btc_addr)
user.authenticate(request.headers.get('Authorization'),
request.headers.get('Date'))

# error template
error_msg = "Ping Failed: {0}"
Expand All @@ -85,6 +93,11 @@ def ping(btc_addr):
return make_response(error_msg.format(msg), 404)


@app.route('/api/address', methods=["GET"])
def get_address():
return jsonify({ "address": app.config["ADDRESS"] })


@app.route('/api/online', methods=["GET"])
def online():
# this could be formatted a bit better, but we just want to publicly display
Expand Down Expand Up @@ -132,6 +145,8 @@ def total():
def set_height(btc_addr, height):
# create Farmer object to represent user
user = Farmer(btc_addr)
user.authenticate(request.headers.get('Authorization'),
request.headers.get('Date'))

# attempt to set height
try:
Expand Down
4 changes: 4 additions & 0 deletions dataserv/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

DATA_DIR = 'data/'
BYTE_SIZE = 1024*1024*128 # 128 MB

ADDRESS = "16ZcxFDdkVJR1P8GMNmWFyhS4EKrRMsWNG" # unique per server address
AUTHENTICATION_TIMEOUT = 10 # seconds
SKIP_AUTHENTICATION = False # only for testing
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ Flask == 0.10.1
Flask-SQLAlchemy == 2.0
RandomIO == 0.2.1
partialhash == 1.1.0
btctxstore == 4.2.1
43 changes: 43 additions & 0 deletions tests/test_App.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import unittest
import json
from btctxstore import BtcTxStore
from dataserv.run import app, db
from dataserv.app import secs_to_mins, online_farmers
from email.utils import formatdate
from datetime import datetime
from datetime import timedelta
from time import mktime


class AppTest(unittest.TestCase):

# setup
def setUp(self):
app.config["SKIP_AUTHENTICATION"] = True # monkey patch
self.app = app.test_client()
db.create_all()

Expand Down Expand Up @@ -185,3 +192,39 @@ def test_farmer_order(self):
# get farmers
farmers = online_farmers()
self.assertEqual(farmers[0].btc_addr, addr1)

def test_get_address(self):
rv = self.app.get('/api/address')
self.assertEqual(rv.status_code, 200)
data = rv.data.decode("utf-8")
self.assertEqual(app.config["ADDRESS"], json.loads(data)["address"])


class AppAuthenticationHeadersTest(unittest.TestCase):

def setUp(self):
app.config["SKIP_AUTHENTICATION"] = False # monkey patch
self.app = app.test_client()
db.create_all()

def tearDown(self):
db.session.remove()
db.drop_all()

def test_success(self):
blockchain = BtcTxStore()
wif = blockchain.create_key()
address = blockchain.get_address(wif)

# create header date and authorization signature
header_date = formatdate(timeval=mktime(datetime.now().timetuple()),
localtime=True, usegmt=True)
message = app.config["ADDRESS"] + " " + header_date
header_authorization = blockchain.sign_unicode(wif, message)

headers = {"Date": header_date, "Authorization": header_authorization }
url = '/api/register/{0}'.format(address)
rv = self.app.get(url, headers=headers)
self.assertEqual(b"User registered.", rv.data)
self.assertEqual(rv.status_code, 200)

50 changes: 49 additions & 1 deletion tests/test_Farmer.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import json
import unittest
from dataserv.app import db
from btctxstore import BtcTxStore
from dataserv.app import db, app
from dataserv.Farmer import sha256
from dataserv.Farmer import Farmer
from email.utils import formatdate
from datetime import datetime
from datetime import timedelta
from time import mktime


class FarmerTest(unittest.TestCase):

def setUp(self):
app.config["SKIP_AUTHENTICATION"] = True # monkey patch
db.create_all()

def tearDown(self):
Expand Down Expand Up @@ -105,3 +111,45 @@ def test_to_json(self):
call_payload = json.loads(farmer.to_json())
self.assertEqual(test_json, call_payload)


class FarmerAuthenticationTest(unittest.TestCase):

def setUp(self):
app.config["SKIP_AUTHENTICATION"] = False # monkey patch
db.create_all()

def tearDown(self):
db.session.remove()
db.drop_all()

def test_authentication_success(self):
blockchain = BtcTxStore()
wif = blockchain.create_key()
address = blockchain.get_address(wif)
farmer = Farmer(address)

header_date = formatdate(timeval=mktime(datetime.now().timetuple()),
localtime=True, usegmt=True)
message = farmer.get_server_address() + " " + header_date
header_authorization = blockchain.sign_unicode(wif, message)
self.assertTrue(farmer.authenticate(header_authorization, header_date))

def test_authentication_timeout(self):
def callback():
blockchain = BtcTxStore()
wif = blockchain.create_key()
address = blockchain.get_address(wif)
farmer = Farmer(address)

timeout = farmer.get_server_authentication_timeout()

date = datetime.now() - timedelta(seconds=timeout)
header_date = formatdate(timeval=mktime(date.timetuple()),
localtime=True, usegmt=True)
message = farmer.get_server_address() + " " + header_date
header_authorization = blockchain.sign_unicode(wif, message)
farmer.authenticate(header_authorization, header_date)
self.assertRaises(ValueError, callback)

# TODO test incorrect address
# TODO test incorrect signature

0 comments on commit b65402f

Please sign in to comment.