oicq
Version:
QQ protocol!
199 lines (187 loc) • 6.28 kB
JavaScript
/**
* tcp上传数据
* 网络下载
*/
"use strict";
const stream = require("stream");
const net = require("net");
const http = require("http");
const https = require("https");
const { randomBytes } = require("crypto");
const tea = require("./algo/tea");
const pb = require("./algo/pb");
const { md5, NOOP, BUF0} = require("./common");
const MAX_UPLOAD_SIZE = 31457280;
/**
* 数字ip转换成通用ip
* @param {number|string} ip
*/
function int32ip2str(ip) {
if (typeof ip === "string")
return ip;
ip = ip & 0xffffffff;
return [
ip & 0xff,
(ip & 0xff00) >> 8,
(ip & 0xff0000) >> 16,
(ip & 0xff000000) >> 24 & 0xff,
].join(".");
}
class HighwayTransform extends stream.Transform {
seq = randomBytes(2).readUInt16BE();
offset = 0;
__ = Buffer.from([41]);
/**
* @param {import("./ref").Client} c
* @param {import("./ref").HighwayUploadStreamObject} obj
*/
constructor(c, obj) {
super();
this.c = c;
this.cmd = obj.cmd;
this.md5 = obj.md5;
this.size = obj.size;
this.ticket = obj.ticket || this.c.storage.sig_session;
this.ext = obj.encrypt ? tea.encrypt(obj.ext, this.c.storage.session_key) : obj.ext;
this.on("error", NOOP);
}
_transform(data, encoding, callback) {
let offset = 0, limit = 1048576;
while (offset < data.length) {
const chunk = data.slice(offset, limit + offset);
const head = pb.encode({
1: {
1: 1,
2: String(this.c.uin),
3: "PicUp.DataUp",
4: this.seq++,
6: this.c.apk.subid,
7: 4096,
8: this.cmd,
10: 2052,
},
2: {
2: this.size,
3: this.offset + offset,
4: chunk.length,
6: this.ticket,
8: md5(chunk),
9: this.md5,
},
3: this.ext
});
offset += chunk.length;
const _ = Buffer.allocUnsafe(9);
_.writeUInt8(40);
_.writeUInt32BE(head.length, 1);
_.writeUInt32BE(chunk.length, 5);
this.push(_);
this.push(head);
this.push(chunk);
this.push(this.__);
}
this.offset += data.length;
callback(null);
}
}
const ERROR_HIGHWAY_FAILED = new Error("ERROR_HIGHWAY_FAILED");
/**
* 将一个可读流经过转换后上传
* @this {import("./ref").Client}
* @param {stream.Readable} readable
* @param {import("./ref").HighwayUploadStreamObject} obj
*/
function highwayUploadStream(readable, obj, ip, port) {
ip = int32ip2str(ip || this.storage.ip);
port = port || this.storage.port;
this.logger.debug(`highway ip:${ip} port:${port}`);
return new Promise((resolve) => {
const highway = new HighwayTransform(this, obj);
const socket = net.connect(
port, ip,
() => readable.pipe(highway).pipe(socket, { end: false })
);
const handleRspHeader = (header) => {
const rsp = pb.decode(header);
if (typeof rsp[3] === "number" && rsp[3] !== 0) {
this.logger.warn(`highway upload failed (code: ${rsp[3]})`);
readable.unpipe(highway).destroy();
highway.unpipe(socket).destroy();
socket.end();
throw ERROR_HIGHWAY_FAILED;
} else {
const percent = (rsp[2][3] + rsp[2][4]) / rsp[2][2] * 100;
this.logger.debug(`highway chunk uploaded (${percent.toFixed(2)}%)`);
if (percent >= 100)
socket.end();
}
}
let _data = BUF0;
socket.on("data", (data) => {
try {
_data = _data.length ? Buffer.concat([_data, data]) : data;
while (_data.length >= 5) {
const len = _data.readInt32BE(1);
if (_data.length >= len + 10) {
handleRspHeader(_data.slice(9, len + 9));
_data = _data.slice(len + 10);
}
}
} catch { }
});
socket.on("close", resolve);
socket.on("error", (err) => {
this.logger.warn(err);
});
readable.on("error", (err) => {
this.logger.warn(err);
socket.end();
});
});
}
const ERROR_SIZE_TOO_BIG = new Error("文件体积超过30MB,拒绝下载");
class DownloadTransform extends stream.Transform {
_size = 0;
_transform(data, encoding, callback) {
this._size += data.length;
if (this._size <= MAX_UPLOAD_SIZE) {
this.push(data);
}
callback(null);
}
}
/**
* 下载(最大30M)
* @param {http.OutgoingHttpHeader|undefined|string} headers
* @returns {Promise<stream.Readable>}
*/
function downloadFromWeb(url, headers, redirect = 0) {
if (typeof headers === "string") {
try {
headers = JSON.parse(headers);
} catch {
headers = null;
}
}
return new Promise((resolve, reject) => {
(url.startsWith("https") ? https : http).get(url, { headers }, (res) => {
if (redirect < 3 && String(res.statusCode).startsWith("3") && res.headers["location"]) {
return downloadFromWeb(res.headers["location"], headers, redirect + 1)
.then(resolve)
.catch(reject);
}
if (res.statusCode !== 200) {
res.destroy();
return reject(new Error("http status code: " + res.statusCode));
}
if (res.headers["content-length"] && res.headers["content-length"] > MAX_UPLOAD_SIZE) {
res.destroy();
return reject(ERROR_SIZE_TOO_BIG);
}
resolve(res.pipe(new DownloadTransform));
}).on("error", reject);
});
}
module.exports = {
downloadFromWeb, highwayUploadStream, int32ip2str, MAX_UPLOAD_SIZE,
};