amule-ec-client 0.5.1 → 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.
- package/README.md +6 -4
- package/dist/AddLinkRequest-DwUbSMXD.mjs +17 -0
- package/dist/AddLinkRequest-DwUbSMXD.mjs.map +1 -0
- package/dist/ClientQueueRequest-D9FaX5K9.mjs +17 -0
- package/dist/ClientQueueRequest-D9FaX5K9.mjs.map +1 -0
- package/dist/ClientQueueResponse-FVQBR_8b.mjs +65 -0
- package/dist/ClientQueueResponse-FVQBR_8b.mjs.map +1 -0
- package/dist/Codes-CjiW7Pdl.mjs +411 -0
- package/dist/Codes-CjiW7Pdl.mjs.map +1 -0
- package/dist/CreateCategoryRequest-Br6yfDhZ.mjs +24 -0
- package/dist/CreateCategoryRequest-Br6yfDhZ.mjs.map +1 -0
- package/dist/DeleteCategoryRequest-C2-p6sx0.mjs +17 -0
- package/dist/DeleteCategoryRequest-C2-p6sx0.mjs.map +1 -0
- package/dist/DownloadCommandRequest-CaULjfNi.mjs +31 -0
- package/dist/DownloadCommandRequest-CaULjfNi.mjs.map +1 -0
- package/dist/DownloadFileRequest-CcNqsBo7.mjs +20 -0
- package/dist/DownloadFileRequest-CcNqsBo7.mjs.map +1 -0
- package/dist/DownloadQueueRequest-bd7lFiMj.mjs +17 -0
- package/dist/DownloadQueueRequest-bd7lFiMj.mjs.map +1 -0
- package/dist/DownloadQueueResponse-DyP1rLTV.mjs +261 -0
- package/dist/DownloadQueueResponse-DyP1rLTV.mjs.map +1 -0
- package/dist/GetPreferencesRequest-C94Su9IH.mjs +18 -0
- package/dist/GetPreferencesRequest-C94Su9IH.mjs.map +1 -0
- package/dist/PreferencesDetailsResponse-BjMxbvVY.mjs +96 -0
- package/dist/PreferencesDetailsResponse-BjMxbvVY.mjs.map +1 -0
- package/dist/PreferencesResponse-Dth-n2sA.mjs +39 -0
- package/dist/PreferencesResponse-Dth-n2sA.mjs.map +1 -0
- package/dist/ReloadSharedFilesRequest-CPwAEJYG.mjs +15 -0
- package/dist/ReloadSharedFilesRequest-CPwAEJYG.mjs.map +1 -0
- package/dist/Request-C0UUuQIX.mjs +160 -0
- package/dist/Request-C0UUuQIX.mjs.map +1 -0
- package/dist/SearchProgressResponse-Bputc9Ww.mjs +17 -0
- package/dist/SearchProgressResponse-Bputc9Ww.mjs.map +1 -0
- package/dist/SearchRequest-CNqz-K1Z.mjs +57 -0
- package/dist/SearchRequest-CNqz-K1Z.mjs.map +1 -0
- package/dist/SearchResultsRequest-DKG508KF.mjs +15 -0
- package/dist/SearchResultsRequest-DKG508KF.mjs.map +1 -0
- package/dist/SearchResultsResponse-ChNrKJ6_.mjs +31 -0
- package/dist/SearchResultsResponse-ChNrKJ6_.mjs.map +1 -0
- package/dist/SearchStatusRequest-Bv6iaC9Y.mjs +15 -0
- package/dist/SearchStatusRequest-Bv6iaC9Y.mjs.map +1 -0
- package/dist/SearchStopRequest-rET-2Nsz.mjs +15 -0
- package/dist/SearchStopRequest-rET-2Nsz.mjs.map +1 -0
- package/dist/ServerConnectRequest-CcO2-Eqd.mjs +20 -0
- package/dist/ServerConnectRequest-CcO2-Eqd.mjs.map +1 -0
- package/dist/ServerDisconnectRequest-DEORM3SF.mjs +15 -0
- package/dist/ServerDisconnectRequest-DEORM3SF.mjs.map +1 -0
- package/dist/ServerListRequest-FwFQBp12.mjs +15 -0
- package/dist/ServerListRequest-FwFQBp12.mjs.map +1 -0
- package/dist/ServerListResponse-ZAKSSCPZ.mjs +52 -0
- package/dist/ServerListResponse-ZAKSSCPZ.mjs.map +1 -0
- package/dist/ServerUpdateFromUrlRequest-D4xBqJDJ.mjs +14 -0
- package/dist/ServerUpdateFromUrlRequest-D4xBqJDJ.mjs.map +1 -0
- package/dist/SetFileCategoryRequest-BwEPJVeR.mjs +17 -0
- package/dist/SetFileCategoryRequest-BwEPJVeR.mjs.map +1 -0
- package/dist/SetPreferencesRequest-D2fVIH3P.mjs +66 -0
- package/dist/SetPreferencesRequest-D2fVIH3P.mjs.map +1 -0
- package/dist/SharedFilePriorityRequest-j6ImgagV.mjs +15 -0
- package/dist/SharedFilePriorityRequest-j6ImgagV.mjs.map +1 -0
- package/dist/SharedFilesRequest-D2M3K0eB.mjs +17 -0
- package/dist/SharedFilesRequest-D2M3K0eB.mjs.map +1 -0
- package/dist/SharedFilesResponse-BiXT_oCx.mjs +41 -0
- package/dist/SharedFilesResponse-BiXT_oCx.mjs.map +1 -0
- package/dist/StatsRequest-B8v9LLHG.mjs +17 -0
- package/dist/StatsRequest-B8v9LLHG.mjs.map +1 -0
- package/dist/StatsResponse-BVUxNPbz.mjs +78 -0
- package/dist/StatsResponse-BVUxNPbz.mjs.map +1 -0
- package/dist/Tag-CZF6BRMY.mjs +262 -0
- package/dist/Tag-CZF6BRMY.mjs.map +1 -0
- package/dist/UpdateCategoryRequest-CgzH7SxP.mjs +24 -0
- package/dist/UpdateCategoryRequest-CgzH7SxP.mjs.map +1 -0
- package/dist/UpdateRequest-Bunl73Ga.mjs +17 -0
- package/dist/UpdateRequest-Bunl73Ga.mjs.map +1 -0
- package/dist/UpdateResponse-1LhyH9Zf.mjs +47 -0
- package/dist/UpdateResponse-1LhyH9Zf.mjs.map +1 -0
- package/dist/download-details-OM0FJUhZ.mjs +12 -0
- package/dist/download-details-OM0FJUhZ.mjs.map +1 -0
- package/dist/index.d.mts +1130 -0
- package/dist/index.mjs +1077 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils-DBnUQnZ-.mjs +26 -0
- package/dist/utils-DBnUQnZ-.mjs.map +1 -0
- package/package.json +5 -5
- package/dist/index.d.ts +0 -1016
- package/dist/index.js +0 -3130
- package/dist/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
import { n as Packet, r as Flags, t as Request } from "./Request-C0UUuQIX.mjs";
|
|
2
|
+
import { a as EcPrefs, i as ECTagName, n as ECOpCode, o as ProtocolVersion, r as ECSearchType, t as ECDetailLevel } from "./Codes-CjiW7Pdl.mjs";
|
|
3
|
+
import { a as StringTag, c as UInt128Tag, d as UShortTag, f as findNumericTag, i as Ipv4Tag, l as UIntTag, n as DoubleTag, o as Tag, r as Hash16Tag, s as UByteTag, t as CustomTag, u as ULongTag } from "./Tag-CZF6BRMY.mjs";
|
|
4
|
+
import { n as PARTSIZE, t as ChunkStatus } from "./download-details-OM0FJUhZ.mjs";
|
|
5
|
+
import * as net from "net";
|
|
6
|
+
import * as zlib from "zlib";
|
|
7
|
+
import * as crypto from "crypto";
|
|
8
|
+
//#region src/exceptions/index.ts
|
|
9
|
+
var AmuleException = class extends Error {
|
|
10
|
+
cause;
|
|
11
|
+
constructor(message, cause) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
this.name = "AmuleException";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var InvalidECException = class extends AmuleException {
|
|
18
|
+
constructor(message, cause) {
|
|
19
|
+
super(message, cause);
|
|
20
|
+
this.name = "InvalidECException";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var CommunicationException = class extends AmuleException {
|
|
24
|
+
constructor(message, cause) {
|
|
25
|
+
super(message, cause);
|
|
26
|
+
this.name = "CommunicationException";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var ServerException = class extends AmuleException {
|
|
30
|
+
constructor(message, cause) {
|
|
31
|
+
super(message, cause);
|
|
32
|
+
this.name = "ServerException";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/ec/Encoding.ts
|
|
37
|
+
/**
|
|
38
|
+
* Reads a UTF-8 encoded number from a buffer
|
|
39
|
+
* Treats UTF-8 bytes as a Unicode code point and returns the numeric value
|
|
40
|
+
*/
|
|
41
|
+
function readUtf8Number(buffer, offset) {
|
|
42
|
+
const firstByte = buffer[offset];
|
|
43
|
+
let length;
|
|
44
|
+
if ((firstByte & 128) === 0) length = 1;
|
|
45
|
+
else if ((firstByte & 224) === 192) length = 2;
|
|
46
|
+
else if ((firstByte & 240) === 224) length = 3;
|
|
47
|
+
else if ((firstByte & 248) === 240) length = 4;
|
|
48
|
+
else throw new Error(`Invalid UTF-8 first byte: 0x${firstByte.toString(16)}`);
|
|
49
|
+
const codePoint = buffer.subarray(offset, offset + length).toString("utf-8").codePointAt(0);
|
|
50
|
+
if (codePoint === void 0) throw new Error("Failed to decode UTF-8 number");
|
|
51
|
+
return {
|
|
52
|
+
value: BigInt(codePoint),
|
|
53
|
+
newOffset: offset + length
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Encodes a number as UTF-8 code point
|
|
58
|
+
* Treats the number as a Unicode code point and encodes it as UTF-8
|
|
59
|
+
*/
|
|
60
|
+
function encodeUtf8Number(value) {
|
|
61
|
+
const num = Number(value);
|
|
62
|
+
const str = String.fromCodePoint(num);
|
|
63
|
+
return Buffer.from(str, "utf-8");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Gets the number of bytes needed to encode a value as UTF-8 code point
|
|
67
|
+
*/
|
|
68
|
+
function numberLength(value) {
|
|
69
|
+
const num = Number(value);
|
|
70
|
+
if (num < 128) return 1;
|
|
71
|
+
else if (num < 2048) return 2;
|
|
72
|
+
else if (num < 65536) return 3;
|
|
73
|
+
else if (num < 1114112) return 4;
|
|
74
|
+
else throw new Error(`Code point out of range: ${num}`);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/ec/tag/TagParser.ts
|
|
78
|
+
const TAG_NAME_SIZE = 2;
|
|
79
|
+
const TAG_TYPE_SIZE = 1;
|
|
80
|
+
const TAG_LENGTH_SIZE = 4;
|
|
81
|
+
const SUBTAG_COUNT_SIZE = 2;
|
|
82
|
+
var TagParser = class {
|
|
83
|
+
buffer;
|
|
84
|
+
offset;
|
|
85
|
+
useUtf8Numbers;
|
|
86
|
+
constructor(buffer, offset = 0, useUtf8Numbers = false) {
|
|
87
|
+
this.buffer = buffer;
|
|
88
|
+
this.offset = offset;
|
|
89
|
+
this.useUtf8Numbers = useUtf8Numbers;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse a single tag from the current position
|
|
93
|
+
*/
|
|
94
|
+
parseTag() {
|
|
95
|
+
return this.parseTagWithMetadata().tag;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Internal parse that returns metadata needed for nested tag calculation
|
|
99
|
+
*/
|
|
100
|
+
parseTagWithMetadata() {
|
|
101
|
+
const tagNameAndSubtags = this.readNumber(TAG_NAME_SIZE);
|
|
102
|
+
const tagName = tagNameAndSubtags >> 1 & 65535;
|
|
103
|
+
const hasSubtags = (tagNameAndSubtags & 1) !== 0;
|
|
104
|
+
const tagType = this.buffer.readUInt8(this.offset);
|
|
105
|
+
this.offset += TAG_TYPE_SIZE;
|
|
106
|
+
const tagLength = this.readNumber(TAG_LENGTH_SIZE);
|
|
107
|
+
let valueStartIndex = this.offset;
|
|
108
|
+
const subTags = [];
|
|
109
|
+
let theoreticalLength = 0;
|
|
110
|
+
let valueEndIndex;
|
|
111
|
+
if (!hasSubtags) valueEndIndex = valueStartIndex + tagLength - 1;
|
|
112
|
+
else {
|
|
113
|
+
const subTagCount = this.readNumber(SUBTAG_COUNT_SIZE);
|
|
114
|
+
valueStartIndex = this.offset;
|
|
115
|
+
for (let i = 0; i < subTagCount; i++) {
|
|
116
|
+
const result = this.parseTagWithMetadata();
|
|
117
|
+
subTags.push(result.tag);
|
|
118
|
+
theoreticalLength += result.theoreticalLength;
|
|
119
|
+
}
|
|
120
|
+
if (subTags.length > subTagCount) throw new InvalidECException(`Error parsing subtags list - Expected subtags ${subTagCount} found subtags ${subTags.length}`);
|
|
121
|
+
valueEndIndex = this.offset + (tagLength - theoreticalLength - 1);
|
|
122
|
+
theoreticalLength += SUBTAG_COUNT_SIZE;
|
|
123
|
+
}
|
|
124
|
+
const valueSize = valueEndIndex - this.offset + 1;
|
|
125
|
+
const tagValue = this.buffer.subarray(this.offset, this.offset + valueSize);
|
|
126
|
+
this.offset += valueSize;
|
|
127
|
+
theoreticalLength += tagValue.length;
|
|
128
|
+
theoreticalLength += TAG_NAME_SIZE;
|
|
129
|
+
theoreticalLength += TAG_TYPE_SIZE;
|
|
130
|
+
theoreticalLength += TAG_LENGTH_SIZE;
|
|
131
|
+
const tag = this.createTagFromValue(tagName, tagType, tagValue);
|
|
132
|
+
if (subTags.length > 0) tag.nestedTags = subTags;
|
|
133
|
+
return {
|
|
134
|
+
tag,
|
|
135
|
+
theoreticalLength,
|
|
136
|
+
endIndex: this.offset - 1
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse multiple tags
|
|
141
|
+
*/
|
|
142
|
+
parseTags(count) {
|
|
143
|
+
const tags = [];
|
|
144
|
+
for (let i = 0; i < count; i++) tags.push(this.parseTag());
|
|
145
|
+
return tags;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read a number with the given fixed size.
|
|
149
|
+
* In UTF-8 mode, reads as UTF-8 codepoint. Otherwise reads as big-endian integer.
|
|
150
|
+
*/
|
|
151
|
+
readNumber(fixedSize) {
|
|
152
|
+
if (this.useUtf8Numbers) {
|
|
153
|
+
const result = readUtf8Number(this.buffer, this.offset);
|
|
154
|
+
this.offset = result.newOffset;
|
|
155
|
+
return Number(result.value);
|
|
156
|
+
} else {
|
|
157
|
+
let value;
|
|
158
|
+
switch (fixedSize) {
|
|
159
|
+
case 1:
|
|
160
|
+
value = this.buffer.readUInt8(this.offset);
|
|
161
|
+
break;
|
|
162
|
+
case 2:
|
|
163
|
+
value = this.buffer.readUInt16BE(this.offset);
|
|
164
|
+
break;
|
|
165
|
+
case 4:
|
|
166
|
+
value = this.buffer.readUInt32BE(this.offset);
|
|
167
|
+
break;
|
|
168
|
+
default: throw new InvalidECException(`Unsupported number size: ${fixedSize}`);
|
|
169
|
+
}
|
|
170
|
+
this.offset += fixedSize;
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Create a tag instance based on type and raw value bytes
|
|
176
|
+
*/
|
|
177
|
+
createTagFromValue(name, type, value) {
|
|
178
|
+
switch (type) {
|
|
179
|
+
case 1: return new CustomTag(name, value);
|
|
180
|
+
case 2: return new UByteTag(name, value.length > 0 ? value.readUInt8(0) : 0);
|
|
181
|
+
case 3: return new UShortTag(name, value.length >= 2 ? value.readUInt16BE(0) : 0);
|
|
182
|
+
case 4: return new UIntTag(name, value.length >= 4 ? value.readUInt32BE(0) : 0);
|
|
183
|
+
case 5: return new ULongTag(name, value.length >= 8 ? value.readBigUInt64BE(0) : 0n);
|
|
184
|
+
case 10: return new UInt128Tag(name, value.length >= 16 ? this.parseUInt128(value) : 0n);
|
|
185
|
+
case 6: return new StringTag(name, value[value.length - 1] === 0 ? value.toString("utf-8", 0, value.length - 1) : value.toString("utf-8"));
|
|
186
|
+
case 7: return new DoubleTag(name, value.length >= 8 ? value.readDoubleBE(0) : 0);
|
|
187
|
+
case 8: {
|
|
188
|
+
if (value.length !== 6) throw new InvalidECException(`Ipv4Tag value must be 6 bytes long, got ${value.length}`);
|
|
189
|
+
const address = `${value[0]}.${value[1]}.${value[2]}.${value[3]}`;
|
|
190
|
+
const port = value.readUInt16BE(4);
|
|
191
|
+
const tag = new Ipv4Tag(name);
|
|
192
|
+
tag["value"] = {
|
|
193
|
+
address,
|
|
194
|
+
port
|
|
195
|
+
};
|
|
196
|
+
return tag;
|
|
197
|
+
}
|
|
198
|
+
case 9: return new Hash16Tag(name, value);
|
|
199
|
+
default: throw new InvalidECException(`Unknown tag type: ${type} for tag ${name}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
parseUInt128(value) {
|
|
203
|
+
if (value.length < 16) throw new InvalidECException(`UInt128 value must be at least 16 bytes long, got ${value.length}`);
|
|
204
|
+
return value.readBigUInt64BE(0) << 64n | value.readBigUInt64BE(8);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get current offset
|
|
208
|
+
*/
|
|
209
|
+
getOffset() {
|
|
210
|
+
return this.offset;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/ec/packet/PacketParser.ts
|
|
215
|
+
/**
|
|
216
|
+
* Packet Parser - Deserializes EC packets from binary data
|
|
217
|
+
*/
|
|
218
|
+
var PacketParser = class {
|
|
219
|
+
static HEADER_SIZE = 8;
|
|
220
|
+
/**
|
|
221
|
+
* Parse a packet from a buffer
|
|
222
|
+
*/
|
|
223
|
+
static parse(buffer) {
|
|
224
|
+
if (buffer.length < this.HEADER_SIZE) throw new InvalidECException(`Buffer too small: ${buffer.length} bytes`);
|
|
225
|
+
const flags = Flags.fromBuffer(buffer, 0);
|
|
226
|
+
const dataLength = buffer.readUInt32BE(4);
|
|
227
|
+
if (buffer.length < this.HEADER_SIZE + dataLength) throw new InvalidECException(`Incomplete packet: expected ${this.HEADER_SIZE + dataLength} bytes, got ${buffer.length}`);
|
|
228
|
+
let data = buffer.subarray(this.HEADER_SIZE, this.HEADER_SIZE + dataLength);
|
|
229
|
+
if (flags.isZlib()) data = this.decompress(data);
|
|
230
|
+
return this.parsePacketData(data, flags);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parse packet data (after decompression)
|
|
234
|
+
*/
|
|
235
|
+
static parsePacketData(data, flags) {
|
|
236
|
+
let offset = 0;
|
|
237
|
+
const opCode = data.readUInt8(offset);
|
|
238
|
+
offset += 1;
|
|
239
|
+
const useUtf8 = flags.isUtf8Numbers();
|
|
240
|
+
let tagCount;
|
|
241
|
+
if (useUtf8) {
|
|
242
|
+
tagCount = data.readUInt8(offset);
|
|
243
|
+
offset += 1;
|
|
244
|
+
} else {
|
|
245
|
+
tagCount = data.readUInt16BE(offset);
|
|
246
|
+
offset += 2;
|
|
247
|
+
}
|
|
248
|
+
return new Packet(opCode, flags, new TagParser(data, offset, useUtf8).parseTags(tagCount));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Decompress ZLIB data
|
|
252
|
+
*/
|
|
253
|
+
static decompress(data) {
|
|
254
|
+
try {
|
|
255
|
+
return zlib.inflateSync(data);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
throw new InvalidECException(`Failed to decompress packet: ${error}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculate expected packet size from header
|
|
262
|
+
*/
|
|
263
|
+
static getExpectedPacketSize(headerBuffer) {
|
|
264
|
+
if (headerBuffer.length < this.HEADER_SIZE) throw new InvalidECException("Header buffer too small");
|
|
265
|
+
const dataLength = headerBuffer.readUInt32BE(4);
|
|
266
|
+
return this.HEADER_SIZE + dataLength;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Check if buffer contains a complete packet
|
|
270
|
+
*/
|
|
271
|
+
static hasCompletePacket(buffer) {
|
|
272
|
+
if (buffer.length < this.HEADER_SIZE) return false;
|
|
273
|
+
const expectedSize = this.getExpectedPacketSize(buffer);
|
|
274
|
+
return buffer.length >= expectedSize;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/ec/tag/TagEncoder.ts
|
|
279
|
+
var TagEncoder = class {
|
|
280
|
+
useUtf8Numbers;
|
|
281
|
+
constructor(useUtf8Numbers = false) {
|
|
282
|
+
this.useUtf8Numbers = useUtf8Numbers;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Calculate total size needed for a tag (Physical Buffer Size)
|
|
286
|
+
*/
|
|
287
|
+
calculateTagSize(tag) {
|
|
288
|
+
let size = 0;
|
|
289
|
+
if (this.useUtf8Numbers) {
|
|
290
|
+
const hasSubtags = (tag.nestedTags?.length ?? 0) > 0 ? 1 : 0;
|
|
291
|
+
const nameAndSubtags = (tag.name & 65535) << 1 | hasSubtags;
|
|
292
|
+
size += numberLength(nameAndSubtags);
|
|
293
|
+
} else size += 2;
|
|
294
|
+
size += 1;
|
|
295
|
+
const reportedLength = this.calculateReportedLength(tag);
|
|
296
|
+
if (this.useUtf8Numbers) size += numberLength(reportedLength);
|
|
297
|
+
else size += 4;
|
|
298
|
+
if ((tag.nestedTags?.length ?? 0) > 0) {
|
|
299
|
+
const nestedCount = tag.nestedTags?.length ?? 0;
|
|
300
|
+
if (this.useUtf8Numbers) size += numberLength(nestedCount);
|
|
301
|
+
else size += 2;
|
|
302
|
+
}
|
|
303
|
+
if (tag.nestedTags) for (const nestedTag of tag.nestedTags) size += this.calculateTagSize(nestedTag);
|
|
304
|
+
size += this.calculateValueSize(tag);
|
|
305
|
+
return size;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Calculate the "Reported Length" of the tag content.
|
|
309
|
+
* This logic mimics Jamule's computeTagLength, which assumes FIXED header sizes
|
|
310
|
+
* (2 bytes name, 1 byte type, 4 bytes length, 2 bytes subtag count)
|
|
311
|
+
* even when UTF-8 encoding is used.
|
|
312
|
+
*/
|
|
313
|
+
calculateReportedLength(tag) {
|
|
314
|
+
let length = 0;
|
|
315
|
+
if (tag.nestedTags) for (const nestedTag of tag.nestedTags) {
|
|
316
|
+
length += this.calculateReportedLength(nestedTag);
|
|
317
|
+
length += 7;
|
|
318
|
+
if ((nestedTag.nestedTags?.length ?? 0) > 0) length += 2;
|
|
319
|
+
}
|
|
320
|
+
length += this.calculateValueSize(tag);
|
|
321
|
+
return length;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Calculate size of tag value
|
|
325
|
+
*/
|
|
326
|
+
calculateValueSize(tag) {
|
|
327
|
+
const encoded = tag.encodeValue();
|
|
328
|
+
switch (tag.type) {
|
|
329
|
+
case 1:
|
|
330
|
+
if (encoded.length === 0) return 0;
|
|
331
|
+
return encoded.length;
|
|
332
|
+
case 6: return encoded.length;
|
|
333
|
+
case 2: return 1;
|
|
334
|
+
case 3: return 2;
|
|
335
|
+
case 4: return 4;
|
|
336
|
+
case 5: return 8;
|
|
337
|
+
case 10: return 16;
|
|
338
|
+
case 7: return 8;
|
|
339
|
+
case 8: return 6;
|
|
340
|
+
case 9: return 16;
|
|
341
|
+
default: return 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Encode a tag to a buffer
|
|
346
|
+
*/
|
|
347
|
+
encodeTag(tag, buffer, offset) {
|
|
348
|
+
let currentOffset = offset;
|
|
349
|
+
const hasSubtags = (tag.nestedTags?.length ?? 0) > 0 ? 1 : 0;
|
|
350
|
+
const nameAndSubtags = (tag.name & 65535) << 1 | hasSubtags;
|
|
351
|
+
if (this.useUtf8Numbers) {
|
|
352
|
+
const encoded = encodeUtf8Number(nameAndSubtags);
|
|
353
|
+
encoded.copy(buffer, currentOffset);
|
|
354
|
+
currentOffset += encoded.length;
|
|
355
|
+
} else {
|
|
356
|
+
buffer.writeUInt16BE(nameAndSubtags, currentOffset);
|
|
357
|
+
currentOffset += 2;
|
|
358
|
+
}
|
|
359
|
+
buffer.writeUInt8(tag.type, currentOffset);
|
|
360
|
+
currentOffset += 1;
|
|
361
|
+
const tagLength = this.calculateReportedLength(tag);
|
|
362
|
+
if (this.useUtf8Numbers) {
|
|
363
|
+
const encoded = encodeUtf8Number(tagLength);
|
|
364
|
+
encoded.copy(buffer, currentOffset);
|
|
365
|
+
currentOffset += encoded.length;
|
|
366
|
+
} else {
|
|
367
|
+
buffer.writeUInt32BE(tagLength, currentOffset);
|
|
368
|
+
currentOffset += 4;
|
|
369
|
+
}
|
|
370
|
+
const nestedCount = tag.nestedTags?.length ?? 0;
|
|
371
|
+
if (hasSubtags) if (this.useUtf8Numbers) {
|
|
372
|
+
const encoded = encodeUtf8Number(nestedCount);
|
|
373
|
+
encoded.copy(buffer, currentOffset);
|
|
374
|
+
currentOffset += encoded.length;
|
|
375
|
+
} else {
|
|
376
|
+
buffer.writeUInt16BE(nestedCount, currentOffset);
|
|
377
|
+
currentOffset += 2;
|
|
378
|
+
}
|
|
379
|
+
if (tag.nestedTags) for (const nestedTag of tag.nestedTags) currentOffset = this.encodeTag(nestedTag, buffer, currentOffset);
|
|
380
|
+
currentOffset = this.encodeValue(tag, buffer, currentOffset);
|
|
381
|
+
return currentOffset;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Encode tag value to buffer
|
|
385
|
+
*/
|
|
386
|
+
encodeValue(tag, buffer, offset) {
|
|
387
|
+
const encoded = tag.encodeValue();
|
|
388
|
+
switch (tag.type) {
|
|
389
|
+
case 1:
|
|
390
|
+
if (encoded.length > 0) {
|
|
391
|
+
encoded.copy(buffer, offset);
|
|
392
|
+
return offset + encoded.length;
|
|
393
|
+
}
|
|
394
|
+
return offset;
|
|
395
|
+
case 2:
|
|
396
|
+
case 3:
|
|
397
|
+
case 4:
|
|
398
|
+
case 5:
|
|
399
|
+
case 6:
|
|
400
|
+
case 7:
|
|
401
|
+
case 8:
|
|
402
|
+
case 9:
|
|
403
|
+
case 10:
|
|
404
|
+
encoded.copy(buffer, offset);
|
|
405
|
+
return offset + encoded.length;
|
|
406
|
+
default: return offset;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Encode multiple tags
|
|
411
|
+
*/
|
|
412
|
+
encodeTags(tags) {
|
|
413
|
+
let totalSize = 0;
|
|
414
|
+
for (const tag of tags) totalSize += this.calculateTagSize(tag);
|
|
415
|
+
const buffer = Buffer.allocUnsafe(totalSize);
|
|
416
|
+
let offset = 0;
|
|
417
|
+
for (const tag of tags) offset = this.encodeTag(tag, buffer, offset);
|
|
418
|
+
return buffer;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/ec/packet/PacketWriter.ts
|
|
423
|
+
/**
|
|
424
|
+
* Packet Writer - Serializes EC packets to binary data
|
|
425
|
+
*/
|
|
426
|
+
var PacketWriter = class {
|
|
427
|
+
static HEADER_SIZE = 8;
|
|
428
|
+
/**
|
|
429
|
+
* Write a packet to a buffer
|
|
430
|
+
*/
|
|
431
|
+
static write(packet) {
|
|
432
|
+
const useUtf8 = packet.flags.isUtf8Numbers();
|
|
433
|
+
const useZlib = packet.flags.isZlib();
|
|
434
|
+
let data = this.encodePacketData(packet, useUtf8);
|
|
435
|
+
if (useZlib) data = this.compress(data);
|
|
436
|
+
const buffer = Buffer.allocUnsafe(this.HEADER_SIZE + data.length);
|
|
437
|
+
packet.flags.writeToBuffer(buffer, 0);
|
|
438
|
+
buffer.writeUInt32BE(data.length, 4);
|
|
439
|
+
data.copy(buffer, this.HEADER_SIZE);
|
|
440
|
+
return buffer;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Encode packet data (before compression)
|
|
444
|
+
*/
|
|
445
|
+
static encodePacketData(packet, useUtf8) {
|
|
446
|
+
const encoder = new TagEncoder(useUtf8);
|
|
447
|
+
let size = 1;
|
|
448
|
+
if (useUtf8) {
|
|
449
|
+
const countBuf = encodeUtf8Number(packet.tags.length);
|
|
450
|
+
size += countBuf.length;
|
|
451
|
+
} else size += 2;
|
|
452
|
+
for (const tag of packet.tags) size += encoder.calculateTagSize(tag);
|
|
453
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
454
|
+
let offset = 0;
|
|
455
|
+
buffer.writeUInt8(packet.opCode, offset);
|
|
456
|
+
offset += 1;
|
|
457
|
+
if (useUtf8) {
|
|
458
|
+
const countBuf = encodeUtf8Number(packet.tags.length);
|
|
459
|
+
countBuf.copy(buffer, offset);
|
|
460
|
+
offset += countBuf.length;
|
|
461
|
+
} else {
|
|
462
|
+
buffer.writeUInt16BE(packet.tags.length, offset);
|
|
463
|
+
offset += 2;
|
|
464
|
+
}
|
|
465
|
+
for (const tag of packet.tags) offset = encoder.encodeTag(tag, buffer, offset);
|
|
466
|
+
return buffer;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Compress data with ZLIB
|
|
470
|
+
*/
|
|
471
|
+
static compress(data) {
|
|
472
|
+
return zlib.deflateSync(data, { level: zlib.constants.Z_BEST_COMPRESSION });
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/request/AuthRequest.ts
|
|
477
|
+
/**
|
|
478
|
+
* Auth Request - Send client info to initiate authentication
|
|
479
|
+
*/
|
|
480
|
+
var AuthClientInfoRequest = class extends Request {
|
|
481
|
+
constructor(clientName = "amule-ts-client", clientVersion = "1.0.0") {
|
|
482
|
+
super(2);
|
|
483
|
+
this.addTag(new StringTag(256, clientName));
|
|
484
|
+
this.addTag(new StringTag(257, clientVersion));
|
|
485
|
+
this.addTag(new UShortTag(2, 516));
|
|
486
|
+
this.addTag(new CustomTag(12, Buffer.alloc(0)));
|
|
487
|
+
this.addTag(new CustomTag(13, Buffer.alloc(0)));
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Auth Password Request - Send hashed password after receiving salt
|
|
492
|
+
*/
|
|
493
|
+
var AuthPasswordRequest = class extends Request {
|
|
494
|
+
constructor(hashedPassword) {
|
|
495
|
+
super(80);
|
|
496
|
+
this.addTag(new Hash16Tag(1, hashedPassword));
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/response/AuthSaltResponse.ts
|
|
501
|
+
var AuthSaltResponse = class AuthSaltResponse {
|
|
502
|
+
salt;
|
|
503
|
+
constructor(salt) {
|
|
504
|
+
this.salt = salt;
|
|
505
|
+
}
|
|
506
|
+
static fromPacket(packet) {
|
|
507
|
+
const saltTag = findNumericTag(packet.tags, 11);
|
|
508
|
+
if (!saltTag) throw new InvalidECException("Missing salt tag in auth salt response");
|
|
509
|
+
return new AuthSaltResponse(saltTag.getLong());
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/auth/PasswordHasher.ts
|
|
514
|
+
/**
|
|
515
|
+
* Password Hasher for aMule EC authentication
|
|
516
|
+
*/
|
|
517
|
+
var PasswordHasher = class {
|
|
518
|
+
/**
|
|
519
|
+
* Hash a password with salt using MD5
|
|
520
|
+
*
|
|
521
|
+
* This matches aMule's password hashing algorithm (from jamule):
|
|
522
|
+
* 1. saltHash = MD5(salt.toHexString().uppercase())
|
|
523
|
+
* 2. passwordHash = MD5(password)
|
|
524
|
+
* 3. return MD5(passwordHash.toHex().lowercase() + saltHash.toHex().lowercase())
|
|
525
|
+
*
|
|
526
|
+
* @param password The password to hash
|
|
527
|
+
* @param salt The salt received from the server
|
|
528
|
+
* @returns The hashed password as a Buffer
|
|
529
|
+
*/
|
|
530
|
+
static hash(password, salt) {
|
|
531
|
+
const saltHexUpper = salt.toString(16).toUpperCase().padStart(16, "0");
|
|
532
|
+
const saltHash = crypto.createHash("md5").update(saltHexUpper, "utf-8").digest();
|
|
533
|
+
const passwordHashHex = crypto.createHash("md5").update(password, "utf-8").digest().toString("hex").toLowerCase();
|
|
534
|
+
const saltHashHex = saltHash.toString("hex").toLowerCase();
|
|
535
|
+
return crypto.createHash("md5").update(passwordHashHex + saltHashHex, "utf-8").digest();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Hash a password without salt (for testing)
|
|
539
|
+
*/
|
|
540
|
+
static hashWithoutSalt(password) {
|
|
541
|
+
const lowerPassword = password.toLowerCase();
|
|
542
|
+
const md5Hash = crypto.createHash("md5");
|
|
543
|
+
md5Hash.update(lowerPassword, "utf-8");
|
|
544
|
+
return md5Hash.digest();
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/client/AmuleConnection.ts
|
|
549
|
+
/**
|
|
550
|
+
* AmuleConnection - Manages the low-level connection to aMule EC
|
|
551
|
+
*/
|
|
552
|
+
var AmuleConnection = class {
|
|
553
|
+
host;
|
|
554
|
+
port;
|
|
555
|
+
password;
|
|
556
|
+
timeout;
|
|
557
|
+
socket;
|
|
558
|
+
connected = false;
|
|
559
|
+
buffer = Buffer.allocUnsafe(0);
|
|
560
|
+
pendingResponses = [];
|
|
561
|
+
connectionPromise;
|
|
562
|
+
requestTimeout = 0;
|
|
563
|
+
debug = false;
|
|
564
|
+
/**
|
|
565
|
+
* Enable or disable debug logging
|
|
566
|
+
*/
|
|
567
|
+
setDebug(enabled) {
|
|
568
|
+
this.debug = !!enabled;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Internal debug logger
|
|
572
|
+
*/
|
|
573
|
+
log(...args) {
|
|
574
|
+
if (!this.debug) return;
|
|
575
|
+
console.log("[DEBUG]", ...args);
|
|
576
|
+
}
|
|
577
|
+
constructor(host, port, password, timeout, requestTimeout = 0) {
|
|
578
|
+
this.host = host;
|
|
579
|
+
this.port = port;
|
|
580
|
+
this.password = password;
|
|
581
|
+
this.timeout = timeout;
|
|
582
|
+
this.requestTimeout = Math.max(0, requestTimeout);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Set default timeout for a single request/response cycle.
|
|
586
|
+
* Use 0 to disable.
|
|
587
|
+
*/
|
|
588
|
+
setRequestTimeout(timeoutMs) {
|
|
589
|
+
this.requestTimeout = Math.max(0, timeoutMs || 0);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Get current default timeout for requests.
|
|
593
|
+
*/
|
|
594
|
+
getRequestTimeout() {
|
|
595
|
+
return this.requestTimeout;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Reject and clear all pending responses.
|
|
599
|
+
*/
|
|
600
|
+
rejectAllPending(error) {
|
|
601
|
+
while (this.pendingResponses.length > 0) {
|
|
602
|
+
const pending = this.pendingResponses.shift();
|
|
603
|
+
if (pending?.timeoutHandle) clearTimeout(pending.timeoutHandle);
|
|
604
|
+
pending?.reject(error);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Reconnect to the server
|
|
609
|
+
*/
|
|
610
|
+
async reconnect() {
|
|
611
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
612
|
+
this.connectionPromise = (async () => {
|
|
613
|
+
this.connected = false;
|
|
614
|
+
if (this.socket) this.socket.destroy();
|
|
615
|
+
this.socket = new net.Socket();
|
|
616
|
+
if (this.timeout > 0) this.socket.setTimeout(this.timeout);
|
|
617
|
+
this.socket.on("data", (data) => this.handleData(Buffer.isBuffer(data) ? data : Buffer.from(data)));
|
|
618
|
+
this.socket.on("error", (error) => this.handleError(error));
|
|
619
|
+
this.socket.on("timeout", () => this.handleTimeout());
|
|
620
|
+
this.socket.on("close", () => this.handleClose());
|
|
621
|
+
try {
|
|
622
|
+
await this.connectSocket();
|
|
623
|
+
await this.authenticate();
|
|
624
|
+
this.connected = true;
|
|
625
|
+
} finally {
|
|
626
|
+
this.connectionPromise = void 0;
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
return this.connectionPromise;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Connect socket to server
|
|
633
|
+
*/
|
|
634
|
+
connectSocket() {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
if (!this.socket) return reject(new CommunicationException("Socket not initialized"));
|
|
637
|
+
this.socket.connect(this.port, this.host, () => {
|
|
638
|
+
resolve();
|
|
639
|
+
});
|
|
640
|
+
this.socket.once("error", reject);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Perform authentication handshake
|
|
645
|
+
*/
|
|
646
|
+
async authenticate() {
|
|
647
|
+
this.log("Starting authentication...");
|
|
648
|
+
const clientInfoRequest = new AuthClientInfoRequest();
|
|
649
|
+
this.log("Sending client info...");
|
|
650
|
+
const saltPacket = await this.sendRequestNoAuth(clientInfoRequest);
|
|
651
|
+
this.log("Received salt response");
|
|
652
|
+
if (saltPacket.opCode === 3) throw new ServerException("Authentication failed: server rejected client");
|
|
653
|
+
if (saltPacket.opCode !== 79) throw new ServerException(`Unexpected response to client info: ${saltPacket.opCode}`);
|
|
654
|
+
const saltResponse = AuthSaltResponse.fromPacket(saltPacket);
|
|
655
|
+
this.log("Salt:", saltResponse.salt);
|
|
656
|
+
const hashedPassword = PasswordHasher.hash(this.password, saltResponse.salt);
|
|
657
|
+
this.log("Hashed password:", hashedPassword.toString("hex"));
|
|
658
|
+
const authRequest = new AuthPasswordRequest(hashedPassword);
|
|
659
|
+
this.log("Sending hashed password...");
|
|
660
|
+
const authPacket = await this.sendRequestNoAuth(authRequest);
|
|
661
|
+
this.log("Received auth response");
|
|
662
|
+
if (authPacket.opCode === 3) throw new ServerException("Authentication failed: invalid password");
|
|
663
|
+
if (authPacket.opCode !== 4) throw new ServerException(`Unexpected auth response: ${authPacket.opCode}`);
|
|
664
|
+
this.log("Authentication successful!");
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Handle incoming data
|
|
668
|
+
*/
|
|
669
|
+
handleData(data) {
|
|
670
|
+
this.log("Received data:", data.length, "bytes, hex:", data.toString("hex").substring(0, 100));
|
|
671
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
672
|
+
while (PacketParser.hasCompletePacket(this.buffer)) try {
|
|
673
|
+
const packet = PacketParser.parse(this.buffer);
|
|
674
|
+
this.log("Parsed packet, opCode:", packet.opCode);
|
|
675
|
+
const consumedBytes = PacketParser.getExpectedPacketSize(this.buffer);
|
|
676
|
+
this.buffer = this.buffer.subarray(consumedBytes);
|
|
677
|
+
const pending = this.pendingResponses.shift();
|
|
678
|
+
if (pending) pending.resolve(packet);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
this.log("Parse error:", error);
|
|
681
|
+
const pending = this.pendingResponses.shift();
|
|
682
|
+
if (pending) pending.reject(error);
|
|
683
|
+
this.buffer = Buffer.allocUnsafe(0);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Handle socket error
|
|
689
|
+
*/
|
|
690
|
+
handleError(error) {
|
|
691
|
+
this.connected = false;
|
|
692
|
+
this.rejectAllPending(new CommunicationException(`Socket error: ${error.message}`));
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Handle socket timeout
|
|
696
|
+
*/
|
|
697
|
+
handleTimeout() {
|
|
698
|
+
this.connected = false;
|
|
699
|
+
this.rejectAllPending(new CommunicationException("Socket timeout"));
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Handle socket close
|
|
703
|
+
*/
|
|
704
|
+
handleClose() {
|
|
705
|
+
this.log("Socket closed! connected:", this.connected, "pendingResponses:", this.pendingResponses.length);
|
|
706
|
+
this.connected = false;
|
|
707
|
+
this.rejectAllPending(new CommunicationException("Socket closed"));
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Send a request and wait for response
|
|
711
|
+
*/
|
|
712
|
+
async sendRequest(request, timeoutMs) {
|
|
713
|
+
if (!this.connected) await this.reconnect();
|
|
714
|
+
try {
|
|
715
|
+
return await this.sendRequestNoAuth(request, timeoutMs);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this.connected = false;
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Send a request without checking authentication
|
|
723
|
+
*/
|
|
724
|
+
async sendRequestNoAuth(request, timeoutMs) {
|
|
725
|
+
if (!this.socket) throw new CommunicationException("Socket not initialized");
|
|
726
|
+
const packet = request.buildPacket();
|
|
727
|
+
const buffer = PacketWriter.write(packet);
|
|
728
|
+
this.log("Sending request:", packet.opCode.toString(16), "Size:", buffer.length);
|
|
729
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
730
|
+
const pending = {
|
|
731
|
+
resolve: (packet) => {
|
|
732
|
+
if (pending.timeoutHandle) clearTimeout(pending.timeoutHandle);
|
|
733
|
+
resolve(packet);
|
|
734
|
+
},
|
|
735
|
+
reject: (error) => {
|
|
736
|
+
if (pending.timeoutHandle) clearTimeout(pending.timeoutHandle);
|
|
737
|
+
reject(error);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const effectiveTimeout = timeoutMs ?? this.requestTimeout;
|
|
741
|
+
if (effectiveTimeout > 0) pending.timeoutHandle = setTimeout(() => {
|
|
742
|
+
if (!this.pendingResponses.includes(pending)) return;
|
|
743
|
+
this.connected = false;
|
|
744
|
+
this.rejectAllPending(new CommunicationException(`Request timeout after ${effectiveTimeout}ms`));
|
|
745
|
+
if (this.socket && !this.socket.destroyed) this.socket.destroy();
|
|
746
|
+
}, effectiveTimeout);
|
|
747
|
+
this.pendingResponses.push(pending);
|
|
748
|
+
});
|
|
749
|
+
this.socket.write(buffer);
|
|
750
|
+
return responsePromise;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/model/index.ts
|
|
755
|
+
let FileStatus = /* @__PURE__ */ function(FileStatus) {
|
|
756
|
+
FileStatus[FileStatus["READY"] = 0] = "READY";
|
|
757
|
+
FileStatus[FileStatus["EMPTY"] = 1] = "EMPTY";
|
|
758
|
+
FileStatus[FileStatus["WAITINGFORHASH"] = 2] = "WAITINGFORHASH";
|
|
759
|
+
FileStatus[FileStatus["HASHING"] = 3] = "HASHING";
|
|
760
|
+
FileStatus[FileStatus["ERROR"] = 4] = "ERROR";
|
|
761
|
+
FileStatus[FileStatus["INSUFFICIENT"] = 5] = "INSUFFICIENT";
|
|
762
|
+
FileStatus[FileStatus["UNKNOWN"] = 6] = "UNKNOWN";
|
|
763
|
+
FileStatus[FileStatus["PAUSED"] = 7] = "PAUSED";
|
|
764
|
+
FileStatus[FileStatus["COMPLETING"] = 8] = "COMPLETING";
|
|
765
|
+
FileStatus[FileStatus["COMPLETE"] = 9] = "COMPLETE";
|
|
766
|
+
FileStatus[FileStatus["ALLOCATING"] = 10] = "ALLOCATING";
|
|
767
|
+
return FileStatus;
|
|
768
|
+
}({});
|
|
769
|
+
let DownloadCommand = /* @__PURE__ */ function(DownloadCommand) {
|
|
770
|
+
DownloadCommand[DownloadCommand["SWAP_A4AF_THIS"] = 22] = "SWAP_A4AF_THIS";
|
|
771
|
+
DownloadCommand[DownloadCommand["SWAP_A4AF_THIS_AUTO"] = 23] = "SWAP_A4AF_THIS_AUTO";
|
|
772
|
+
DownloadCommand[DownloadCommand["SWAP_A4AF_OTHERS"] = 24] = "SWAP_A4AF_OTHERS";
|
|
773
|
+
DownloadCommand[DownloadCommand["PAUSE"] = 25] = "PAUSE";
|
|
774
|
+
DownloadCommand[DownloadCommand["RESUME"] = 26] = "RESUME";
|
|
775
|
+
DownloadCommand[DownloadCommand["STOP"] = 27] = "STOP";
|
|
776
|
+
DownloadCommand[DownloadCommand["DELETE"] = 29] = "DELETE";
|
|
777
|
+
return DownloadCommand;
|
|
778
|
+
}({});
|
|
779
|
+
let SearchType = /* @__PURE__ */ function(SearchType) {
|
|
780
|
+
SearchType[SearchType["GLOBAL"] = 1] = "GLOBAL";
|
|
781
|
+
SearchType[SearchType["KAD"] = 2] = "KAD";
|
|
782
|
+
SearchType[SearchType["LOCAL"] = 0] = "LOCAL";
|
|
783
|
+
SearchType[SearchType["WEB"] = 3] = "WEB";
|
|
784
|
+
return SearchType;
|
|
785
|
+
}({});
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/client/AmuleClient.ts
|
|
788
|
+
/**
|
|
789
|
+
* Main aMule EC Client
|
|
790
|
+
*/
|
|
791
|
+
var AmuleClient = class {
|
|
792
|
+
connection;
|
|
793
|
+
constructor(options) {
|
|
794
|
+
this.connection = new AmuleConnection(options.host, options.port, options.password, options.timeout || 0, options.requestTimeout || 0);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Set default timeout for individual request/response cycles.
|
|
798
|
+
* Use 0 to disable request timeout.
|
|
799
|
+
*/
|
|
800
|
+
setRequestTimeout(timeoutMs) {
|
|
801
|
+
this.connection.setRequestTimeout(timeoutMs);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get current default timeout for requests.
|
|
805
|
+
*/
|
|
806
|
+
getRequestTimeout() {
|
|
807
|
+
return this.connection.getRequestTimeout();
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Reconnect to the server
|
|
811
|
+
*/
|
|
812
|
+
async reconnect() {
|
|
813
|
+
await this.connection.reconnect();
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Get server statistics
|
|
817
|
+
*/
|
|
818
|
+
async getStats() {
|
|
819
|
+
const { StatsRequest } = await import("./StatsRequest-B8v9LLHG.mjs");
|
|
820
|
+
const { StatsResponseParser } = await import("./StatsResponse-BVUxNPbz.mjs");
|
|
821
|
+
const request = new StatsRequest();
|
|
822
|
+
const packet = await this.connection.sendRequest(request);
|
|
823
|
+
return StatsResponseParser.fromPacket(packet);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get incremental updates for files, clients, servers, and friends
|
|
827
|
+
*/
|
|
828
|
+
async getUpdate(detailLevel = 4) {
|
|
829
|
+
const { UpdateRequest } = await import("./UpdateRequest-Bunl73Ga.mjs");
|
|
830
|
+
const { UpdateResponseParser } = await import("./UpdateResponse-1LhyH9Zf.mjs");
|
|
831
|
+
const request = new UpdateRequest(detailLevel);
|
|
832
|
+
const packet = await this.connection.sendRequest(request);
|
|
833
|
+
return UpdateResponseParser.fromPacket(packet);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Start an asynchronous search
|
|
837
|
+
*/
|
|
838
|
+
async searchAsync(query, searchType, filters) {
|
|
839
|
+
const { SearchRequest } = await import("./SearchRequest-CNqz-K1Z.mjs");
|
|
840
|
+
const request = new SearchRequest(query, searchType, filters);
|
|
841
|
+
await this.connection.sendRequest(request);
|
|
842
|
+
return query;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Get search status (0-1)
|
|
846
|
+
*/
|
|
847
|
+
async searchStatus() {
|
|
848
|
+
const { SearchStatusRequest } = await import("./SearchStatusRequest-Bv6iaC9Y.mjs");
|
|
849
|
+
const { SearchProgressResponse } = await import("./SearchProgressResponse-Bputc9Ww.mjs");
|
|
850
|
+
const request = new SearchStatusRequest();
|
|
851
|
+
const packet = await this.connection.sendRequest(request);
|
|
852
|
+
return SearchProgressResponse.fromPacket(packet).progress;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Get search results
|
|
856
|
+
*/
|
|
857
|
+
async searchResults() {
|
|
858
|
+
const { SearchResultsRequest } = await import("./SearchResultsRequest-DKG508KF.mjs");
|
|
859
|
+
const { SearchResultsResponseParser } = await import("./SearchResultsResponse-ChNrKJ6_.mjs");
|
|
860
|
+
const request = new SearchResultsRequest();
|
|
861
|
+
const packet = await this.connection.sendRequest(request);
|
|
862
|
+
return SearchResultsResponseParser.fromPacket(packet);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Perform a synchronous search (waits for results)
|
|
866
|
+
*/
|
|
867
|
+
async searchSync(query, searchType, filters, timeout = 3e4) {
|
|
868
|
+
await this.searchAsync(query, searchType, filters);
|
|
869
|
+
const startTime = Date.now();
|
|
870
|
+
while (Date.now() - startTime < timeout) {
|
|
871
|
+
if (await this.searchStatus() >= 1) return await this.searchResults();
|
|
872
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
873
|
+
}
|
|
874
|
+
throw new Error("Search timeout");
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Stop current search
|
|
878
|
+
*/
|
|
879
|
+
async searchStop() {
|
|
880
|
+
const { SearchStopRequest } = await import("./SearchStopRequest-rET-2Nsz.mjs");
|
|
881
|
+
const request = new SearchStopRequest();
|
|
882
|
+
await this.connection.sendRequest(request);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Download a file from search results
|
|
886
|
+
*/
|
|
887
|
+
async downloadSearchResult(hash) {
|
|
888
|
+
const { DownloadFileRequest } = await import("./DownloadFileRequest-CcNqsBo7.mjs");
|
|
889
|
+
const request = new DownloadFileRequest(hash);
|
|
890
|
+
await this.connection.sendRequest(request);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Download from an ed2k link
|
|
894
|
+
*/
|
|
895
|
+
async downloadEd2kLink(link) {
|
|
896
|
+
const { AddLinkRequest } = await import("./AddLinkRequest-DwUbSMXD.mjs");
|
|
897
|
+
const request = new AddLinkRequest(link);
|
|
898
|
+
await this.connection.sendRequest(request);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get download queue
|
|
902
|
+
*/
|
|
903
|
+
async getDownloadQueue() {
|
|
904
|
+
const { DownloadQueueRequest } = await import("./DownloadQueueRequest-bd7lFiMj.mjs");
|
|
905
|
+
const { DownloadQueueResponseParser } = await import("./DownloadQueueResponse-DyP1rLTV.mjs");
|
|
906
|
+
const request = new DownloadQueueRequest();
|
|
907
|
+
const packet = await this.connection.sendRequest(request);
|
|
908
|
+
return DownloadQueueResponseParser.fromPacket(packet).files;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Get shared files
|
|
912
|
+
*/
|
|
913
|
+
async getSharedFiles() {
|
|
914
|
+
const { SharedFilesRequest } = await import("./SharedFilesRequest-D2M3K0eB.mjs");
|
|
915
|
+
const { SharedFilesResponseParser } = await import("./SharedFilesResponse-BiXT_oCx.mjs");
|
|
916
|
+
const request = new SharedFilesRequest();
|
|
917
|
+
const packet = await this.connection.sendRequest(request);
|
|
918
|
+
return SharedFilesResponseParser.fromPacket(packet).files;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Reload shared files list
|
|
922
|
+
*/
|
|
923
|
+
async reloadSharedFiles() {
|
|
924
|
+
const { ReloadSharedFilesRequest } = await import("./ReloadSharedFilesRequest-CPwAEJYG.mjs");
|
|
925
|
+
const request = new ReloadSharedFilesRequest();
|
|
926
|
+
await this.connection.sendRequest(request);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get client upload/download queue (clients we are uploading to/downloading from)
|
|
930
|
+
*/
|
|
931
|
+
async getClientQueue() {
|
|
932
|
+
const { ClientQueueRequest } = await import("./ClientQueueRequest-D9FaX5K9.mjs");
|
|
933
|
+
const { ClientQueueResponseParser } = await import("./ClientQueueResponse-FVQBR_8b.mjs");
|
|
934
|
+
const request = new ClientQueueRequest();
|
|
935
|
+
const packet = await this.connection.sendRequest(request);
|
|
936
|
+
return ClientQueueResponseParser.fromPacket(packet).clients;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Get the list of servers
|
|
940
|
+
*/
|
|
941
|
+
async getServerList() {
|
|
942
|
+
const { ServerListRequest } = await import("./ServerListRequest-FwFQBp12.mjs");
|
|
943
|
+
const { ServerListResponseParser } = await import("./ServerListResponse-ZAKSSCPZ.mjs");
|
|
944
|
+
const request = new ServerListRequest();
|
|
945
|
+
const packet = await this.connection.sendRequest(request);
|
|
946
|
+
return ServerListResponseParser.fromPacket(packet).servers;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Connect to a specific server
|
|
950
|
+
*/
|
|
951
|
+
async connectToServer(ip, port) {
|
|
952
|
+
const { ServerConnectRequest } = await import("./ServerConnectRequest-CcO2-Eqd.mjs");
|
|
953
|
+
const request = new ServerConnectRequest(ip, port);
|
|
954
|
+
await this.connection.sendRequest(request);
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Disconnect from the current server
|
|
958
|
+
*/
|
|
959
|
+
async disconnectFromServer() {
|
|
960
|
+
const { ServerDisconnectRequest } = await import("./ServerDisconnectRequest-DEORM3SF.mjs");
|
|
961
|
+
const request = new ServerDisconnectRequest();
|
|
962
|
+
await this.connection.sendRequest(request);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Create a category
|
|
966
|
+
*/
|
|
967
|
+
async createCategory(category) {
|
|
968
|
+
const { CreateCategoryRequest } = await import("./CreateCategoryRequest-Br6yfDhZ.mjs");
|
|
969
|
+
const request = new CreateCategoryRequest(category);
|
|
970
|
+
await this.connection.sendRequest(request);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Update a category
|
|
974
|
+
*/
|
|
975
|
+
async updateCategory(id, category) {
|
|
976
|
+
const { UpdateCategoryRequest } = await import("./UpdateCategoryRequest-CgzH7SxP.mjs");
|
|
977
|
+
const request = new UpdateCategoryRequest(id, category);
|
|
978
|
+
await this.connection.sendRequest(request);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Delete a category
|
|
982
|
+
*/
|
|
983
|
+
async deleteCategory(id) {
|
|
984
|
+
const { DeleteCategoryRequest } = await import("./DeleteCategoryRequest-C2-p6sx0.mjs");
|
|
985
|
+
const request = new DeleteCategoryRequest(id);
|
|
986
|
+
await this.connection.sendRequest(request);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get all categories
|
|
990
|
+
*/
|
|
991
|
+
async getCategories() {
|
|
992
|
+
const { GetPreferencesRequest } = await import("./GetPreferencesRequest-C94Su9IH.mjs");
|
|
993
|
+
const { PreferencesResponseParser } = await import("./PreferencesResponse-Dth-n2sA.mjs");
|
|
994
|
+
const request = new GetPreferencesRequest(1);
|
|
995
|
+
const packet = await this.connection.sendRequest(request);
|
|
996
|
+
return PreferencesResponseParser.fromPacket(packet).categories;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Set file category
|
|
1000
|
+
*/
|
|
1001
|
+
async setFileCategory(hash, categoryId) {
|
|
1002
|
+
const { SetFileCategoryRequest } = await import("./SetFileCategoryRequest-BwEPJVeR.mjs");
|
|
1003
|
+
const request = new SetFileCategoryRequest(hash, categoryId);
|
|
1004
|
+
await this.connection.sendRequest(request);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Send a download command (PAUSE, RESUME, STOP, DELETE, etc.)
|
|
1008
|
+
*/
|
|
1009
|
+
async sendDownloadCommand(hash, command) {
|
|
1010
|
+
const { DownloadCommandRequest } = await import("./DownloadCommandRequest-CaULjfNi.mjs");
|
|
1011
|
+
const request = new DownloadCommandRequest(hash, command);
|
|
1012
|
+
await this.connection.sendRequest(request);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Pause a download
|
|
1016
|
+
*/
|
|
1017
|
+
async pauseDownload(hash) {
|
|
1018
|
+
await this.sendDownloadCommand(hash, 25);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Resume a download
|
|
1022
|
+
*/
|
|
1023
|
+
async resumeDownload(hash) {
|
|
1024
|
+
await this.sendDownloadCommand(hash, 26);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Stop a download
|
|
1028
|
+
*/
|
|
1029
|
+
async stopDownload(hash) {
|
|
1030
|
+
await this.sendDownloadCommand(hash, 27);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Delete a download
|
|
1034
|
+
*/
|
|
1035
|
+
async deleteDownload(hash) {
|
|
1036
|
+
await this.sendDownloadCommand(hash, 29);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Trigger a server list update from an URL.
|
|
1040
|
+
*/
|
|
1041
|
+
async updateServerListFromUrl(url) {
|
|
1042
|
+
const { ServerUpdateFromUrlRequest } = await import("./ServerUpdateFromUrlRequest-D4xBqJDJ.mjs");
|
|
1043
|
+
const request = new ServerUpdateFromUrlRequest(url);
|
|
1044
|
+
await this.connection.sendRequest(request);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Set upload priority for a shared file.
|
|
1048
|
+
*/
|
|
1049
|
+
async setSharedFilePriority(hash, priority) {
|
|
1050
|
+
const { SharedFilePriorityRequest } = await import("./SharedFilePriorityRequest-j6ImgagV.mjs");
|
|
1051
|
+
const request = new SharedFilePriorityRequest(hash, priority);
|
|
1052
|
+
await this.connection.sendRequest(request);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get daemon preferences for general/connection/servers/security sections.
|
|
1056
|
+
*/
|
|
1057
|
+
async getPreferences() {
|
|
1058
|
+
const { GetPreferencesRequest } = await import("./GetPreferencesRequest-C94Su9IH.mjs");
|
|
1059
|
+
const { PreferencesDetailsResponseParser } = await import("./PreferencesDetailsResponse-BjMxbvVY.mjs");
|
|
1060
|
+
const request = new GetPreferencesRequest(2118);
|
|
1061
|
+
const packet = await this.connection.sendRequest(request);
|
|
1062
|
+
return PreferencesDetailsResponseParser.fromPacket(packet).preferences;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Set daemon preferences. Only sections present in the input object are sent.
|
|
1066
|
+
*/
|
|
1067
|
+
async setPreferences(preferences) {
|
|
1068
|
+
if (!preferences.general && !preferences.connection && !preferences.servers && !preferences.security) return;
|
|
1069
|
+
const { SetPreferencesRequest } = await import("./SetPreferencesRequest-D2fVIH3P.mjs");
|
|
1070
|
+
const request = new SetPreferencesRequest(preferences);
|
|
1071
|
+
await this.connection.sendRequest(request);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
//#endregion
|
|
1075
|
+
export { AmuleClient, AmuleException, ChunkStatus, CommunicationException, CustomTag, DoubleTag, DownloadCommand, ECDetailLevel, ECOpCode, ECSearchType, ECTagName, EcPrefs, FileStatus, Flags, Hash16Tag, InvalidECException, Ipv4Tag, PARTSIZE, Packet, PacketParser, PacketWriter, ProtocolVersion, SearchType, ServerException, StringTag, Tag, UByteTag, UInt128Tag, UIntTag, ULongTag, UShortTag };
|
|
1076
|
+
|
|
1077
|
+
//# sourceMappingURL=index.mjs.map
|