Files
LogMaster/dist-server/s3Source.js
Kevin 378b30cdc8
Some checks failed
LogMaster CI/CD / build-and-test (push) Has been cancelled
LogMaster CI/CD / docker (push) Has been cancelled
Fix: S3 key encoding env quote stripping test assertions remove unused import
2026-05-24 12:50:35 +02:00

194 lines
7.7 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3Source = void 0;
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const crypto = __importStar(require("crypto"));
const zlib = __importStar(require("zlib"));
/**
* Minimal S3 client without external dependencies.
* Supports AWS S3 and S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
*/
class S3Source {
config;
host;
useHttps;
constructor(config) {
this.config = config;
if (config.endpoint) {
const url = new URL(config.endpoint);
this.host = url.host;
this.useHttps = url.protocol === 'https:';
}
else {
this.host = `${config.bucket}.s3.${config.region}.amazonaws.com`;
this.useHttps = true;
}
}
/**
* List objects in the bucket with optional prefix
*/
async listObjects(prefix) {
const usedPrefix = prefix || this.config.prefix || '';
const queryParams = usedPrefix ? `list-type=2&prefix=${encodeURIComponent(usedPrefix)}` : 'list-type=2';
const path = this.config.endpoint ? `/${this.config.bucket}/?${queryParams}` : `/?${queryParams}`;
const response = await this.request('GET', path, '');
const objects = [];
// Simple XML parsing for S3 ListObjectsV2 response
const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
let match;
while ((match = contentRegex.exec(response)) !== null) {
const content = match[1];
const key = this.extractXmlValue(content, 'Key');
const size = parseInt(this.extractXmlValue(content, 'Size') || '0', 10);
const lastModified = this.extractXmlValue(content, 'LastModified') || '';
if (key && !key.endsWith('/')) {
objects.push({ key, size, lastModified });
}
}
return objects;
}
/**
* Download an object's content as string
*/
async getObject(key) {
// Encode each path segment separately to preserve /
const encodedKey = key.split('/').map(s => encodeURIComponent(s)).join('/');
const path = this.config.endpoint
? `/${this.config.bucket}/${encodedKey}`
: `/${encodedKey}`;
const response = await this.requestRaw('GET', path, '');
// Auto-decompress if gzipped
if (key.endsWith('.gz') || key.endsWith('.gzip')) {
try {
const decompressed = zlib.gunzipSync(response);
return decompressed.toString('utf-8');
}
catch {
return response.toString('utf-8');
}
}
return response.toString('utf-8');
}
extractXmlValue(xml, tag) {
const regex = new RegExp(`<${tag}>(.*?)</${tag}>`);
const match = xml.match(regex);
return match ? match[1] : null;
}
async request(method, path, body) {
const buf = await this.requestRaw(method, path, body);
return buf.toString('utf-8');
}
async requestRaw(method, path, body) {
const now = new Date();
const dateStamp = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
const shortDate = dateStamp.substring(0, 8);
const headers = {
'host': this.host,
'x-amz-date': dateStamp,
'x-amz-content-sha256': this.sha256(body),
};
// Create canonical request
const [pathPart, queryPart] = path.split('?');
const canonicalQueryString = queryPart || '';
const signedHeaderKeys = Object.keys(headers).sort();
const signedHeaders = signedHeaderKeys.join(';');
const canonicalHeaders = signedHeaderKeys.map(k => `${k}:${headers[k]}\n`).join('');
const canonicalRequest = [
method,
pathPart,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
headers['x-amz-content-sha256'],
].join('\n');
// Create string to sign
const credentialScope = `${shortDate}/${this.config.region}/s3/aws4_request`;
const stringToSign = [
'AWS4-HMAC-SHA256',
dateStamp,
credentialScope,
this.sha256(canonicalRequest),
].join('\n');
// Calculate signature
const signingKey = this.getSignatureKey(shortDate);
const signature = this.hmac(signingKey, stringToSign).toString('hex');
// Authorization header
headers['authorization'] = `AWS4-HMAC-SHA256 Credential=${this.config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return new Promise((resolve, reject) => {
const options = {
hostname: this.host.split(':')[0],
port: this.host.includes(':') ? parseInt(this.host.split(':')[1]) : (this.useHttps ? 443 : 80),
path: path,
method: method,
headers: headers,
};
const transport = this.useHttps ? https : http;
const req = transport.request(options, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks);
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(`S3 error ${res.statusCode}: ${data.toString('utf-8').substring(0, 200)}`));
}
else {
resolve(data);
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('S3 request timeout')); });
if (body)
req.write(body);
req.end();
});
}
sha256(data) {
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
hmac(key, data) {
return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
}
getSignatureKey(dateStamp) {
const kDate = this.hmac(`AWS4${this.config.secretAccessKey}`, dateStamp);
const kRegion = this.hmac(kDate, this.config.region);
const kService = this.hmac(kRegion, 's3');
return this.hmac(kService, 'aws4_request');
}
}
exports.S3Source = S3Source;
//# sourceMappingURL=s3Source.js.map