dsh-remote-mobile 1.1.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/LICENSE +21 -0
- package/README.md +178 -0
- package/README_EN.md +178 -0
- package/cordis.patch.yml +4 -0
- package/lib/auth/crypto.d.ts +24 -0
- package/lib/auth/crypto.js +144 -0
- package/lib/auth/gate.d.ts +14 -0
- package/lib/auth/gate.js +87 -0
- package/lib/auth/tailscale.d.ts +36 -0
- package/lib/auth/tailscale.js +128 -0
- package/lib/auth/token.d.ts +130 -0
- package/lib/auth/token.js +845 -0
- package/lib/bridge/compat.d.ts +50 -0
- package/lib/bridge/compat.js +191 -0
- package/lib/client.d.ts +5 -0
- package/lib/client.js +3606 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +117 -0
- package/lib/routes/api.d.ts +11 -0
- package/lib/routes/api.js +364 -0
- package/lib/routes/login-page.d.ts +4 -0
- package/lib/routes/login-page.js +339 -0
- package/lib/types/auth.d.ts +49 -0
- package/lib/types/auth.js +4 -0
- package/package.json +77 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断 IP 是否为 Tailscale 官方虚拟私网 IP (CGNAT 网段 100.64.0.0/10)
|
|
3
|
+
* 范围: 100.64.0.0 ~ 100.127.255.255
|
|
4
|
+
*/
|
|
5
|
+
export declare function isTailscaleIp(rawIp?: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* 判断 IP 是否为局域网私有 IP (RFC 1918 规范: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isLanIp(rawIp?: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 提取请求的客户端真实 IP(基于底层 Socket 连接,防止外部伪造 X-Forwarded-For,且兼容虚拟化场景)
|
|
12
|
+
*/
|
|
13
|
+
export declare function getClientIp(req: {
|
|
14
|
+
socket?: {
|
|
15
|
+
remoteAddress?: string;
|
|
16
|
+
__dsh_real_remote_address__?: string;
|
|
17
|
+
};
|
|
18
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
19
|
+
}): string;
|
|
20
|
+
/**
|
|
21
|
+
* 获取当前本机的 Tailscale IP
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLocalTailscaleIp(): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* 获取当前本机局域网 IP (优先常见 en0 / eth0 / wlan0 等)
|
|
26
|
+
*/
|
|
27
|
+
export declare function getLocalLanIp(): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* 获取本机所有可用的网络 IP
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAllNetworkIps(): {
|
|
32
|
+
name: string;
|
|
33
|
+
ip: string;
|
|
34
|
+
isTailscale: boolean;
|
|
35
|
+
isLan: boolean;
|
|
36
|
+
}[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { networkInterfaces } from 'node:os';
|
|
3
|
+
/**
|
|
4
|
+
* 判断 IP 是否为 Tailscale 官方虚拟私网 IP (CGNAT 网段 100.64.0.0/10)
|
|
5
|
+
* 范围: 100.64.0.0 ~ 100.127.255.255
|
|
6
|
+
*/
|
|
7
|
+
export function isTailscaleIp(rawIp) {
|
|
8
|
+
if (!rawIp)
|
|
9
|
+
return false;
|
|
10
|
+
// 规范化 IPv4 (处理 ::ffff:100.x.y.z 格式)
|
|
11
|
+
const ip = rawIp.replace(/^::ffff:/, '').trim();
|
|
12
|
+
if (!ip.startsWith('100.'))
|
|
13
|
+
return false;
|
|
14
|
+
const parts = ip.split('.');
|
|
15
|
+
if (parts.length !== 4)
|
|
16
|
+
return false;
|
|
17
|
+
const second = parseInt(parts[1], 10);
|
|
18
|
+
return second >= 64 && second <= 127;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 判断 IP 是否为局域网私有 IP (RFC 1918 规范: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
|
|
22
|
+
*/
|
|
23
|
+
export function isLanIp(rawIp) {
|
|
24
|
+
if (!rawIp)
|
|
25
|
+
return false;
|
|
26
|
+
const ip = rawIp.replace(/^::ffff:/, '').trim();
|
|
27
|
+
if (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost' || isTailscaleIp(ip))
|
|
28
|
+
return false;
|
|
29
|
+
const parts = ip.split('.').map((p) => parseInt(p, 10));
|
|
30
|
+
if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255))
|
|
31
|
+
return false;
|
|
32
|
+
const [a, b] = parts;
|
|
33
|
+
// 10.0.0.0/8
|
|
34
|
+
if (a === 10)
|
|
35
|
+
return true;
|
|
36
|
+
// 172.16.0.0/12
|
|
37
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
38
|
+
return true;
|
|
39
|
+
// 192.168.0.0/16
|
|
40
|
+
if (a === 192 && b === 168)
|
|
41
|
+
return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 提取请求的客户端真实 IP(基于底层 Socket 连接,防止外部伪造 X-Forwarded-For,且兼容虚拟化场景)
|
|
46
|
+
*/
|
|
47
|
+
export function getClientIp(req) {
|
|
48
|
+
const realAddress = req.socket?.__dsh_real_remote_address__ || req.socket?.remoteAddress;
|
|
49
|
+
return realAddress?.replace(/^::ffff:/, '') || '127.0.0.1';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 获取当前本机的 Tailscale IP
|
|
53
|
+
*/
|
|
54
|
+
export function getLocalTailscaleIp() {
|
|
55
|
+
// 1. 尝试通过 CLI 快速获取
|
|
56
|
+
try {
|
|
57
|
+
const out = execSync('tailscale ip -4', { encoding: 'utf8', timeout: 800, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
58
|
+
if (out && isTailscaleIp(out)) {
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// CLI 不可用或未在 PATH 中,回退到网卡扫描
|
|
64
|
+
}
|
|
65
|
+
// 2. 扫描系统网卡
|
|
66
|
+
try {
|
|
67
|
+
const interfaces = networkInterfaces();
|
|
68
|
+
for (const name of Object.keys(interfaces)) {
|
|
69
|
+
for (const info of interfaces[name] || []) {
|
|
70
|
+
if (info.family === 'IPv4' && !info.internal && isTailscaleIp(info.address)) {
|
|
71
|
+
return info.address;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 获取当前本机局域网 IP (优先常见 en0 / eth0 / wlan0 等)
|
|
81
|
+
*/
|
|
82
|
+
export function getLocalLanIp() {
|
|
83
|
+
try {
|
|
84
|
+
const interfaces = networkInterfaces();
|
|
85
|
+
// 优先匹配局域网常规网卡名
|
|
86
|
+
const preferredOrder = ['en0', 'eth0', 'wlan0', 'Wi-Fi', 'en1', 'eth1'];
|
|
87
|
+
for (const name of preferredOrder) {
|
|
88
|
+
for (const info of interfaces[name] || []) {
|
|
89
|
+
if (info.family === 'IPv4' && !info.internal && isLanIp(info.address)) {
|
|
90
|
+
return info.address;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 遍历所有其他可用网卡
|
|
95
|
+
for (const name of Object.keys(interfaces)) {
|
|
96
|
+
for (const info of interfaces[name] || []) {
|
|
97
|
+
if (info.family === 'IPv4' && !info.internal && isLanIp(info.address)) {
|
|
98
|
+
return info.address;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 获取本机所有可用的网络 IP
|
|
108
|
+
*/
|
|
109
|
+
export function getAllNetworkIps() {
|
|
110
|
+
const list = [];
|
|
111
|
+
try {
|
|
112
|
+
const interfaces = networkInterfaces();
|
|
113
|
+
for (const name of Object.keys(interfaces)) {
|
|
114
|
+
for (const info of interfaces[name] || []) {
|
|
115
|
+
if (info.family === 'IPv4' && !info.internal) {
|
|
116
|
+
list.push({
|
|
117
|
+
name,
|
|
118
|
+
ip: info.address,
|
|
119
|
+
isTailscale: isTailscaleIp(info.address),
|
|
120
|
+
isLan: isLanIp(info.address),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
return list;
|
|
128
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
export declare const AUTH_COOKIE_NAME = "dsh_mobile_token";
|
|
4
|
+
export declare const DEFAULT_PERSIST_FILE: string;
|
|
5
|
+
export declare const GLOBAL_SETTINGS_FILE: string;
|
|
6
|
+
/**
|
|
7
|
+
* 从 ~/.dsh/settings.yaml 中安全读取 dsh-remote-mobile 命名空间的最新配置
|
|
8
|
+
*/
|
|
9
|
+
export declare function readFromSettingsYaml(filePath?: string): Partial<SessionStoreOptions>;
|
|
10
|
+
/**
|
|
11
|
+
* 从 ~/.dsh/settings.yaml 中读取全局语言偏好 (locale.preference)
|
|
12
|
+
*/
|
|
13
|
+
export declare function readGlobalLocale(filePath?: string): 'zh' | 'en';
|
|
14
|
+
/**
|
|
15
|
+
* 将最新的插件配置安全写回 ~/.dsh/settings.yaml 中的 dsh-remote-mobile 命名空间
|
|
16
|
+
*/
|
|
17
|
+
export declare function writeBackToSettingsYaml(options: SessionStoreOptions, filePath?: string): boolean;
|
|
18
|
+
export interface SessionRecord {
|
|
19
|
+
token: string;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
lastSeenAt: number;
|
|
22
|
+
ip?: string;
|
|
23
|
+
deviceName?: string;
|
|
24
|
+
userAgent?: string;
|
|
25
|
+
authType?: string;
|
|
26
|
+
isBypass?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ShortCodeRecord {
|
|
29
|
+
code: string;
|
|
30
|
+
token: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
}
|
|
34
|
+
export interface IpSecurityStat {
|
|
35
|
+
ip: string;
|
|
36
|
+
authVisits: number;
|
|
37
|
+
authVisitsInWindow: number;
|
|
38
|
+
windowStart: number;
|
|
39
|
+
failedAttempts: number;
|
|
40
|
+
totalAttempts: number;
|
|
41
|
+
lockedUntil: number;
|
|
42
|
+
lastAttemptAt: number;
|
|
43
|
+
lastSeenAt: number;
|
|
44
|
+
authType?: string;
|
|
45
|
+
deviceName?: string;
|
|
46
|
+
userAgent?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SessionStoreOptions {
|
|
49
|
+
allowTailscale?: boolean;
|
|
50
|
+
allowLan?: boolean;
|
|
51
|
+
secret?: string;
|
|
52
|
+
secretHash?: string;
|
|
53
|
+
devicesFile?: string;
|
|
54
|
+
maxFailedAttempts?: number;
|
|
55
|
+
lockDurationMs?: number;
|
|
56
|
+
maxVisitsPerMinute?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 将 User-Agent 字符串解析为人类友好的设备名称
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseDeviceName(ua?: string): string;
|
|
62
|
+
export declare class SessionStore extends EventEmitter {
|
|
63
|
+
private sessions;
|
|
64
|
+
private shortCodes;
|
|
65
|
+
private ipStats;
|
|
66
|
+
private options;
|
|
67
|
+
private settingsMutator?;
|
|
68
|
+
readonly persistPath: string;
|
|
69
|
+
readonly settingsFilePath: string | null;
|
|
70
|
+
constructor(options?: SessionStoreOptions);
|
|
71
|
+
getOptions(): Required<SessionStoreOptions>;
|
|
72
|
+
hasSecret(): boolean;
|
|
73
|
+
setSettingsMutator(mutator: (patch: Partial<SessionStoreOptions>) => void): void;
|
|
74
|
+
updateOptions(newOpts: Partial<SessionStoreOptions>, syncToSettings?: boolean): void;
|
|
75
|
+
/**
|
|
76
|
+
* 记录 IP 打开 /auth 页面的计数并进行限频判定与设备指纹解析
|
|
77
|
+
*/
|
|
78
|
+
recordAuthVisit(ip: string, userAgent?: string): {
|
|
79
|
+
allowed: boolean;
|
|
80
|
+
lockedUntil?: number;
|
|
81
|
+
reason?: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* 检查 IP 是否被防暴力破解锁定
|
|
85
|
+
*/
|
|
86
|
+
isIpLocked(ip: string): {
|
|
87
|
+
locked: boolean;
|
|
88
|
+
lockedUntil?: number;
|
|
89
|
+
remainingSeconds?: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 获取所有 IP 的安全审计统计数据
|
|
93
|
+
*/
|
|
94
|
+
getIpSecurityStats(): IpSecurityStat[];
|
|
95
|
+
/**
|
|
96
|
+
* 管理员手动解锁某个 IP
|
|
97
|
+
*/
|
|
98
|
+
unlockIp(ip: string): boolean;
|
|
99
|
+
generateShortCode(): {
|
|
100
|
+
code: string;
|
|
101
|
+
token: string;
|
|
102
|
+
expiresAt: number;
|
|
103
|
+
};
|
|
104
|
+
verify(credential: string, userAgent?: string, clientIp?: string): {
|
|
105
|
+
success: boolean;
|
|
106
|
+
token?: string;
|
|
107
|
+
reason?: string;
|
|
108
|
+
locked?: boolean;
|
|
109
|
+
remainingSeconds?: number;
|
|
110
|
+
};
|
|
111
|
+
private handleFailedAttempt;
|
|
112
|
+
validateToken(token: string, clientIp?: string): boolean;
|
|
113
|
+
extractTokenFromRequest(req: IncomingMessage): string | null;
|
|
114
|
+
/**
|
|
115
|
+
* 自动登记并归因免密直连设备(Tailscale / LAN)
|
|
116
|
+
*/
|
|
117
|
+
registerBypassDevice(ip: string, userAgent: string | undefined, type: 'Tailscale' | 'LAN'): SessionRecord;
|
|
118
|
+
getSessionsList(): {
|
|
119
|
+
count: number;
|
|
120
|
+
devices: Array<Omit<SessionRecord, 'userAgent'>>;
|
|
121
|
+
};
|
|
122
|
+
revokeDevice(token: string): boolean;
|
|
123
|
+
revokeAllSessions(): void;
|
|
124
|
+
/**
|
|
125
|
+
* 清空所有 IP 安全审计与防暴破统计记录
|
|
126
|
+
*/
|
|
127
|
+
clearAllIpSecurityStats(): void;
|
|
128
|
+
loadPersistedData(): void;
|
|
129
|
+
savePersistedData(): void;
|
|
130
|
+
}
|