Skip to content

Commit

Permalink
Initial prebid-cache commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
dbemiller committed Sep 15, 2017
0 parents commit 264d680
Show file tree
Hide file tree
Showing 16 changed files with 1,414 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vendor
prebid-cache
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# FROM golang:onbuild
FROM ubuntu:16.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y

RUN apt-get install --assume-yes apt-utils
RUN apt-get install -y ca-certificates

ADD ./prebid-cache /app/prebid-cache
ADD ./config.yaml /app/

WORKDIR /app
CMD ["./prebid-cache"]
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
all:
@echo ""
@echo " init: Install glide dependencies. Assumes go and glide are installed already."
@echo " test: Run the unit tests and code style validation"
@echo " build: Build a linux binary which runs prebid-cache"
@echo " image: Build a docker which runs prebid-cache"
@echo ""

.PHONY: init test build image

init:
glide install

# Validates the code for style and unit tests
test:
./validate.sh --nofmt

# Run the tests and make a linux binary for the app. For details about this strategy,
# see https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
build: test
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .

# Build a docker image which runs the binary
image: build
docker build -t prebid-cache .

105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Prebid Cache

This application stores short-term data for use in Prebid.

It exists to support Video Ads from Prebid.js, as well as prebid-native

## API

### POST /cache

Adds one or more values to the cache. Values can be given as either JSON or XML. A sample request is below.

```json
{
"puts": [
{
"type": "xml",
"value": "<tag>Your XML content goes here.</tag>"
},
{
"type": "json",
"value": [1, true, "JSON value of any type can go here."]
}
]
}
```

If any of the `puts` are invalid, then it responds with a **400** none of the values will be retrievable.
Assuming that all of the values are well-formed, then the server will respond with IDs which can be used to
fetch the values later.

```json
{
"responses": [
{"uuid": "279971e4-70f0-4b18-bd65-5c6e7aa75d40"},
{"uuid": "147c9934-894b-4c1f-9a32-e7bb9cd15376"}
]
}
```


### GET /cache?uuid={id}

Retrieves a single value from the cache. If the `id` isn't recognized, then it will return a 404.

Assuming the above POST calls have been made, here are some sample GET responses.

---

**GET** */cache?uuid=279971e4-70f0-4b18-bd65-5c6e7aa75d40*

```
HTTP/1.1 200 OK
Content-Type: application/xml
<tag>Your XML content goes here.</tag>
```

---

**GET** */cache?uuid=147c9934-894b-4c1f-9a32-e7bb9cd15376*

```
HTTP/1.1 200 OK
Content-Type: application/json
[1, true, "JSON value of any type can go here."]
```

### Limitations

This section does not describe permanent API contracts; it just describes limitations on the current implementation.

- This application does *not* validate XML. If users `POST` malformed XML, they'll `GET` a bad response too.
- No more than 10 values are allowed in a single POST request
- Each cached value must be less than 10 KB

## Development

### Prerequisites

[Golang](https://golang.org/doc/install) and [Glide](https://github.com/Masterminds/glide#install) must be installed on your system.

### Automated tests

`./validate.sh` runs the unit tests and reformats your code with [gofmt](https://golang.org/cmd/gofmt/).
`./validate.sh --nofmt` runs the unit tests, but will _not_ reformat your code.

### Manual testing

Run `prebid-cache` locally with:

```bash
go build .
./prebid-cache
```

The service will respond to requests on `localhost:2424`, and the admin data will be available on `localhost:2525`

### Profiling

[pprof stats](http://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/) can be accessed from a running app on `localhost:2525`

## Todo
- Authorization (token based)
214 changes: 214 additions & 0 deletions azure_table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package main

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"

"context"
"crypto/tls"
log "github.com/Sirupsen/logrus"
"golang.org/x/net/context/ctxhttp"
"net/http/httptrace"
)

type AzureValue struct {
ID string `json:"id"`
Value string `json:"value"`
}

type AzureTableBackend struct {
Client *http.Client
Account string
Key string
URI string
}

func NewAzureBackend(account string, key string) *AzureTableBackend {

log.Debugf("New Azure Backend: Account %s Key %s", account, key)
tr := &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 60 * time.Second,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
}

c := &AzureTableBackend{
Account: account,
Key: key,
Client: &http.Client{
//TODO add to configMap
Transport: tr,
},
URI: fmt.Sprintf("https://%s.documents.azure.com", account),
}

log.Info("New Azure Client", account)

return c
}

func (c *AzureTableBackend) signReq(verb, resourceType, resourceLink, date string) string {

strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n\n",
strings.ToLower(verb),
resourceType,
resourceLink,
strings.ToLower(date),
)

decodedKey, _ := base64.StdEncoding.DecodeString(c.Key)
sha256 := hmac.New(sha256.New, []byte(decodedKey))
sha256.Write([]byte(strToSign))

signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
u := url.QueryEscape(fmt.Sprintf("type=master&ver=1.0&sig=%s", signature))

return u
}

func formattedRequestTime() string {
t := time.Now().UTC()
return t.Format("Mon, 02 Jan 2006 15:04:05 GMT")
}

func (c *AzureTableBackend) Send(ctx context.Context, req *http.Request, resourceType string, resourceId string) (*http.Response, error) {
date := formattedRequestTime()
req.Header.Add("x-ms-date", date)
req.Header.Add("x-ms-version", "2017-01-19")
req.Header.Add("Authorization", c.signReq(req.Method, resourceType, resourceId, date))

ctx = httptrace.WithClientTrace(ctx, newHttpTracer())

resp, err := ctxhttp.Do(ctx, c.Client, req)
if err != nil {
return nil, err
}

return resp, err
}

func (c *AzureTableBackend) Do(ctx context.Context, method string, resourceLink string, resourceType string, resourceId string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.URI, resourceLink), body)
if err != nil {
return nil, err
}
return c.Send(ctx, req, resourceType, resourceId)
}

func (c *AzureTableBackend) Get(ctx context.Context, key string) (string, error) {

if key == "" {
return "", fmt.Errorf("Invalid Key")
}

// Full key for the stupid gets
resourceLink := fmt.Sprintf("/dbs/prebidcache/colls/cache/docs/%s", key)
resp, err := c.Do(ctx, "GET", resourceLink, "docs", resourceLink[1:], nil)
if err != nil {
log.Debugf("Failed to make request")
return "", err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Debugf("Failed to read the request body")
return "", err
}

av := AzureValue{}
err = json.Unmarshal(body, &av)
if err != nil {
log.Debugf("Failed to decode request body into JSON")
return "", err
}

if av.Value == "" {
return "", fmt.Errorf("Key not found")
}

return av.Value, nil
}

func (c *AzureTableBackend) Put(ctx context.Context, key string, value string) error {

if key == "" {
return fmt.Errorf("Invalid Key")
}

if value == "" {
return fmt.Errorf("Invalid Value")
}

av := AzureValue{
ID: key,
Value: value,
}

b, err := json.Marshal(&av)
if err != nil {
return err
}

resourceLink := "/dbs/prebidcache/colls/cache/docs"
resp, err := c.Do(ctx, "POST", resourceLink, "docs", "dbs/prebidcache/colls/cache", bytes.NewBuffer(b))
if err != nil {
return err
}
defer resp.Body.Close()

// Read the whole body so that the Transport knows it's safe to reuse the connection.
// See the docs on http.Response.Body
ioutil.ReadAll(resp.Body)
return nil
}

func newHttpTracer() *httptrace.ClientTrace {
return &httptrace.ClientTrace{
PutIdleConn: func(err error) {
if err != nil {
log.Infof("Failed adding idle connection to the pool: %v", err.Error())
}
},

ConnectDone: func(network, addr string, err error) {
if err != nil {
log.Warnf("Failed to connect. Network: %s, Addr: %s, err: %v", network, addr, err)
}
},

DNSDone: func(info httptrace.DNSDoneInfo) {
if info.Err != nil {
log.Warnf("Failed DNS lookup: %v", info.Err)
}
},

TLSHandshakeDone: func(state tls.ConnectionState, err error) {
if err != nil {
log.Warnf("Failed TLS Handshake: %v", err)
}
},

WroteRequest: func(info httptrace.WroteRequestInfo) {
if info.Err != nil {
log.Warnf("Failed to write request: %v", info.Err)
}
},
}
}
Loading

0 comments on commit 264d680

Please sign in to comment.