Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The proxy relies on the following environment variables:
* `PROXY_PORT` -- the port the proxy listens on
* `BACKEND_PORT` -- the port of the backend application that requests are forwarded to
* `PAYMENT_POINTER` -- an [Interledger Payment Pointer](https://paymentpointers.org/) string
* `RECEIPT_SUBMISSION_URL` -- (optional) URL at which to POST [STREAM receipts](https://interledger.org/rfcs/0039-stream-receipts/) for received payment

Reference the [example Deployment](hack/example-deployment.yaml) to see how you might configure these in Kubernetes.

Expand Down
16 changes: 10 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
)

type Config struct {
ProxyPort int
BackendPort int
PaymentPointer string
ProxyPort int
BackendPort int
PaymentPointer string
ReceiptSubmissionUrl string
}

func Load() (*Config, error) {
Expand Down Expand Up @@ -42,10 +43,13 @@ func Load() (*Config, error) {
return nil, fmt.Errorf("PAYMENT_POINTER is required")
}

receiptSubmissionUrl := os.Getenv("RECEIPT_SUBMISSION_URL")

c := &Config{
ProxyPort: proxyPort,
BackendPort: backendPort,
PaymentPointer: paymentPointer,
ProxyPort: proxyPort,
BackendPort: backendPort,
PaymentPointer: paymentPointer,
ReceiptSubmissionUrl: receiptSubmissionUrl,
}

return c, nil
Expand Down
26 changes: 26 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func TestConfig(t *testing.T) {
if err != nil {
t.Error(err)
}
err = os.Setenv("RECEIPT_SUBMISSION_URL", "https://verifier.com/balances/123:creditReceipt")
if err != nil {
t.Error(err)
}

})

Expand All @@ -47,6 +51,10 @@ func TestConfig(t *testing.T) {
if cfg.PaymentPointer != "$wallet.example.com/🤑" {
t.Errorf("Expected PaymentPointer '%s' to match '$wallet.example.com/🤑'", cfg.PaymentPointer)
}

if cfg.ReceiptSubmissionUrl != "https://verifier.com/balances/123:creditReceipt" {
t.Errorf("Expected ReceiptSubmissionUrl '%s' to match 'https://verifier.com/balances/123:creditReceipt'", cfg.ReceiptSubmissionUrl)
}
})

when("PROXY_PORT is not provided", func() {
Expand Down Expand Up @@ -160,5 +168,23 @@ func TestConfig(t *testing.T) {
}
})
})

when("RECEIPT_SUBMISSION_URL is not provided", func() {
it("uses empty string", func() {
err := os.Unsetenv("RECEIPT_SUBMISSION_URL")
if err != nil {
t.Error(err)
}

cfg, err = config.Load()
if err != nil {
t.Error(err)
}

if cfg.ReceiptSubmissionUrl != "" {
t.Errorf("Expected ReceiptSubmissionUrl '%s' to match \"\"", cfg.ReceiptSubmissionUrl)
}
})
})
})
}
2 changes: 2 additions & 0 deletions hack/example-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ spec:
value: "2368" # default ghost port
- name: PAYMENT_POINTER
value: "$wallet.example.com/your-wallet-here"
- name: RECEIPT_SUBMISSION_URL
value: "https://receipt-verifier.com/balances/id:creditReceipt"
# Example backend app
- image: ghost # https://hub.docker.com/_/ghost
name: ghost-blog
Expand Down
54 changes: 45 additions & 9 deletions handlers/proxy_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,28 @@ import (
)

type ProxyHandler struct {
BackendPort int
PaymentPointer string
BackendPort int
PaymentPointer string
ReceiptSubmitter string
}

func NewProxyHandler(backendPort int, paymentPointer string, receiptSubmissionUrl string) *ProxyHandler {
var receiptSubmitter string
if receiptSubmissionUrl != "" {
receiptSubmitter = fmt.Sprintf(`document.monetization&&document.monetization.addEventListener("monetizationprogress",e=>{const{receipt:t}=e.detail;null!==t&&fetch("%s",{method:"POST",body:t})});`, receiptSubmissionUrl)
}
return &ProxyHandler{
BackendPort: backendPort,
PaymentPointer: paymentPointer,
ReceiptSubmitter: receiptSubmitter,
}
}

