Upload - NodeJS
使用 NodeJS 编程语言编写的请求示例。
import {describe, it} from "mocha";
import {assert} from "chai";
import fs from "fs";
import https from "https";
import http from "http";
import RsaUtil from "../../src/util/rsa_util.mjs";
import crypto, {RsaPrivateKey, RsaPublicKey} from "crypto";
/**
* 模拟 agency 向 ice-run 发送请求
*/
describe("fire_file", (): void => {
/**
* 服务商(agency)的编号
* 此处使用 agency_no = 100000000000000000 作为模拟
* 真实请求场景,请咨询 ice-run 的技术支持获取正式的 服务商编号 agency_no
*/
const AGENCY_NO: string = "100000000000000000";
/**
* 安全算法 RSA
*/
const SECURITY: string = "RSA";
/**
* ice-run 的 公钥
*/
const PUBLIC_KEY: string = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC78QqLKTkVcwvB2Scm4IoImOXmIthVmx4HHr5Im09aol2r2UlMK3jl4lamdxktvjFswWx41hQBD4tyYFHTY+mQJsRK8Zwm7LZmLJDHcgibIuLxEddln+MI+rWwT+SOdOt7lBvd9PcO4nYr/3Nqkyk8L4PrT+GF7dpVk8iE/VpTwQIDAQAB";
/**
* agency 的 私钥
*/
const PRIVATE_KEY: string = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOyC76Y+/2NqkkIVUtAHkbZkBYI2XXkuqE9o4oIDkbyAY04jC5MLa3oVc3uGG7T/Elodo9dIbIxNh7/ir7GQLR/hY+ookHAewurJ1z4Jn5TQeRk3+nYQcX6a6orfUOMffwcn0Jtat8D+cNAu3uCi+N7Ez/yNpItNORYqTa8AkgzrAgMBAAECgYAS/rsfl3ysb+E6RHsnsQvvYZ4dpJ8iPfCPnCVg+sdoI8mV+3ORBkBGCFYDjDRKd5fyO+IuRqdNJ2bpLtwcfy9YchcV/x40GIuAlre6A6qt4Raq+0l4FJufaqgdmSiLxd5zPX79gdMvBqTbdYzCSDGP5Pf3pTqIS3iKvSixFTQBzQJBAO6k6rE6JE/EHrbRjVtZNitDZ8rouCL0s9nGsgIDBSfgOCClO+HlZKcHq6W9ry8j1gOFeGH2rF1yMslA/ZymRL0CQQD9tk/gk9Xs1Eg1if7SLwvNxUP+z96oEqQTF26tSudPRwOO88fnfE4ZZ4heBt1QI+s7IhG39qecsFW269nvKfbHAkEArXmEgUBalQFjslGyB+1Zyyk8keuJrx9ifbRKQdwgK1R6eICkfxlZiXGx/NFeP041jGnBkLTXpzYUZOexc+YJoQJBANR3r87vnxAU+l+zr62O7oCk+XtT0y/HZJYEYpBHEQyn+MfnSXqG89R8iovLjd0GJ4E+173KlrU2SqHEQ57w8pMCQAEoDjFUXpYzBl8e0M9XsSpGY531mbOl8ASnVp4OSyavaCsn56eEkL9TI5q69KrYUEA4PN3BazMWfll2s/wu6G0=";
/**
* ice-run 的 test 环境的 API 网关 的域名地址
* 正式生产环境的域名地址请查阅 ice-run 的技术文档
*/
const DOMAIN: string = "test-fire-api.ice.run";
/**
* 文件上传接口地址
*/
const UPLOAD: string = "/file-service/api/upload";
/**
* 模拟请求 文件上传 接口
*/
it("file", (): void => {
/**
* 对方公钥
*/
const publicKey: RsaPublicKey = RsaUtil.publicKey(PUBLIC_KEY);
/**
* 己方私钥
*/
const privateKey: RsaPrivateKey = RsaUtil.privateKey(PRIVATE_KEY);
/**
* 此处使用 `${URL}` 模拟请求地址
*/
const URL: string = `https://${DOMAIN}${UPLOAD}`;
console.log("request url", URL)
/**
* 读取文件内容
*/
const filename: string = "/tmp/0.txt";
const fileData: Buffer = fs.readFileSync(filename);
console.log("file name", filename);
/**
* 文件 hash
*/
const hashPlains: string = crypto.createHash("sha256").update(fileData).digest("hex");
console.log("hash plains", hashPlains)
/**
* hash 密文:agency 使用 ice-run 公钥 对 hash 明文加密后得到的密文
*/
const hashCipher: string = RsaUtil.encrypt(publicKey, hashPlains);
console.log("hash cipher", hashCipher);
/**
* 请求签名:agency 使用 私钥 对 hash 密文进行签名
* 当 ice-run 收到请求时,将 会使用 agency 公钥 进行验签
*/
const requestSign: string = RsaUtil.sign(privateKey, hashCipher);
console.log("request sign", requestSign);
/**
* 请求时间,当前世界标准时间的字符串,格式为 RFC-1123 标准,服务器会校验时间差,不允许超过设置的时间间隔(默认为 2 分钟)
*/
const time: string = new Date().toUTCString();
/**
* 链路追踪号(防止重放攻击的请求编号)
* 单位时间内(默认为 1 分钟)不重复的字符串,建议使用 uuid 或者时间戳+随机数。限制长度 = 32。正则表达式 = `^[0-9a-f]{32}$`
*/
const trace: string = crypto.randomUUID().replaceAll("-", "");
/**
* 请求头 boundary
*/
const boundary: string = "boundary" + "--" + Date.now().toString(16);
const requestOptions: http.RequestOptions = {
method: "POST",
hostname: DOMAIN,
path: UPLOAD,
headers: {
"Content-Type": "multipart/form-data; boundary=" + `${boundary}`,
"X-Client": AGENCY_NO,
"X-Security": SECURITY,
"X-HASH": hashCipher,
"X-Sign": requestSign,
"X-Time": time,
"X-Trace": trace,
},
}
console.log("request headers", requestOptions.headers);
/**
* demo 客户端,此处使用 node 标准库模拟,实际场景建议自行选择 HTTP 客户端组件包
* 发起 HTTP 请求之前,请确认 `${DOMAIN}` 可以正常访问,部分客户端的服务器环境可能需要设置请求代理
*/
const request: http.ClientRequest = https.request(requestOptions, (response: http.IncomingMessage): any => {
/**
* 响应状态码
*/
const statusCode: number | undefined = response.statusCode;
console.log("response status", statusCode);
assert.equal(statusCode, 200);
/**
* 响 应 header
*/
const headers: http.IncomingHttpHeaders = response.headers;
console.log("response headers", headers);
/**
* 响应 header X-Sign
*/
const responseSign: string = (headers["X-Sign"] ?? headers["x-sign"])?.toString() ?? "";
assert.isNotEmpty(responseSign);
console.log("response sign", responseSign);
const trunks: Buffer[] = [];
response.on("data", (chunk: Buffer): void => {
trunks.push(chunk);
});
response.on("end", (): void => {
/**
* 响应 body ,json 格式的字符串
*/
const responseBody: string = Buffer.concat(trunks).toString();
console.log("response body", responseBody);
/**
* 响应数据 response body
* 此处使用 json 对象 模拟,实际场景建议定义数据模型类
* 数据的 键 有 code 和 message 和 data
* 其中 data 为 json 序列化后的字符串 RSA 加密后的密文
*/
const responseBodyObject: { code: string, message: string, data: string } = JSON.parse(responseBody);
console.log("response body object", responseBodyObject);
/**
* 响应码,默认 000000 表示请求成功
*/
const code: string = responseBodyObject.code;
console.log("response code", code);
assert.equal(code, "000000");
/**
* 响应 数据密文
*/
const dataCipher: string = responseBodyObject.data;
console.log("data cipher", dataCipher);
/**
* 响应验签:ice-run 处理响应时,会使用 ice-run 私钥对响应密文进行签名
* agency 收到响应后,需要使用 ice-run 公钥进行验签
*/
const verify: boolean = RsaUtil.verify(publicKey, dataCipher, responseSign);
console.log("response verify", verify);
assert.ok(verify);
/**
* 响应 数据明文
*/
const dataPlains: string = RsaUtil.decrypt(privateKey, dataCipher);
console.log("data plains", dataPlains);
});
return response;
}).on("error", (error: Error): void => {
console.error(error);
assert.fail(error.message);
});
/**
* 请求数据 request body
* 此处使用手动拼装标准的 multipart/form-data 的 body 的数据格式,仅用于模拟。
* 实际场景可以使用其它三方库或者自行封装。
*/
const readStream = fs.createReadStream(filename);
request.write(`--${boundary}\r\n`);
request.write('Content-Disposition: form-data; name="hash"\r\n\r\n');
request.write(hashPlains);
request.write(`\r\n--${boundary}\r\n`);
request.write(`Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`);
request.write(`Content-Type: application/octet-stream\r\n\r\n`)
/**
* 文件数据,使用流(stream)来发送文件数据。这样可以保证数据的完整性,并且可以处理大文件,而不会消耗过多的内存
* 不能直接将 Buffer 对象转换为字符串,因为这可能会导致数据损坏 request.write(fileData)
*/
readStream.pipe(request, {end: false});
readStream.on("end", () => {
request.write(`\r\n--${boundary}--`);
request.end();
})
});
});