const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); const { PassThrough } = require('node:stream'); const { isPrivateIp } = require('../src/utils/urlSafety'); const { decodeBase64, inspect, download, MAX_BYTES } = require('../src/utils/generatedImageStorage'); const { args, budgetLimit } = require('../src/utils/generatedImages'); const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL9sAAAAASUVORK5CYII=', 'base64'); test('mapped IPv6, noncanonical IPs, internal/special networks and transition ranges fail closed', () => { for (const ip of ['::ffff:127.0.0.1','::ffff:7f00:1','0:0:0:0:0:ffff:a00:1','::ffff:8.8.8.8','64:ff9b::a00:1','2002:7f00:1::','2001:db8::1','fe90::1','fec0::1','ff02::1','::1','10.0.0.1','100.64.0.1','169.254.169.254','172.16.0.1','192.0.0.1','192.0.2.1','198.18.1.1','198.51.100.1','203.0.113.2','127.1','0x7f000001','2130706433']) assert.equal(isPrivateIp(ip), true, ip); assert.equal(isPrivateIp('8.8.8.8'), false); assert.equal(isPrivateIp('2606:4700:4700::1111'), false); }); test('base64, byte bound, MIME/magic, assembled argument fields and budget types are strict', () => { assert.equal(decodeBase64(png.toString('base64')).mime, 'image/png'); for (const text of ['', 'a', '!!!!', png.toString('base64') + '\n', png.toString('base64').replace(/=$/, ''), 'eHh4']) assert.throws(() => decodeBase64(text)); assert.throws(() => inspect(Buffer.alloc(MAX_BYTES + 1))); assert.throws(() => inspect(png, 'text/html')); for (const value of [false, {}, [1000], 999, 32001, '1.5']) assert.throws(() => budgetLimit(value)); assert.equal(budgetLimit(null), 32000); for (const input of [null, [], { prompt: 'ok', owner: 7 }, {prompt: 'ok', model: 'paid'}, {prompt: 'ok', layout: 'https://example.test'}]) assert.throws(() => args(input)); }); test('URL download pins vetted DNS results into HTTPS lookup, never forwards credentials or follows redirect, enforces size', async () => { let calls = 0; const lookup = async () => [{ address:'8.8.8.8',family:4 }]; function request(status = 200, body = png, headers = {}) { return (url, options, callback) => { calls++; assert.equal(url.hostname, 'provider-assets.test'); assert.equal(options.agent, false); assert.equal(options.headers.Authorization, undefined); options.lookup(url.hostname, {all:true}, (err, result) => { assert.equal(err,null); assert.deepEqual(result,[{address:'8.8.8.8',family:4}]); }); const req = new EventEmitter(); req.destroy = err => req.emit('error',err); process.nextTick(() => { const res = new PassThrough(); res.statusCode=status; res.headers={'content-type':'image/png',...headers}; callback(res); if (!res.destroyed) res.end(body); }); return req; }; } const result = await download('https://provider-assets.test/image',{lookup,request:request()}); assert.deepEqual(result.bytes,png); for (const url of ['http://provider-assets.test/x','https://user:secret@provider-assets.test/x','https://provider-assets.test:8443/x']) await assert.rejects(download(url,{lookup,request:request()})); await assert.rejects(download('https://provider-assets.test/x',{lookup:async()=>[{address:'8.8.8.8',family:4},{address:'::ffff:127.0.0.1',family:6}],request:request()})); assert.equal(calls,1); await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(302,png,{location:'https://127.0.0.1/'})})); await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(200,png,{'content-length':String(MAX_BYTES+1)})})); await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(200,Buffer.alloc(MAX_BYTES+1))})); }); test('storage preflight verifies read permission as well as writes before authorizing payment', async () => { const fs = require('node:fs'); const vm = require('node:vm'); const sdk = require('@aws-sdk/client-s3'); const commands = []; const module = { exports: {} }; vm.runInNewContext(fs.readFileSync('src/utils/generatedImageStorage.js', 'utf8'), { module, Buffer, URL, require(name) { if (name === 'fs') return { readFileSync: () => 'synthetic-only' }; if (name === './urlSafety') return { isPrivateIp }; if (name === '@aws-sdk/client-s3') return { ...sdk, S3Client: class { constructor(options) { assert.equal(options.forcePathStyle, true); } async send(command) { commands.push(command.constructor.name); if (command instanceof sdk.HeadObjectCommand) throw Error('synthetic denied read'); return {}; } } }; return require(name); } }); const store = module.exports.createStorage({ GENERATED_IMAGES_S3_ENDPOINT: 'http://synthetic.invalid', GENERATED_IMAGES_S3_ACCESS_KEY_FILE: '/synthetic/access', GENERATED_IMAGES_S3_SECRET_KEY_FILE: '/synthetic/secret' }); await assert.rejects(store.ready(), /denied read/); assert.deepEqual(commands, ['HeadBucketCommand', 'PutObjectCommand', 'HeadObjectCommand', 'DeleteObjectCommand']); }); test('public 2001 IPv6 and mixed public A/AAAA remain usable by images and Nextcloud/WebDAV', async () => { const fs=require('fs'),vm=require('vm'); for(const ip of ['2001:4860:4860::8888','2001:200::1','2001:db7::1','2001:db9::1','3fff:1000::1']) assert.equal(isPrivateIp(ip),false,ip); for(const ip of ['2001::1','2001:1ff:ffff::1','2001:2::1','2001:10::1','2001:20::1','2001:db8::1','2002::1','3ffe::1','3fff::1','3fff:fff::1','64:ff9b:1::1','fc00::1']) assert.equal(isPrivateIp(ip),true,ip); let addresses=[{address:'8.8.8.8',family:4},{address:'2001:4860:4860::8888',family:6}]; const module={exports:{}};vm.runInNewContext(fs.readFileSync('src/utils/urlSafety.js','utf8'),{module,URL,require:n=>n==='dns'?{promises:{lookup:async()=>addresses}}:require(n)}); assert.equal((await module.exports.assertSafeHttpsUrl('https://nextcloud.synthetic.test/files')).hostname,'nextcloud.synthetic.test'); addresses=addresses.concat({address:'2001:db8::1',family:6});await assert.rejects(module.exports.assertSafeHttpsUrl('https://nextcloud.synthetic.test/files'),/private IP/); });