well, its closer
This commit is contained in:
@@ -28,37 +28,45 @@ if (!pcapFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A store for all active WTVP sessions, keyed by stream identifier.
|
// A store for all active WTVP sessions, keyed by stream identifier.
|
||||||
// The identifier is a sorted combination of src/dst ip:port pairs.
|
|
||||||
const wtvpSessions = {};
|
const wtvpSessions = {};
|
||||||
|
|
||||||
const parser = pcapParser.parse(pcapFile);
|
const parser = pcapParser.parse(pcapFile);
|
||||||
|
|
||||||
parser.on('packet', (packet) => {
|
parser.on('packet', (packet) => {
|
||||||
|
|
||||||
const data = packet.data;
|
const data = packet.data;
|
||||||
const ethType = data.readUInt16BE(12);
|
const ethType = data.readUInt16BE(12);
|
||||||
if (ethType !== 0x0800) return; // Not IPv4
|
if (ethType !== 0x0800) return; // Not IPv4
|
||||||
|
|
||||||
const ipHeader = data.slice(14, 34);
|
// IP header parsing
|
||||||
|
const ipHeaderLength = (data[14] & 0x0F) * 4;
|
||||||
|
const ipHeader = data.slice(14, 14 + ipHeaderLength);
|
||||||
const protocol = ipHeader[9];
|
const protocol = ipHeader[9];
|
||||||
if (protocol !== 6) return; // Not TCP
|
if (protocol !== 6) return; // Not TCP
|
||||||
|
|
||||||
const srcIP = ipHeader.slice(12, 16).join('.');
|
const srcIP = ipHeader.slice(12, 16).join('.');
|
||||||
const dstIP = ipHeader.slice(16, 20).join('.');
|
const dstIP = ipHeader.slice(16, 20).join('.');
|
||||||
const tcpHeaderStart = 34;
|
|
||||||
|
// TCP header parsing
|
||||||
|
const tcpHeaderStart = 14 + ipHeaderLength;
|
||||||
|
const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4;
|
||||||
const srcPort = data.readUInt16BE(tcpHeaderStart);
|
const srcPort = data.readUInt16BE(tcpHeaderStart);
|
||||||
const dstPort = data.readUInt16BE(tcpHeaderStart + 2);
|
const dstPort = data.readUInt16BE(tcpHeaderStart + 2);
|
||||||
const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4;
|
const seq = data.readUInt32BE(tcpHeaderStart + 4);
|
||||||
|
const flags = data[tcpHeaderStart + 13];
|
||||||
|
const isSYN = (flags & 0x02) !== 0;
|
||||||
|
const isFIN = (flags & 0x01) !== 0;
|
||||||
|
|
||||||
const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen;
|
const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen;
|
||||||
|
|
||||||
const payload = data.slice(tcpPayloadOffset);
|
const payload = data.slice(tcpPayloadOffset);
|
||||||
|
const tcpPayloadLength = payload.length;
|
||||||
|
|
||||||
// Create a unique key for the TCP session, independent of direction
|
console.log(`[DEBUG] data.length=${data.length}, tcpPayloadOffset=${tcpPayloadOffset}`);
|
||||||
|
// Create a unique key for the TCP session
|
||||||
const src = `${srcIP}:${srcPort}`;
|
const src = `${srcIP}:${srcPort}`;
|
||||||
const dst = `${dstIP}:${dstPort}`;
|
const dst = `${dstIP}:${dstPort}`;
|
||||||
const sessionKey = [src, dst].sort().join('-');
|
const sessionKey = [src, dst].sort().join('-');
|
||||||
|
|
||||||
// If it's a new session, initialize its state
|
// Initialize session state if new
|
||||||
if (!wtvpSessions[sessionKey]) {
|
if (!wtvpSessions[sessionKey]) {
|
||||||
console.log(`[+] New TCP Session detected: ${sessionKey}`);
|
console.log(`[+] New TCP Session detected: ${sessionKey}`);
|
||||||
wtvpSessions[sessionKey] = {
|
wtvpSessions[sessionKey] = {
|
||||||
@@ -66,45 +74,152 @@ parser.on('packet', (packet) => {
|
|||||||
serverAddr: null,
|
serverAddr: null,
|
||||||
wtvsec: null,
|
wtvsec: null,
|
||||||
secureMode: false,
|
secureMode: false,
|
||||||
|
// TCP stream reassembly state, keyed by source ip:port
|
||||||
|
streams: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore packets without a payload
|
|
||||||
if (!payload || payload.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSession = wtvpSessions[sessionKey];
|
const currentSession = wtvpSessions[sessionKey];
|
||||||
const sourceAddr = `${srcIP}:${srcPort}`;
|
|
||||||
const payloadStr = payload.toString('utf8');
|
|
||||||
|
|
||||||
// 1. Identify Client and Server
|
// Ensure a stream object exists for the source of this packet
|
||||||
if (!currentSession.clientAddr) {
|
if (!currentSession.streams[src]) {
|
||||||
|
currentSession.streams[src] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false };
|
||||||
|
}
|
||||||
|
const stream = currentSession.streams[src];
|
||||||
|
|
||||||
|
// 1. Identify Client and Server (if not already done)
|
||||||
|
if (!currentSession.clientAddr && payload.length > 0) {
|
||||||
|
const payloadStr = payload.toString('utf8');
|
||||||
if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) {
|
if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) {
|
||||||
currentSession.clientAddr = sourceAddr;
|
console.log(`[*] Client identified as ${src}, Server as ${dst}`);
|
||||||
currentSession.serverAddr = `${dstIP}:${dstPort}`;
|
currentSession.clientAddr = src;
|
||||||
console.log(`[*] Client identified as ${currentSession.clientAddr}`);
|
currentSession.serverAddr = dst;
|
||||||
|
|
||||||
|
// Mark the current stream (from src) as the client
|
||||||
|
stream.isClient = true;
|
||||||
|
|
||||||
|
// Ensure the server's stream object exists as well
|
||||||
|
if (!currentSession.streams[dst]) {
|
||||||
|
currentSession.streams[dst] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This check handles cases where the first packet didn't identify the client.
|
// Set the isClient flag for every packet now that identification might have happened
|
||||||
if (!currentSession.clientAddr) {
|
if(currentSession.clientAddr){
|
||||||
return;
|
stream.isClient = src === currentSession.clientAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isClient = sourceAddr === currentSession.clientAddr;
|
//
|
||||||
const direction = isClient ? '[CLIENT -> SERVER]' : '[SERVER -> CLIENT]';
|
|
||||||
|
|
||||||
console.log(`\n${'='.repeat(20)} ${direction} (${payload.length} bytes) ${'='.repeat(20)}`);
|
// This is the expected in-order packet. Append its payload.
|
||||||
|
stream.data = Buffer.concat([stream.data, payload]);
|
||||||
|
stream.nextSeq += tcpPayloadLength;
|
||||||
|
if (isSYN || isFIN) stream.nextSeq++;
|
||||||
|
|
||||||
// 2. Process data based on whether we are in secure mode or not
|
// Process any buffered out-of-order packets that are now in sequence
|
||||||
if (!currentSession.secureMode) {
|
let nextSeqInChain = stream.nextSeq;
|
||||||
handlePlaintext(currentSession, payloadStr, isClient);
|
while (stream.outOfOrder[nextSeqInChain]) {
|
||||||
} else {
|
const bufferedPayload = stream.outOfOrder[nextSeqInChain];
|
||||||
handleEncrypted(currentSession, payload, isClient);
|
const bufferedPayloadLength = bufferedPayload.length;
|
||||||
|
|
||||||
|
stream.data = Buffer.concat([stream.data, bufferedPayload]);
|
||||||
|
delete stream.outOfOrder[nextSeqInChain];
|
||||||
|
|
||||||
|
nextSeqInChain += bufferedPayloadLength;
|
||||||
|
}
|
||||||
|
stream.nextSeq = nextSeqInChain;
|
||||||
|
|
||||||
|
// Now that we have new contiguous data, try to process it as application messages
|
||||||
|
for (const addr in currentSession.streams) {
|
||||||
|
const s = currentSession.streams[addr];
|
||||||
|
if (s.data.length > 0) {
|
||||||
|
processStream(currentSession, addr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the reassembled data buffer for a session, looking for complete messages.
|
||||||
|
* @param {object} session - The session state object.
|
||||||
|
* @param {string} sourceAddr - The source address (ip:port) of the stream being processed.
|
||||||
|
*/
|
||||||
|
function processStream(session, sourceAddr) {
|
||||||
|
const stream = session.streams[sourceAddr];
|
||||||
|
console.log(`[DEBUG] Processing stream: ${sourceAddr} isClient: ${stream.isClient}, buffer length: ${stream.data.length}`);
|
||||||
|
|
||||||
|
if (!stream || !session.clientAddr) return; // Don't process until client is identified
|
||||||
|
|
||||||
|
const isClient = stream.isClient;
|
||||||
|
const direction = isClient ? '[CLIENT -> SERVER]' : '[SERVER -> CLIENT]';
|
||||||
|
|
||||||
|
// Loop to process all complete messages currently in the buffer
|
||||||
|
while (true) {
|
||||||
|
let buffer = stream.data;
|
||||||
|
if (buffer.length === 0) break;
|
||||||
|
if (buffer.length === 6) {
|
||||||
|
// Special case: buffer is exactly 6 bytes (likely a keepalive or unknown control message)
|
||||||
|
// Remove the 6 bytes from the buffer and continue
|
||||||
|
stream.data = buffer.slice(6);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lfSeparator = Buffer.from('\n\n');
|
||||||
|
const crlfSeparator = Buffer.from('\r\n\r\n');
|
||||||
|
|
||||||
|
let separatorIndex = buffer.indexOf(lfSeparator);
|
||||||
|
let separatorLength = lfSeparator.length;
|
||||||
|
const crlfIndex = buffer.indexOf(crlfSeparator);
|
||||||
|
|
||||||
|
if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) {
|
||||||
|
separatorIndex = crlfIndex;
|
||||||
|
separatorLength = crlfSeparator.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (separatorIndex === -1) {
|
||||||
|
// Incomplete message (no full headers yet), wait for more data.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headersPart = buffer.slice(0, separatorIndex);
|
||||||
|
const headers = parseHeaders(headersPart.toString('utf8'));
|
||||||
|
const headerBlockLength = separatorIndex + separatorLength;
|
||||||
|
|
||||||
|
let messageToProcess;
|
||||||
|
let consumedSize;
|
||||||
|
|
||||||
|
if (headers['content-length']) {
|
||||||
|
const contentLength = parseInt(headers['content-length'], 10);
|
||||||
|
const totalMessageSize = headerBlockLength + contentLength;
|
||||||
|
|
||||||
|
if (buffer.length < totalMessageSize) {
|
||||||
|
// We have headers, but the body is not fully here yet. Wait for more data.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
messageToProcess = buffer.slice(0, totalMessageSize);
|
||||||
|
consumedSize = totalMessageSize;
|
||||||
|
} else {
|
||||||
|
// No content-length. Assume the rest of the buffer is the message.
|
||||||
|
messageToProcess = buffer.slice(0, headerBlockLength);
|
||||||
|
consumedSize = headerBlockLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`\n${'='.repeat(20)} Processing Message: ${direction} (${messageToProcess.length} bytes) ${'='.repeat(20)}`);
|
||||||
|
|
||||||
|
if (!session.secureMode) {
|
||||||
|
handlePlaintext(session, messageToProcess.toString('utf8'), isClient);
|
||||||
|
} else {
|
||||||
|
handleEncrypted(session, messageToProcess, isClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice the processed message from the front of the buffer
|
||||||
|
stream.data = buffer.slice(consumedSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
parser.on('end', () => {
|
parser.on('end', () => {
|
||||||
console.log('\n[*] PCAP file processing complete.');
|
console.log('\n[*] PCAP file processing complete.');
|
||||||
});
|
});
|
||||||
@@ -115,21 +230,24 @@ parser.on('error', (err) => {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles plaintext WTVP messages to set up the security context.
|
* Handles a single complete plaintext WTVP message.
|
||||||
* @param {object} session - The session state object.
|
* @param {object} session - The session state object.
|
||||||
* @param {string} payload - The plaintext payload.
|
* @param {string} message - The plaintext message string.
|
||||||
* @param {boolean} isClient - True if the message is from the client.
|
* @param {boolean} isClient - True if the message is from the client.
|
||||||
*/
|
*/
|
||||||
function handlePlaintext(session, payload, isClient) {
|
function handlePlaintext(session, message, isClient) {
|
||||||
console.log(payload);
|
const headers = parseHeaders(message);
|
||||||
const headers = parseHeaders(payload);
|
if (!headers['wtv-encrypted']) {
|
||||||
|
console.log('[PLAINTEXT MESSAGE]:');
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
if (wtvsec && !session.wtvsec) {
|
if (wtvsec && !session.wtvsec) {
|
||||||
session.wtvsec = wtvsec;
|
session.wtvsec = wtvsec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isClient) {
|
if (isClient) {
|
||||||
// Check for the SECURE ON command from the client
|
if (message.includes('SECURE ON')) {
|
||||||
if (payload.startsWith('SECURE ON')) {
|
|
||||||
if (session.wtvsec) {
|
if (session.wtvsec) {
|
||||||
console.log('[*] SECURE ON detected. Initializing RC4 session.');
|
console.log('[*] SECURE ON detected. Initializing RC4 session.');
|
||||||
session.wtvsec.SecureOn();
|
session.wtvsec.SecureOn();
|
||||||
@@ -138,7 +256,6 @@ function handlePlaintext(session, payload, isClient) {
|
|||||||
console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.');
|
console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check for wtv-incarnation header
|
|
||||||
if (headers['wtv-incarnation']) {
|
if (headers['wtv-incarnation']) {
|
||||||
const incarnation = parseInt(headers['wtv-incarnation'], 10);
|
const incarnation = parseInt(headers['wtv-incarnation'], 10);
|
||||||
if (session.wtvsec) {
|
if (session.wtvsec) {
|
||||||
@@ -157,81 +274,72 @@ function handlePlaintext(session, payload, isClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Server
|
} else { // Server
|
||||||
// Look for the initial key to bootstrap the WTVSec instance
|
|
||||||
if (headers['wtv-initial-key']) {
|
if (headers['wtv-initial-key']) {
|
||||||
const initialKey = headers['wtv-initial-key'];
|
const initialKey = headers['wtv-initial-key'];
|
||||||
console.log(`[*] Captured wtv-initial-key: ${initialKey}`);
|
console.log(`[*] Captured wtv-initial-key: ${initialKey}`);
|
||||||
minisrv_config.config.keys.initial_shared_key = initialKey;
|
minisrv_config.config.keys.initial_shared_key = initialKey;
|
||||||
wtvsec = new WTVSec(minisrv_config);
|
wtvsec = new WTVSec(minisrv_config);
|
||||||
}
|
}
|
||||||
// Process the challenge from the server
|
|
||||||
if (headers['wtv-challenge'] && wtvsec) {
|
if (headers['wtv-challenge'] && wtvsec) {
|
||||||
const challenge = headers['wtv-challenge'];
|
const challenge = headers['wtv-challenge'];
|
||||||
console.log(`[*] Captured wtv-challenge. Processing...`);
|
console.log(`[*] Captured wtv-challenge. Processing...`);
|
||||||
wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64)
|
wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64)
|
||||||
session.wtvsec = wtvsec; // Ensure session has the WTVSec instance
|
session.wtvsec = wtvsec; // Ensure session has the WTVSec instance
|
||||||
}
|
}
|
||||||
if (headers['wtv-lzpf'] !== undefined) {
|
if (typeof headers['wtv-lzpf'] !== 'undefined') {
|
||||||
session.lzpf = true;
|
session.lzpf = true;
|
||||||
}
|
}
|
||||||
|
if (headers['wtv-encrypted']) {
|
||||||
|
handleEncrypted(session, Buffer.from(message), isClient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles encrypted WTVP messages.
|
* Handles a single complete encrypted WTVP message.
|
||||||
* @param {object} session - The session state object.
|
* @param {object} session - The session state object.
|
||||||
* @param {Buffer} data - The raw TCP data buffer.
|
* @param {Buffer} message - The raw message buffer.
|
||||||
* @param {boolean} isClient - True if the message is from the client.
|
* @param {boolean} isClient - True if the message is from the client.
|
||||||
*/
|
*/
|
||||||
function handleEncrypted(session, data, isClient) {
|
function handleEncrypted(session, message, isClient) {
|
||||||
// The encrypted data comes after the headers and a double newline.
|
const lfSeparator = Buffer.from('\n\n');
|
||||||
var lzpf = false;
|
const crlfSeparator = Buffer.from('\r\n\r\n');
|
||||||
const separator = '\n\n';
|
|
||||||
const dataStr = data.toString('binary');
|
|
||||||
const separatorIndex = dataStr.indexOf(separator);
|
|
||||||
|
|
||||||
|
let separatorIndex = message.indexOf(lfSeparator);
|
||||||
|
let separatorLength = lfSeparator.length;
|
||||||
|
const crlfIndex = message.indexOf(crlfSeparator);
|
||||||
|
|
||||||
|
if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) {
|
||||||
|
separatorIndex = crlfIndex;
|
||||||
|
separatorLength = crlfSeparator.length;
|
||||||
|
}
|
||||||
if (separatorIndex === -1) {
|
if (separatorIndex === -1) {
|
||||||
console.log('[!] Encrypted message without header separator found. Assuming entire payload is encrypted.');
|
console.log('[!] Encrypted message without header separator. This should not happen with reassembled streams.');
|
||||||
// This can happen if headers are in a separate packet from the body.
|
|
||||||
// For simplicity, we try to decrypt the whole payload.
|
|
||||||
// A more robust solution would buffer data across packets.
|
|
||||||
try {
|
|
||||||
const keyNum = isClient ? 0 : 1;
|
|
||||||
const decryptedBody = session.wtvsec.Decrypt(keyNum, data);
|
|
||||||
if (session.lzpf) {
|
|
||||||
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD (ASSUMED)]:\n' + decryptedBody.toString('utf8'));
|
|
||||||
var lzpf = new LZPF();
|
|
||||||
decryptedBody = lzpf.decompress(decryptedBody);
|
|
||||||
session.lzpf = false; // Reset after decompression
|
|
||||||
} else {
|
|
||||||
console.log('\n[DECRYPTED PAYLOAD (ASSUMED)]:\n' + decryptedBody.toString('utf8'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[!] Decryption failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headersPart = data.slice(0, separatorIndex).toString('utf8');
|
const headersPart = message.slice(0, separatorIndex).toString('utf8');
|
||||||
const encryptedBody = data.slice(separatorIndex + separator.length);
|
const encryptedBody = message.slice(separatorIndex + separatorLength);
|
||||||
|
|
||||||
console.log('[HEADERS]:');
|
console.log('[ENCRYPTED HEADERS]:');
|
||||||
console.log(headersPart);
|
console.log(headersPart);
|
||||||
|
|
||||||
if (encryptedBody.length > 0) {
|
if (encryptedBody.length > 0) {
|
||||||
// Decrypt based on message direction
|
const keyNum = isClient ? 0 : 1;
|
||||||
const keyNum = isClient ? 0 : 1; // 0 for client-to-server, 1 for server-to-client
|
|
||||||
try {
|
try {
|
||||||
const decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody);
|
let decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody);
|
||||||
if (session.lzpf) {
|
|
||||||
|
// Check for compression flag in the now-decrypted headers
|
||||||
|
const headers = parseHeaders(headersPart);
|
||||||
|
if (typeof headers['wtv-lzpf'] !== 'undefined') {
|
||||||
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:');
|
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:');
|
||||||
var lzpf = new LZPF();
|
var lzpfHandler = new LZPF();
|
||||||
decryptedBody = lzpf.decompress(decryptedBody);
|
decryptedBody = lzpfHandler.expand(decryptedBody);
|
||||||
session.lzpf = false; // Reset after decompression
|
|
||||||
} else {
|
} else {
|
||||||
console.log('\n[DECRYPTED PAYLOAD]:');
|
console.log('\n[DECRYPTED PAYLOAD]:');
|
||||||
}
|
}
|
||||||
console.log(decryptedBody.toString('utf8'));
|
console.log(decryptedBody.toString('utf8'));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[!] Decryption failed: ${e.message}`);
|
console.error(`[!] Decryption failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -240,8 +348,9 @@ function handleEncrypted(session, data, isClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple utility to parse HTTP-like headers into an object.
|
* A utility to parse HTTP-like headers into an object.
|
||||||
* @param {string} payload - The raw text payload.
|
* @param {string} payload - The raw text payload.
|
||||||
* @returns {object} A key-value map of the headers.
|
* @returns {object} A key-value map of the headers.
|
||||||
*/
|
*/
|
||||||
@@ -250,8 +359,8 @@ function parseHeaders(payload) {
|
|||||||
const lines = payload.split(/\r?\n/);
|
const lines = payload.split(/\r?\n/);
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const parts = line.split(':');
|
const parts = line.split(':');
|
||||||
if (parts.length === 2) {
|
if (parts.length >= 2) {
|
||||||
headers[parts[0].toLowerCase()] = parts[1].trim();
|
headers[parts[0].toLowerCase().trim()] = parts.slice(1).join(':').trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return headers;
|
return headers;
|
||||||
|
|||||||
Reference in New Issue
Block a user