From df235db6b7792c465e2ca3cb96631a3ba4d57afb Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 21 Mar 2022 19:15:53 -0400 Subject: [PATCH] feat: c-bindings for waku relay (#212) --- Makefile | 53 ++- README.md | 6 + examples/c-bindings/Makefile | 45 +++ examples/c-bindings/main.c | 190 ++++++++++ examples/c-bindings/main.h | 14 + examples/c-bindings/nxjson.c | 387 +++++++++++++++++++ examples/c-bindings/nxjson.h | 65 ++++ go.mod | 1 + library/api.go | 703 +++++++++++++++++++++++++++++++++++ library/ios.go | 12 + library/response.go | 70 ++++ library/signals.c | 23 ++ library/signals.go | 71 ++++ library/types.go | 74 ++++ tests/.gitkeep | 0 waku/v2/node/waku_payload.go | 1 - waku/v2/node/wakuoptions.go | 2 +- 17 files changed, 1713 insertions(+), 4 deletions(-) create mode 100644 examples/c-bindings/Makefile create mode 100644 examples/c-bindings/main.c create mode 100644 examples/c-bindings/main.h create mode 100644 examples/c-bindings/nxjson.c create mode 100644 examples/c-bindings/nxjson.h create mode 100644 library/api.go create mode 100644 library/ios.go create mode 100644 library/response.go create mode 100644 library/signals.c create mode 100644 library/signals.go create mode 100644 library/types.go delete mode 100644 tests/.gitkeep diff --git a/Makefile b/Makefile index 4831a456..3cf797c4 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,30 @@ GO_HTML_COV := ./coverage.html GO_TEST_OUTFILE := ./c.out CC_PREFIX := github.com/status-im/go-waku -.PHONY: all build lint test coverage build-example +SHELL := bash # the shell used internally by Make + +.PHONY: all build lint test coverage build-example static-library dynamic-library test-c test-c-template + +ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10... + detected_OS := Windows +else + detected_OS := $(strip $(shell uname)) +endif + +ifeq ($(detected_OS),Darwin) + GOBIN_SHARED_LIB_EXT := dylib + ifeq ("$(shell sysctl -nq hw.optional.arm64)","1") + # Building on M1 is still not supported, so in the meantime we crosscompile to amd64 + GOBIN_SHARED_LIB_CFLAGS=CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 + endif +else ifeq ($(detected_OS),Windows) + # on Windows need `--export-all-symbols` flag else expected symbols will not be found in libgowaku.dll + GOBIN_SHARED_LIB_CGO_LDFLAGS := CGO_LDFLAGS="-Wl,--export-all-symbols" + GOBIN_SHARED_LIB_EXT := dll +else + GOBIN_SHARED_LIB_EXT := so + GOBIN_SHARED_LIB_CGO_LDFLAGS := CGO_LDFLAGS="-Wl,-soname,libgowaku.so.0" +endif all: build @@ -60,4 +83,30 @@ build-example-chat-2: build-example-filter2: cd examples/filter2 && $(MAKE) -build-example: build-example-basic2 build-example-chat-2 build-example-filter2 \ No newline at end of file +build-example: build-example-basic2 build-example-chat-2 build-example-filter2 + +static-library: ##@cross-compile Build go-waku as static library for current platform + mkdir -p ./build/lib + @echo "Building static library..." + go build \ + -buildmode=c-archive \ + -o ./build/lib/libgowaku.a \ + ./library/ + @echo "Static library built:" + @ls -la ./build/lib/libgowaku.* + +dynamic-library: ##@cross-compile Build status-go as shared library for current platform + mkdir -p ./build/lib + @echo "Building shared library..." + $(GOBIN_SHARED_LIB_CFLAGS) $(GOBIN_SHARED_LIB_CGO_LDFLAGS) go build \ + -buildmode=c-shared \ + -o ./build/lib/libgowaku.$(GOBIN_SHARED_LIB_EXT) \ + ./library/ +ifeq ($(detected_OS),Linux) + cd ./build/lib && \ + ls -lah . && \ + mv ./libgowaku.$(GOBIN_SHARED_LIB_EXT) ./libgowaku.$(GOBIN_SHARED_LIB_EXT).0 && \ + ln -s ./libgowaku.$(GOBIN_SHARED_LIB_EXT).0 ./libgowaku.$(GOBIN_SHARED_LIB_EXT) +endif + @echo "Shared library built:" + @ls -la ./build/lib/libgowaku.* diff --git a/README.md b/README.md index 5c2ede3f..d318aaa9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ docker run go-waku:latest --help go get github.com/status-im/go-waku ``` +## C Bindings +``` +make static-library +make dynamic-library +``` + ## Examples Examples of usage of go-waku as a library can be found in the examples folder. There is a fully featured chat example. diff --git a/examples/c-bindings/Makefile b/examples/c-bindings/Makefile new file mode 100644 index 00000000..014495a6 --- /dev/null +++ b/examples/c-bindings/Makefile @@ -0,0 +1,45 @@ +SHELL := bash # the shell used internally by Make + +.PHONY: all build run + +ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10... + detected_OS := Windows +else + detected_OS := $(strip $(shell uname)) +endif + +all: build + +ifeq ($(detected_OS),Linux) + PLATFORM_FLAGS_TEST_C ?= -ldl +else ifeq ($(detected_OS),macOS) + PLATFORM_FLAGS_TEST_C ?= -Wl,-headerpad_max_install_names +endif + +build: + cd ../../ && $(MAKE) static-library # Building library + rm -rf build && \ + echo "Compiling 'main.c'" + + mkdir -p build + $(CC) \ + -I../../build/lib/ \ + main.c \ + ../../build/lib/libgowaku.a \ + -lm \ + -pthread \ + $(PLATFORM_FLAGS_TEST_C) \ + -o build/main + + +run: + echo "Executing './build/main.c'" +ifeq ($(detected_OS),macOS) + ./build/main +else ifeq ($(detected_OS),Windows) + PATH="$(PATH_TEST)" \ + ./build/main +else + ./build/main +endif + + diff --git a/examples/c-bindings/main.c b/examples/c-bindings/main.c new file mode 100644 index 00000000..d8484db8 --- /dev/null +++ b/examples/c-bindings/main.c @@ -0,0 +1,190 @@ +#include +#include +#include +#include + +#include "libgowaku.h" +#include "nxjson.c" +#include "main.h" + + +char *alicePrivKey = "0x4f012057e1a1458ce34189cb27daedbbe434f3df0825c1949475dec786e2c64e"; +char *alicePubKey = "0x0440f05847c4c7166f57ae8ecaaf72d31bddcbca345e26713ca9e26c93fb8362ddcd5ae7f4533ee956428ad08a89cd18b234c2911a3b1c7fbd1c0047610d987302"; + + +char *bobPrivKey = "0xb91d6b2df8fb6ef8b53b51b2b30a408c49d5e2b530502d58ac8f94e5c5de1453"; +char *bobPubKey = "0x045eef61a98ba1cf44a2736fac91183ea2bd86e67de20fe4bff467a71249a8a0c05f795dd7f28ced7c15eaa69c89d4212cc4f526ca5e9a62e88008f506d850cccd"; + + +int main(int argc, char *argv[]) +{ + char *response; + gowaku_set_event_callback(callBack); + + char *configJSON = "{\"host\": \"0.0.0.0\", \"port\": 60000}"; + response = gowaku_new(configJSON); // configJSON can be NULL too to use defaults + if (isError(response)) + return 1; + int nodeID = getIntValue(response); // Obtain the nodeID from the response + + + + response = gowaku_start(nodeID); // Start the node, enabling the waku protocols + if (isError(response)) + return 1; + + + + response = gowaku_id(nodeID); // Obtain the node peerID + if (isError(response)) + return 1; + char *nodePeerID = getStrValue(response); + printf("PeerID: %s\n", nodePeerID); + + + /* + response = gowaku_dial_peer(nodeID, "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS", 0); // Connect to a node + if (isError(response)) + return 1; + */ + + + response = gowaku_relay_subscribe(nodeID, NULL); + if (isError(response)) + return 1; + char *subscriptionID = getStrValue(response); + printf("SubscriptionID: %s\n", subscriptionID); + + + + int i = 0; + int version = 1; + while (true){ + i++; + + response = gowaku_encode_data("Hello World!", ASYMMETRIC, bobPubKey, alicePrivKey, version); // Send a message encrypting it with Bob's PubK, and signing it with Alice PrivK + if (isError(response)) + return 1; + char *encodedData = getStrValue(response); + + + char *contentTopic = getStrValue(gowaku_content_topic("example", 1, "default", "rfc26")); + + + char wakuMsg[1000]; + sprintf(wakuMsg, "{\"payload\":\"%s\",\"contentTopic\":\"%s\",\"version\":%d,\"timestamp\":%d}", encodedData, contentTopic, version, i); + + response = gowaku_relay_publish(nodeID, wakuMsg, NULL, 0); // Broadcast a message + if (isError(response)) + return 1; + // char *messageID = getStrValue(response); + + sleep(1); + } + + + + response = gowaku_stop(nodeID); + if (isError(response)) + return 1; + + return 0; +} + +void callBack(char *signal) +{ + // This callback will be executed each time a new message is received + + // Example signal: + /*{ + "nodeId":1, + "type":"message", + "event":{ + "messageID":"0x6496491e40dbe0b6c3a2198c2426b16301688a2daebc4f57ad7706115eac3ad1", + "pubsubTopic":"/waku/2/default-waku/proto", + "wakuMessage":{ + "payload":"BPABASUqWgRkgp73aW/FHIyGtJDYnStvaQvCoX9MdaNsOH39Vet0em6ipZc3lZ7kK9uFFtbJgIWfRaqTxSRjiFOPx88gXt1JeSm2SUwGSz+1gh2xTy0am8tXkc8OWSSjamdkEbXuVgAueLxHOnV3xlGwYt7nx2G5DWYqUu1BXv4yWHPOoiH2yx3fxX0OajgKGBwiMbadRNUuAUFPRM90f+bzG2y22ssHctDV/U6sXOa9ljNgpAx703Q3WIFleSRozto7ByNAdRFwWR0RGGV4l0btJXM7JpnrYcVC24dB0tJ3HVWuD0ZcwOM1zTL0wwc0hTezLHvI+f6bHSzsFGcCWIlc03KSoMjK1XENNL4dtDmSFI1DQCGgq09c2Bc3Je3Ci6XJHu+FP1F1pTnRzevv2WP8FSBJiTXpmJXdm6evB7V1Xxj4QlzQDvmHLRpBOL6PSttxf1Dc0IwC6BfZRN5g0dNmItNlS2pcY1MtZLxD5zpj", + "contentTopic":"ABC", + "version":1, + "timestamp":1647826358000000000 + } + } + }*/ + + const nx_json *json = nx_json_parse(signal, 0); + const char *type = nx_json_get(json, "type")->text_value; + + if (strcmp(type,"message") == 0){ + const char *encodedPayload = nx_json_get(nx_json_get(nx_json_get(json, "event"), "wakuMessage"), "payload")->text_value; + int version = nx_json_get(nx_json_get(nx_json_get(json, "event"), "wakuMessage"), "version")->int_value; + + char *decodedData = gowaku_decode_data((char*)encodedPayload, ASYMMETRIC, bobPrivKey, version); + if(isError(decodedData)) return; + + const nx_json *dataJson = nx_json_parse(decodedData, 0); + const char *pubkey = nx_json_get(nx_json_get(dataJson, "result"), "pubkey")->text_value; + const char *base64data = nx_json_get(nx_json_get(dataJson, "result"), "data")->text_value; + char *data = gowaku_utils_base64_decode((char*)base64data); + + printf("Received \"%s\" from %s\n", getStrValue(data), pubkey); + fflush(stdout); + + nx_json_free(dataJson); + } + + nx_json_free(json); +} + +bool isError(char *input) +{ + char *jsonStr = malloc(strlen(input) + 1); + strcpy(jsonStr, input); + const nx_json *json = nx_json_parse(jsonStr, 0); + bool result = false; + if (json) + { + const char *errTxt = nx_json_get(json, "error")->text_value; + result = errTxt != NULL; + if (result) + { + printf("ERROR: %s\n", errTxt); + } + } + nx_json_free(json); + free(jsonStr); + return result; +} + +int getIntValue(char *input) +{ + char *jsonStr = malloc(strlen(input) + 1); + strcpy(jsonStr, input); + const nx_json *json = nx_json_parse(jsonStr, 0); + int result = -1; + if (json) + { + result = nx_json_get(json, "result")->int_value; + } + nx_json_free(json); + free(jsonStr); + + return result; +} + +char* getStrValue(char *input) +{ + char *jsonStr = malloc(strlen(input) + 1); + strcpy(jsonStr, input); + const nx_json *json = nx_json_parse(jsonStr, 0); + char* result = ""; + if (json) + { + const char* text_value = nx_json_get(json, "result")->text_value; + result = strdup(text_value); + } + + nx_json_free(json); + free(jsonStr); + + return result; +} diff --git a/examples/c-bindings/main.h b/examples/c-bindings/main.h new file mode 100644 index 00000000..8ece08e4 --- /dev/null +++ b/examples/c-bindings/main.h @@ -0,0 +1,14 @@ +#ifndef MAIN_H +#define MAIN_H + +#include + +void callBack(char *signal); + +bool isError(char *input); + +int getIntValue(char *input); + +char* getStrValue(char *input); + +#endif /* MAIN_H */ \ No newline at end of file diff --git a/examples/c-bindings/nxjson.c b/examples/c-bindings/nxjson.c new file mode 100644 index 00000000..327ef0fe --- /dev/null +++ b/examples/c-bindings/nxjson.c @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2013 Yaroslav Stavnichiy + * + * This file is part of NXJSON. + * + * NXJSON is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * NXJSON is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NXJSON. If not, see . + */ + +// this file can be #included in your code +#ifndef NXJSON_C +#define NXJSON_C + +#ifdef __cplusplus +extern "C" { +#endif + + +#include +#include +#include +#include +#include + +#include "nxjson.h" + +// redefine NX_JSON_CALLOC & NX_JSON_FREE to use custom allocator +#ifndef NX_JSON_CALLOC +#define NX_JSON_CALLOC() calloc(1, sizeof(nx_json)) +#define NX_JSON_FREE(json) free((void*)(json)) +#endif + +// redefine NX_JSON_REPORT_ERROR to use custom error reporting +#ifndef NX_JSON_REPORT_ERROR +#define NX_JSON_REPORT_ERROR(msg, p) fprintf(stderr, "NXJSON PARSE ERROR (%d): " msg " at %s\n", __LINE__, p) +#endif + +#define IS_WHITESPACE(c) ((unsigned char)(c)<=(unsigned char)' ') + +static const nx_json dummy={ NX_JSON_NULL }; + +static nx_json* create_json(nx_json_type type, const char* key, nx_json* parent) { + nx_json* js=NX_JSON_CALLOC(); + assert(js); + js->type=type; + js->key=key; + if (!parent->last_child) { + parent->child=parent->last_child=js; + } + else { + parent->last_child->next=js; + parent->last_child=js; + } + parent->length++; + return js; +} + +void nx_json_free(const nx_json* js) { + nx_json* p=js->child; + nx_json* p1; + while (p) { + p1=p->next; + nx_json_free(p); + p=p1; + } + NX_JSON_FREE(js); +} + +static int unicode_to_utf8(unsigned int codepoint, char* p, char** endp) { + // code from http://stackoverflow.com/a/4609989/697313 + if (codepoint<0x80) *p++=codepoint; + else if (codepoint<0x800) *p++=192+codepoint/64, *p++=128+codepoint%64; + else if (codepoint-0xd800u<0x800) return 0; // surrogate must have been treated earlier + else if (codepoint<0x10000) *p++=224+codepoint/4096, *p++=128+codepoint/64%64, *p++=128+codepoint%64; + else if (codepoint<0x110000) *p++=240+codepoint/262144, *p++=128+codepoint/4096%64, *p++=128+codepoint/64%64, *p++=128+codepoint%64; + else return 0; // error + *endp=p; + return 1; +} + +nx_json_unicode_encoder nx_json_unicode_to_utf8=unicode_to_utf8; + +static inline int hex_val(char c) { + if (c>='0' && c<='9') return c-'0'; + if (c>='a' && c<='f') return c-'a'+10; + if (c>='A' && c<='F') return c-'A'+10; + return -1; +} + +static char* unescape_string(char* s, char** end, nx_json_unicode_encoder encoder) { + char* p=s; + char* d=s; + char c; + while ((c=*p++)) { + if (c=='"') { + *d='\0'; + *end=p; + return s; + } + else if (c=='\\') { + switch (*p) { + case '\\': + case '/': + case '"': + *d++=*p++; + break; + case 'b': + *d++='\b'; p++; + break; + case 'f': + *d++='\f'; p++; + break; + case 'n': + *d++='\n'; p++; + break; + case 'r': + *d++='\r'; p++; + break; + case 't': + *d++='\t'; p++; + break; + case 'u': // unicode + if (!encoder) { + // leave untouched + *d++=c; + break; + } + char* ps=p-1; + int h1, h2, h3, h4; + if ((h1=hex_val(p[1]))<0 || (h2=hex_val(p[2]))<0 || (h3=hex_val(p[3]))<0 || (h4=hex_val(p[4]))<0) { + NX_JSON_REPORT_ERROR("invalid unicode escape", p-1); + return 0; + } + unsigned int codepoint=h1<<12|h2<<8|h3<<4|h4; + if ((codepoint & 0xfc00)==0xd800) { // high surrogate; need one more unicode to succeed + p+=6; + if (p[-1]!='\\' || *p!='u' || (h1=hex_val(p[1]))<0 || (h2=hex_val(p[2]))<0 || (h3=hex_val(p[3]))<0 || (h4=hex_val(p[4]))<0) { + NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps); + return 0; + } + unsigned int codepoint2=h1<<12|h2<<8|h3<<4|h4; + if ((codepoint2 & 0xfc00)!=0xdc00) { + NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps); + return 0; + } + codepoint=0x10000+((codepoint-0xd800)<<10)+(codepoint2-0xdc00); + } + if (!encoder(codepoint, d, &d)) { + NX_JSON_REPORT_ERROR("invalid codepoint", ps); + return 0; + } + p+=5; + break; + default: + // leave untouched + *d++=c; + break; + } + } + else { + *d++=c; + } + } + NX_JSON_REPORT_ERROR("no closing quote for string", s); + return 0; +} + +static char* skip_block_comment(char* p) { + // assume p[-2]=='/' && p[-1]=='*' + char* ps=p-2; + if (!*p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; + } + REPEAT: + p=strchr(p+1, '/'); + if (!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; + } + if (p[-1]!='*') goto REPEAT; + return p+1; +} + +static char* parse_key(const char** key, char* p, nx_json_unicode_encoder encoder) { + // on '}' return with *p=='}' + char c; + while ((c=*p++)) { + if (c=='"') { + *key=unescape_string(p, &p, encoder); + if (!*key) return 0; // propagate error + while (*p && IS_WHITESPACE(*p)) p++; + if (*p==':') return p+1; + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; + } + else if (IS_WHITESPACE(c) || c==',') { + // continue + } + else if (c=='}') { + return p-1; + } + else if (c=='/') { + if (*p=='/') { // line comment + char* ps=p-1; + p=strchr(p+1, '\n'); + if (!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; // error + } + p++; + } + else if (*p=='*') { // block comment + p=skip_block_comment(p+1); + if (!p) return 0; + } + else { + NX_JSON_REPORT_ERROR("unexpected chars", p-1); + return 0; // error + } + } + else { + NX_JSON_REPORT_ERROR("unexpected chars", p-1); + return 0; // error + } + } + NX_JSON_REPORT_ERROR("unexpected chars", p-1); + return 0; // error +} + +static char* parse_value(nx_json* parent, const char* key, char* p, nx_json_unicode_encoder encoder) { + nx_json* js; + while (1) { + switch (*p) { + case '\0': + NX_JSON_REPORT_ERROR("unexpected end of text", p); + return 0; // error + case ' ': case '\t': case '\n': case '\r': + case ',': + // skip + p++; + break; + case '{': + js=create_json(NX_JSON_OBJECT, key, parent); + p++; + while (1) { + const char* new_key; + p=parse_key(&new_key, p, encoder); + if (!p) return 0; // error + if (*p=='}') return p+1; // end of object + p=parse_value(js, new_key, p, encoder); + if (!p) return 0; // error + } + case '[': + js=create_json(NX_JSON_ARRAY, key, parent); + p++; + while (1) { + p=parse_value(js, 0, p, encoder); + if (!p) return 0; // error + if (*p==']') return p+1; // end of array + } + case ']': + return p; + case '"': + p++; + js=create_json(NX_JSON_STRING, key, parent); + js->text_value=unescape_string(p, &p, encoder); + if (!js->text_value) return 0; // propagate error + return p; + case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + { + js=create_json(NX_JSON_INTEGER, key, parent); + char* pe; + js->int_value=strtoll(p, &pe, 0); + if (pe==p || errno==ERANGE) { + NX_JSON_REPORT_ERROR("invalid number", p); + return 0; // error + } + if (*pe=='.' || *pe=='e' || *pe=='E') { // double value + js->type=NX_JSON_DOUBLE; + js->dbl_value=strtod(p, &pe); + if (pe==p || errno==ERANGE) { + NX_JSON_REPORT_ERROR("invalid number", p); + return 0; // error + } + } + else { + js->dbl_value=js->int_value; + } + return pe; + } + case 't': + if (!strncmp(p, "true", 4)) { + js=create_json(NX_JSON_BOOL, key, parent); + js->int_value=1; + return p+4; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case 'f': + if (!strncmp(p, "false", 5)) { + js=create_json(NX_JSON_BOOL, key, parent); + js->int_value=0; + return p+5; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case 'n': + if (!strncmp(p, "null", 4)) { + create_json(NX_JSON_NULL, key, parent); + return p+4; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case '/': // comment + if (p[1]=='/') { // line comment + char* ps=p; + p=strchr(p+2, '\n'); + if (!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; // error + } + p++; + } + else if (p[1]=='*') { // block comment + p=skip_block_comment(p+2); + if (!p) return 0; + } + else { + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + } + break; + default: + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + } + } +} + +const nx_json* nx_json_parse_utf8(char* text) { + return nx_json_parse(text, unicode_to_utf8); +} + +const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder) { + nx_json js={0}; + if (!parse_value(&js, 0, text, encoder)) { + if (js.child) nx_json_free(js.child); + return 0; + } + return js.child; +} + +const nx_json* nx_json_get(const nx_json* json, const char* key) { + if (!json || !key) return &dummy; // never return null + nx_json* js; + for (js=json->child; js; js=js->next) { + if (js->key && !strcmp(js->key, key)) return js; + } + return &dummy; // never return null +} + +const nx_json* nx_json_item(const nx_json* json, int idx) { + if (!json) return &dummy; // never return null + nx_json* js; + for (js=json->child; js; js=js->next) { + if (!idx--) return js; + } + return &dummy; // never return null +} + + +#ifdef __cplusplus +} +#endif + +#endif /* NXJSON_C */ \ No newline at end of file diff --git a/examples/c-bindings/nxjson.h b/examples/c-bindings/nxjson.h new file mode 100644 index 00000000..e5528b21 --- /dev/null +++ b/examples/c-bindings/nxjson.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2013 Yaroslav Stavnichiy + * + * This file is part of NXJSON. + * + * NXJSON is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * NXJSON is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NXJSON. If not, see . + */ + +#ifndef NXJSON_H +#define NXJSON_H + +#ifdef __cplusplus +extern "C" { +#endif + + +typedef enum nx_json_type { + NX_JSON_NULL, // this is null value + NX_JSON_OBJECT, // this is an object; properties can be found in child nodes + NX_JSON_ARRAY, // this is an array; items can be found in child nodes + NX_JSON_STRING, // this is a string; value can be found in text_value field + NX_JSON_INTEGER, // this is an integer; value can be found in int_value field + NX_JSON_DOUBLE, // this is a double; value can be found in dbl_value field + NX_JSON_BOOL // this is a boolean; value can be found in int_value field +} nx_json_type; + +typedef struct nx_json { + nx_json_type type; // type of json node, see above + const char* key; // key of the property; for object's children only + const char* text_value; // text value of STRING node + long long int_value; // the value of INTEGER or BOOL node + double dbl_value; // the value of DOUBLE node + int length; // number of children of OBJECT or ARRAY + struct nx_json* child; // points to first child + struct nx_json* next; // points to next child + struct nx_json* last_child; +} nx_json; + +typedef int (*nx_json_unicode_encoder)(unsigned int codepoint, char* p, char** endp); + +extern nx_json_unicode_encoder nx_json_unicode_to_utf8; + +const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder); +const nx_json* nx_json_parse_utf8(char* text); +void nx_json_free(const nx_json* js); +const nx_json* nx_json_get(const nx_json* json, const char* key); // get object's property by key +const nx_json* nx_json_item(const nx_json* json, int idx); // get array element by index + + +#ifdef __cplusplus +} +#endif + +#endif /* NXJSON_H */ \ No newline at end of file diff --git a/go.mod b/go.mod index b63a1362..2256beaf 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/ethereum/go-ethereum v1.10.13 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/rpc v1.2.0 github.com/ipfs/go-ds-sql v0.2.0 github.com/ipfs/go-log v1.0.5 diff --git a/library/api.go b/library/api.go new file mode 100644 index 00000000..10158913 --- /dev/null +++ b/library/api.go @@ -0,0 +1,703 @@ +package main + +/* +#include +#include + +typedef struct { + size_t len; + char* data; +} ByteArray; + +#define SYMMETRIC "Symmetric" +#define ASYMMETRIC "Asymmetric" +#define NONE "None" +*/ +import "C" +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "time" + "unsafe" + + "sync" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/google/uuid" + "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" + "github.com/status-im/go-waku/waku/v2/protocol/relay" +) + +var nodes map[int]*node.WakuNode = make(map[int]*node.WakuNode) +var subscriptions map[string]*relay.Subscription = make(map[string]*relay.Subscription) +var mutex sync.Mutex + +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"` +} + +var DefaultHost = "0.0.0.0" +var DefaultPort = 60000 +var DefaultKeepAliveInterval = 20 +var DefaultEnableRelay = true + +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 + } + + return config, nil +} + +//export gowaku_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 gowaku_new(configJSON *C.char) *C.char { + 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.WithWakuRelay()) + } + + ctx := context.Background() + wakuNode, err := node.New(ctx, opts...) + + if err != nil { + return makeJSONResponse(err) + } + + mutex.Lock() + defer mutex.Unlock() + + id := len(nodes) + 1 + nodes[id] = wakuNode + + return prepareJSONResponse(id, nil) +} + +//export gowaku_start +// Starts the waku node +func gowaku_start(nodeID C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + if err := wakuNode.Start(); err != nil { + return makeJSONResponse(err) + } + + return makeJSONResponse(nil) +} + +//export gowaku_stop +// Stops a waku node +func gowaku_stop(nodeID C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + wakuNode.Stop() + nodes[int(nodeID)] = nil + + return makeJSONResponse(nil) +} + +//export gowaku_id +// Obtain the peer ID of the waku node +func gowaku_id(nodeID C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + return prepareJSONResponse(wakuNode.ID(), nil) +} + +//export gowaku_listen_addresses +// Obtain the multiaddresses the wakunode is listening to +func gowaku_listen_addresses(nodeID C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + addrs, err := json.Marshal(wakuNode.ListenAddresses()) + return prepareJSONResponse(addrs, err) +} + +//export gowaku_add_peer +// Add node multiaddress and protocol to the wakunode peerstore +func gowaku_add_peer(nodeID C.int, address *C.char, protocolID *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || 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 gowaku_dial_peer +// Dial peer at multiaddress. if ms > 0, cancel the function execution if it takes longer than N milliseconds +func gowaku_dial_peer(nodeID C.int, address *C.char, ms C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || 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 gowaku_dial_peerid +// Dial known peer by peerID. if ms > 0, cancel the function execution if it takes longer than N milliseconds +func gowaku_dial_peerid(nodeID C.int, id *C.char, ms C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + var ctx context.Context + var cancel context.CancelFunc + + peerID, err := peer.Decode(C.GoString(id)) + 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, peerID) + return makeJSONResponse(err) +} + +//export gowaku_close_peer +// Close connection to peer at multiaddress +func gowaku_close_peer(nodeID C.int, address *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + err := wakuNode.ClosePeerByAddress(C.GoString(address)) + return makeJSONResponse(err) +} + +//export gowaku_close_peerid +// Close connection to a known peer by peerID +func gowaku_close_peerid(nodeID C.int, id *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + peerID, err := peer.Decode(C.GoString(id)) + if err != nil { + return makeJSONResponse(err) + } + + err = wakuNode.ClosePeerById(peerID) + return makeJSONResponse(err) +} + +//export gowaku_peer_cnt +// Get number of connected peers +func gowaku_peer_cnt(nodeID C.int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + return prepareJSONResponse(wakuNode.PeerCount(), nil) +} + +//export gowaku_content_topic +// Create a content topic string according to RFC 23 +func gowaku_content_topic(applicationName *C.char, applicationVersion C.uint, contentTopicName *C.char, encoding *C.char) *C.char { + return prepareJSONResponse(protocol.NewContentTopic(C.GoString(applicationName), uint(applicationVersion), C.GoString(contentTopicName), C.GoString(encoding)).String(), nil) +} + +//export gowaku_pubsub_topic +// Create a pubsub topic string according to RFC 23 +func gowaku_pubsub_topic(name *C.char, encoding *C.char) *C.char { + return prepareJSONResponse(protocol.NewPubsubTopic(C.GoString(name), C.GoString(encoding)).String(), nil) +} + +//export gowaku_default_pubsub_topic +// Get the default pubsub topic used in waku2: /waku/2/default-waku/proto +func gowaku_default_pubsub_topic() *C.char { + return prepareJSONResponse(protocol.DefaultPubsubTopic().String(), nil) +} + +func publish(nodeID int, message string, pubsubTopic string, ms int) (string, error) { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[nodeID] + if !ok || wakuNode == nil { + return "", ErrWakuNodeNotReady + } + + var msg pb.WakuMessage + err := json.Unmarshal([]byte(message), &msg) + if err != nil { + return "", err + } + + 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() + } + + hash, err := wakuNode.Relay().PublishToTopic(ctx, &msg, pubsubTopic) + return hexutil.Encode(hash), err +} + +//export gowaku_relay_publish +// Publish a message using waku relay. Use NULL for topic to use the default pubsub topic +// If ms is greater than 0, the broadcast of the message must happen before the timeout +// (in milliseconds) is reached, or an error will be returned +func gowaku_relay_publish(nodeID C.int, messageJSON *C.char, topic *C.char, ms C.int) *C.char { + topicToPublish := "" + if topic != nil { + topicToPublish = C.GoString(topic) + } else { + topicToPublish = protocol.DefaultPubsubTopic().String() + } + + hash, err := publish(int(nodeID), C.GoString(messageJSON), topicToPublish, int(ms)) + return prepareJSONResponse(hash, err) +} + +//export gowaku_enough_peers +// Determine if there are enough peers to publish a message on a topic. Use NULL +// to verify the number of peers in the default pubsub topic +func gowaku_enough_peers(nodeID C.int, topic *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + topicToCheck := protocol.DefaultPubsubTopic().String() + if topic != nil { + topicToCheck = C.GoString(topic) + } + + return prepareJSONResponse(wakuNode.Relay().EnoughPeersToPublishToTopic(topicToCheck), nil) +} + +//export gowaku_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 gowaku_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 gowaku_relay_subscribe +// Subscribe to a WakuRelay topic. Set the topic to NULL to subscribe +// to the default topic. Returns a json response containing the subscription ID +// or an error message. When a message is received, a "message" is emitted containing +// the message, pubsub topic, and nodeID in which the message was received +func gowaku_relay_subscribe(nodeID int, topic *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + topicToSubscribe := protocol.DefaultPubsubTopic().String() + if topic != nil { + topicToSubscribe = C.GoString(topic) + } + + subscription, err := wakuNode.Relay().SubscribeToTopic(context.Background(), topicToSubscribe) + if err != nil { + return makeJSONResponse(err) + } + + subsID := uuid.New().String() + subscriptions[subsID] = subscription + + go func() { + for envelope := range subscription.C { + send(nodeID, "message", toSubscriptionMessage(envelope)) + } + }() + + return prepareJSONResponse(subsID, nil) +} + +//export gowaku_relay_unsubscribe_from_topic +// Closes the pubsub subscription to a pubsub topic. Existing subscriptions +// will not be closed, but they will stop receiving messages +func gowaku_relay_unsubscribe_from_topic(nodeID int, topic *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + topicToUnsubscribe := protocol.DefaultPubsubTopic().String() + if topic != nil { + topicToUnsubscribe = C.GoString(topic) + } + + err := wakuNode.Relay().Unsubscribe(context.Background(), topicToUnsubscribe) + if err != nil { + return makeJSONResponse(err) + } + + return makeJSONResponse(nil) +} + +//export gowaku_relay_close_subscription +// Closes a waku relay subscription +func gowaku_relay_close_subscription(nodeID int, subsID *C.char) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || wakuNode == nil { + return makeJSONResponse(ErrWakuNodeNotReady) + } + + subscription, ok := subscriptions[C.GoString(subsID)] + if !ok { + return makeJSONResponse(errors.New("Subscription does not exist")) + } + + subscription.Unsubscribe() + + delete(subscriptions, C.GoString(subsID)) + + return makeJSONResponse(nil) +} + +//export gowaku_peers +// Retrieve the list of peers connected to the waku node +func gowaku_peers(nodeID int) *C.char { + mutex.Lock() + defer mutex.Unlock() + wakuNode, ok := nodes[int(nodeID)] + if !ok || 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 nil, errors.New("invalid public key") + } + return &ecdsa.PublicKey{Curve: secp256k1.S256(), X: x, Y: y}, nil +} + +//export gowaku_encode_data +// Encode a byte array. `keyType` defines the type of key to use: `NONE`, +// `ASYMMETRIC` and `SYMMETRIC`. `version` is used to define the type of +// payload encryption: +// When `version` is 0 +// - No encryption is used +// When `version` is 1 +// - If using `ASYMMETRIC` encoding, `key` must contain a secp256k1 public key +// to encrypt the data with, +// - If using `SYMMETRIC` encoding, `key` must contain a 32 bytes symmetric key. +// The `signingKey` can contain an optional secp256k1 private key to sign the +// encoded message, otherwise NULL can be used. +func gowaku_encode_data(data *C.char, keyType *C.char, key *C.char, signingKey *C.char, version C.int) *C.char { + keyInfo := &node.KeyInfo{ + Kind: node.KeyKind(C.GoString(keyType)), + } + + keyBytes, err := hexutil.Decode(C.GoString(key)) + if err != nil { + return makeJSONResponse(err) + } + + if signingKey != nil { + signingKeyBytes, err := hexutil.Decode(C.GoString(signingKey)) + if err != nil { + return makeJSONResponse(err) + } + + privK, err := crypto.ToECDSA(signingKeyBytes) + if err != nil { + return makeJSONResponse(err) + } + keyInfo.PrivKey = privK + } + + switch keyInfo.Kind { + case node.Symmetric: + keyInfo.SymKey = keyBytes + case node.Asymmetric: + pubK, err := unmarshalPubkey(keyBytes) + if err != nil { + return makeJSONResponse(err) + } + keyInfo.PubKey = *pubK + } + + payload := node.Payload{ + Data: []byte(C.GoString(data)), + Key: keyInfo, + } + + response, err := payload.Encode(uint32(version)) + return prepareJSONResponse(response, err) +} + +//export gowaku_decode_data +// Decode a byte array. `keyType` defines the type of key used: `NONE`, +// `ASYMMETRIC` and `SYMMETRIC`. `version` is used to define the type of +// encryption that was used in the payload: +// When `version` is 0 +// - No encryption was used. It will return the original message payload +// When `version` is 1 +// - If using `ASYMMETRIC` encoding, `key` must contain a secp256k1 public key +// to decrypt the data with, +// - If using `SYMMETRIC` encoding, `key` must contain a 32 bytes symmetric key. +func gowaku_decode_data(data *C.char, keyType *C.char, key *C.char, version C.int) *C.char { + b, err := base64.StdEncoding.DecodeString(C.GoString(data)) + if err != nil { + return makeJSONResponse(err) + } + + keyInfo := &node.KeyInfo{ + Kind: node.KeyKind(C.GoString(keyType)), + } + + keyBytes, err := hexutil.Decode(C.GoString(key)) + if err != nil { + return makeJSONResponse(err) + } + + switch keyInfo.Kind { + case node.Symmetric: + keyInfo.SymKey = keyBytes + case node.Asymmetric: + privK, err := crypto.ToECDSA(keyBytes) + if err != nil { + return makeJSONResponse(err) + } + keyInfo.PrivKey = privK + } + + msg := pb.WakuMessage{ + Payload: b, + Version: uint32(version), + } + 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 gowaku_utils_base64_decode +// Decode a base64 string (useful for reading the payload from waku messages) +func gowaku_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 gowaku_utils_base64_encode +// Encode data to base64 (useful for creating the payload of a waku message in the +// format understood by gowaku_relay_publish) +func gowaku_utils_base64_encode(data *C.char) *C.char { + str := base64.StdEncoding.EncodeToString([]byte(C.GoString(data))) + return prepareJSONResponse(str, nil) +} + +// TODO: +// connected/disconnected +// dns discovery +// func gowaku_relay_publish_msg(msg C.WakuMessage, pubsubTopic *C.char, ms C.int) *C.char +// getFastestPeer(protocol) +// getRandomPeer(protocol) +// func (wakuLP *WakuLightPush) PublishToTopic(ctx context.Context, message *pb.WakuMessage, topic string, peer, requestId nil) ([]byte, error) { +// func (wakuLP *WakuLightPush) Publish(ctx context.Context, message *pb.WakuMessage, peer, requestId nil) ([]byte, error) { +// func (query) diff --git a/library/ios.go b/library/ios.go new file mode 100644 index 00000000..25c4d2ad --- /dev/null +++ b/library/ios.go @@ -0,0 +1,12 @@ +// +build darwin,cgo + +package main + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation +#include +#include +extern bool StatusServiceSignalEvent( const char *jsonEvent ); +*/ +import "C" diff --git a/library/response.go b/library/response.go new file mode 100644 index 00000000..a5e5b14f --- /dev/null +++ b/library/response.go @@ -0,0 +1,70 @@ +package main + +import "C" +import ( + "encoding/json" +) + +const ( + codeUnknown int = iota + // special codes + codeFailedParseResponse + // codeFailedParseParams +) + +var errToCodeMap = map[error]int{ + //transactions.ErrInvalidTxSender: codeErrInvalidTxSender, +} + +type jsonrpcSuccessfulResponse struct { + Result interface{} `json:"result"` +} + +type jsonrpcErrorResponse struct { + Error jsonError `json:"error"` +} + +type jsonError struct { + Code int `json:"code,omitempty"` + Message string `json:"message"` +} + +func prepareJSONResponse(result interface{}, err error) *C.char { + code := codeUnknown + if c, ok := errToCodeMap[err]; ok { + code = c + } + + return prepareJSONResponseWithCode(result, err, code) +} + +func prepareJSONResponseWithCode(result interface{}, err error, code int) *C.char { + if err != nil { + errResponse := jsonrpcErrorResponse{ + Error: jsonError{Code: code, Message: err.Error()}, + } + response, _ := json.Marshal(&errResponse) + return C.CString(string(response)) + } + + data, err := json.Marshal(jsonrpcSuccessfulResponse{result}) + if err != nil { + return prepareJSONResponseWithCode(nil, err, codeFailedParseResponse) + } + return C.CString(string(data)) +} + +func makeJSONResponse(err error) *C.char { + var errString *string = nil + if err != nil { + errStr := err.Error() + errString = &errStr + } + + out := APIResponse{ + Error: errString, + } + outBytes, _ := json.Marshal(out) + + return C.CString(string(outBytes)) +} diff --git a/library/signals.c b/library/signals.c new file mode 100644 index 00000000..b06ff9fd --- /dev/null +++ b/library/signals.c @@ -0,0 +1,23 @@ +// ====================================================================================== +// cgo compilation (for desktop platforms and local tests) +// ====================================================================================== + +#include +#include +#include +#include "_cgo_export.h" + +typedef void (*callback)(const char *jsonEvent); +callback gCallback = 0; + +bool StatusServiceSignalEvent(const char *jsonEvent) { + if (gCallback) { + gCallback(jsonEvent); + } + + return true; +} + +void SetEventCallback(void *cb) { + gCallback = (callback)cb; +} diff --git a/library/signals.go b/library/signals.go new file mode 100644 index 00000000..be6f0d40 --- /dev/null +++ b/library/signals.go @@ -0,0 +1,71 @@ +package main + +/* +#include +#include + +extern bool StatusServiceSignalEvent(const char *jsonEvent); +extern void SetEventCallback(void *cb); +*/ +import "C" +import ( + "encoding/json" + "fmt" + "unsafe" +) + +// SignalHandler is a simple callback function that gets called when any signal is received +type MobileSignalHandler func([]byte) + +// storing the current mobile signal handler here +var mobileSignalHandler MobileSignalHandler + +// SignalEnvelope is a general signal sent upward from node to app +type SignalEnvelope struct { + NodeID int `json:"nodeId"` + Type string `json:"type"` + Event interface{} `json:"event"` +} + +// NewEnvelope creates new envlope of given type and event payload. +func NewEnvelope(nodeId int, typ string, event interface{}) *SignalEnvelope { + return &SignalEnvelope{ + NodeID: nodeId, + Type: typ, + Event: event, + } +} + +// send sends application signal (in JSON) upwards to application (via default notification handler) +func send(node int, typ string, event interface{}) { + signal := NewEnvelope(node, typ, event) + data, err := json.Marshal(&signal) + if err != nil { + fmt.Println("marshal signal error", err) + return + } + // If a Go implementation of signal handler is set, let's use it. + if mobileSignalHandler != nil { + mobileSignalHandler(data) + } else { + // ...and fallback to C implementation otherwise. + str := C.CString(string(data)) + C.StatusServiceSignalEvent(str) + C.free(unsafe.Pointer(str)) + } +} + +// SetMobileSignalHandler setup geth callback to notify about new signal +// used for gomobile builds +//nolint +func SetMobileSignalHandler(handler SignalHandler) { + mobileSignalHandler = func(data []byte) { + if len(data) > 0 { + handler.HandleSignal(string(data)) + } + } +} + +func setEventCallback(cb unsafe.Pointer) { + C.SetEventCallback(cb) +} diff --git a/library/types.go b/library/types.go new file mode 100644 index 00000000..fda855f6 --- /dev/null +++ b/library/types.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "fmt" + "strings" +) + +// APIResponse generic response from API. +type APIResponse struct { + Error *string `json:"error"` +} + +// APIDetailedResponse represents a generic response +// with possible errors. +//nolint +type APIDetailedResponse struct { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + FieldErrors []APIFieldError `json:"field_errors,omitempty"` +} + +// Error string representation of APIDetailedResponse. +//nolint +func (r APIDetailedResponse) Error() string { + buf := bytes.NewBufferString("") + + for _, err := range r.FieldErrors { + buf.WriteString(err.Error() + "\n") // nolint: gas + } + + return strings.TrimSpace(buf.String()) +} + +// APIFieldError represents a set of errors +// related to a parameter. +//nolint +type APIFieldError struct { + Parameter string `json:"parameter,omitempty"` + Errors []APIError `json:"errors"` +} + +// Error string representation of APIFieldError. +func (e APIFieldError) Error() string { + if len(e.Errors) == 0 { + return "" + } + + buf := bytes.NewBufferString(fmt.Sprintf("Parameter: %s\n", e.Parameter)) + + for _, err := range e.Errors { + buf.WriteString(err.Error() + "\n") // nolint: gas + } + + return strings.TrimSpace(buf.String()) +} + +// APIError represents a single error. +//nolint +type APIError struct { + Message string `json:"message"` +} + +// Error string representation of APIError. +func (e APIError) Error() string { + return fmt.Sprintf("message=%s", e.Message) +} + +// SignalHandler defines a minimal interface +// a signal handler needs to implement. +//nolint +type SignalHandler interface { + HandleSignal(string) +} diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/waku/v2/node/waku_payload.go b/waku/v2/node/waku_payload.go index ad6d4d0e..58e92fd6 100644 --- a/waku/v2/node/waku_payload.go +++ b/waku/v2/node/waku_payload.go @@ -45,7 +45,6 @@ type KeyInfo struct { SymKey []byte // If the encryption is Symmetric, a Symmetric key must be specified PubKey ecdsa.PublicKey // If the encryption is Asymmetric, the public key of the message receptor must be specified PrivKey *ecdsa.PrivateKey // Set a privkey if the message requires a signature - } // Encode encodes a payload depending on the version parameter. diff --git a/waku/v2/node/wakuoptions.go b/waku/v2/node/wakuoptions.go index 3197265e..cc12ff6e 100644 --- a/waku/v2/node/wakuoptions.go +++ b/waku/v2/node/wakuoptions.go @@ -27,7 +27,7 @@ import ( const clientId string = "Go Waku v2 node" // Default minRelayPeersToPublish -const defaultMinRelayPeersToPublish = 1 +const defaultMinRelayPeersToPublish = 0 type WakuNodeParameters struct { hostAddr *net.TCPAddr