mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-05 20:28:25 +02:00
282 lines
8.8 KiB
JavaScript
282 lines
8.8 KiB
JavaScript
function filterIPv4(ips) {
|
|
var result = []
|
|
if (!ips || typeof ips.length !== "number") return result
|
|
for (var i = 0; i < ips.length; i++) {
|
|
var ip = String(ips[i] || "")
|
|
if (/^100\./.test(ip)) result.push(ip)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function filterIPv6(ips) {
|
|
var result = []
|
|
if (!ips || typeof ips.length !== "number") return result
|
|
for (var i = 0; i < ips.length; i++) {
|
|
var ip = String(ips[i] || "")
|
|
if (/^fd7a:115c:a1e0:/i.test(ip)) result.push(ip)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function cleanDnsName(name) {
|
|
var value = String(name || "")
|
|
return value.charAt(value.length - 1) === "." ? value.slice(0, -1) : value
|
|
}
|
|
|
|
function shortDnsName(name) {
|
|
var clean = cleanDnsName(name)
|
|
if (clean === "") return ""
|
|
return clean.split(".")[0] || clean
|
|
}
|
|
|
|
function displayHostName(hostName, dnsName) {
|
|
var host = String(hostName || "")
|
|
if (host !== "" && host.toLowerCase() !== "localhost") return host
|
|
return shortDnsName(dnsName) || host || "Unknown"
|
|
}
|
|
|
|
function isMullvadPeer(peer) {
|
|
var hostName = String((peer && peer.HostName) || "").toLowerCase()
|
|
var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase()
|
|
return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1
|
|
}
|
|
|
|
function osIcon(os) {
|
|
var value = String(os || "").toLowerCase()
|
|
if (value === "linux") return ""
|
|
if (value === "macos" || value === "ios") return ""
|
|
if (value === "windows") return ""
|
|
if (value === "android") return ""
|
|
if (value === "mullvad") return ""
|
|
return ""
|
|
}
|
|
|
|
function accountLabel(account) {
|
|
if (!account) return "Unknown account"
|
|
if (account.nickname) return String(account.nickname)
|
|
if (account.tailnet) return String(account.tailnet)
|
|
if (account.account) return String(account.account)
|
|
return String(account.id || "Unknown account")
|
|
}
|
|
|
|
function peerFromStatus(id, peer) {
|
|
return {
|
|
id: id,
|
|
HostName: displayHostName(peer.HostName, peer.DNSName),
|
|
DNSName: cleanDnsName(peer.DNSName),
|
|
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
|
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
|
TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []),
|
|
Online: peer.Online === true,
|
|
OS: String(peer.OS || ""),
|
|
Tags: peer.Tags || [],
|
|
ExitNodeOption: peer.ExitNodeOption === true,
|
|
ExitNode: peer.ExitNode === true,
|
|
Mullvad: isMullvadPeer(peer)
|
|
}
|
|
}
|
|
|
|
function sliceTableColumn(line, start, end) {
|
|
var text = String(line || "")
|
|
if (start < 0 || start >= text.length) return ""
|
|
if (end < 0) return text.substring(start).trim()
|
|
return text.substring(start, Math.min(end, text.length)).trim()
|
|
}
|
|
|
|
function parseExitNodeList(raw) {
|
|
var lines = String(raw || "").split(/\r?\n/)
|
|
var header = ""
|
|
var headerIndex = -1
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (/^\s*IP\s+HOSTNAME\s+COUNTRY\s+CITY\s+STATUS\s*$/.test(lines[i])) {
|
|
header = lines[i]
|
|
headerIndex = i
|
|
break
|
|
}
|
|
}
|
|
if (headerIndex === -1) return []
|
|
|
|
var ipStart = header.indexOf("IP")
|
|
var hostStart = header.indexOf("HOSTNAME")
|
|
var countryStart = header.indexOf("COUNTRY")
|
|
var cityStart = header.indexOf("CITY")
|
|
var statusStart = header.indexOf("STATUS")
|
|
var byHost = {}
|
|
|
|
for (var j = headerIndex + 1; j < lines.length; j++) {
|
|
var line = lines[j]
|
|
if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue
|
|
|
|
var ip = sliceTableColumn(line, ipStart, hostStart)
|
|
var host = sliceTableColumn(line, hostStart, countryStart)
|
|
var country = sliceTableColumn(line, countryStart, cityStart)
|
|
var city = sliceTableColumn(line, cityStart, statusStart)
|
|
var status = sliceTableColumn(line, statusStart, -1)
|
|
if (host.indexOf(".mullvad.ts.net") === -1) continue
|
|
|
|
byHost[host] = {
|
|
id: "mullvad:" + host,
|
|
HostName: host,
|
|
DNSName: host,
|
|
DisplayName: (city && city !== "Any" ? city + ", " : "") + country,
|
|
TailscaleIPs: ip ? [ip] : [],
|
|
TailscaleIPv6: [],
|
|
Online: true,
|
|
OS: "mullvad",
|
|
Tags: [],
|
|
ExitNodeOption: true,
|
|
ExitNode: status !== "" && status !== "-",
|
|
Mullvad: true,
|
|
Country: country,
|
|
City: city,
|
|
Status: status
|
|
}
|
|
}
|
|
|
|
var result = []
|
|
for (var hostName in byHost) result.push(byHost[hostName])
|
|
result.sort(function(a, b) {
|
|
var countryCompare = String(a.Country).localeCompare(String(b.Country))
|
|
if (countryCompare !== 0) return countryCompare
|
|
return String(a.DisplayName).localeCompare(String(b.DisplayName))
|
|
})
|
|
return result
|
|
}
|
|
|
|
function mullvadRegionOptions(nodes) {
|
|
var byRegion = {}
|
|
var values = Array.isArray(nodes) ? nodes : []
|
|
for (var i = 0; i < values.length; i++) {
|
|
var node = values[i] || {}
|
|
if (node.Mullvad !== true) continue
|
|
var country = String(node.Country || "").trim()
|
|
var city = String(node.City || "").trim()
|
|
if (country === "") continue
|
|
if (city === "" || city === "Any") continue
|
|
|
|
var key = country + "\n" + city
|
|
if (byRegion[key]) continue
|
|
|
|
var option = {}
|
|
for (var propertyName in node) option[propertyName] = node[propertyName]
|
|
option.id = "mullvad-region:" + key
|
|
option.DisplayName = city + ", " + country
|
|
option.Country = country
|
|
option.City = city
|
|
option.MullvadRegion = true
|
|
byRegion[key] = option
|
|
}
|
|
|
|
var result = []
|
|
for (var name in byRegion) result.push(byRegion[name])
|
|
result.sort(function(a, b) {
|
|
var countryCompare = String(a.Country).localeCompare(String(b.Country))
|
|
if (countryCompare !== 0) return countryCompare
|
|
return String(a.City).localeCompare(String(b.City))
|
|
})
|
|
return result
|
|
}
|
|
|
|
function mullvadCountryOptions(nodes) {
|
|
return mullvadRegionOptions(nodes)
|
|
}
|
|
|
|
function parseStatus(raw) {
|
|
var text = String(raw || "").trim()
|
|
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
|
|
|
|
try {
|
|
var data = JSON.parse(text)
|
|
var backendState = String(data.BackendState || "Unknown")
|
|
var self = data.Self || {}
|
|
var selfIps = filterIPv4(self.TailscaleIPs || data.TailscaleIPs || [])
|
|
var peers = []
|
|
var exitNodes = []
|
|
var rawPeers = data.Peer || {}
|
|
|
|
for (var id in rawPeers) {
|
|
var peer = rawPeers[id] || {}
|
|
var normalized = peerFromStatus(id, peer)
|
|
if (normalized.Mullvad) continue
|
|
if (normalized.Online) {
|
|
peers.push(normalized)
|
|
if (normalized.ExitNodeOption) exitNodes.push(normalized)
|
|
}
|
|
}
|
|
|
|
peers.sort(function(a, b) {
|
|
return String(a.HostName).localeCompare(String(b.HostName))
|
|
})
|
|
exitNodes.sort(function(a, b) {
|
|
return String(a.HostName).localeCompare(String(b.HostName))
|
|
})
|
|
|
|
return {
|
|
ok: true,
|
|
unavailable: false,
|
|
backendState: backendState,
|
|
running: backendState === "Running",
|
|
needsLogin: backendState === "NeedsLogin",
|
|
authUrl: String(data.AuthURL || ""),
|
|
selfName: displayHostName(self.HostName, self.DNSName),
|
|
selfDnsName: cleanDnsName(self.DNSName),
|
|
selfIp: selfIps.length > 0 ? selfIps[0] : "",
|
|
peers: peers,
|
|
exitNodes: exitNodes
|
|
}
|
|
} catch (e) {
|
|
return { ok: false, unavailable: true, message: "Status error", error: "Failed to parse tailscale status" }
|
|
}
|
|
}
|
|
|
|
function parseAccounts(raw) {
|
|
var text = String(raw || "").trim()
|
|
if (text === "") return { accounts: [], selectedAccountId: "", selectedAccountLabel: "" }
|
|
|
|
try {
|
|
var parsed = JSON.parse(text)
|
|
var next = []
|
|
var selected = null
|
|
if (parsed && typeof parsed.length === "number") {
|
|
for (var i = 0; i < parsed.length; i++) {
|
|
var rawAccount = parsed[i] || {}
|
|
var account = {
|
|
id: String(rawAccount.id || rawAccount.ID || ""),
|
|
nickname: String(rawAccount.nickname || rawAccount.Nickname || rawAccount.name || rawAccount.Name || ""),
|
|
tailnet: String(rawAccount.tailnet || rawAccount.Tailnet || ""),
|
|
account: String(rawAccount.account || rawAccount.Account || rawAccount.loginName || rawAccount.LoginName || rawAccount.user || rawAccount.User || ""),
|
|
selected: rawAccount.selected === true || rawAccount.Selected === true
|
|
}
|
|
next.push(account)
|
|
if (account.selected === true) selected = account
|
|
}
|
|
}
|
|
return {
|
|
accounts: next,
|
|
selectedAccountId: selected ? String(selected.id || "") : "",
|
|
selectedAccountLabel: selected ? accountLabel(selected) : ""
|
|
}
|
|
} catch (e) {
|
|
return { accounts: [], selectedAccountId: "", selectedAccountLabel: "" }
|
|
}
|
|
}
|
|
|
|
if (typeof module !== "undefined") {
|
|
module.exports = {
|
|
filterIPv4: filterIPv4,
|
|
filterIPv6: filterIPv6,
|
|
cleanDnsName: cleanDnsName,
|
|
shortDnsName: shortDnsName,
|
|
displayHostName: displayHostName,
|
|
osIcon: osIcon,
|
|
accountLabel: accountLabel,
|
|
isMullvadPeer: isMullvadPeer,
|
|
peerFromStatus: peerFromStatus,
|
|
parseExitNodeList: parseExitNodeList,
|
|
mullvadRegionOptions: mullvadRegionOptions,
|
|
mullvadCountryOptions: mullvadCountryOptions,
|
|
parseStatus: parseStatus,
|
|
parseAccounts: parseAccounts
|
|
}
|
|
}
|