Commit 5293b708 authored by Sybren A. Stüvel's avatar Sybren A. Stüvel

Further stuff

parent bc637952
......@@ -3,7 +3,9 @@ package bunqapi
import (
"bytes"
"crypto/rsa"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
......@@ -15,6 +17,8 @@ type Client struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
httpClient *http.Client
}
func NewClient(creds Credentials) *Client {
......@@ -28,12 +32,30 @@ func NewClient(creds Credentials) *Client {
return &Client{
creds: creds,
privateKey: privateKey,
publicKey: publicRSAKey,
publicKey: publicRSAKey,
httpClient: &http.Client{
Timeout: 15 * time.Minute,
},
}
}
func (c *Client) NewRequest(method, endpoint string, body []byte) *http.Request {
func (c *Client) NewRequest(method, endpoint string, payload interface{}) *http.Request {
url := c.creds.Endpoint(endpoint)
var body []byte
var err error
if payload != nil {
body, err = json.Marshal(payload)
if err != nil {
log.WithFields(logrus.Fields{
"url": url,
"payload": payload,
logrus.ErrorKey: err,
}).Panic("unable to marshal payload as JSON")
}
fmt.Printf("\n%s\n\n", string(body))
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
log.WithFields(logrus.Fields{
......@@ -42,10 +64,11 @@ func (c *Client) NewRequest(method, endpoint string, body []byte) *http.Request
}).Panic("unable to create HTTP request")
}
req.Header.Set(headerAccept, headerAcceptJSON)
req.Header.Set(headerCacheControl, headerCacheControlNone)
req.Header.Set(headerUserAgent, headerUserAgentAPI)
req.Header.Set(headerXBunqGeolocation, headerGeolocationZero)
req.Header.Set(headerXBunqLanguage, headerLanguageEnglish)
req.Header.Set(headerXBunqLanguage, headerLanguageNederlands)
req.Header.Set(headerXBunqRegion, headerRegionNL)
req.Header.Set(headerXBunqClientRequestID, time.Now().String())
......@@ -55,3 +78,56 @@ func (c *Client) NewRequest(method, endpoint string, body []byte) *http.Request
return req
}
func (c *Client) DoRequest(method, endpoint string, payload interface{}, response interface{}) *ErrorResponse {
logger := log.WithFields(logrus.Fields{
"method": method,
"endpoint": endpoint,
})
req := c.NewRequest(method, endpoint, payload)
logger = logger.WithField("url", req.URL)
c.SignRequest(req)
logger.Debug("performing request")
resp, err := c.httpClient.Do(req)
if err != nil {
logger.WithError(err).Fatal("unable to perform HTTP call")
}
// TODO: verify server signature
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.WithError(err).Fatal("unable to read HTTP body")
}
contentType := resp.Header.Get(headerContentType)
if contentType != "application/json" {
fields := logrus.Fields{
"contentType": contentType,
"body": string(respBody),
}
for headerKey := range resp.Header {
fields["http-"+headerKey] = resp.Header.Get(headerKey)
}
logger.WithFields(fields).Fatal("invalid content type received")
}
logger = logger.WithField("status", resp.StatusCode)
if resp.StatusCode != 200 {
errorResponse := ErrorResponse{}
if err := json.Unmarshal(respBody, &errorResponse); err != nil {
logger.WithFields(logrus.Fields{
logrus.ErrorKey: err,
"body": string(respBody),
}).Fatal("unable to parse error response JSON")
}
return &errorResponse
}
if err := json.Unmarshal(respBody, response); err != nil {
logger.WithError(err).Fatal("unable to parse response JSON")
}
return nil
}
......@@ -4,7 +4,8 @@ const libraryVersion = "0.0.1"
// URLs of the bunq API.
const (
urlSandbox = "https://public-api.sandbox.bunq.com/v1/"
// urlSandbox = "https://public-api.sandbox.bunq.com/v1/"
urlSandbox = "http://localhost:4444/v1/"
urlProduction = "https://api.bunq.com/v1/"
)
......@@ -17,6 +18,7 @@ const (
// HTTP headers
const (
headerAccept = "Accept"
headerCacheControl = "Cache-Control"
headerContentType = "Content-Type"
headerUserAgent = "User-Agent"
......@@ -32,9 +34,11 @@ const (
// Default header values
const (
headerCacheControlNone = "no-cache"
headerGeolocationZero = "0 0 0 0 NL"
headerLanguageEnglish = "en_GB"
headerRegionNL = "nl_NL"
headerUserAgentAPI = "bunqapi-golang/" + libraryVersion
headerAcceptJSON = "application/json"
headerCacheControlNone = "no-cache"
headerGeolocationZero = "0 0 0 0 NL"
headerLanguageEnglish = "en_GB"
headerLanguageNederlands = "nl_NL"
headerRegionNL = "nl_NL"
headerUserAgentAPI = "bunqapi-golang/" + libraryVersion
)
package bunqapi
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
)
type installationRequest struct {
ClientPublicKey string `json:"client_public_key"`
}
type installationResponse struct {
ID BunqID `json:"Id"`
Token struct {
ID int `json:"id"`
Created string `json:"created"`
Updated string `json:"updated"`
Token string `json:"token"`
} `json:"Token"`
}
func publicKeyString(privateKey *rsa.PrivateKey) string {
if privateKey == nil {
log.Panic("private key cannot be nil")
}
pubKeyDer, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
log.WithError(err).Fatal("error serializing public key to DER-encoded PKIX format")
}
pubKeyPemBlock := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: pubKeyDer,
}
pubKeyPem := pem.EncodeToMemory(&pubKeyPemBlock)
return string(pubKeyPem)
}
func (c *Client) PostInstallation() {
payload := installationRequest{
ClientPublicKey: publicKeyString(c.privateKey),
}
response := installationResponse{}
errResp := c.DoRequest("POST", "installation", &payload, &response)
if errResp != nil {
log.WithFields(errResp.LogFields()).Fatal("error performing installation request")
}
}
package bunqapi
import "github.com/sirupsen/logrus"
type BunqID struct {
ID string `json:"id"`
}
type ErrorResponse struct {
Error struct {
Description string `json:"error_description"`
Translated string `json:"error_description_translated"`
} `json:"Error"`
}
func (er *ErrorResponse) LogFields() logrus.Fields {
return logrus.Fields{
"errorDescription": er.Error.Description,
"errorTranslated": er.Error.Translated,
}
}
......@@ -20,8 +20,8 @@ func main() {
logrus.WithField("apiMode", creds.APIMode).Info("loaded credentials")
client := bunqapi.NewClient(creds)
// client.PostInstallation()
client.PostInstallation()
req := client.NewRequest("POST", "user", nil)
client.SignRequest(req)
// req := client.NewRequest("POST", "user", nil)
// client.SignRequest(req)
}
......@@ -3,6 +3,7 @@ package bunqapi
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"fmt"
"io"
......@@ -17,6 +18,7 @@ import (
var headersToSign = map[string]bool{
headerCacheControl: true,
headerUserAgent: true,
headerAccept: true,
}
func sortedHeaderKeys(r *http.Request) []string {
......@@ -29,6 +31,10 @@ func sortedHeaderKeys(r *http.Request) []string {
}
func (c *Client) SignRequest(r *http.Request) error {
if r.Header.Get(headerXBunqClientSignature) != "" {
log.Panic("this request has already been signed")
}
hasher := sha256.New()
hash := func(value string) {
log.WithField("value", value).Debug("writing to hasher")
......@@ -47,17 +53,29 @@ func (c *Client) SignRequest(r *http.Request) error {
}
}
hash("\n")
io.Copy(hasher, r.Body)
if seeker, ok := r.Body.(io.Seeker); ok {
seeker.Seek(0, io.SeekStart)
}
sum := hasher.Sum(nil)
signature, err := c.privateKey.Sign(rand.Reader, sum, crypto.SHA256)
signature, err := rsa.SignPKCS1v15(rand.Reader, c.privateKey, crypto.SHA256, sum)
if err != nil {
log.WithError(err).Fatal("unable to sign with private RSA key")
}
// Verify the signature just to be sure ;-)
if err := rsa.VerifyPKCS1v15(c.publicKey, crypto.SHA256, sum, signature); err != nil {
log.WithError(err).Fatal("unable to verify signature with public RSA key")
}
log.WithFields(logrus.Fields{
"shaSum": fmt.Sprintf("%x", sum),
"shaSum": fmt.Sprintf("%x", sum),
"signature": fmt.Sprintf("%x", signature),
}).Debug("calculated signature")
r.Header.Set(headerXBunqClientSignature, fmt.Sprintf("%x", signature))
return nil
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment