Client Library (How To)

The Social OS API is straightforward to use, particularly with a modern
HTTP library such as Python's Requests. But for advanced use,
particularly in applications that use admin accounts and stored s
essions, there are enough intricacies that it's valuable to have an API
client library to smooth the way. This example presents a simple API
client library for Python.

Our client module presents just two classes: API for the actual API
access, and Session for managing application sessions. This naming may be a
little confusing when first reading the code, as the API module also
manages sessions, in this case for efficient communication with the
Social OS API. These API sessions are generally short-lived,
and only used to bundle multiple requests into a single HTTPS
connection, to avoid the SSL setup delay.

The application sessions are designed to be stored in a local database.
Here we use MongoDB or TokuMX. Later we'll examine other options for
session storage, and how to create a pluggable session engine.

This version of the API client doesn't explicitly wrap the API classes
and methods, instead simply providing a convenient way to construct the
API requests and return the decoded results. Future articles will
examine more advanced client-side functionality.

Here is the client code:

# coding=utf-8

import datetime
import json

from hashlib import sha1

import requests

import database

from conf import apiurl, adminurl, adminuser, adminpass, securekey, ttl
from tools import decrypt
from errors import Unauthorized, Unimplemented

import cherrypy


class Session(object):
    def __init__(self, id=None, data=None, token=None):
        '''To initialise a session from user login, provide an id and data.  To initialise
           from session id, provide the key token.  id is a hash of the token so that even
           in the event of a security breach, it is not possible to extract both the token
           and the password for a key.'''
        if id and data:
            session = database.Session.objects.find_one(id=id)
            if session:
                self.data = session.data
                session.accessed = datetime.datetime.now()
                session.save()
            else:
                raise Unauthorized

            self.id = id
            self.data = data

        elif token:
            id = sha1(token).hexdigest()
            session = database.Session.objects.find_one(id=id)
            if not session:
                raise Unauthorized

            self.id = id
            self.data = session.data

        else:
            raise Unauthorized

    @classmethod
    def login(_class, user, password):
        api = API()
        key = api.post('key', data={'ttl': ttl}, user=user, password=password)
        id = sha1(key['token']).hexdigest()
        pcrypt = encrypt(key['password'], '%s%s' % (securekey, key['token']))
        setcookie('session', key['token'])
        expirecookie(['session'], ttl)

        return _class.__init__(id, data={'auth': pcrypt, 'user': user})

    def logout(self):
        '''To log out, delete the API key, the session record, the data in the
        session object, and the session cookie, n that order.'''
        assert self.id, 'No session data available.'

        api = API()
        api.auth()
        api.delete('key/%s' % cherrypy.response.cookie['session'])
        database.Session.objects.delete_one(id=self.id)

        self.id = None
        self.data = None

        delcookie('session')

    def authdetails(self):
        assert self.id, 'No session data available.'

        session = database.Session.objects.find_one(id=self.id)
        if session:
            data = session.data
            user = data['user']
            password = decrypt(data['auth'], '%s%s' %
              (securekey, cherrypy.response.cookie['session']))

            return user, password

        raise Unauthorized


class API(object):
    def __init__(self):
        self.session = requests.Session()

    def auth(self, user=None, password=None):
        if user and password:
            self.session.auth(user, password)
        else:
            self.session.auth(Session().authdetails())

    def admin(self, method, data=None, action='GET', adminuser=adminuser, adminpass=adminpass):
        return self.call(method, adminuser, adminpass, data=data, action=action, base=adminurl)

    def call(self, method, user=None, password=None, data=None, action='GET', base=apiurl):
        if user and password:
            self.session.auth(user, password)
        url = '%s/%s' % (base, method)

        return json.loads(requests.request(action, url, data=data))

    @staticmethod
    def easyadmin(method, data=None, action='GET', base=adminurl,
                  adminuser=adminuser, adminpass=adminpass):
        session = requests.Session()
        session.auth(adminuser, adminpass)

        url = '%s/%s' % (base, method)

        return json.loads(requests.request(action, url, data=data))

    @staticmethod
    def easy(method, data=None, action='GET', base=apiurl):
        session = requests.Session()
        session.auth(Session().authdetails())

        url = '%s/%s' % (base, method)

        return json.loads(requests.request(action, url, data=data))

    get = call

    def post(self, method, action='POST', **kw):
        self.call(method, action=action, **kw)

    def put(self, method, action='PUT', **kw):
        self.call(method, action=action, **kw)

    def patch(self, method, action='PATCH', **kw):
        self.call(method, action=action, **kw)

    def delete(self, method, action='DELETE', **kw):
        self.call(method, action=action, **kw)

    def search(self, method, action='SEARCH', **kw):
        self.call(method, action=action, **kw)

And here are the database definitions, using the MongoEngine library:

# coding=utf-8

from conf import dbname
from MongoEngine import *

class Session(Document):
    id = StringField()
    data = DictField()
    accessed = DateTimeField()

    meta = {'indexes': [{'fields': ['accessed'], 'expireAfterSeconds': 3600, 'unique': False},
                        ],
            }

connect(dbname)

The tools module is a simple collection of functions that helps to
remove duplicate code and centralise dependencies:

# coding=utf-8

import base64

import cherrypy

from Crypto.Cipher import AES

from conf import securekey
assert securekey, 'Required security settings missing in conf.py.'

log = cherrypy.log
start = cherrypy.quickstart


def encrypt(cleartext, key=securekey):
    secret = AES.new(key[:32])
    paddedtext = (str(cleartext) + (AES.block_size - len(str(cleartext)) % AES.block_size) * '\0')
    ciphertext = base64.b64encode(secret.encrypt(paddedtext))

    return ciphertext


def decrypt(ciphertext, key=securekey):
    secret = AES.new(key[:32])
    decrypted = secret.decrypt(base64.b64decode(ciphertext))
    cleartext = decrypted.rstrip('\0')
    return cleartext


def getcookie(name):
    return cherrypy.response.cookie[name]


def setcookie(name, value):
    cherrypy.response.cookie[name] = value


def expirecookie(name, ttl):
    cherrypy.response.cookie[name]['expires'] = ttl