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 . 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
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
class ApiKey(models.Model):
|
||||
''' 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
|
||||
a string "keyId$key".
|
||||
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",
|
||||
max_length=128)
|
||||
name = models.CharField("Key name",
|
||||
|
@ -26,16 +28,31 @@ class ApiKey(models.Model):
|
|||
def everUsed(self):
|
||||
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)
|
||||
|
||||
def isCorrect(self, timestamp, inpMac):
|
||||
def __str__(self):
|
||||
return self.displayValue
|
||||
|
||||
def isCorrect(self, timestamp, inpMac, data):
|
||||
claimedDate = datetime.fromtimestamp(timestamp)
|
||||
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
||||
return False
|
||||
|
||||
mac = hmac.new(self.key,
|
||||
msg=int(claimedDate.timestamp()),
|
||||
mac = hmac.new(self.key.encode('utf-8'),
|
||||
msg=str(int(claimedDate.timestamp())).encode('utf-8'),
|
||||
digestmod=hashlib.sha256)
|
||||
mac.update(data.encode('utf-8'))
|
||||
|
||||
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
||||
|
|
|
@ -4,4 +4,5 @@ from . import views
|
|||
app_name = 'manisite'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^publish$', views.publishApiView),
|
||||
]
|
||||
|
|
55
api/views.py
55
api/views.py
|
@ -1,5 +1,6 @@
|
|||
from django.http import response
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
import json
|
||||
import datetime
|
||||
|
||||
|
@ -7,7 +8,7 @@ from . import models
|
|||
import mainsite.models as mainModels
|
||||
|
||||
|
||||
def authentify(data):
|
||||
def authentify(data, payload):
|
||||
''' returns whether the request's authentification is correct '''
|
||||
required = ['keyId', 'timestamp', 'hmac']
|
||||
for field in required:
|
||||
|
@ -15,38 +16,42 @@ def authentify(data):
|
|||
return response.HttpResponseForbidden(
|
||||
'Missing required field "{}"'.format(field))
|
||||
try:
|
||||
key = models.ApiKey.objects.get(keyId=data['keyId'])
|
||||
key = models.ApiKey.objects.get(id=data['keyId'])
|
||||
except models.ApiKey.DoesNotExist:
|
||||
response.HttpResponseForbidden('Bad authentication')
|
||||
return response.HttpResponseForbidden('Bad authentication')
|
||||
|
||||
if not key.isCorrect(data['timestamp'], data['hmac']):
|
||||
response.HttpResponseForbidden('Bad authentication')
|
||||
normPayload = json.dumps(payload)
|
||||
if not key.isCorrect(data['timestamp'], data['hmac'], normPayload):
|
||||
return response.HttpResponseForbidden('Bad authentication')
|
||||
|
||||
|
||||
def apiView(fct, required=[]):
|
||||
def wrap(request, *args, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.decoder.JSONDecoreError:
|
||||
return response.HttpResponseBadRequest("Bad json")
|
||||
def apiView(required=[]):
|
||||
def decorator(fct):
|
||||
@csrf_exempt
|
||||
def wrap(request, *args, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.decoder.JSONDecoreError:
|
||||
return response.HttpResponseBadRequest("Bad json")
|
||||
|
||||
try:
|
||||
authData = data['auth']
|
||||
reqData = data['req']
|
||||
except KeyError:
|
||||
return response.HttpResponseBadRequest("Bad request format")
|
||||
try:
|
||||
authData = data['auth']
|
||||
reqData = data['req']
|
||||
except KeyError:
|
||||
return response.HttpResponseBadRequest("Bad request format")
|
||||
|
||||
for field in required:
|
||||
if field not in reqData:
|
||||
return response.HttpResponseBadRequest(
|
||||
"Missing field {}".format(field))
|
||||
for field in required:
|
||||
if field not in reqData:
|
||||
return response.HttpResponseBadRequest(
|
||||
"Missing field {}".format(field))
|
||||
|
||||
authVal = authentify(authData)
|
||||
if authVal is not None:
|
||||
return authVal
|
||||
authVal = authentify(authData, reqData)
|
||||
if authVal is not None:
|
||||
return authVal
|
||||
|
||||
return fct(request, reqData, *args, **kwargs)
|
||||
return wrap
|
||||
return fct(request, reqData, *args, **kwargs)
|
||||
return wrap
|
||||
return decorator
|
||||
|
||||
|
||||
@apiView(required=["id", "url", "date"])
|
||||
|
|
Loading…
Reference in a new issue