pinokiod 3.8.37 → 3.8.38
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.
- package/kernel/caddy.js +219 -100
- package/kernel/util.js +10 -5
- package/package.json +1 -1
- package/server/index.js +4 -2
- package/server/views/proxy.ejs +5 -3
package/kernel/caddy.js
CHANGED
|
@@ -37,10 +37,20 @@ class Caddy {
|
|
|
37
37
|
let internal_ip = "127.0.0.1:42000"
|
|
38
38
|
let namespaced_url = "pinokio.localhost"
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
let current_lan_ip = this.kernel.peer.current_ip
|
|
41
|
+
let current_lan_username = this.kernel.peer.account(current_lan_ip)
|
|
42
|
+
|
|
43
|
+
//if (!this._mapping[internal_ip]) {
|
|
44
|
+
// this._mapping[internal_ip] = new Set()
|
|
45
|
+
//}
|
|
46
|
+
//this._mapping[internal_ip].add(namespaced_url)
|
|
47
|
+
if (!this._mapping[current_lan_ip]) {
|
|
48
|
+
this._mapping[current_lan_ip] = {}
|
|
49
|
+
}
|
|
50
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
51
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
42
52
|
}
|
|
43
|
-
this._mapping[internal_ip].add(namespaced_url)
|
|
53
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
44
54
|
|
|
45
55
|
return {
|
|
46
56
|
"apps": {
|
|
@@ -158,13 +168,25 @@ class Caddy {
|
|
|
158
168
|
}]
|
|
159
169
|
}
|
|
160
170
|
|
|
171
|
+
let current_lan_ip = ip
|
|
172
|
+
let current_lan_username = this.kernel.peer.account(ip)
|
|
173
|
+
|
|
161
174
|
// proxy proxy => proxy
|
|
162
175
|
if (key === "url") {
|
|
163
176
|
let namespaced_url = `${name}.${account}.localhost`.toLowerCase()
|
|
164
|
-
if (!this._mapping[internal_ip]) {
|
|
165
|
-
|
|
177
|
+
//if (!this._mapping[internal_ip]) {
|
|
178
|
+
// this._mapping[internal_ip] = new Set()
|
|
179
|
+
//}
|
|
180
|
+
//this._mapping[internal_ip].add(namespaced_url)
|
|
181
|
+
|
|
182
|
+
if (!this._mapping[current_lan_ip]) {
|
|
183
|
+
this._mapping[current_lan_ip] = {}
|
|
184
|
+
}
|
|
185
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
186
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
166
187
|
}
|
|
167
|
-
this._mapping[internal_ip].add(namespaced_url)
|
|
188
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
189
|
+
|
|
168
190
|
payload.apps.http.servers.main.routes.push({
|
|
169
191
|
"match": [{
|
|
170
192
|
"host": [namespaced_url]
|
|
@@ -189,10 +211,19 @@ class Caddy {
|
|
|
189
211
|
})
|
|
190
212
|
}
|
|
191
213
|
let namespaced_url = `${name}.${account}.localhost`.toLowerCase()
|
|
192
|
-
if (!this._mapping[internal_ip]) {
|
|
193
|
-
|
|
214
|
+
//if (!this._mapping[internal_ip]) {
|
|
215
|
+
// this._mapping[internal_ip] = new Set()
|
|
216
|
+
//}
|
|
217
|
+
//this._mapping[internal_ip].add(namespaced_url)
|
|
218
|
+
|
|
219
|
+
if (!this._mapping[current_lan_ip]) {
|
|
220
|
+
this._mapping[current_lan_ip] = {}
|
|
221
|
+
}
|
|
222
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
223
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
194
224
|
}
|
|
195
|
-
this._mapping[internal_ip].add(namespaced_url)
|
|
225
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
226
|
+
|
|
196
227
|
payload.apps.http.servers.main.routes.push({
|
|
197
228
|
"match": [{
|
|
198
229
|
"host": [namespaced_url],
|
|
@@ -246,14 +277,27 @@ class Caddy {
|
|
|
246
277
|
let chunks = internal_ip.split(":")
|
|
247
278
|
if (chunks.length === 2) {
|
|
248
279
|
if (key === "url") {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
//if (!this._mapping[val]) {
|
|
281
|
+
// this._mapping[val] = new Set()
|
|
282
|
+
//}
|
|
283
|
+
//this._mapping[val].add(namespaced_url)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
let namespaced_url = `${name}.localhost`.toLowerCase()
|
|
288
|
+
let current_lan_ip = this.kernel.peer.current_ip
|
|
289
|
+
let current_lan_username = this.kernel.peer.account(current_lan_ip)
|
|
290
|
+
if (!this._mapping[current_lan_ip]) {
|
|
291
|
+
this._mapping[current_lan_ip] = {}
|
|
292
|
+
}
|
|
293
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
294
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
252
295
|
}
|
|
253
|
-
this._mapping[
|
|
296
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
297
|
+
|
|
254
298
|
payload.apps.http.servers.main.routes.push({
|
|
255
299
|
"match": [{
|
|
256
|
-
"host": [
|
|
300
|
+
"host": [namespaced_url]
|
|
257
301
|
}],
|
|
258
302
|
"handle": [
|
|
259
303
|
{
|
|
@@ -274,9 +318,21 @@ class Caddy {
|
|
|
274
318
|
]
|
|
275
319
|
})
|
|
276
320
|
}
|
|
321
|
+
let namespaced_url = `${key}.${name}.localhost`.toLowerCase()
|
|
322
|
+
|
|
323
|
+
let current_lan_ip = this.kernel.peer.current_ip
|
|
324
|
+
let current_lan_username = this.kernel.peer.account(current_lan_ip)
|
|
325
|
+
if (!this._mapping[current_lan_ip]) {
|
|
326
|
+
this._mapping[current_lan_ip] = {}
|
|
327
|
+
}
|
|
328
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
329
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
330
|
+
}
|
|
331
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
332
|
+
|
|
277
333
|
payload.apps.http.servers.main.routes.push({
|
|
278
334
|
"match": [{
|
|
279
|
-
"host": [
|
|
335
|
+
"host": [namespaced_url],
|
|
280
336
|
}],
|
|
281
337
|
"handle": [
|
|
282
338
|
{
|
|
@@ -316,86 +372,110 @@ class Caddy {
|
|
|
316
372
|
// caddy_mapping := { IP => HTTPS_DOMAIN }
|
|
317
373
|
let caddy_mapping = this.kernel.peer.caddy(ip)
|
|
318
374
|
console.log({ caddy_mapping })
|
|
319
|
-
for(let
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
"
|
|
344
|
-
|
|
375
|
+
for(let lan_ip in caddy_mapping) {
|
|
376
|
+
let https_domains_mapping = caddy_mapping[lan_ip]
|
|
377
|
+
for(let internal_ip in https_domains_mapping) {
|
|
378
|
+
|
|
379
|
+
// create a proxy proxy
|
|
380
|
+
// 1. create an exposed port listening on 0.0.0.0
|
|
381
|
+
// 2. the exposed port points to the internal ip
|
|
382
|
+
let chunks = internal_ip.split(":")
|
|
383
|
+
let INTERNAL_HOST = chunks[0]
|
|
384
|
+
let INTERNAL_PORT = chunks[1]
|
|
385
|
+
|
|
386
|
+
this.ip_mapping[INTERNAL_PORT] = ip
|
|
387
|
+
|
|
388
|
+
payload.apps.http.servers[":" + INTERNAL_PORT] = {
|
|
389
|
+
"listen": ["0.0.0.0:PORT_PLACEHOLDER_" + ip + ":" + INTERNAL_PORT],
|
|
390
|
+
"routes": [{
|
|
391
|
+
"handle": [
|
|
392
|
+
{
|
|
393
|
+
"handler": "reverse_proxy",
|
|
394
|
+
"upstreams": [{ "dial": internal_ip }]
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"handler": "headers",
|
|
398
|
+
"response": {
|
|
399
|
+
"set": {
|
|
400
|
+
"Access-Control-Allow-Origin": ["*"],
|
|
401
|
+
"Access-Control-Allow-Methods": ["GET, POST, OPTIONS, PUT, DELETE"],
|
|
402
|
+
"Access-Control-Allow-Headers": ["*"],
|
|
403
|
+
"Vary": ["Origin"]
|
|
404
|
+
}
|
|
345
405
|
}
|
|
346
406
|
}
|
|
347
|
-
|
|
348
|
-
]
|
|
349
|
-
}]
|
|
350
|
-
}
|
|
351
|
-
let https_domains = caddy_mapping[internal_ip]
|
|
352
|
-
console.log(">>>>>>>> https_domains", https_domains)
|
|
353
|
-
|
|
354
|
-
// 1. if https_domains is made up of one item <port>.localhost,
|
|
355
|
-
// add <port>.<account>.localhost
|
|
356
|
-
// 2. if https_domains is made up of two items <port>.localhost and <port>.<account>.localhost, don't do anything
|
|
357
|
-
for(let https_domain of https_domains) {
|
|
358
|
-
// insert the $account value right before the last part (1234.localhost => 1234.x.localhost)
|
|
359
|
-
let chunks = https_domain.split(".")
|
|
360
|
-
let namespaced_url
|
|
361
|
-
if (chunks.length === 3) {
|
|
362
|
-
namespaced_url = https_domain
|
|
363
|
-
} else if (chunks.length === 2) {
|
|
364
|
-
chunks.splice(chunks.length - 1, 0, account)
|
|
365
|
-
namespaced_url = chunks.join(".")
|
|
366
|
-
}
|
|
367
|
-
console.log("##x", { chunks, namespaced_url, https_domain })
|
|
368
|
-
if (!this._mapping[internal_ip]) {
|
|
369
|
-
this._mapping[internal_ip] = new Set()
|
|
407
|
+
]
|
|
408
|
+
}]
|
|
370
409
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
console.log(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
//let https_domains = caddy_mapping[internal_ip]
|
|
413
|
+
let https_domains = https_domains_mapping[internal_ip]
|
|
414
|
+
console.log(">>>>>>>> https_domains", https_domains)
|
|
415
|
+
|
|
416
|
+
// 1. if https_domains is made up of one item <port>.localhost,
|
|
417
|
+
// add <port>.<account>.localhost
|
|
418
|
+
// 2. if https_domains is made up of two items <port>.localhost and <port>.<account>.localhost, don't do anything
|
|
419
|
+
for(let https_domain of https_domains) {
|
|
420
|
+
// insert the $account value right before the last part (1234.localhost => 1234.x.localhost)
|
|
421
|
+
let chunks = https_domain.split(".")
|
|
422
|
+
let namespaced_url
|
|
423
|
+
if (chunks.length === 3) {
|
|
424
|
+
namespaced_url = https_domain
|
|
425
|
+
} else if (chunks.length === 2) {
|
|
426
|
+
chunks.splice(chunks.length - 1, 0, account)
|
|
427
|
+
namespaced_url = chunks.join(".")
|
|
428
|
+
}
|
|
429
|
+
console.log("##x", { chunks, namespaced_url, https_domain })
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
//if (!this._mapping[internal_ip]) {
|
|
433
|
+
// this._mapping[internal_ip] = new Set()
|
|
434
|
+
//}
|
|
435
|
+
//console.log("this.mapping", this._mapping)
|
|
436
|
+
//console.log("internal_ip", internal_ip)
|
|
437
|
+
//console.log("result", this._mapping[internal_ip])
|
|
438
|
+
//this._mapping[internal_ip].add(namespaced_url)
|
|
439
|
+
//console.log({ namespaced_url, https_domain, internal_ip })
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
let current_lan_ip = ip
|
|
443
|
+
let current_lan_username = this.kernel.peer.account(current_lan_ip)
|
|
444
|
+
if (!this._mapping[current_lan_ip]) {
|
|
445
|
+
this._mapping[current_lan_ip] = {}
|
|
446
|
+
}
|
|
447
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
448
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
449
|
+
}
|
|
450
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
451
|
+
console.log({ internal_ip, current_lan_ip })
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
// 1. point proxy URL to the 127.0.0.1:EXPOSED_PORT
|
|
455
|
+
payload.apps.http.servers.main.routes.push({
|
|
456
|
+
"match": [{
|
|
457
|
+
"host": [namespaced_url],
|
|
458
|
+
}],
|
|
459
|
+
"handle": [
|
|
460
|
+
{
|
|
461
|
+
"handler": "reverse_proxy",
|
|
462
|
+
"upstreams": [{ "dial": ip + ":PORT_PLACEHOLDER_" + ip + ":" + INTERNAL_PORT }]
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"handler": "headers",
|
|
466
|
+
"response": {
|
|
467
|
+
"set": {
|
|
468
|
+
"Access-Control-Allow-Origin": ["*"],
|
|
469
|
+
"Access-Control-Allow-Methods": ["GET, POST, OPTIONS, PUT, DELETE"],
|
|
470
|
+
"Access-Control-Allow-Headers": ["*"],
|
|
471
|
+
"Vary": ["Origin"]
|
|
472
|
+
}
|
|
394
473
|
}
|
|
395
474
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
475
|
+
]
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
399
479
|
}
|
|
400
480
|
}
|
|
401
481
|
}
|
|
@@ -407,15 +487,28 @@ class Caddy {
|
|
|
407
487
|
if (!list) {
|
|
408
488
|
list = await this.kernel.processes.refresh()
|
|
409
489
|
}
|
|
490
|
+
console.log(">>>>>>>>>> LIST", list)
|
|
410
491
|
for(let item of list) {
|
|
411
|
-
let
|
|
412
|
-
if (!this._mapping[item.ip]) {
|
|
413
|
-
|
|
492
|
+
let namespaced_url = `${item.port}.localhost`
|
|
493
|
+
//if (!this._mapping[item.ip]) {
|
|
494
|
+
// this._mapping[item.ip] = new Set()
|
|
495
|
+
//}
|
|
496
|
+
//this._mapping[item.ip].add(namespaced_url)
|
|
497
|
+
|
|
498
|
+
let internal_ip = item.ip
|
|
499
|
+
let current_lan_ip = this.kernel.peer.current_ip
|
|
500
|
+
let current_lan_username = this.kernel.peer.account(current_lan_ip)
|
|
501
|
+
if (!this._mapping[current_lan_ip]) {
|
|
502
|
+
this._mapping[current_lan_ip] = {}
|
|
503
|
+
}
|
|
504
|
+
if (!this._mapping[current_lan_ip][internal_ip]) {
|
|
505
|
+
this._mapping[current_lan_ip][internal_ip] = new Set()
|
|
414
506
|
}
|
|
415
|
-
this._mapping[
|
|
507
|
+
this._mapping[current_lan_ip][internal_ip].add(namespaced_url)
|
|
508
|
+
|
|
416
509
|
payload.apps.http.servers.main.routes.push({
|
|
417
510
|
"match": [{
|
|
418
|
-
"host": [
|
|
511
|
+
"host": [namespaced_url],
|
|
419
512
|
}],
|
|
420
513
|
"handle": [
|
|
421
514
|
{
|
|
@@ -478,13 +571,32 @@ class Caddy {
|
|
|
478
571
|
// replace :PORT_PLACEHOLDER with available ports
|
|
479
572
|
|
|
480
573
|
const jsonString = JSON.stringify(payload)
|
|
481
|
-
|
|
482
|
-
const
|
|
574
|
+
// find all PORT_PLACEHOLDER patterns
|
|
575
|
+
const matches = jsonString.match(/:PORT_PLACEHOLDER_[0-9.:]+/g)
|
|
576
|
+
console.log("matches", matches)
|
|
577
|
+
|
|
578
|
+
// iterate through them and see if the port already exists
|
|
579
|
+
let count = 0
|
|
580
|
+
if (matches) {
|
|
581
|
+
for(let match of matches) {
|
|
582
|
+
console.log("testing", match)
|
|
583
|
+
const port_match = match.match(/[0-9.]+:([0-9]+)/);
|
|
584
|
+
let port = port_match ? port_match[1] : null
|
|
585
|
+
console.log({ port, port_match })
|
|
586
|
+
if (port && !this.port_mapping[port]) {
|
|
587
|
+
count++
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
console.log("count", count)
|
|
593
|
+
console.log("&&", JSON.stringify(payload, null, 2))
|
|
483
594
|
|
|
484
595
|
if (count > 0) {
|
|
485
596
|
let ports = await this.kernel.ports(count)
|
|
486
597
|
console.log({ count, ports })
|
|
487
|
-
|
|
598
|
+
console.log("PORT_CACHE", this.port_cache)
|
|
599
|
+
let response = Util.fill_object(payload, /PORT_PLACEHOLDER_.+/, ports, this.port_cache)
|
|
488
600
|
payload = response.result
|
|
489
601
|
let replaced_map = response.replaced_map
|
|
490
602
|
for(let key in replaced_map) {
|
|
@@ -504,11 +616,18 @@ class Caddy {
|
|
|
504
616
|
//let original_port = key.replace("PORT_PLACEHOLDER_", "")
|
|
505
617
|
this.port_mapping[original_port] = replaced_map[key]
|
|
506
618
|
}
|
|
619
|
+
this.port_cache = replaced_map
|
|
507
620
|
}
|
|
508
621
|
|
|
509
622
|
this.mapping = {}
|
|
510
|
-
for(let
|
|
511
|
-
|
|
623
|
+
for(let lan_ip in this._mapping) {
|
|
624
|
+
let internal_maps = this._mapping[lan_ip]
|
|
625
|
+
if (!this.mapping[lan_ip]) {
|
|
626
|
+
this.mapping[lan_ip] = {}
|
|
627
|
+
}
|
|
628
|
+
for(let url in internal_maps) {
|
|
629
|
+
this.mapping[lan_ip][url] = Array.from(internal_maps[url])
|
|
630
|
+
}
|
|
512
631
|
}
|
|
513
632
|
|
|
514
633
|
|
package/kernel/util.js
CHANGED
|
@@ -307,19 +307,24 @@ const update_env = async (filepath, changes) => {
|
|
|
307
307
|
}, normalizeEOL(env));
|
|
308
308
|
await fs.promises.writeFile(filepath, newval)
|
|
309
309
|
};
|
|
310
|
-
function fill_object(obj, pattern, list) {
|
|
310
|
+
function fill_object(obj, pattern, list, cache) {
|
|
311
311
|
console.log("FILL_OBJECT")
|
|
312
312
|
const map = {};
|
|
313
|
-
const replaced_map = {}
|
|
313
|
+
const replaced_map = cache || {}
|
|
314
314
|
let index = 0;
|
|
315
315
|
|
|
316
316
|
function recurse(value) {
|
|
317
317
|
if (typeof value === 'string') {
|
|
318
318
|
return value.replace(pattern, (match) => {
|
|
319
319
|
if (!(match in map)) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
console.log( "fill_object", { replaced_map, match })
|
|
321
|
+
if (replaced_map[match]) {
|
|
322
|
+
map[match] = replaced_map[match]
|
|
323
|
+
} else {
|
|
324
|
+
if (index >= list.length) throw new Error("Not enough items provided");
|
|
325
|
+
map[match] = list[index++];
|
|
326
|
+
replaced_map[match] = map[match]
|
|
327
|
+
}
|
|
323
328
|
}
|
|
324
329
|
return map[match];
|
|
325
330
|
});
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -2661,9 +2661,11 @@ class Server {
|
|
|
2661
2661
|
this.app.get("/proxy", ex(async (req, res) => {
|
|
2662
2662
|
await this.kernel.caddy.update()
|
|
2663
2663
|
let list = await this.kernel.processes.refresh()
|
|
2664
|
+
let current_lan_ip = this.kernel.peer.current_ip
|
|
2664
2665
|
list = list.map((item) => {
|
|
2665
2666
|
return {
|
|
2666
|
-
proxy: this.kernel.caddy.mapping[item.ip] || [],
|
|
2667
|
+
//proxy: this.kernel.caddy.mapping[item.ip] || [],
|
|
2668
|
+
proxy: this.kernel.caddy.mapping[current_lan_ip][item.ip] || [],
|
|
2667
2669
|
...item
|
|
2668
2670
|
}
|
|
2669
2671
|
})
|
|
@@ -2751,7 +2753,7 @@ class Server {
|
|
|
2751
2753
|
}
|
|
2752
2754
|
}
|
|
2753
2755
|
|
|
2754
|
-
console.log({ port_mapping: this.kernel.caddy.port_mapping, ip_mapping: this.kernel.caddy.ip_mapping })
|
|
2756
|
+
console.log({ port_mapping: this.kernel.caddy.port_mapping, ip_mapping: this.kernel.caddy.ip_mapping, lan: this.kernel.caddy.local_network_mapping, ip: this.kernel.peer.current_ip })
|
|
2755
2757
|
res.render("proxy", {
|
|
2756
2758
|
ip: this.kernel.peer.current_ip,
|
|
2757
2759
|
peers,
|
package/server/views/proxy.ejs
CHANGED
|
@@ -274,9 +274,11 @@ td:first-child, th:first-child {
|
|
|
274
274
|
<div>
|
|
275
275
|
<a target="_blank" href="http://localhost:<%=item.port%>">http://localhost:<%=item.port%></a>
|
|
276
276
|
</div>
|
|
277
|
-
|
|
278
|
-
<
|
|
279
|
-
|
|
277
|
+
<% if (lan[ip]) { %>
|
|
278
|
+
<div>
|
|
279
|
+
<a target="_blank" href="http://<%=ip%>:<%=lan[ip][item.port]%>">http://<%=ip%>:<%=lan[ip][item.port]%></a>
|
|
280
|
+
</div>
|
|
281
|
+
<% } %>
|
|
280
282
|
</td>
|
|
281
283
|
</tr>
|
|
282
284
|
<% }) %>
|