简体   繁体   中英

How do I sign the body of a requests.Request in an Auth object's __call__ method?

I'm trying to write a nice auth helper for kraken. I want it to be as automatic as possible, so it needs to:

  1. add a nonce ( time.time()*1000 ) to the POST body
  2. calculate a signature over the POST body
  3. put the signature into the headers

I wrote the obvious code based on this answer:

class KrakenAuth(AuthBase):                                                                                                                                         
    """a requests-module-compatible auth module for kraken.com"""                                                                                                                  
    def __init__(self, key, secret):                                                                                                                                
        self.api_key    = key                                                                                                                                       
        self.secret_key = secret                                                                                                                                    

    def __call__(self, request):                                                                                                                                    
        #print("Auth got a %r" % type(request))                                                                                                                      
        nonce = int(1000*time.time())                                                                                                                               
        request.data = getattr(request, 'data', {})                                                                                                                 
        request.data['nonce'] = nonce                                                                                                                               
        request.prepare()                                                                                                                                           

        message = request.path_url + hashlib.sha256(str(nonce) + request.body).digest()                                                                             
        hmac_key = base64.b64decode(self.secret_key)                                                                                                                
        signature = hmac.new(hmac_key, message, hashlib.sha512).digest()                                                                                            
        signature = base64.b64encode(signature)                                                                                                                     

        request.headers.update({                                                                                                                                    
            'API-Key': self.api_key,                                                                                                                                
            'API-Sign': signature                                                                                                                                   
        })                                                                                                                                                          
        return request                                         

and them I'm calling it (from a wrapper method on another object) like:

def _request(self, method, url, **kwargs):
    if not self._auth:
        self._auth = KrakenAuth(key, secret)
    if 'auth' not in kwargs:
        kwargs['auth'] = self._auth
    return self._session.request(method, URL + url, **kwargs)                                                                                             

...but it doesn't work. The commented-out print() statement shows that it's getting a PreparedRequest object not a Request object, and thus the call to request.prepare() is a call to PreparedRequest.prepare does nothing useful because there's no request.data because it's already been converted into a body attribute.

You can't access the data attribute of the request, because the authentication object is applied to a requests.PreparedRequest() instance , which has no .data attribute .

The normal flow for a Session.request() call (used by all the request.<method> and session.<method> calls), is as follows:

  • A Request() instance is created with all the same arguments as the original call
  • The request is passed to Session.prepare_request() , which merges in session-stored base values with the arguments of the original call first, then
  • A PreparedRequest() instance is created
  • The PreparedRequest.prepare() method is called on that prepared request instance, passing in the merged data from the Request instance and the session.
  • The prepare() method calls the various self.prepare_* methods, including PreparedRequest.prepare_auth() .
  • PreparedRequest.prepare_auth() calls auth(self) to give the authentication object a chance to attach information to the request.

Unfortunately for you, at no point during this flow will the original data mapping be available to anyone else but PreparedRequest.prepare() and PreparedRequest.prepare_body() , and in those methods the mapping is a local variable. You can't access it from the authentication object.

Your options are then:

  • To decode the body again, and call prepare_body() with the updated mapping.

  • To not use an authentication object, but use the other path from my answer; to explicitly create a prepared request and manipulate data first.

  • To play merry hell with the Python stack and extract locals from the prepare() method that is two frames up. I really can't recommend this path.

To keep the authentication method encapsulated nicely, I'd go with decoding / re-encoding; the latter is simple enough by reusing PreparedRequest.prepare_body() :

import base64
import hashlib
import hmac
import time
try:
    # Python 3
    from urllib.parse import parse_qs
except ImportError:
    # Python 2
    from urlparse import parse_qs

from requests import AuthBase

URL_ENCODED = 'application/x-www-form-urlencoded'


class KrakenAuth(AuthBase):
    """a requests-module-compatible auth module for kraken.com"""
    def __init__(self, key, secret):
        self.api_key    = key
        self.secret_key = secret

    def __call__(self, request):
        ctheader = request.headers.get('Content-Type')
        assert (
            request.method == 'POST' and (
                ctheader == URL_ENCODED or
                requests.headers.get('Content-Length') == '0'
            )
        ), "Must be a POST request using form data, or empty"

        # insert the nonce in the encoded body
        data = parse_qs(request.body)
        data['nonce'] = nonce
        request.prepare_body(data, None, None)

        body = request.body
        if not isinstance(body, bytes):   # Python 3
            body = body.encode('latin1')  # standard encoding for HTTP

        message = request.path_url + hashlib.sha256(b'%s%s' % (nonce, body)).digest()
        hmac_key = base64.b64decode(self.secret_key)
        signature = hmac.new(hmac_key, message, hashlib.sha512).digest()
        signature = base64.b64encode(signature)

        request.headers.update({
            'API-Key': self.api_key,
            'API-Sign': signature
        })
        return request

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM