@openziti/ziti-sdk-nodejs 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/build.yml +220 -0
  2. package/.github/workflows/codeql-analysis.yml +71 -0
  3. package/.github/workflows/mattermost-ziti-webhook.yml +26 -0
  4. package/.gitmodules +4 -0
  5. package/.travis.yml-obsolete +99 -0
  6. package/CODE_OF_CONDUCT.md +17 -0
  7. package/CONTRIBUTING.md +6 -0
  8. package/LICENSE +201 -0
  9. package/README.md +155 -0
  10. package/appveyor.yml-obsolete +32 -0
  11. package/binding.gyp +227 -0
  12. package/lib/index.js +17 -0
  13. package/lib/ziti.js +40 -0
  14. package/package.json +56 -0
  15. package/scripts/build-appveyor.bat +198 -0
  16. package/scripts/install_node.sh +99 -0
  17. package/scripts/validate_tag.sh +24 -0
  18. package/src/Ziti_https_request.c +960 -0
  19. package/src/Ziti_https_request_data.c +250 -0
  20. package/src/Ziti_https_request_end.c +79 -0
  21. package/src/stack_traces.c +334 -0
  22. package/src/utils.c +108 -0
  23. package/src/utils.h +85 -0
  24. package/src/ziti-add-on.c +88 -0
  25. package/src/ziti-nodejs.h +209 -0
  26. package/src/ziti_close.c +79 -0
  27. package/src/ziti_dial.c +375 -0
  28. package/src/ziti_enroll.c +245 -0
  29. package/src/ziti_hello.c +52 -0
  30. package/src/ziti_init.c +315 -0
  31. package/src/ziti_service_available.c +222 -0
  32. package/src/ziti_shutdown.c +47 -0
  33. package/src/ziti_websocket_connect.c +458 -0
  34. package/src/ziti_websocket_write.c +235 -0
  35. package/src/ziti_write.c +223 -0
  36. package/tests/enroll-test.js +38 -0
  37. package/tests/hello.js +12 -0
  38. package/tests/https-test.js +206 -0
  39. package/tests/mattermost-test.js +124 -0
  40. package/tests/websocket-test.js +119 -0
  41. package/ziti.js +1 -0
  42. package/ziti.png +0 -0
@@ -0,0 +1,960 @@
1
+ /*
2
+ Copyright 2019-2020 Netfoundry, Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ #include "ziti-nodejs.h"
18
+ #include <ziti/ziti_src.h>
19
+
20
+ static const unsigned int U1 = 1;
21
+
22
+ /**
23
+ * Number of hosts we can have client pools for
24
+ */
25
+ enum { listMapCapacity = 50 };
26
+
27
+ /**
28
+ * Number of simultaneous HTTP requests we can have active against a single host before queueing
29
+ */
30
+ enum { perKeyListMapCapacity = 25 };
31
+
32
+ struct ListMap* HttpsClientListMap;
33
+
34
+ struct key_value {
35
+ char* key;
36
+ void* value;
37
+ };
38
+
39
+ struct ListMap {
40
+ struct key_value kvPairs[listMapCapacity];
41
+ size_t count;
42
+ uv_sem_t sem;
43
+ };
44
+
45
+ struct ListMap* newListMap() {
46
+ struct ListMap* listMap = calloc(1, sizeof *listMap);
47
+ return listMap;
48
+ }
49
+
50
+ uv_mutex_t client_pool_lock;
51
+
52
+ bool listMapInsert(struct ListMap* collection, char* key, void* value) {
53
+
54
+ if (collection->count == listMapCapacity) {
55
+ ZITI_NODEJS_LOG(ERROR, "collection->count already at capacity [%d], insert FAIL", listMapCapacity);
56
+ return false;
57
+ }
58
+
59
+ collection->kvPairs[collection->count].key = strdup(key);
60
+ collection->kvPairs[collection->count].value = value;
61
+ collection->count++;
62
+ ZITI_NODEJS_LOG(DEBUG, "collection->count total is: [%zd]", collection->count);
63
+
64
+ return true;
65
+ }
66
+
67
+
68
+ struct ListMap* getInnerListMapValueForKey(struct ListMap* collection, char* key) {
69
+ struct ListMap* value = NULL;
70
+ for (size_t i = 0 ; i < collection->count && value == NULL ; ++i) {
71
+ if (strcmp(collection->kvPairs[i].key, key) == 0) {
72
+ value = collection->kvPairs[i].value;
73
+ }
74
+ }
75
+ return value;
76
+ }
77
+
78
+ HttpsClient* getHttpsClientForKey(struct ListMap* collection, char* key) {
79
+ HttpsClient* value = NULL;
80
+ int busyCount = 0;
81
+ for (size_t i = 0 ; i < collection->count && value == NULL ; ++i) {
82
+ if (strcmp(collection->kvPairs[i].key, key) == 0) {
83
+ value = collection->kvPairs[i].value;
84
+ if (value->active) { // if it's in use
85
+ value = NULL; // then keep searching
86
+ }
87
+ else if (value->purge) { // if it's broken
88
+ value = NULL; // then keep searching
89
+ } else {
90
+ value->active = true; // mark the one we will return as 'in use
91
+ }
92
+ busyCount++;
93
+ }
94
+ }
95
+ ZITI_NODEJS_LOG(DEBUG, "returning value '%p', collection->count is: [%zd], busy-count is: [%d]", value, collection->count, busyCount);
96
+ return value;
97
+ }
98
+
99
+ void freeListMap(struct ListMap* collection) {
100
+ if (collection == NULL) {
101
+ return;
102
+ }
103
+ for (size_t i = 0 ; i < collection->count ; ++i) {
104
+ free(collection->kvPairs[i].value);
105
+ }
106
+ free(collection);
107
+ }
108
+
109
+ /**
110
+ *
111
+ */
112
+ static int purge_and_replace_bad_clients(struct ListMap* clientListMap, HttpsAddonData* addon_data) {
113
+ int numReplaced = 0;
114
+ for (int i = 0; i < perKeyListMapCapacity; i++) {
115
+
116
+ HttpsClient* httpsClient = clientListMap->kvPairs[i].value;
117
+
118
+ if (httpsClient->purge) {
119
+
120
+ ZITI_NODEJS_LOG(DEBUG, "*********** purging client [%p] from slot [%d]", httpsClient, i);
121
+
122
+ // FIXME: find out why the following free causes things to eventually crash in the uv_loop
123
+ // free(httpsClient->scheme_host_port);
124
+ // free(httpsClient);
125
+
126
+ httpsClient = calloc(1, sizeof *httpsClient);
127
+ httpsClient->scheme_host_port = strdup(addon_data->scheme_host_port);
128
+ ziti_src_init(thread_loop, &(httpsClient->ziti_src), addon_data->service, ztx );
129
+ um_http_init_with_src(thread_loop, &(httpsClient->client), addon_data->scheme_host_port, (um_src_t *)&(httpsClient->ziti_src) );
130
+
131
+ clientListMap->kvPairs[i].value = httpsClient;
132
+
133
+ ZITI_NODEJS_LOG(DEBUG, "*********** new client [%p] now occupying slot [%d]", httpsClient, i);
134
+
135
+ numReplaced++;
136
+ }
137
+ }
138
+ return numReplaced;
139
+ }
140
+
141
+ /**
142
+ *
143
+ */
144
+ static void allocate_client(uv_work_t* req) {
145
+
146
+ ZITI_NODEJS_LOG(DEBUG, "allocate_client() entered, uv_work_t is: %p", req);
147
+
148
+ HttpsAddonData* addon_data = (HttpsAddonData*) req->data;
149
+
150
+ ZITI_NODEJS_LOG(DEBUG, "addon_data is: %p", addon_data);
151
+
152
+ // if (uv_mutex_trylock(&client_pool_lock)) {
153
+ // ZITI_NODEJS_LOG(ERROR, "uv_mutex_lock failure");
154
+ // }
155
+
156
+ struct ListMap* clientListMap = getInnerListMapValueForKey(HttpsClientListMap, addon_data->scheme_host_port);
157
+
158
+ if (NULL == clientListMap) { // If first time seeing this key, spawn a pool of clients for it
159
+
160
+ clientListMap = newListMap();
161
+ listMapInsert(HttpsClientListMap, addon_data->scheme_host_port, (void*)clientListMap);
162
+
163
+ uv_sem_init(&(clientListMap->sem), perKeyListMapCapacity);
164
+
165
+ for (int i = 0; i < perKeyListMapCapacity; i++) {
166
+
167
+ HttpsClient* httpsClient = calloc(1, sizeof *httpsClient);
168
+ httpsClient->scheme_host_port = strdup(addon_data->scheme_host_port);
169
+ ziti_src_init(thread_loop, &(httpsClient->ziti_src), addon_data->service, ztx );
170
+ um_http_init_with_src(thread_loop, &(httpsClient->client), addon_data->scheme_host_port, (um_src_t *)&(httpsClient->ziti_src) );
171
+
172
+ listMapInsert(clientListMap, addon_data->scheme_host_port, (void*)httpsClient);
173
+
174
+ }
175
+ }
176
+ else {
177
+
178
+ purge_and_replace_bad_clients(clientListMap, addon_data);
179
+
180
+ }
181
+
182
+ ZITI_NODEJS_LOG(DEBUG, "----------> acquiring sem");
183
+ uv_sem_wait(&(clientListMap->sem));
184
+ ZITI_NODEJS_LOG(DEBUG, "----------> successfully acquired sem");
185
+
186
+ addon_data->httpsClient = getHttpsClientForKey(clientListMap, addon_data->scheme_host_port);
187
+ ZITI_NODEJS_LOG(DEBUG, "----------> client is: [%p]", addon_data->httpsClient);
188
+
189
+ if (NULL == addon_data->httpsClient) {
190
+ ZITI_NODEJS_LOG(DEBUG, "----------> client is NULL, so we must purge_and_replace_bad_clients");
191
+ purge_and_replace_bad_clients(clientListMap, addon_data);
192
+
193
+ addon_data->httpsClient = getHttpsClientForKey(clientListMap, addon_data->scheme_host_port);
194
+ ZITI_NODEJS_LOG(DEBUG, "----------> client is: [%p]", addon_data->httpsClient);
195
+ }
196
+
197
+ if (NULL == addon_data->httpsClient) {
198
+ ZITI_NODEJS_LOG(DEBUG, "----------> client is NULL, so we are in an unrecoverable state!");
199
+ }
200
+ }
201
+
202
+
203
+
204
+ /**
205
+ * This function is responsible for calling the JavaScript on_resp_body callback function
206
+ * that was specified when the Ziti_https_request(...) was called from JavaScript.
207
+ */
208
+ static void CallJs_on_resp_body(napi_env env, napi_value js_cb, void* context, void* data) {
209
+
210
+ ZITI_NODEJS_LOG(DEBUG, "entered");
211
+
212
+ // This parameter is not used.
213
+ (void) context;
214
+
215
+ // Retrieve the HttpsRespBodyItem created by the worker thread.
216
+ HttpsRespBodyItem* item = (HttpsRespBodyItem*)data;
217
+
218
+
219
+ // env and js_cb may both be NULL if Node.js is in its cleanup phase, and
220
+ // items are left over from earlier thread-safe calls from the worker thread.
221
+ // When env is NULL, we simply skip over the call into Javascript
222
+ if (env != NULL) {
223
+
224
+ napi_value undefined;
225
+
226
+ // Retrieve the JavaScript `undefined` value so we can use it as the `this`
227
+ // value of the JavaScript function call.
228
+ napi_get_undefined(env, &undefined);
229
+
230
+ // const obj = {}
231
+ napi_value js_http_item, js_req, js_len, js_buffer;
232
+ void* result_data;
233
+ int rc = napi_create_object(env, &js_http_item);
234
+ if (rc != napi_ok) {
235
+ napi_throw_error(env, "EINVAL", "failure to create object");
236
+ }
237
+
238
+ // obj.req = req
239
+ napi_create_int64(env, (int64_t)item->req, &js_req);
240
+ if (rc != napi_ok) {
241
+ napi_throw_error(env, "EINVAL", "failure to create resp.req");
242
+ }
243
+ rc = napi_set_named_property(env, js_http_item, "req", js_req);
244
+ if (rc != napi_ok) {
245
+ napi_throw_error(env, "EINVAL", "failure to set named property req");
246
+ }
247
+ ZITI_NODEJS_LOG(DEBUG, "js_req: %p", item->req);
248
+
249
+ // obj.len = len
250
+ rc = napi_create_int32(env, item->len, &js_len);
251
+ if (rc != napi_ok) {
252
+ napi_throw_error(env, "EINVAL", "failure to create resp.len");
253
+ }
254
+ rc = napi_set_named_property(env, js_http_item, "len", js_len);
255
+ if (rc != napi_ok) {
256
+ napi_throw_error(env, "EINVAL", "failure to set named property len");
257
+ }
258
+ ZITI_NODEJS_LOG(DEBUG, "len: %zd", item->len);
259
+
260
+ // obj.body = body
261
+ if (NULL != item->body) {
262
+ rc = napi_create_buffer_copy(env, item->len, (const void*)item->body, (void**)&result_data, &js_buffer);
263
+ if (rc != napi_ok) {
264
+ napi_throw_error(env, "EINVAL", "failure to create resp.data");
265
+ }
266
+ rc = napi_set_named_property(env, js_http_item, "body", js_buffer);
267
+ if (rc != napi_ok) {
268
+ napi_throw_error(env, "EINVAL", "failure to set named property body");
269
+ }
270
+ } else {
271
+ rc = napi_set_named_property(env, js_http_item, "body", undefined);
272
+ }
273
+
274
+ // Call the JavaScript function and pass it the HttpsRespItem
275
+ rc = napi_call_function(
276
+ env,
277
+ undefined,
278
+ js_cb,
279
+ 1,
280
+ &js_http_item,
281
+ NULL
282
+ );
283
+ if (rc != napi_ok) {
284
+ napi_throw_error(env, "EINVAL", "failure to invoke JS callback");
285
+ }
286
+
287
+ }
288
+ }
289
+
290
+
291
+ /**
292
+ *
293
+ */
294
+ void on_resp_body(um_http_req_t *req, const char *body, ssize_t len) {
295
+
296
+ // ZITI_NODEJS_LOG(DEBUG, "len: %zd, body is: \n>>>>>%s<<<<<", len, body);
297
+ ZITI_NODEJS_LOG(DEBUG, "body: %p", body);
298
+ ZITI_NODEJS_LOG(DEBUG, "len: %zd", len);
299
+
300
+ HttpsAddonData* addon_data = (HttpsAddonData*) req->data;
301
+ ZITI_NODEJS_LOG(DEBUG, "addon_data->httpsClient is: %p", addon_data->httpsClient);
302
+
303
+ HttpsRespBodyItem* item = calloc(1, sizeof(*item));
304
+ ZITI_NODEJS_LOG(DEBUG, "new HttpsRespBodyItem is: %p", item);
305
+
306
+ // Grab everything off the um_http_resp_t that we need to eventually pass on to the JS on_resp_body callback.
307
+ // If we wait until CallJs_on_resp_body is invoked to do that work, the um_http_resp_t may have already been free'd by the C-SDK
308
+
309
+ item->req = req;
310
+
311
+ if (NULL != body) {
312
+ item->body = calloc(1, len);
313
+ memcpy((void*)item->body, body, len);
314
+ } else {
315
+ item->body = NULL;
316
+ }
317
+ item->len = len;
318
+
319
+ if ((NULL == body) && (UV_EOF == len)) {
320
+ ZITI_NODEJS_LOG(DEBUG, "<--------- returning httpsClient [%p] back to pool", addon_data->httpsClient);
321
+ addon_data->httpsClient->active = false;
322
+
323
+ struct ListMap* clientListMap = getInnerListMapValueForKey(HttpsClientListMap, addon_data->scheme_host_port);
324
+
325
+ ZITI_NODEJS_LOG(DEBUG, "<-------- returning sem for client: [%p] ", addon_data->httpsClient);
326
+ uv_sem_post(&(clientListMap->sem));
327
+ ZITI_NODEJS_LOG(DEBUG, " after returning sem for client: [%p] ", addon_data->httpsClient);
328
+ }
329
+
330
+ ZITI_NODEJS_LOG(DEBUG, "calling tsfn_on_resp_body: %p", addon_data->tsfn_on_resp_body);
331
+
332
+ // Initiate the call into the JavaScript callback.
333
+ int rc = napi_call_threadsafe_function(
334
+ addon_data->tsfn_on_resp_body,
335
+ item,
336
+ napi_tsfn_blocking);
337
+ if (rc != napi_ok) {
338
+ napi_throw_error(addon_data->env, "EINVAL", "failure to invoke JS callback");
339
+ }
340
+
341
+ }
342
+
343
+
344
+ /**
345
+ * This function is responsible for calling the JavaScript on_resp callback function
346
+ * that was specified when the Ziti_https_request(...) was called from JavaScript.
347
+ */
348
+ static void CallJs_on_resp(napi_env env, napi_value js_cb, void* context, void* data) {
349
+
350
+ ZITI_NODEJS_LOG(DEBUG, "entered");
351
+
352
+ // This parameter is not used.
353
+ (void) context;
354
+
355
+ // Retrieve the HttpsRespItem created by the worker thread.
356
+ HttpsRespItem* item = (HttpsRespItem*)data;
357
+ ZITI_NODEJS_LOG(DEBUG, "HttpsRespItem is: %p", item);
358
+
359
+
360
+ // env and js_cb may both be NULL if Node.js is in its cleanup phase, and
361
+ // items are left over from earlier thread-safe calls from the worker thread.
362
+ // When env is NULL, we simply skip over the call into Javascript
363
+ if (env != NULL) {
364
+
365
+ napi_value undefined;
366
+
367
+ // Retrieve the JavaScript `undefined` value so we can use it as the `this`
368
+ // value of the JavaScript function call.
369
+ napi_get_undefined(env, &undefined);
370
+
371
+ // const obj = {}
372
+ napi_value js_http_item, js_req, js_code, js_status;
373
+ int rc = napi_create_object(env, &js_http_item);
374
+ if (rc != napi_ok) {
375
+ napi_throw_error(env, "EINVAL", "failure to create object");
376
+ }
377
+
378
+ // obj.req = req
379
+ napi_create_int64(env, (int64_t)item->req, &js_req);
380
+ if (rc != napi_ok) {
381
+ napi_throw_error(env, "EINVAL", "failure to create resp.req");
382
+ }
383
+ rc = napi_set_named_property(env, js_http_item, "req", js_req);
384
+ if (rc != napi_ok) {
385
+ napi_throw_error(env, "EINVAL", "failure to set named property req");
386
+ }
387
+ ZITI_NODEJS_LOG(DEBUG, "js_req: %p", item->req);
388
+
389
+ // obj.code = code
390
+ rc = napi_create_int32(env, item->code, &js_code);
391
+ if (rc != napi_ok) {
392
+ napi_throw_error(env, "EINVAL", "failure to create resp.code");
393
+ }
394
+ rc = napi_set_named_property(env, js_http_item, "code", js_code);
395
+ if (rc != napi_ok) {
396
+ napi_throw_error(env, "EINVAL", "failure to set named property code");
397
+ }
398
+ ZITI_NODEJS_LOG(DEBUG, "code: %d", item->code);
399
+
400
+ // obj.status = status
401
+ rc = napi_create_string_utf8(env, (const char*)item->status, strlen(item->status), &js_status);
402
+ if (rc != napi_ok) {
403
+ napi_throw_error(env, "EINVAL", "failure to create resp.status");
404
+ }
405
+ rc = napi_set_named_property(env, js_http_item, "status", js_status);
406
+ if (rc != napi_ok) {
407
+ napi_throw_error(env, "EINVAL", "failure to set named property status");
408
+ }
409
+ ZITI_NODEJS_LOG(DEBUG, "status: %s", item->status);
410
+
411
+ // const headers = {}
412
+ um_http_hdr *h;
413
+ int i = 0;
414
+ napi_value js_headers, js_header_value;
415
+ napi_value js_cookies_array = NULL;
416
+
417
+ rc = napi_create_object(env, &js_headers);
418
+ if (rc != napi_ok) {
419
+ napi_throw_error(env, "EINVAL", "failure to create js_headers object");
420
+ }
421
+
422
+ for (h = item->headers; h != NULL && h->name != NULL; h++) {
423
+
424
+ if (strcasecmp(h->name, "set-cookie") == 0) {
425
+ if (NULL == js_cookies_array) {
426
+ rc = napi_create_array(env, &js_cookies_array);
427
+ if (rc != napi_ok) {
428
+ napi_throw_error(env, "EINVAL", "failure to create js_cookies_array");
429
+ }
430
+ }
431
+ rc = napi_create_string_utf8(env, (const char*)h->value, strlen(h->value), &js_header_value);
432
+ if (rc != napi_ok) {
433
+ napi_throw_error(env, "EINVAL", "failure to create js_header_value");
434
+ }
435
+ rc = napi_set_element(env, js_cookies_array, i++, js_header_value);
436
+ if (rc != napi_ok) {
437
+ napi_throw_error(env, "EINVAL", "failure to set js_header_element");
438
+ }
439
+ } else {
440
+ rc = napi_create_string_utf8(env, (const char*)h->value, strlen(h->value), &js_header_value);
441
+ if (rc != napi_ok) {
442
+ napi_throw_error(env, "EINVAL", "failure to create js_header_value");
443
+ }
444
+ rc = napi_set_named_property(env, js_headers, h->name, js_header_value);
445
+ if (rc != napi_ok) {
446
+ napi_throw_error(env, "EINVAL", "failure to set header");
447
+ }
448
+ }
449
+ }
450
+ if (NULL != js_cookies_array) {
451
+ rc = napi_set_named_property(env, js_headers, "Set-Cookie", js_cookies_array);
452
+ if (rc != napi_ok) {
453
+ napi_throw_error(env, "EINVAL", "failure to set set-cookie header");
454
+ }
455
+ }
456
+
457
+ // obj.headers = headers
458
+ rc = napi_set_named_property(env, js_http_item, "headers", js_headers);
459
+ if (rc != napi_ok) {
460
+ napi_throw_error(env, "EINVAL", "failure to set named property headers");
461
+ }
462
+
463
+ // Call the JavaScript function and pass it the HttpsRespItem
464
+ rc = napi_call_function(
465
+ env,
466
+ undefined,
467
+ js_cb,
468
+ 1,
469
+ &js_http_item,
470
+ NULL
471
+ );
472
+ if (rc != napi_ok) {
473
+ napi_throw_error(env, "EINVAL", "failure to invoke JS callback");
474
+ }
475
+
476
+ }
477
+ }
478
+
479
+
480
+ /**
481
+ *
482
+ */
483
+ void on_resp(um_http_resp_t *resp, void *data) {
484
+ ZITI_NODEJS_LOG(DEBUG, "resp: %p", resp);
485
+
486
+ HttpsAddonData* addon_data = (HttpsAddonData*) data;
487
+ ZITI_NODEJS_LOG(DEBUG, "addon_data->httpsReq is: %p", addon_data->httpsReq);
488
+
489
+ addon_data->httpsReq->on_resp_has_fired = true;
490
+ addon_data->httpsReq->respCode = resp->code;
491
+
492
+ HttpsRespItem* item = calloc(1, sizeof(*item));
493
+ ZITI_NODEJS_LOG(DEBUG, "new HttpsRespItem is: %p", item);
494
+
495
+ // Grab everything off the um_http_resp_t that we need to eventually pass on to the JS on_resp callback.
496
+ // If we wait until CallJs_on_resp is invoked to do that work, the um_http_resp_t may have already been free'd by the C-SDK
497
+
498
+ item->req = resp->req;
499
+ ZITI_NODEJS_LOG(DEBUG, "item->req: %p", item->req);
500
+
501
+ item->code = resp->code;
502
+ ZITI_NODEJS_LOG(DEBUG, "item->code: %d", item->code);
503
+
504
+ item->status = strdup(resp->status);
505
+ ZITI_NODEJS_LOG(DEBUG, "item->status: %s", item->status);
506
+
507
+ int header_cnt = 0;
508
+ um_http_hdr *h;
509
+ LIST_FOREACH(h, &resp->headers, _next) {
510
+ header_cnt++;
511
+ }
512
+ ZITI_NODEJS_LOG(DEBUG, "header_cnt: %d", header_cnt);
513
+
514
+ item->headers = calloc(header_cnt + 1, sizeof(um_http_hdr));
515
+ header_cnt = 0;
516
+ LIST_FOREACH(h, &resp->headers, _next) {
517
+ item->headers[header_cnt].name = strdup(h->name);
518
+ item->headers[header_cnt].value = strdup(h->value);
519
+ ZITI_NODEJS_LOG(DEBUG, "item->headers[%d]: %s : %s", header_cnt, item->headers[header_cnt].name, item->headers[header_cnt].value);
520
+ header_cnt++;
521
+ }
522
+
523
+ if ((UV_EOF == resp->code) || (resp->code < 0)) {
524
+ ZITI_NODEJS_LOG(ERROR, "<--------- returning httpsClient [%p] back to pool due to error: [%d]", addon_data->httpsClient, resp->code);
525
+ addon_data->httpsClient->active = false;
526
+
527
+ // Before we fully release this client (via semaphore post) let's indicate purge is needed, because after errs happen on a client,
528
+ // subsequent requests using that client never get processed.
529
+ ZITI_NODEJS_LOG(DEBUG, "*********** due to error, purge now necessary for client: [%p]", addon_data->httpsClient);
530
+ addon_data->httpsClient->purge = true;
531
+
532
+ struct ListMap* clientListMap = getInnerListMapValueForKey(HttpsClientListMap, addon_data->scheme_host_port);
533
+
534
+ ZITI_NODEJS_LOG(DEBUG, "<-------- returning sem for client: [%p] ", addon_data->httpsClient);
535
+ uv_sem_post(&(clientListMap->sem));
536
+ ZITI_NODEJS_LOG(DEBUG, " after returning sem for client: [%p] ", addon_data->httpsClient);
537
+ }
538
+
539
+ // Initiate the call into the JavaScript callback. The call into JavaScript will not have happened when this function returns, but it will be queued.
540
+ int rc = napi_call_threadsafe_function(
541
+ addon_data->tsfn_on_resp,
542
+ item,
543
+ napi_tsfn_blocking);
544
+ if (rc != napi_ok) {
545
+ napi_throw_error(addon_data->env, "EINVAL", "failure to invoke JS callback");
546
+ }
547
+
548
+ if (UV_EOF != resp->code) {
549
+ // We need body of the HTTP response, so wire up that callback now
550
+ resp->body_cb = on_resp_body;
551
+ }
552
+ }
553
+
554
+
555
+ /**
556
+ * This function is responsible for calling the JavaScript on_resp callback function
557
+ * that was specified when the Ziti_https_request(...) was called from JavaScript.
558
+ */
559
+ static void CallJs_on_req(napi_env env, napi_value js_cb, void* context, void* data) {
560
+
561
+ ZITI_NODEJS_LOG(DEBUG, "entered");
562
+
563
+ // This parameter is not used.
564
+ (void) context;
565
+
566
+ // Retrieve the addon_data created by the worker thread.
567
+ HttpsAddonData* addon_data = (HttpsAddonData*) data;
568
+
569
+ // env and js_cb may both be NULL if Node.js is in its cleanup phase, and
570
+ // items are left over from earlier thread-safe calls from the worker thread.
571
+ // When env is NULL, we simply skip over the call into Javascript
572
+ if (env != NULL) {
573
+
574
+ napi_value undefined;
575
+
576
+ // Retrieve the JavaScript `undefined` value so we can use it as the `this`
577
+ // value of the JavaScript function call.
578
+ napi_get_undefined(env, &undefined);
579
+
580
+ // const obj = {}
581
+ napi_value js_http_item, js_req;
582
+ int rc = napi_create_object(env, &js_http_item);
583
+ if (rc != napi_ok) {
584
+ napi_throw_error(env, "EINVAL", "failure to create object");
585
+ }
586
+
587
+ // obj.req = req
588
+ napi_create_int64(env, (int64_t)addon_data->httpsReq, &js_req);
589
+ if (rc != napi_ok) {
590
+ napi_throw_error(env, "EINVAL", "failure to create resp.req");
591
+ }
592
+ rc = napi_set_named_property(env, js_http_item, "req", js_req);
593
+ if (rc != napi_ok) {
594
+ napi_throw_error(env, "EINVAL", "failure to set named property req");
595
+ }
596
+ ZITI_NODEJS_LOG(DEBUG, "js_req: %p", addon_data->httpsReq);
597
+
598
+ // Call the JavaScript function and pass it the req
599
+ rc = napi_call_function(
600
+ env,
601
+ undefined,
602
+ js_cb,
603
+ 1,
604
+ &js_http_item,
605
+ NULL
606
+ );
607
+ if (rc != napi_ok) {
608
+ napi_throw_error(env, "EINVAL", "failure to invoke JS callback");
609
+ }
610
+
611
+ }
612
+ }
613
+
614
+
615
+ /**
616
+ *
617
+ */
618
+ void on_client(uv_work_t* req, int status) {
619
+
620
+ ZITI_NODEJS_LOG(DEBUG, "on_client() entered, uv_work_t is: %p, status is: %d", req, status);
621
+
622
+ HttpsAddonData* addon_data = (HttpsAddonData*) req->data;
623
+ ZITI_NODEJS_LOG(DEBUG, "client is: [%p]", addon_data->httpsClient);
624
+
625
+ // Initiate the request: HTTP -> TLS -> Ziti -> Service
626
+ um_http_req_t *r = um_http_req(
627
+ &(addon_data->httpsClient->client),
628
+ addon_data->method,
629
+ addon_data->path,
630
+ on_resp,
631
+ addon_data /* Pass our addon data around so we can eventually find our way back to the JS callback */
632
+ );
633
+
634
+ ZITI_NODEJS_LOG(DEBUG, "req: %p", r);
635
+ addon_data->httpsReq->req = r;
636
+
637
+ // Add headers to request
638
+ for (int i = 0; i < (int)addon_data->headers_array_length; i++) {
639
+ um_http_req_header(r, addon_data->header_name[i], addon_data->header_value[i]);
640
+ // free(addon_data->header_name[i]);
641
+ // free(addon_data->header_value[i]);
642
+ }
643
+
644
+ // Initiate the call into the JavaScript callback. The call into JavaScript will not have happened when this function returns, but it will be queued.
645
+ int rc = napi_call_threadsafe_function(
646
+ addon_data->tsfn_on_req,
647
+ addon_data,
648
+ napi_tsfn_blocking);
649
+ if (rc != napi_ok) {
650
+ napi_throw_error(addon_data->env, "EINVAL", "failure to invoke JS callback");
651
+ }
652
+ }
653
+
654
+
655
+
656
+
657
+ /**
658
+ * Initiate an HTTPS request
659
+ *
660
+ * @param {string} [0] url
661
+ * @param {string} [1] method
662
+ * @param {string[]} [2] headers; Array of strings of the form "name:value"
663
+ * @param {func} [3] JS on_req callback; This is invoked from 'on_client' function above
664
+ * @param {func} [4] JS on_resp callback; This is invoked from 'on_resp' function above
665
+ * @param {func} [5] JS on_resp_data callback; This is invoked from 'on_resp_data' function above
666
+ *
667
+ * @returns {um_http_req_t} req This allows the JS to subsequently write the Body to the request (see _Ziti_http_request_data)
668
+
669
+ */
670
+ napi_value _Ziti_http_request(napi_env env, const napi_callback_info info) {
671
+
672
+ napi_status status;
673
+ size_t result;
674
+ napi_value jsRetval;
675
+ char* query = "";
676
+ int rc;
677
+
678
+ ZITI_NODEJS_LOG(DEBUG, "entered");
679
+
680
+ if (NULL == HttpsClientListMap) {
681
+ HttpsClientListMap = newListMap();
682
+ }
683
+
684
+ size_t argc = 6;
685
+ napi_value args[6];
686
+ status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
687
+ if (status != napi_ok) {
688
+ napi_throw_error(env, NULL, "Failed to parse arguments");
689
+ }
690
+
691
+ if (argc < 6) {
692
+ napi_throw_error(env, "EINVAL", "Too few arguments");
693
+ return NULL;
694
+ }
695
+
696
+ HttpsAddonData* addon_data = calloc(1, sizeof(HttpsAddonData));
697
+ ZITI_NODEJS_LOG(DEBUG, "allocated addon_data : %p", addon_data);
698
+ addon_data->env = env;
699
+
700
+ // Obtain url length
701
+ size_t url_len;
702
+ status = napi_get_value_string_utf8(env, args[0], NULL, 0, &url_len);
703
+ if (status != napi_ok) {
704
+ napi_throw_error(env, "EINVAL", "url is not a string");
705
+ }
706
+
707
+ // Obtain url
708
+ char* url = calloc(1, url_len+2);
709
+ status = napi_get_value_string_utf8(env, args[0], url, url_len+1, &result);
710
+ if (status != napi_ok) {
711
+ napi_throw_error(env, "EINVAL", "Failed to obtain url");
712
+ }
713
+
714
+ ZITI_NODEJS_LOG(DEBUG, "url: %s", url);
715
+
716
+ struct http_parser_url url_parse = {0};
717
+ rc = http_parser_parse_url(url, strlen(url), false, &url_parse);
718
+ if (rc != 0) {
719
+ napi_throw_error(env, "EINVAL", "Failed to parse url");
720
+ }
721
+
722
+ if (url_parse.field_set & (U1 << (unsigned int) UF_HOST)) {
723
+ addon_data->service = strndup(url + url_parse.field_data[UF_HOST].off, url_parse.field_data[UF_HOST].len);
724
+ }
725
+ else {
726
+ ZITI_NODEJS_LOG(ERROR, "invalid URL: no host");
727
+ napi_throw_error(env, "EINVAL", "invalid URL: no host");
728
+ }
729
+
730
+ ZITI_NODEJS_LOG(DEBUG, "service: %s", addon_data->service);
731
+
732
+ if (url_parse.field_set & (U1 << (unsigned int) UF_PATH)) {
733
+ addon_data->path = strndup(url + url_parse.field_data[UF_PATH].off, url_parse.field_data[UF_PATH].len);
734
+ }
735
+ else {
736
+ ZITI_NODEJS_LOG(ERROR, "invalid URL: no path");
737
+ napi_throw_error(env, "EINVAL", "invalid URL: no path");
738
+ }
739
+
740
+ ZITI_NODEJS_LOG(DEBUG, "path: [%s]", addon_data->path);
741
+
742
+ if (url_parse.field_set & (U1 << (unsigned int) UF_QUERY)) {
743
+ query = strndup(url + url_parse.field_data[UF_QUERY].off, url_parse.field_data[UF_QUERY].len);
744
+ ZITI_NODEJS_LOG(DEBUG, "query: [%s]", query);
745
+ char* expanded_path = calloc(1, strlen(addon_data->path)+strlen(query)+2);
746
+
747
+ strcat(expanded_path, addon_data->path);
748
+ strcat(expanded_path, "?");
749
+ strcat(expanded_path, query);
750
+ ZITI_NODEJS_LOG(DEBUG, "expanded_path: [%s]", expanded_path);
751
+ free(addon_data->path);
752
+ addon_data->path = expanded_path;
753
+ }
754
+ else {
755
+ ZITI_NODEJS_LOG(DEBUG, "URL: no query found");
756
+ }
757
+ ZITI_NODEJS_LOG(DEBUG, "adjusted path: [%s]", addon_data->path);
758
+
759
+ addon_data->scheme_host_port = strndup(url + url_parse.field_data[UF_SCHEMA].off, url_parse.field_data[UF_PATH].off);
760
+
761
+ ZITI_NODEJS_LOG(DEBUG, "scheme_host_port: %s", addon_data->scheme_host_port);
762
+
763
+
764
+ // Obtain method length
765
+ size_t method_len;
766
+ status = napi_get_value_string_utf8(env, args[1], NULL, 0, &method_len);
767
+ if (status != napi_ok) {
768
+ napi_throw_error(env, "EINVAL", "method is not a string");
769
+ }
770
+ // Obtain method
771
+ addon_data->method = calloc(1, method_len+2);
772
+ status = napi_get_value_string_utf8(env, args[1], addon_data->method, method_len+1, &result);
773
+ if (status != napi_ok) {
774
+ napi_throw_error(env, "EINVAL", "Failed to obtain method");
775
+ }
776
+
777
+ ZITI_NODEJS_LOG(DEBUG, "method: %s", addon_data->method);
778
+
779
+ // Obtain ptr to JS on_req callback function
780
+ napi_value js_cb = args[3];
781
+ napi_value work_name;
782
+
783
+
784
+ HttpsReq* httpsReq = calloc(1, sizeof(HttpsReq));
785
+ addon_data->httpsReq = httpsReq;
786
+ httpsReq->addon_data = addon_data;
787
+
788
+ // Create a string to describe this asynchronous operation.
789
+ rc = napi_create_string_utf8(env, "on_req", NAPI_AUTO_LENGTH, &work_name);
790
+ if (rc != napi_ok) {
791
+ napi_throw_error(env, "EINVAL", "Failed to create string");
792
+ }
793
+
794
+ // Convert the callback retrieved from JavaScript into a thread-safe function (tsfn)
795
+ // which we can call from a worker thread.
796
+ rc = napi_create_threadsafe_function(
797
+ env,
798
+ js_cb,
799
+ NULL,
800
+ work_name,
801
+ 0,
802
+ 1,
803
+ NULL,
804
+ NULL,
805
+ NULL,
806
+ CallJs_on_req,
807
+ &(addon_data->tsfn_on_req)
808
+ );
809
+ if (rc != napi_ok) {
810
+ napi_throw_error(env, "EINVAL", "Failed to create threadsafe_function");
811
+ }
812
+ ZITI_NODEJS_LOG(DEBUG, "napi_create_threadsafe_function addon_data->tsfn_on_req() : %p", addon_data->tsfn_on_req);
813
+
814
+ // Obtain ptr to JS on_resp callback function
815
+ napi_value js_cb2 = args[4];
816
+
817
+ // Create a string to describe this asynchronous operation.
818
+ rc = napi_create_string_utf8(env, "on_resp", NAPI_AUTO_LENGTH, &work_name);
819
+ if (rc != napi_ok) {
820
+ napi_throw_error(env, "EINVAL", "Failed to create string");
821
+ }
822
+
823
+ // Convert the callback retrieved from JavaScript into a thread-safe function (tsfn)
824
+ // which we can call from a worker thread.
825
+ rc = napi_create_threadsafe_function(
826
+ env,
827
+ js_cb2,
828
+ NULL,
829
+ work_name,
830
+ 0,
831
+ 1,
832
+ NULL,
833
+ NULL,
834
+ NULL,
835
+ CallJs_on_resp,
836
+ &(addon_data->tsfn_on_resp)
837
+ );
838
+ if (rc != napi_ok) {
839
+ napi_throw_error(env, "EINVAL", "Failed to create threadsafe_function");
840
+ }
841
+ ZITI_NODEJS_LOG(DEBUG, "napi_create_threadsafe_function addon_data->tsfn_on_resp() : %p", addon_data->tsfn_on_resp);
842
+
843
+
844
+ // Obtain ptr to JS on_resp_data callback function
845
+ napi_value js_cb3 = args[5];
846
+
847
+ // Create a string to describe this asynchronous operation.
848
+ rc = napi_create_string_utf8(env, "on_resp_data", NAPI_AUTO_LENGTH, &work_name);
849
+ if (rc != napi_ok) {
850
+ napi_throw_error(env, "EINVAL", "Failed to create string");
851
+ }
852
+
853
+ // Convert the callback retrieved from JavaScript into a thread-safe function (tsfn)
854
+ // which we can call from a worker thread.
855
+ rc = napi_create_threadsafe_function(
856
+ env,
857
+ js_cb3,
858
+ NULL,
859
+ work_name,
860
+ 0,
861
+ 1,
862
+ NULL,
863
+ NULL,
864
+ NULL,
865
+ CallJs_on_resp_body,
866
+ &(addon_data->tsfn_on_resp_body)
867
+ );
868
+ if (rc != napi_ok) {
869
+ napi_throw_error(env, "EINVAL", "Failed to create threadsafe_function");
870
+ }
871
+ ZITI_NODEJS_LOG(DEBUG, "napi_create_threadsafe_function addon_data->tsfn_on_resp_body() : %p", addon_data->tsfn_on_resp_body);
872
+
873
+ //
874
+ // Capture headers
875
+ //
876
+ uint32_t i;
877
+ status = napi_get_array_length(env, args[2], &addon_data->headers_array_length);
878
+ if (status != napi_ok) {
879
+ napi_throw_error(env, "EINVAL", "Failed to obtain headers array");
880
+ }
881
+ ZITI_NODEJS_LOG(DEBUG, "headers_array_length: %d", addon_data->headers_array_length);
882
+ for (i = 0; i < addon_data->headers_array_length; i++) {
883
+
884
+ napi_value headers_array_element;
885
+ status = napi_get_element(env, args[2], i, &headers_array_element);
886
+ if (status != napi_ok) {
887
+ napi_throw_error(env, "EINVAL", "Failed to obtain headers element");
888
+ }
889
+
890
+ // Obtain element length
891
+ size_t element_len;
892
+ status = napi_get_value_string_utf8(env, headers_array_element, NULL, 0, &element_len);
893
+ if (status != napi_ok) {
894
+ napi_throw_error(env, "EINVAL", "header arry element is not a string");
895
+ }
896
+ ZITI_NODEJS_LOG(DEBUG, "element_len: %zd", element_len);
897
+
898
+ if (element_len > 8*1024) {
899
+ ZITI_NODEJS_LOG(DEBUG, "skipping header element; length too long");
900
+ addon_data->httpsReq->on_resp_has_fired = true;
901
+ break;
902
+ }
903
+
904
+ // Obtain element
905
+ char* header_element = calloc(1, element_len+2);
906
+ status = napi_get_value_string_utf8(env, headers_array_element, header_element, element_len+1, &result);
907
+ if (status != napi_ok) {
908
+ napi_throw_error(env, "EINVAL", "Failed to obtain element");
909
+ }
910
+ ZITI_NODEJS_LOG(DEBUG, "header_element: %s", header_element);
911
+
912
+ char * header_name = strtok(header_element, ":");
913
+ if (NULL == header_name) {
914
+ napi_throw_error(env, "EINVAL", "Failed to split header element");
915
+ }
916
+ char * header_value = strtok(NULL, ":");
917
+ if (strlen(header_value) < 1) {
918
+ napi_throw_error(env, "EINVAL", "Failed to split header element");
919
+ }
920
+
921
+ addon_data->header_name[i] = strdup(header_name);
922
+ addon_data->header_value[i] = strdup(header_value);
923
+
924
+ free(header_element);
925
+ }
926
+
927
+ //
928
+ // Queue the HTTP request. First thing that happens in the flow is to allocate a client from the pool
929
+ //
930
+ addon_data->uv_req.data = addon_data;
931
+ uv_queue_work(thread_loop, &addon_data->uv_req, allocate_client, on_client);
932
+
933
+ ZITI_NODEJS_LOG(DEBUG, "uv_queue_work of allocate_client returned req: %p", &(addon_data->uv_req));
934
+
935
+ //
936
+ // We always return zero. The real results/status are returned via the multiple callbacks
937
+ //
938
+ status = napi_create_int64(env, (int64_t)0, &jsRetval);
939
+ if (status != napi_ok) {
940
+ napi_throw_error(env, NULL, "Unable to create return value");
941
+ }
942
+ return jsRetval;
943
+ }
944
+
945
+
946
+ void expose_ziti_https_request(napi_env env, napi_value exports) {
947
+ napi_status status;
948
+ napi_value fn;
949
+
950
+ status = napi_create_function(env, NULL, 0, _Ziti_http_request, NULL, &fn);
951
+ if (status != napi_ok) {
952
+ napi_throw_error(env, NULL, "Unable to wrap native function '_Ziti_http_request");
953
+ }
954
+
955
+ status = napi_set_named_property(env, exports, "Ziti_http_request", fn);
956
+ if (status != napi_ok) {
957
+ napi_throw_error(env, NULL, "Unable to populate exports for 'Ziti_http_request");
958
+ }
959
+
960
+ }