Fix publish API, add basic client
This commit is contained in:
parent
25102a8233
commit
b0f42fbe4b
6 changed files with 154 additions and 34 deletions
12
api/admin.py
12
api/admin.py
|
@ -1,3 +1,13 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from . import models
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
@admin.register(models.ApiKey)
|
||||||
|
class ApiKeyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'last_used', 'displayValue']
|
||||||
|
readonly_fields = ['keyId', 'key', 'last_used', 'displayValue']
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change:
|
||||||
|
obj.initialFill()
|
||||||
|
super(ApiKeyAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
61
api/client/apiclient.py
Normal file
61
api/client/apiclient.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
''' API client for bocal-site '''
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def sendReq(url):
|
||||||
|
def send(payload, host):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request('http://{}/{}'.format(host, url),
|
||||||
|
json.dumps(payload).encode('ascii'))
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
handle = urllib.request.urlopen(req)
|
||||||
|
code = handle.getcode()
|
||||||
|
content = handle.read()
|
||||||
|
handle.close()
|
||||||
|
return (code, content.decode('utf-8'))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return (e.code, e.read().decode('utf-8'))
|
||||||
|
|
||||||
|
def authentify(apiKey, payload):
|
||||||
|
keyId, key = apiKey.split('$')
|
||||||
|
keyId = int(keyId)
|
||||||
|
time = datetime.now().timestamp()
|
||||||
|
mac = hmac.new(key.encode('utf-8'),
|
||||||
|
msg=str(int(time)).encode('utf-8'),
|
||||||
|
digestmod=hashlib.sha256)
|
||||||
|
mac.update(json.dumps(payload).encode('utf-8'))
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
'keyId': keyId,
|
||||||
|
'timestamp': time,
|
||||||
|
'hmac': mac.hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'auth': auth,
|
||||||
|
'req': payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
def decorator(fct):
|
||||||
|
''' Decorator. Adds authentication layer. '''
|
||||||
|
def wrap(host, apiKey, *args, **kwargs):
|
||||||
|
innerReq = fct(*args, **kwargs)
|
||||||
|
payload = authentify(apiKey, innerReq)
|
||||||
|
return send(payload, host)
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@sendReq(url='api/publish')
|
||||||
|
def publish(bocId, url, date):
|
||||||
|
return {
|
||||||
|
'id': bocId,
|
||||||
|
'url': url,
|
||||||
|
'date': date.strftime('%Y-%m-%d'),
|
||||||
|
}
|
26
api/migrations/0001_initial.py
Normal file
26
api/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.5 on 2017-09-24 14:53
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApiKey',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('key', models.CharField(max_length=128, verbose_name='API key')),
|
||||||
|
('name', models.CharField(help_text='Where is this key used from?', max_length=256, verbose_name='Key name')),
|
||||||
|
('last_used', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 1, 0), verbose_name='Last used')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,7 +2,8 @@ from django.db import models
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
class ApiKey(models.Model):
|
class ApiKey(models.Model):
|
||||||
''' An API key, to login using the API
|
''' An API key, to login using the API
|
||||||
|
@ -11,10 +12,11 @@ class ApiKey(models.Model):
|
||||||
containing any dollar ($) sign. It is saved on the client's machine as
|
containing any dollar ($) sign. It is saved on the client's machine as
|
||||||
a string "keyId$key".
|
a string "keyId$key".
|
||||||
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
||||||
a timestamp `ts`, the key id `kid` and hmac = `HMAC(key, ts, sha256)`.
|
a timestamp `ts`, the key id `kid` and
|
||||||
|
hmac = `HMAC(key, ts + data, sha256)`
|
||||||
|
where `data` is the normalized (`json.dumps(json.loads(...))`) value of
|
||||||
|
the data part of the request.
|
||||||
'''
|
'''
|
||||||
keyId = models.IntegerField("API key id",
|
|
||||||
primary_key=True)
|
|
||||||
key = models.CharField("API key",
|
key = models.CharField("API key",
|
||||||
max_length=128)
|
max_length=128)
|
||||||
name = models.CharField("Key name",
|
name = models.CharField("Key name",
|
||||||
|
@ -26,16 +28,31 @@ class ApiKey(models.Model):
|
||||||
def everUsed(self):
|
def everUsed(self):
|
||||||
return self.last_used > datetime.fromtimestamp(0)
|
return self.last_used > datetime.fromtimestamp(0)
|
||||||
|
|
||||||
def __str__(self):
|
def initialFill(self):
|
||||||
|
if not self.key:
|
||||||
|
KEY_SIZE = 64
|
||||||
|
KEY_CHARS = string.ascii_letters + string.digits
|
||||||
|
self.key = ''.join(random.choices(KEY_CHARS, k=KEY_SIZE))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def keyId(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayValue(self):
|
||||||
return "{}${}".format(self.keyId, self.key)
|
return "{}${}".format(self.keyId, self.key)
|
||||||
|
|
||||||
def isCorrect(self, timestamp, inpMac):
|
def __str__(self):
|
||||||
|
return self.displayValue
|
||||||
|
|
||||||
|
def isCorrect(self, timestamp, inpMac, data):
|
||||||
claimedDate = datetime.fromtimestamp(timestamp)
|
claimedDate = datetime.fromtimestamp(timestamp)
|
||||||
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
mac = hmac.new(self.key,
|
mac = hmac.new(self.key.encode('utf-8'),
|
||||||
msg=int(claimedDate.timestamp()),
|
msg=str(int(claimedDate.timestamp())).encode('utf-8'),
|
||||||
digestmod=hashlib.sha256)
|
digestmod=hashlib.sha256)
|
||||||
|
mac.update(data.encode('utf-8'))
|
||||||
|
|
||||||
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
||||||
|
|
|
@ -4,4 +4,5 @@ from . import views
|
||||||
app_name = 'manisite'
|
app_name = 'manisite'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
url(r'^publish$', views.publishApiView),
|
||||||
]
|
]
|
||||||
|
|
19
api/views.py
19
api/views.py
|
@ -1,5 +1,6 @@
|
||||||
from django.http import response
|
from django.http import response
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ from . import models
|
||||||
import mainsite.models as mainModels
|
import mainsite.models as mainModels
|
||||||
|
|
||||||
|
|
||||||
def authentify(data):
|
def authentify(data, payload):
|
||||||
''' returns whether the request's authentification is correct '''
|
''' returns whether the request's authentification is correct '''
|
||||||
required = ['keyId', 'timestamp', 'hmac']
|
required = ['keyId', 'timestamp', 'hmac']
|
||||||
for field in required:
|
for field in required:
|
||||||
|
@ -15,15 +16,18 @@ def authentify(data):
|
||||||
return response.HttpResponseForbidden(
|
return response.HttpResponseForbidden(
|
||||||
'Missing required field "{}"'.format(field))
|
'Missing required field "{}"'.format(field))
|
||||||
try:
|
try:
|
||||||
key = models.ApiKey.objects.get(keyId=data['keyId'])
|
key = models.ApiKey.objects.get(id=data['keyId'])
|
||||||
except models.ApiKey.DoesNotExist:
|
except models.ApiKey.DoesNotExist:
|
||||||
response.HttpResponseForbidden('Bad authentication')
|
return response.HttpResponseForbidden('Bad authentication')
|
||||||
|
|
||||||
if not key.isCorrect(data['timestamp'], data['hmac']):
|
normPayload = json.dumps(payload)
|
||||||
response.HttpResponseForbidden('Bad authentication')
|
if not key.isCorrect(data['timestamp'], data['hmac'], normPayload):
|
||||||
|
return response.HttpResponseForbidden('Bad authentication')
|
||||||
|
|
||||||
|
|
||||||
def apiView(fct, required=[]):
|
def apiView(required=[]):
|
||||||
|
def decorator(fct):
|
||||||
|
@csrf_exempt
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
|
@ -41,12 +45,13 @@ def apiView(fct, required=[]):
|
||||||
return response.HttpResponseBadRequest(
|
return response.HttpResponseBadRequest(
|
||||||
"Missing field {}".format(field))
|
"Missing field {}".format(field))
|
||||||
|
|
||||||
authVal = authentify(authData)
|
authVal = authentify(authData, reqData)
|
||||||
if authVal is not None:
|
if authVal is not None:
|
||||||
return authVal
|
return authVal
|
||||||
|
|
||||||
return fct(request, reqData, *args, **kwargs)
|
return fct(request, reqData, *args, **kwargs)
|
||||||
return wrap
|
return wrap
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@apiView(required=["id", "url", "date"])
|
@apiView(required=["id", "url", "date"])
|
||||||
|
|
Loading…
Reference in a new issue