func (h *ProxyHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
url, _ := url.Parse(fmt.Sprintf("http://0.0.0.0:%d", h.BackendPort))

proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ModifyResponse = BuildMonetizationResponseModifier(h.PaymentPointer)
proxy.ModifyResponse = BuildMonetizationResponseModifier(h.PaymentPointer, h.ReceiptSubmitter)

req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
Expand All @@ -37,7 +50,7 @@ func (h *ProxyHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
proxy.ServeHTTP(resp, req)
}

func BuildMonetizationResponseModifier(paymentPointer string) func(*http.Response) error {
func BuildMonetizationResponseModifier(paymentPointer string, receiptSubmitter string) func(*http.Response) error {
return func(r *http.Response) error {
if !contentTypeIsHTML(r) {
log.Println("Content-Type is not HTML")
Expand All @@ -51,7 +64,7 @@ func BuildMonetizationResponseModifier(paymentPointer string) func(*http.Respons
}
defer r.Body.Close()

insertMonetizationMeta(doc, paymentPointer)
insertInHead(doc, paymentPointer, receiptSubmitter)
buf := bytes.NewBuffer([]byte{})
html.Render(buf, doc)

Expand All @@ -61,6 +74,21 @@ func BuildMonetizationResponseModifier(paymentPointer string) func(*http.Respons
}
}

func insertInHead(n *html.Node, paymentPointer string, receiptSubmitter string) {
if n.Type == html.ElementNode && n.Data == "head" {
insertMonetizationMeta(n, paymentPointer)
if receiptSubmitter != "" {
insertReceiptSubmitter(n, receiptSubmitter)
}

return
}

for c := n.FirstChild; c != nil; c = c.NextSibling {
insertInHead(c, paymentPointer, receiptSubmitter)
}
}

func insertMonetizationMeta(n *html.Node, paymentPointer string) {
if n.Type == html.ElementNode && n.Data == "head" {
n.AppendChild(&html.Node{
Expand All @@ -71,12 +99,20 @@ func insertMonetizationMeta(n *html.Node, paymentPointer string) {
{Key: "content", Val: paymentPointer},
},
})

return
}
}

for c := n.FirstChild; c != nil; c = c.NextSibling {
insertMonetizationMeta(c, paymentPointer)
func insertReceiptSubmitter(n *html.Node, receiptSubmitter string) {
if n.Type == html.ElementNode && n.Data == "head" {
script := html.Node{
Type: html.ElementNode,
Data: "script",
}
script.AppendChild(&html.Node{
Type: html.TextNode,
Data: receiptSubmitter,
})
n.AppendChild(&script)
}
}

Expand Down
86 changes: 72 additions & 14 deletions handlers/proxy_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,36 @@ import (
func TestAddWebMonetizationMeta(t *testing.T) {
spec.Run(t, "TestBuildMonetizationResponseModifier", func(t *testing.T, when spec.G, it spec.S) {
var response *http.Response
var proxyHandler handlers.ProxyHandler
var addWebMonetizationMetaFunc func(*http.Response) error
var proxyHandler *handlers.ProxyHandler
var proxyScriptHandler *handlers.ProxyHandler
var addWmMetaFunc func(*http.Response) error
var addWmMetaAndScriptFunc func(*http.Response) error
expectedScript :=
`<script>document.monetization&&document.monetization.addEventListener("monetizationprogress",e=>{const{receipt:t}=e.detail;null!==t&&fetch("https://verifier.com/balances/123:creditReceipt",{method:"POST",body:t})});</script>`

it.Before(func() {
proxyHandler = handlers.ProxyHandler{
BackendPort: 1337,
PaymentPointer: "$wallet.example.com/🤑",
}
addWebMonetizationMetaFunc = handlers.BuildMonetizationResponseModifier(proxyHandler.PaymentPointer)
proxyHandler = handlers.NewProxyHandler(1337, "$wallet.example.com/🤑", "")
addWmMetaFunc = handlers.BuildMonetizationResponseModifier(proxyHandler.PaymentPointer, proxyHandler.ReceiptSubmitter)
proxyScriptHandler = handlers.NewProxyHandler(1337, "$wallet.example.com/🤑", "https://verifier.com/balances/123:creditReceipt")
addWmMetaAndScriptFunc = handlers.BuildMonetizationResponseModifier(proxyScriptHandler.PaymentPointer, proxyScriptHandler.ReceiptSubmitter)
})

when("the response is not HTML", func() {
bodyString := "console.log('hello world')"
it.Before(func() {
response = &http.Response{
StatusCode: 200,
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: ioutil.NopCloser(bytes.NewBufferString("console.log('hello world')")),
Body: ioutil.NopCloser(bytes.NewBufferString(bodyString)),
}

response.Header.Set("Content-Type", mime.TypeByExtension(".js"))
})

it("does not modify the response", func() {
if err := addWebMonetizationMetaFunc(response); err != nil {
if err := addWmMetaFunc(response); err != nil {
t.Error(err)
}

Expand All @@ -50,9 +54,23 @@ func TestAddWebMonetizationMeta(t *testing.T) {
t.Error(err)
}

bodyString := string(bodyBytes)
if strings.Contains(bodyString, "<meta name=\"monetization\" content=\"$wallet.example.com/🤑\"/>") {
t.Error(fmt.Sprintf("<meta> tag was added: %s", bodyString))
if string(bodyBytes) != bodyString {
t.Error(fmt.Sprintf("response was modified: %s", bodyString))
}
})

it("does not modify the response with a script", func() {
if err := addWmMetaAndScriptFunc(response); err != nil {
t.Error(err)
}

bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
t.Error(err)
}

if string(bodyBytes) != bodyString {
t.Error(fmt.Sprintf("response was modified: %s", bodyString))
}
})
})
Expand All @@ -72,7 +90,23 @@ func TestAddWebMonetizationMeta(t *testing.T) {
})

it("adds the monetization <meta> tag", func() {
if err := addWebMonetizationMetaFunc(response); err != nil {
if err := addWmMetaFunc(response); err != nil {
t.Error(err)
}

bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
t.Error(err)
}

bodyString := string(bodyBytes)
if !strings.Contains(bodyString, "<head><meta name=\"monetization\" content=\"$wallet.example.com/🤑\"/></head>") {
t.Error(fmt.Sprintf("<meta> tag not added: %s", bodyString))
}
})

it("adds the monetization <meta> tag and receipt submission <script>", func() {
if err := addWmMetaAndScriptFunc(response); err != nil {
t.Error(err)
}

Expand All @@ -85,6 +119,10 @@ func TestAddWebMonetizationMeta(t *testing.T) {
if !strings.Contains(bodyString, "<meta name=\"monetization\" content=\"$wallet.example.com/🤑\"/>") {
t.Error(fmt.Sprintf("<meta> tag not added: %s", bodyString))
}

if !strings.Contains(bodyString, expectedScript) {
t.Error(fmt.Sprintf("<script> tag not added: %s", bodyString))
}
})
})

Expand All @@ -102,7 +140,7 @@ func TestAddWebMonetizationMeta(t *testing.T) {
})

it("adds the monetization <meta> tag and <head> tag", func() {
if err := addWebMonetizationMetaFunc(response); err != nil {
if err := addWmMetaFunc(response); err != nil {
t.Error(err)
}

Expand All @@ -116,6 +154,26 @@ func TestAddWebMonetizationMeta(t *testing.T) {
t.Error(fmt.Sprintf("<meta> tag not added: %s", bodyString))
}
})

it("adds the monetization <meta>, receipt submission <script>, and <head> tags", func() {
if err := addWmMetaAndScriptFunc(response); err != nil {
t.Error(err)
}

bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
t.Error(err)
}

bodyString := string(bodyBytes)
if !strings.Contains(bodyString, "<meta name=\"monetization\" content=\"$wallet.example.com/🤑\"/>") {
t.Error(fmt.Sprintf("<meta> tag not added: %s", bodyString))
}

if !strings.Contains(bodyString, expectedScript) {
t.Error(fmt.Sprintf("<script> tag not added: %s", bodyString))
}
})
})
})
})
Expand Down
5 changes: 1 addition & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ func main() {
log.Fatal(err)
}

proxyHandler := &handlers.ProxyHandler{
BackendPort: cfg.BackendPort,
PaymentPointer: cfg.PaymentPointer,
}
proxyHandler := handlers.NewProxyHandler(cfg.BackendPort, cfg.PaymentPointer, cfg.ReceiptSubmissionUrl)

mux := http.NewServeMux()
mux.Handle("/", proxyHandler)
Expand Down