go-waku/library/api.go

499 lines
13 KiB
Go

package main
/*
#include <stdlib.h>
#include <stddef.h>
*/
import "C"
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"time"
"unsafe"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/libp2p/go-libp2p-core/peer"
p2pproto "github.com/libp2p/go-libp2p-core/protocol"
"github.com/multiformats/go-multiaddr"
"github.com/status-im/go-waku/waku/v2/node"
"github.com/status-im/go-waku/waku/v2/protocol"
"github.com/status-im/go-waku/waku/v2/protocol/pb"
)
var wakuNode *node.WakuNode
var ErrWakuNodeNotReady = errors.New("go-waku not initialized")
func randomHex(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func main() {}
type WakuConfig struct {
Host *string `json:"host,omitempty"`
Port *int `json:"port,omitempty"`
AdvertiseAddress *string `json:"advertiseAddr,omitempty"`
NodeKey *string `json:"nodeKey,omitempty"`
KeepAliveInterval *int `json:"keepAliveInterval,omitempty"`
EnableRelay *bool `json:"relay"`
MinPeersToPublish *int `json:"minPeersToPublish"`
}
var DefaultHost = "0.0.0.0"
var DefaultPort = 60000
var DefaultKeepAliveInterval = 20
var DefaultEnableRelay = true
var DefaultMinPeersToPublish = 0
func getConfig(configJSON *C.char) (WakuConfig, error) {
var config WakuConfig
if configJSON != nil {
err := json.Unmarshal([]byte(C.GoString(configJSON)), &config)
if err != nil {
return WakuConfig{}, err
}
}
if config.Host == nil {
config.Host = &DefaultHost
}
if config.EnableRelay == nil {
config.EnableRelay = &DefaultEnableRelay
}
if config.Host == nil {
config.Host = &DefaultHost
}
if config.Port == nil {
config.Port = &DefaultPort
}
if config.KeepAliveInterval == nil {
config.KeepAliveInterval = &DefaultKeepAliveInterval
}
if config.MinPeersToPublish == nil {
config.MinPeersToPublish = &DefaultMinPeersToPublish
}
return config, nil
}
//export waku_new
// Initialize a waku node. Receives a JSON string containing the configuration
// for the node. It can be NULL. Example configuration:
// ```
// {"host": "0.0.0.0", "port": 60000, "advertiseAddr": "1.2.3.4", "nodeKey": "0x123...567", "keepAliveInterval": 20, "relay": true}
// ```
// All keys are optional. If not specified a default value will be set:
// - host: IP address. Default 0.0.0.0
// - port: TCP port to listen. Default 60000. Use 0 for random
// - advertiseAddr: External IP
// - nodeKey: secp256k1 private key. Default random
// - keepAliveInterval: interval in seconds to ping all peers
// - relay: Enable WakuRelay. Default `true`
// This function will return a nodeID which should be used in all calls from this API that require
// interacting with the node.
func waku_new(configJSON *C.char) *C.char {
if wakuNode != nil {
return makeJSONResponse(errors.New("go-waku already initialized. stop it first"))
}
config, err := getConfig(configJSON)
if err != nil {
return makeJSONResponse(err)
}
hostAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", *config.Host, *config.Port))
if err != nil {
return makeJSONResponse(err)
}
var prvKey *ecdsa.PrivateKey
if config.NodeKey != nil {
prvKey, err = crypto.HexToECDSA(*config.NodeKey)
if err != nil {
return makeJSONResponse(err)
}
} else {
key, err := randomHex(32)
if err != nil {
return makeJSONResponse(err)
}
prvKey, err = crypto.HexToECDSA(key)
if err != nil {
return makeJSONResponse(err)
}
}
opts := []node.WakuNodeOption{
node.WithPrivateKey(prvKey),
node.WithHostAddress(hostAddr),
node.WithKeepAlive(time.Duration(*config.KeepAliveInterval) * time.Second),
}
if *config.EnableRelay {
opts = append(opts, node.WithWakuRelayAndMinPeers(*config.MinPeersToPublish))
}
ctx := context.Background()
w, err := node.New(ctx, opts...)
if err != nil {
return makeJSONResponse(err)
}
wakuNode = w
return makeJSONResponse(nil)
}
//export waku_start
// Starts the waku node
func waku_start() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
if err := wakuNode.Start(); err != nil {
return makeJSONResponse(err)
}
return makeJSONResponse(nil)
}
//export waku_stop
// Stops a waku node
func waku_stop() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
wakuNode.Stop()
wakuNode = nil
return makeJSONResponse(nil)
}
//export waku_peerid
// Obtain the peer ID of the waku node
func waku_peerid() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
return prepareJSONResponse(wakuNode.ID(), nil)
}
//export waku_listen_addresses
// Obtain the multiaddresses the wakunode is listening to
func waku_listen_addresses() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
var addresses []string
for _, addr := range wakuNode.ListenAddresses() {
addresses = append(addresses, addr.String())
}
return prepareJSONResponse(addresses, nil)
}
//export waku_add_peer
// Add node multiaddress and protocol to the wakunode peerstore
func waku_add_peer(address *C.char, protocolID *C.char) *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
ma, err := multiaddr.NewMultiaddr(C.GoString(address))
if err != nil {
return makeJSONResponse(err)
}
peerID, err := wakuNode.AddPeer(ma, p2pproto.ID(C.GoString(protocolID)))
return prepareJSONResponse(peerID, err)
}
//export waku_connect
// Connect to peer at multiaddress. if ms > 0, cancel the function execution if it takes longer than N milliseconds
func waku_connect(address *C.char, ms C.int) *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
var ctx context.Context
var cancel context.CancelFunc
if ms > 0 {
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(int(ms))*time.Millisecond)
defer cancel()
} else {
ctx = context.Background()
}
err := wakuNode.DialPeer(ctx, C.GoString(address))
return makeJSONResponse(err)
}
//export waku_connect_peerid
// Connect to known peer by peerID. if ms > 0, cancel the function execution if it takes longer than N milliseconds
func waku_connect_peerid(peerID *C.char, ms C.int) *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
var ctx context.Context
var cancel context.CancelFunc
pID, err := peer.Decode(C.GoString(peerID))
if err != nil {
return makeJSONResponse(err)
}
if ms > 0 {
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(int(ms))*time.Millisecond)
defer cancel()
} else {
ctx = context.Background()
}
err = wakuNode.DialPeerByID(ctx, pID)
return makeJSONResponse(err)
}
//export waku_disconnect
// Close connection to a known peer by peerID
func waku_disconnect(peerID *C.char) *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
pID, err := peer.Decode(C.GoString(peerID))
if err != nil {
return makeJSONResponse(err)
}
err = wakuNode.ClosePeerById(pID)
return makeJSONResponse(err)
}
//export waku_peer_cnt
// Get number of connected peers
func waku_peer_cnt() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
return prepareJSONResponse(wakuNode.PeerCount(), nil)
}
//export waku_content_topic
// Create a content topic string according to RFC 23
func waku_content_topic(applicationName *C.char, applicationVersion C.uint, contentTopicName *C.char, encoding *C.char) *C.char {
return C.CString(protocol.NewContentTopic(C.GoString(applicationName), uint(applicationVersion), C.GoString(contentTopicName), C.GoString(encoding)).String())
}
//export waku_pubsub_topic
// Create a pubsub topic string according to RFC 23
func waku_pubsub_topic(name *C.char, encoding *C.char) *C.char {
return prepareJSONResponse(protocol.NewPubsubTopic(C.GoString(name), C.GoString(encoding)).String(), nil)
}
//export waku_default_pubsub_topic
// Get the default pubsub topic used in waku2: /waku/2/default-waku/proto
func waku_default_pubsub_topic() *C.char {
return C.CString(protocol.DefaultPubsubTopic().String())
}
func getTopic(topic *C.char) string {
result := ""
if topic != nil {
result = C.GoString(topic)
} else {
result = protocol.DefaultPubsubTopic().String()
}
return result
}
//export waku_set_event_callback
// Register callback to act as signal handler and receive application signal
// (in JSON) which are used o react to asyncronous events in waku. The function
// signature for the callback should be `void myCallback(char* signalJSON)`
func waku_set_event_callback(cb unsafe.Pointer) {
setEventCallback(cb)
}
type SubscriptionMsg struct {
MessageID string `json:"messageID"`
PubsubTopic string `json:"pubsubTopic"`
Message *pb.WakuMessage `json:"wakuMessage"`
}
func toSubscriptionMessage(msg *protocol.Envelope) *SubscriptionMsg {
return &SubscriptionMsg{
MessageID: hexutil.Encode(msg.Hash()),
PubsubTopic: msg.PubsubTopic(),
Message: msg.Message(),
}
}
//export waku_peers
// Retrieve the list of peers known by the waku node
func waku_peers() *C.char {
if wakuNode == nil {
return makeJSONResponse(ErrWakuNodeNotReady)
}
peers, err := wakuNode.Peers()
return prepareJSONResponse(peers, err)
}
func unmarshalPubkey(pub []byte) (ecdsa.PublicKey, error) {
x, y := elliptic.Unmarshal(secp256k1.S256(), pub)
if x == nil {
return ecdsa.PublicKey{}, errors.New("invalid public key")
}
return ecdsa.PublicKey{Curve: secp256k1.S256(), X: x, Y: y}, nil
}
//export waku_decode_symmetric
// Decode a waku message using a 32 bytes symmetric key. The key must be a hex encoded string with "0x" prefix
func waku_decode_symmetric(messageJSON *C.char, symmetricKey *C.char) *C.char {
var msg pb.WakuMessage
err := json.Unmarshal([]byte(C.GoString(messageJSON)), &msg)
if err != nil {
return makeJSONResponse(err)
}
if msg.Version == 0 {
return prepareJSONResponse(msg.Payload, nil)
} else if msg.Version > 1 {
return makeJSONResponse(errors.New("unsupported wakumessage version"))
}
keyInfo := &node.KeyInfo{
Kind: node.Symmetric,
}
keyInfo.SymKey, err = hexutil.Decode(C.GoString(symmetricKey))
if err != nil {
return makeJSONResponse(err)
}
payload, err := node.DecodePayload(&msg, keyInfo)
if err != nil {
return makeJSONResponse(err)
}
response := struct {
PubKey string `json:"pubkey"`
Signature string `json:"signature"`
Data []byte `json:"data"`
Padding []byte `json:"padding"`
}{
PubKey: hexutil.Encode(crypto.FromECDSAPub(payload.PubKey)),
Signature: hexutil.Encode(payload.Signature),
Data: payload.Data,
Padding: payload.Padding,
}
return prepareJSONResponse(response, err)
}
//export waku_decode_asymmetric
// Decode a waku message using a secp256k1 private key. The key must be a hex encoded string with "0x" prefix
func waku_decode_asymmetric(messageJSON *C.char, privateKey *C.char) *C.char {
var msg pb.WakuMessage
err := json.Unmarshal([]byte(C.GoString(messageJSON)), &msg)
if err != nil {
return makeJSONResponse(err)
}
if msg.Version == 0 {
return prepareJSONResponse(msg.Payload, nil)
} else if msg.Version > 1 {
return makeJSONResponse(errors.New("unsupported wakumessage version"))
}
keyInfo := &node.KeyInfo{
Kind: node.Asymmetric,
}
keyBytes, err := hexutil.Decode(C.GoString(privateKey))
if err != nil {
return makeJSONResponse(err)
}
keyInfo.PrivKey, err = crypto.ToECDSA(keyBytes)
if err != nil {
return makeJSONResponse(err)
}
payload, err := node.DecodePayload(&msg, keyInfo)
if err != nil {
return makeJSONResponse(err)
}
response := struct {
PubKey string `json:"pubkey"`
Signature string `json:"signature"`
Data []byte `json:"data"`
Padding []byte `json:"padding"`
}{
PubKey: hexutil.Encode(crypto.FromECDSAPub(payload.PubKey)),
Signature: hexutil.Encode(payload.Signature),
Data: payload.Data,
Padding: payload.Padding,
}
return prepareJSONResponse(response, err)
}
//export waku_utils_base64_decode
// Decode a base64 string (useful for reading the payload from waku messages)
func waku_utils_base64_decode(data *C.char) *C.char {
b, err := base64.StdEncoding.DecodeString(C.GoString(data))
if err != nil {
return makeJSONResponse(err)
}
return prepareJSONResponse(string(b), nil)
}
//export waku_utils_base64_encode
// Encode data to base64 (useful for creating the payload of a waku message in the
// format understood by waku_relay_publish)
func waku_utils_base64_encode(data *C.char) *C.char {
str := base64.StdEncoding.EncodeToString([]byte(C.GoString(data)))
return C.CString(string(str))
}
//export waku_utils_free
// Frees a char* since all strings returned by gowaku are allocated in the C heap using malloc.
func waku_utils_free(data *C.char) {
C.free(unsafe.Pointer(data))
}
// TODO:
// connected/disconnected