Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(BUX-497): Routes are hardcoded instead of initialized by configured capabilities #71

Merged
merged 8 commits into from
Jan 22, 2024
34 changes: 32 additions & 2 deletions examples/server/run_server/run_server.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package main

import (
"github.com/bitcoin-sv/go-paymail/logging"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/bitcoin-sv/go-paymail/logging"

"github.com/bitcoin-sv/go-paymail/server"
"github.com/julienschmidt/httprouter"
)

func main() {
Expand All @@ -19,18 +24,43 @@ func main() {
config, err := server.NewConfig(
new(demoServiceProvider),
server.WithBasicRoutes(),
server.WithDomain("localhost"), // todo: make this work locally?
server.WithDomain("localhost"),
server.WithDomain("another.com"),
server.WithDomain("test.com"),
server.WithGenericCapabilities(),
server.WithPort(3000),
server.WithServiceName("BsvAliasCustom"),
server.WithTimeout(15*time.Second),
server.WithCapabilities(customCapabilities()),
)
config.Prefix = "http://" //normally paymail requires https, but for demo purposes we'll use http
if err != nil {
logger.Fatal().Msg(err.Error())
}

// Create & start the server
server.StartServer(server.CreateServer(config), config.Logger)
}

func customCapabilities() map[string]any {
exampleBrfcKey := "406cef0ae2d6"
return map[string]any{
"custom_static_boolean": false,
"custom_static_int": 10,
exampleBrfcKey: true,
"custom_callable_cap": server.CallableCapability{
Path: fmt.Sprintf("/display_paymail/%s", server.PaymailAddressTemplate),
Method: http.MethodGet,
Handler: func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
incomingPaymail := p.ByName(server.PaymailAddressParamName)

response := map[string]string{
"paymail": incomingPaymail,
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
},
},
}
}
154 changes: 126 additions & 28 deletions server/capabilities.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,152 @@
package server

import (
"fmt"
"net/http"
"strings"

"github.com/bitcoin-sv/go-paymail"
"github.com/julienschmidt/httprouter"
)

// GenericCapabilities will make generic capabilities
func GenericCapabilities(bsvAliasVersion string, senderValidation bool) *paymail.CapabilitiesPayload {
return &paymail.CapabilitiesPayload{
BsvAlias: bsvAliasVersion,
Capabilities: map[string]interface{}{
paymail.BRFCPaymentDestination: "/address/{alias}@{domain.tld}",
paymail.BRFCPki: "/id/{alias}@{domain.tld}",
paymail.BRFCPublicProfile: "/public-profile/{alias}@{domain.tld}",
paymail.BRFCSenderValidation: senderValidation,
paymail.BRFCVerifyPublicKeyOwner: "/verify-pubkey/{alias}@{domain.tld}/{pubkey}",
type CallableCapability struct {
Path string
Method string
Handler httprouter.Handle
}

type CallableCapabilitiesMap map[string]CallableCapability
type StaticCapabilitiesMap map[string]any

func (c *Configuration) SetGenericCapabilities() {
_addCapabilities(c.callableCapabilities,
CallableCapabilitiesMap{
paymail.BRFCPaymentDestination: CallableCapability{
Path: fmt.Sprintf("/address/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.resolveAddress,
},
paymail.BRFCPki: CallableCapability{
Path: fmt.Sprintf("/id/%s", PaymailAddressTemplate),
Method: http.MethodGet,
Handler: c.showPKI,
},
paymail.BRFCPublicProfile: CallableCapability{
Path: fmt.Sprintf("/public-profile/%s", PaymailAddressTemplate),
Method: http.MethodGet,
Handler: c.publicProfile,
},
paymail.BRFCVerifyPublicKeyOwner: CallableCapability{
Path: fmt.Sprintf("/verify-pubkey/%s/%s", PaymailAddressTemplate, PubKeyTemplate),
Method: http.MethodGet,
Handler: c.verifyPubKey,
},
},
}
)
_addCapabilities(c.staticCapabilities,
StaticCapabilitiesMap{
paymail.BRFCSenderValidation: c.SenderValidationEnabled,
},
)
}

func (c *Configuration) SetP2PCapabilities() {
_addCapabilities(c.callableCapabilities,
CallableCapabilitiesMap{
paymail.BRFCP2PTransactions: CallableCapability{
Path: fmt.Sprintf("/receive-transaction/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.p2pReceiveTx,
},
paymail.BRFCP2PPaymentDestination: CallableCapability{
Path: fmt.Sprintf("/p2p-payment-destination/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.p2pDestination,
},
},
)
}

// P2PCapabilities will make generic capabilities & add additional p2p capabilities
func P2PCapabilities(bsvAliasVersion string, senderValidation bool) *paymail.CapabilitiesPayload {
c := GenericCapabilities(bsvAliasVersion, senderValidation)
c.Capabilities[paymail.BRFCP2PTransactions] = "/receive-transaction/{alias}@{domain.tld}"
c.Capabilities[paymail.BRFCP2PPaymentDestination] = "/p2p-payment-destination/{alias}@{domain.tld}"
return c
func (c *Configuration) SetBeefCapabilities() {
_addCapabilities(c.callableCapabilities,
CallableCapabilitiesMap{
paymail.BRFCBeefTransaction: CallableCapability{
Path: fmt.Sprintf("/beef/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.p2pReceiveBeefTx,
},
},
)
}

// BeefCapabilities will add beef capabilities to given ones
func BeefCapabilities(c *paymail.CapabilitiesPayload) *paymail.CapabilitiesPayload {
c.Capabilities[paymail.BRFCBeefTransaction] = "/beef/{alias}@{domain.tld}"
return c
func _addCapabilities[T any](base map[string]T, newCaps map[string]T) {
for key, val := range newCaps {
base[key] = val
}
}

// showCapabilities will return the service discovery results for the server
// and list all active capabilities of the Paymail server
//
// Specs: http://bsvalias.org/02-02-capability-discovery.html
func (c *Configuration) showCapabilities(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
// Check the domain (allowed, and used for capabilities response)
// todo: bake this into middleware? This is protecting the "req" domain name (like CORs)
domain := getHost(req)
if !c.IsAllowedDomain(domain) {
ErrorResponse(w, req, ErrorUnknownDomain, "domain unknown: "+domain, http.StatusBadRequest, c.Logger)
// Check the host (allowed, and used for capabilities response)
// todo: bake this into middleware? This is protecting the "req" host name (like CORs)
host := ""
if req.URL.IsAbs() || len(req.URL.Host) == 0 {
host = req.Host
} else {
host = req.URL.Host
}

if !c.IsAllowedDomain(host) {
ErrorResponse(w, req, ErrorUnknownDomain, "domain unknown: "+host, http.StatusBadRequest, c.Logger)
return
}

capabilities, err := c.EnrichCapabilities(host)
if err != nil {
ErrorResponse(w, req, ErrorEncodingResponse, err.Error(), http.StatusBadRequest, c.Logger)
return
}

// Set the service URL
capabilities := c.EnrichCapabilities(domain)
writeJsonResponse(w, req, c.Logger, capabilities)
}

// EnrichCapabilities will update the capabilities with the appropriate service url
func (c *Configuration) EnrichCapabilities(host string) (*paymail.CapabilitiesPayload, error) {
serviceUrl, err := generateServiceURL(c.Prefix, host, c.APIVersion, c.ServiceName)
if err != nil {
return nil, err
}
payload := &paymail.CapabilitiesPayload{
BsvAlias: c.BSVAliasVersion,
Capabilities: make(map[string]interface{}),
}
for key, cap := range c.staticCapabilities {
payload.Capabilities[key] = cap
}
for key, cap := range c.callableCapabilities {
payload.Capabilities[key] = serviceUrl + string(cap.Path)
}
return payload, nil
}

func generateServiceURL(prefix, domain, apiVersion, serviceName string) (string, error) {
if len(prefix) == 0 || len(domain) == 0 {
return "", ErrPrefixOrDomainMissing
}
strBuilder := new(strings.Builder)
strBuilder.WriteString(prefix)
strBuilder.WriteString(domain)
if len(apiVersion) > 0 {
strBuilder.WriteString("/")
strBuilder.WriteString(apiVersion)
}
if len(serviceName) > 0 {
strBuilder.WriteString("/")
strBuilder.WriteString(serviceName)
}

return strBuilder.String(), nil
}
64 changes: 25 additions & 39 deletions server/capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,48 @@ package server
import (
"testing"

"github.com/bitcoin-sv/go-paymail"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestGenericCapabilities will test the method GenericCapabilities()
func TestGenericCapabilities(t *testing.T) {
func TestGenerateServiceURL(t *testing.T) {
t.Parallel()

t.Run("valid values", func(t *testing.T) {
c := GenericCapabilities("test", true)
require.NotNil(t, c)
assert.Equal(t, "test", c.BsvAlias)
assert.Equal(t, 5, len(c.Capabilities))
u, err := generateServiceURL("https://", "test.com", "v1", "bsvalias")
assert.NoError(t, err)
assert.Equal(t, "https://test.com/v1/bsvalias", u)
})

t.Run("no alias version", func(t *testing.T) {
c := GenericCapabilities("", true)
require.NotNil(t, c)
assert.Equal(t, "", c.BsvAlias)
t.Run("all invalid values", func(t *testing.T) {
_, err := generateServiceURL("", "", "", "")
assert.Error(t, err)
})

t.Run("sender validation", func(t *testing.T) {
c := GenericCapabilities("", true)
require.NotNil(t, c)
assert.Equal(t, true, c.Capabilities[paymail.BRFCSenderValidation])
t.Run("missing prefix", func(t *testing.T) {
_, err := generateServiceURL("", "test.com", "v1", "")
assert.Error(t, err)
})
}

// TestP2PCapabilities will test the method P2PCapabilities()
func TestP2PCapabilities(t *testing.T) {
t.Parallel()

t.Run("valid values", func(t *testing.T) {
c := P2PCapabilities("test", true)
require.NotNil(t, c)
assert.Equal(t, "test", c.BsvAlias)
assert.Equal(t, 7, len(c.Capabilities))
t.Run("missing domain", func(t *testing.T) {
_, err := generateServiceURL("https://", "", "v1", "")
assert.Error(t, err)
})

t.Run("no alias version", func(t *testing.T) {
c := P2PCapabilities("", true)
require.NotNil(t, c)
assert.Equal(t, "", c.BsvAlias)
t.Run("no api version", func(t *testing.T) {
u, err := generateServiceURL("https://", "test", "", "bsvalias")
assert.NoError(t, err)
assert.Equal(t, "https://test/bsvalias", u)
})

t.Run("sender validation", func(t *testing.T) {
c := P2PCapabilities("", true)
require.NotNil(t, c)
assert.Equal(t, true, c.Capabilities[paymail.BRFCSenderValidation])
t.Run("no service name", func(t *testing.T) {
u, err := generateServiceURL("https://", "test", "v1", "")
assert.NoError(t, err)
assert.Equal(t, "https://test/v1", u)
})

t.Run("has p2p routes", func(t *testing.T) {
c := P2PCapabilities("", true)
require.NotNil(t, c)
assert.NotEmpty(t, c.Capabilities[paymail.BRFCP2PTransactions])
assert.NotEmpty(t, c.Capabilities[paymail.BRFCP2PPaymentDestination])
t.Run("service with explicit port", func(t *testing.T) {
u, err := generateServiceURL("https://", "test:1234", "v1", "bsvalias")
assert.NoError(t, err)
assert.Equal(t, "https://test:1234/v1/bsvalias", u)
})
}
Loading
Loading