pinokiod 3.250.0 → 3.251.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.
package/kernel/peer.js CHANGED
@@ -10,6 +10,8 @@ class PeerDiscovery {
10
10
  this.kill_message = Buffer.from("kill")
11
11
  this.interval = interval;
12
12
  this.peers = new Set();
13
+ this.interface_addresses = []
14
+ this.host_candidates = []
13
15
  this.host = this._getLocalIPAddress()
14
16
  this.default_port = 42000
15
17
  this.peers.add(this.host)
@@ -261,8 +263,11 @@ class PeerDiscovery {
261
263
  let internal_host = chunks.slice(0, chunks.length-1).join(":")
262
264
  let external_port = port_mapping[internal_port]
263
265
  let merged
266
+ const external_hosts = this._buildExternalHostEntries(external_port)
264
267
  let external_ip
265
- if (external_port) {
268
+ if (external_hosts.length > 0) {
269
+ external_ip = external_hosts[0].url
270
+ } else if (external_port) {
266
271
  external_ip = `${this.host}:${external_port}`
267
272
  }
268
273
  let internal_router = []
@@ -285,9 +290,10 @@ class PeerDiscovery {
285
290
  let info = {
286
291
  external_router: router[external_ip] || [],
287
292
  internal_router,
293
+ external_hosts,
288
294
  external_ip,
289
- external_port: parseInt(external_port),
290
- internal_port: parseInt(internal_port),
295
+ external_port: external_port ? parseInt(external_port, 10) : undefined,
296
+ internal_port: internal_port ? parseInt(internal_port, 10) : undefined,
291
297
  ...proc,
292
298
  }
293
299
  const usingCustomDomain = this.kernel.router_kind === 'custom-domain'
@@ -349,7 +355,11 @@ class PeerDiscovery {
349
355
  let internal_port = chunks[chunks.length - 1]
350
356
  let internal_host = chunks.slice(0, chunks.length - 1).join(":")
351
357
  let external_port = port_mapping ? port_mapping[internal_port] : undefined
352
- let external_ip = external_port ? `${this.host}:${external_port}` : undefined
358
+ const external_hosts = this._buildExternalHostEntries(external_port)
359
+ let external_ip = external_hosts.length > 0 ? external_hosts[0].url : undefined
360
+ if (!external_ip && external_port) {
361
+ external_ip = `${this.host}:${external_port}`
362
+ }
353
363
 
354
364
  let internal_router = []
355
365
  // Check common local keys
@@ -367,9 +377,10 @@ class PeerDiscovery {
367
377
  const info = {
368
378
  external_router: (router && external_ip && router[external_ip]) ? router[external_ip] : [],
369
379
  internal_router,
380
+ external_hosts,
370
381
  external_ip,
371
- external_port: external_port ? parseInt(external_port) : undefined,
372
- internal_port: internal_port ? parseInt(internal_port) : undefined,
382
+ external_port: external_port ? parseInt(external_port, 10) : undefined,
383
+ internal_port: internal_port ? parseInt(internal_port, 10) : undefined,
373
384
  ...proc,
374
385
  }
375
386
 
@@ -489,6 +500,7 @@ class PeerDiscovery {
489
500
  name: this.name,
490
501
  host: this.host,
491
502
  peers: peers,
503
+ host_candidates: this.host_candidates,
492
504
  port_mapping: this.kernel.router.port_mapping,
493
505
  rewrite_mapping: this.kernel.router.rewrite_mapping,
494
506
  proc: this.kernel.processes.info,
@@ -554,29 +566,170 @@ class PeerDiscovery {
554
566
  // }
555
567
  }
556
568
  _isLocalLAN(ip) {
557
- return ip.startsWith('192.168.') || ip.startsWith('10.') || (ip.startsWith('172.') && is172Private(ip));
569
+ return this.isRFC1918(ip)
558
570
  }
559
571
  _getLocalIPAddress() {
560
- const interfaces = os.networkInterfaces();
561
- for (const ifaceList of Object.values(interfaces)) {
572
+ this.interface_addresses = this._collectInterfaceAddresses()
573
+ const shareable = this.interface_addresses.filter((entry) => entry.shareable)
574
+ this.host_candidates = shareable
575
+ const lanCandidate = shareable.find((entry) => entry.scope === 'lan')
576
+ if (lanCandidate) {
577
+ return lanCandidate.address
578
+ }
579
+ const cgnatCandidate = shareable.find((entry) => entry.scope === 'cgnat')
580
+ if (cgnatCandidate) {
581
+ return cgnatCandidate.address
582
+ }
583
+ const publicCandidate = shareable.find((entry) => entry.scope === 'public')
584
+ if (publicCandidate) {
585
+ return publicCandidate.address
586
+ }
587
+ return shareable.length > 0 ? shareable[0].address : null
588
+ }
589
+ isPrivateOrCGNAT(ip) {
590
+ return this.isRFC1918(ip) || this.isCGNAT(ip)
591
+ }
592
+ isRFC1918(ip) {
593
+ if (!ip || typeof ip !== 'string') {
594
+ return false
595
+ }
596
+ const octets = ip.split('.').map(Number)
597
+ if (octets.length !== 4 || octets.some((value) => Number.isNaN(value))) {
598
+ return false
599
+ }
600
+ if (octets[0] === 10) return true
601
+ if (octets[0] === 172 && this.is172Private(octets[1])) return true
602
+ if (octets[0] === 192 && octets[1] === 168) return true
603
+ return false
604
+ }
605
+ isCGNAT(ip) {
606
+ if (!ip || typeof ip !== 'string') {
607
+ return false
608
+ }
609
+ const octets = ip.split('.').map(Number)
610
+ if (octets.length !== 4 || octets.some((value) => Number.isNaN(value))) {
611
+ return false
612
+ }
613
+ return octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127
614
+ }
615
+ is172Private(secondOctet) {
616
+ if (typeof secondOctet !== 'number' || Number.isNaN(secondOctet)) {
617
+ return false
618
+ }
619
+ return secondOctet >= 16 && secondOctet <= 31
620
+ }
621
+ _collectInterfaceAddresses() {
622
+ const interfaces = os.networkInterfaces()
623
+ const results = []
624
+ const seen = new Set()
625
+ for (const [ifaceName, ifaceList] of Object.entries(interfaces)) {
626
+ if (!Array.isArray(ifaceList)) {
627
+ continue
628
+ }
562
629
  for (const iface of ifaceList) {
563
- if (iface.family === 'IPv4' && !iface.internal) {
564
- const ip = iface.address;
565
- if (this.isPrivateOrCGNAT(ip)) {
566
- return ip;
567
- }
630
+ if (!iface || iface.family !== 'IPv4') {
631
+ continue
568
632
  }
633
+ const address = String(iface.address || '').trim()
634
+ if (!address) {
635
+ continue
636
+ }
637
+ if (seen.has(address)) {
638
+ continue
639
+ }
640
+ seen.add(address)
641
+ const classification = this.classifyAddress(address, Boolean(iface.internal))
642
+ results.push({
643
+ address,
644
+ interface: ifaceName,
645
+ internal: Boolean(iface.internal),
646
+ scope: classification.scope,
647
+ shareable: classification.shareable
648
+ })
569
649
  }
570
650
  }
571
- return null;
651
+ return results
572
652
  }
573
- isPrivateOrCGNAT(ip) {
574
- const octets = ip.split('.').map(Number);
575
- if (octets[0] === 10) return true;
576
- if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return true;
577
- if (octets[0] === 192 && octets[1] === 168) return true;
578
- if (octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127) return true; // CGNAT
579
- return false;
653
+ classifyAddress(address, isInternal = false) {
654
+ if (!address || typeof address !== 'string') {
655
+ return { scope: 'unknown', shareable: false }
656
+ }
657
+ if (isInternal || address.startsWith('127.')) {
658
+ return { scope: 'loopback', shareable: false }
659
+ }
660
+ if (this.isRFC1918(address)) {
661
+ return { scope: 'lan', shareable: true }
662
+ }
663
+ if (this.isCGNAT(address)) {
664
+ return { scope: 'cgnat', shareable: true }
665
+ }
666
+ if (address.startsWith('169.254.')) {
667
+ return { scope: 'linklocal', shareable: false }
668
+ }
669
+ const octets = address.split('.').map(Number)
670
+ if (octets.length !== 4 || octets.some((value) => Number.isNaN(value))) {
671
+ return { scope: 'unknown', shareable: false }
672
+ }
673
+ if (octets[0] === 0) {
674
+ return { scope: 'unspecified', shareable: false }
675
+ }
676
+ return { scope: 'public', shareable: true }
677
+ }
678
+ _buildExternalHostEntries(externalPort) {
679
+ if (!externalPort && externalPort !== 0) {
680
+ return []
681
+ }
682
+ const normalizedPort = parseInt(externalPort, 10)
683
+ if (!Number.isFinite(normalizedPort) || normalizedPort <= 0) {
684
+ return []
685
+ }
686
+ const prioritize = []
687
+ const pushCandidate = (candidate) => {
688
+ if (!candidate || !candidate.shareable || !candidate.address) {
689
+ return
690
+ }
691
+ if (prioritize.some((entry) => entry.address === candidate.address)) {
692
+ return
693
+ }
694
+ prioritize.push(candidate)
695
+ }
696
+ if (this.host && Array.isArray(this.host_candidates)) {
697
+ const primary = this.host_candidates.find((candidate) => candidate.address === this.host)
698
+ if (primary) {
699
+ pushCandidate(primary)
700
+ }
701
+ }
702
+ if (Array.isArray(this.host_candidates)) {
703
+ this.host_candidates.forEach((candidate) => pushCandidate(candidate))
704
+ }
705
+ if (prioritize.length === 0 && this.host) {
706
+ pushCandidate({
707
+ address: this.host,
708
+ scope: this.classifyAddress(this.host, false).scope,
709
+ shareable: true
710
+ })
711
+ }
712
+ const seen = new Set()
713
+ const entries = []
714
+ for (const candidate of prioritize) {
715
+ const host = candidate.address
716
+ if (!host) {
717
+ continue
718
+ }
719
+ const url = `${host}:${normalizedPort}`
720
+ if (seen.has(url)) {
721
+ continue
722
+ }
723
+ seen.add(url)
724
+ entries.push({
725
+ host,
726
+ port: normalizedPort,
727
+ scope: candidate.scope,
728
+ interface: candidate.interface || null,
729
+ url
730
+ })
731
+ }
732
+ return entries
580
733
  }
581
734
  }
582
735
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.250.0",
3
+ "version": "3.251.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1539,15 +1539,12 @@ class Server {
1539
1539
  drive: path.resolve(this.kernel.homedir, "drive"),
1540
1540
  }
1541
1541
  }
1542
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
1543
- let peer_qr = null
1544
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
1542
+ const peerAccess = await this.composePeerAccessPayload()
1545
1543
  let list = this.getPeers()
1546
1544
  res.render("settings", {
1547
1545
  list,
1548
1546
  current_host: this.kernel.peer.host,
1549
- peer_url,
1550
- peer_qr,
1547
+ ...peerAccess,
1551
1548
  platform,
1552
1549
  version: this.version,
1553
1550
  logo: this.logo,
@@ -2286,9 +2283,7 @@ class Server {
2286
2283
  qr_cloudflare = await QRCode.toDataURL(this.cloudflare_pub)
2287
2284
  }
2288
2285
 
2289
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
2290
- let peer_qr = null
2291
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
2286
+ const peerAccess = await this.composePeerAccessPayload()
2292
2287
 
2293
2288
  // custom theme
2294
2289
  let exists = await fse.pathExists(this.kernel.path("web"))
@@ -2319,8 +2314,7 @@ class Server {
2319
2314
  res.render("index", {
2320
2315
  list,
2321
2316
  current_host: this.kernel.peer.host,
2322
- peer_url,
2323
- peer_qr,
2317
+ ...peerAccess,
2324
2318
  current_urls,
2325
2319
  portal: this.portal,
2326
2320
  install: this.install,
@@ -3036,6 +3030,99 @@ class Server {
3036
3030
  }
3037
3031
  return list
3038
3032
  }
3033
+ scopeLabelForAccessPoint(scope) {
3034
+ if (!scope || typeof scope !== 'string') {
3035
+ return ''
3036
+ }
3037
+ const normalized = scope.trim().toLowerCase()
3038
+ switch (normalized) {
3039
+ case 'lan':
3040
+ return 'LAN'
3041
+ case 'cgnat':
3042
+ return 'VPN'
3043
+ case 'public':
3044
+ return 'Public'
3045
+ case 'loopback':
3046
+ return 'Local'
3047
+ case 'linklocal':
3048
+ return 'Link-Local'
3049
+ default:
3050
+ return ''
3051
+ }
3052
+ }
3053
+ async buildPeerAccessPoints() {
3054
+ const hostMap = new Map()
3055
+ const addHost = (candidate = {}) => {
3056
+ const raw = candidate && candidate.host ? candidate.host : candidate.address
3057
+ if (!raw || typeof raw !== 'string') {
3058
+ return
3059
+ }
3060
+ const host = raw.trim()
3061
+ if (!host) {
3062
+ return
3063
+ }
3064
+ if (candidate.shareable === false) {
3065
+ return
3066
+ }
3067
+ const existing = hostMap.get(host)
3068
+ const classify = () => {
3069
+ if (candidate.scope) {
3070
+ return candidate.scope
3071
+ }
3072
+ if (this.kernel && this.kernel.peer && typeof this.kernel.peer.classifyAddress === 'function') {
3073
+ const classification = this.kernel.peer.classifyAddress(host, false)
3074
+ return classification && classification.scope ? classification.scope : undefined
3075
+ }
3076
+ return undefined
3077
+ }
3078
+ const mergedScope = existing && existing.scope ? existing.scope : classify() || 'unknown'
3079
+ const mergedInterface = existing && existing.interface ? existing.interface : (candidate.interface || null)
3080
+ hostMap.set(host, {
3081
+ host,
3082
+ scope: mergedScope,
3083
+ interface: mergedInterface
3084
+ })
3085
+ }
3086
+
3087
+ addHost({ host: this.kernel.peer.host })
3088
+ const candidates = Array.isArray(this.kernel.peer.host_candidates) ? this.kernel.peer.host_candidates : []
3089
+ candidates.forEach((candidate) => addHost(candidate))
3090
+
3091
+ const accessPoints = []
3092
+ for (const meta of hostMap.values()) {
3093
+ const url = `http://${meta.host}:${DEFAULT_PORT}`
3094
+ let qr = null
3095
+ try {
3096
+ qr = await QRCode.toDataURL(url)
3097
+ } catch (_) {}
3098
+ accessPoints.push({
3099
+ ...meta,
3100
+ url,
3101
+ qr,
3102
+ scope_label: this.scopeLabelForAccessPoint(meta.scope)
3103
+ })
3104
+ }
3105
+ return accessPoints
3106
+ }
3107
+ async composePeerAccessPayload() {
3108
+ let peer_access_points = []
3109
+ try {
3110
+ peer_access_points = await this.buildPeerAccessPoints()
3111
+ } catch (error) {
3112
+ peer_access_points = []
3113
+ }
3114
+ let peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
3115
+ let peer_qr = null
3116
+ if (peer_access_points.length > 0) {
3117
+ peer_url = peer_access_points[0].url
3118
+ peer_qr = peer_access_points[0].qr
3119
+ } else {
3120
+ try {
3121
+ peer_qr = await QRCode.toDataURL(peer_url)
3122
+ } catch (_) {}
3123
+ }
3124
+ return { peer_access_points, peer_url, peer_qr }
3125
+ }
3039
3126
 
3040
3127
 
3041
3128
  async syncConfig() {
@@ -4033,9 +4120,7 @@ class Server {
4033
4120
  }))
4034
4121
  */
4035
4122
  this.app.get("/tools", ex(async (req, res) => {
4036
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4037
- let peer_qr = null
4038
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4123
+ const peerAccess = await this.composePeerAccessPayload()
4039
4124
  let list = this.getPeers()
4040
4125
  let installs = []
4041
4126
  for(let key in this.kernel.bin.installed) {
@@ -4067,8 +4152,7 @@ class Server {
4067
4152
  }
4068
4153
  res.render("tools", {
4069
4154
  current_host: this.kernel.peer.host,
4070
- peer_url,
4071
- peer_qr,
4155
+ ...peerAccess,
4072
4156
  pending,
4073
4157
  installs,
4074
4158
  bundles,
@@ -4153,14 +4237,11 @@ class Server {
4153
4237
  return (a.name || '').localeCompare(b.name || '')
4154
4238
  })
4155
4239
 
4156
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4157
- let peer_qr = null
4158
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4240
+ const peerAccess = await this.composePeerAccessPayload()
4159
4241
  const list = this.getPeers()
4160
4242
  res.render("agents", {
4161
4243
  current_host: this.kernel.peer.host,
4162
- peer_url,
4163
- peer_qr,
4244
+ ...peerAccess,
4164
4245
  pluginMenu,
4165
4246
  apps,
4166
4247
  portal: this.portal,
@@ -4191,14 +4272,11 @@ class Server {
4191
4272
  res.redirect(301, "/agents")
4192
4273
  })
4193
4274
  this.app.get("/screenshots", ex(async (req, res) => {
4194
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4195
- let peer_qr = null
4196
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4275
+ const peerAccess = await this.composePeerAccessPayload()
4197
4276
  let list = this.getPeers()
4198
4277
  res.render("screenshots", {
4199
4278
  current_host: this.kernel.peer.host,
4200
- peer_url,
4201
- peer_qr,
4279
+ ...peerAccess,
4202
4280
  version: this.version,
4203
4281
  portal: this.portal,
4204
4282
  logo: this.logo,
@@ -4469,14 +4547,11 @@ class Server {
4469
4547
  drive: path.resolve(this.kernel.homedir, "drive"),
4470
4548
  }
4471
4549
  }
4472
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4473
- let peer_qr = null
4474
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4550
+ const peerAccess = await this.composePeerAccessPayload()
4475
4551
  let list = this.getPeers()
4476
4552
  res.render("settings", {
4477
4553
  current_host: this.kernel.peer.host,
4478
- peer_url,
4479
- peer_qr,
4554
+ ...peerAccess,
4480
4555
  list,
4481
4556
  platform,
4482
4557
  version: this.version,
@@ -4592,9 +4667,7 @@ class Server {
4592
4667
  return
4593
4668
  }
4594
4669
 
4595
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4596
- let peer_qr = null
4597
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4670
+ const peerAccess = await this.composePeerAccessPayload()
4598
4671
  let list = this.getPeers()
4599
4672
  let ai = await this.kernel.proto.ai()
4600
4673
  ai = [{
@@ -4608,8 +4681,7 @@ class Server {
4608
4681
  list,
4609
4682
  ai,
4610
4683
  current_host: this.kernel.peer.host,
4611
- peer_url,
4612
- peer_qr,
4684
+ ...peerAccess,
4613
4685
  cwd: this.kernel.path("api"),
4614
4686
  name: null,
4615
4687
  // name: req.params.name,
@@ -4710,14 +4782,11 @@ class Server {
4710
4782
  } catch (e) {
4711
4783
  }
4712
4784
  }
4713
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4714
- let peer_qr = null
4715
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4785
+ const peerAccess = await this.composePeerAccessPayload()
4716
4786
  res.render(`connect`, {
4717
4787
  current_urls,
4718
4788
  current_host: this.kernel.peer.host,
4719
- peer_url,
4720
- peer_qr,
4789
+ ...peerAccess,
4721
4790
  list,
4722
4791
  portal: this.portal,
4723
4792
  logo: this.logo,
@@ -5609,9 +5678,7 @@ class Server {
5609
5678
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5610
5679
  return this.kernel.router.rewrite_mapping[key]
5611
5680
  })
5612
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5613
- let peer_qr = null
5614
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5681
+ const peerAccess = await this.composePeerAccessPayload()
5615
5682
  const allow_dns_creation = req.params.name === this.kernel.peer.name
5616
5683
  res.render("net", {
5617
5684
  static_routes,
@@ -5631,8 +5698,7 @@ class Server {
5631
5698
  peer,
5632
5699
  protocol,
5633
5700
  current_host: this.kernel.peer.host,
5634
- peer_url,
5635
- peer_qr,
5701
+ ...peerAccess,
5636
5702
  cwd: this.kernel.path("api"),
5637
5703
  allow_dns_creation,
5638
5704
  })
@@ -5776,9 +5842,7 @@ class Server {
5776
5842
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5777
5843
  return this.kernel.router.rewrite_mapping[key]
5778
5844
  })
5779
- const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5780
- let peer_qr = null
5781
- try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5845
+ const peerAccess = await this.composePeerAccessPayload()
5782
5846
  res.render("network", {
5783
5847
  static_routes,
5784
5848
  host,
@@ -5804,8 +5868,7 @@ class Server {
5804
5868
  peer_active: this.kernel.peer.active,
5805
5869
  port_mapping: this.kernel.router.port_mapping,
5806
5870
  // port_mapping: this.kernel.caddy.port_mapping,
5807
- peer_url,
5808
- peer_qr,
5871
+ ...peerAccess,
5809
5872
  // ip_mapping: this.kernel.caddy.ip_mapping,
5810
5873
  lan: this.kernel.router.local_network_mapping,
5811
5874
  agent: req.agent,
@@ -2581,6 +2581,19 @@ body.dark .mode-selector .btn2.selected {
2581
2581
  border-radius: 5px;
2582
2582
  }
2583
2583
 
2584
+ body.dark aside .qr {
2585
+ }
2586
+ aside .qr {
2587
+ text-align: center;
2588
+ padding: 15px;
2589
+ }
2590
+ body.dark aside .qr img {
2591
+ border: 2px solid rgba(255,255,255,0.1);
2592
+ }
2593
+ aside .qr img {
2594
+ margin-bottom: 5px;
2595
+ border: 2px solid rgba(0,0,0,0.1);
2596
+ }
2584
2597
 
2585
2598
 
2586
2599
  @media only screen and (max-width: 768px) {