81 lines
6.1 KiB
JavaScript
81 lines
6.1 KiB
JavaScript
const {test} = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const Module = require('node:module')
|
|
const ts = require('typescript')
|
|
const root = path.resolve(__dirname, '..')
|
|
const folder = fs.existsSync(path.join(root,'src/utils/file-access-core.ts')) ? 'src/utils' : 'src/sheep/utils'
|
|
const corePath = path.join(root,folder,'file-access-core.ts')
|
|
const compile = source => ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText
|
|
const core = new Module(corePath,module); core.filename=corePath; core.paths=Module._nodeModulePaths(path.dirname(corePath))
|
|
core._compile(compile(fs.readFileSync(corePath,'utf8')),corePath)
|
|
const vectors=JSON.parse(fs.readFileSync(path.join(root,'test-vectors/file-upload.json'),'utf8'))
|
|
for(const v of vectors) test('upload signature vector: '+v.target,()=>{
|
|
assert.equal(core.exports.fileUploadCanonical(v.target,v.appId,v.kid,v.ts,v.nonce),v.canonical)
|
|
assert.equal(core.exports.fileUploadSignature(v.target,v.appId,v.kid,v.ts,v.nonce,v.key),v.sign)
|
|
})
|
|
test('upload credentials bind the purpose and target, renew per request and never leak to other hosts or imports',()=>{
|
|
let time=1793232000000
|
|
const client=core.exports.createFileAccess({appId:'admin',kid:'v1',key:vectors[0].key,baseUrl:()=> 'https://files.example/gateway/api',now:()=>time,allowedOrigins:['https://legacy.example']})
|
|
for(const [source,target] of [['/api/admin/sys-file/upload','upload'],['/sys-file/inner-upload','inner-upload'],['https://legacy.example/admin/sys-file/qn/upload','qn-upload'],['https://files.example/gateway/api/admin/sys-file/upload','upload']]){
|
|
const headers=client.uploadHeaders(source,vectors[0].nonce)
|
|
assert.equal(headers['X-File-Signature'],core.exports.fileUploadSignature(target,'admin','v1',1793232000,vectors[0].nonce,vectors[0].key))
|
|
assert.equal(Object.values(headers).includes(vectors[0].key),false)
|
|
}
|
|
const first=client.uploadHeaders('/admin/sys-file/upload'), second=client.uploadHeaders('/admin/sys-file/upload')
|
|
assert.notEqual(first['X-File-Nonce'],second['X-File-Nonce'])
|
|
assert.notEqual(first['X-File-Signature'],second['X-File-Signature'])
|
|
assert.equal(first['X-File-Timestamp'],'1793232000')
|
|
client.synchronize(time+86400000,1800,0)
|
|
assert.equal(client.uploadHeaders('/admin/sys-file/upload')['X-File-Timestamp'],'1793318400')
|
|
for(const source of ['https://outside.example/admin/sys-file/upload','https://files.example.evil/admin/sys-file/upload','/admin/sys-file/oss/file?fileName=x','/admin/user/import','data:upload','blob:upload']){
|
|
assert.deepEqual(client.uploadHeaders(source),{})
|
|
}
|
|
assert.notEqual(core.exports.fileAccessSignature('a.jpg','admin','v1',1793232000,vectors[0].key),first['X-File-Signature'])
|
|
})
|
|
function runtime(){
|
|
const filename=path.join(root,folder,'file-access.ts')
|
|
const env={VITE_FILE_ACCESS_APP_ID:'admin',VITE_FILE_ACCESS_KEY_ID:'v1',VITE_FILE_ACCESS_KEY:vectors[0].key,VITE_IMAGE_BASE_PATH:'https://files.example/api',VITE_IMAGE_DEV_PATH:'https://files.example/api',VITE_APP_DEV_SERVICE:'https://files.example/api',VITE_APP_PROD_IMAGE:'https://files.example/api',VITE_APP_PROD_SERVICE:'https://files.example/api'}
|
|
const loaded=new Module(filename,module);loaded.filename=filename;loaded.paths=core.paths
|
|
loaded.require=name=>name==='./file-access-core'?core.exports:name==='./app'?{isDevEnv:()=>true}:core.require(name)
|
|
loaded._compile(compile(fs.readFileSync(filename,'utf8').replace(/import\.meta\.env/g,'('+JSON.stringify(env)+')')),filename)
|
|
return loaded.exports
|
|
}
|
|
if(folder==='src/utils') test('the real Axios interceptor signs each file upload and preserves login and tenant headers',()=>{
|
|
const api=runtime();let onRequest
|
|
const axios={create:()=>({interceptors:{request:{use(fn){onRequest=fn}},response:{use(){}}}})}
|
|
const filename=path.join(root,'src/utils/request.ts');const loaded=new Module(filename,module);loaded.filename=filename;loaded.paths=core.paths
|
|
loaded.require=name=>{
|
|
if(name==='axios')return {default:axios}
|
|
if(name==='./file-access')return api
|
|
if(name==='./other')return {default:{adaptationUrl:url=>url}}
|
|
if(name==='/@/utils/storage')return {Session:{getToken:()=> 'user-token',getTenant:()=> '42'}}
|
|
if(name==='/@/hooks/message')return {}
|
|
return core.require(name)
|
|
}
|
|
loaded._compile(compile(fs.readFileSync(filename,'utf8')),filename)
|
|
const first=onRequest({method:'post',url:'/admin/sys-file/upload',headers:{},data:{file:'unchanged'}})
|
|
const second=onRequest({method:'post',url:'/admin/sys-file/upload',headers:{},data:{file:'unchanged'}})
|
|
assert.equal(first.headers.Authorization,'Bearer user-token');assert.equal(first.headers['TENANT-ID'],'42')
|
|
assert.ok(first.headers['X-File-Signature']);assert.notEqual(first.headers['X-File-Nonce'],second.headers['X-File-Nonce'])
|
|
assert.deepEqual(first.data,{file:'unchanged'})
|
|
const other=onRequest({method:'post',url:'https://outside.example/admin/sys-file/upload',headers:{}})
|
|
assert.equal(other.headers['X-File-Signature'],undefined)
|
|
})
|
|
else test('the actual uni upload interceptor signs photo, video and rich text calls without changing form data',()=>{
|
|
const previous=global.uni;let interceptor
|
|
global.uni={addInterceptor(name,hook){assert.equal(name,'uploadFile');interceptor=hook},onNetworkStatusChange(){}}
|
|
try{
|
|
const api=runtime();api.installFileAccess({config:{globalProperties:{}},mixin(){}})
|
|
const first={url:'/api/admin/sys-file/upload',header:{Authorization:'Bearer user','TENANT-ID':'42'},formData:{dir:'photos'},filePath:'local-photo.png'}
|
|
const second={...first,header:{...first.header}}
|
|
interceptor.invoke(first);interceptor.invoke(second)
|
|
assert.ok(first.header['X-File-Signature']);assert.notEqual(first.header['X-File-Nonce'],second.header['X-File-Nonce'])
|
|
assert.equal(first.header.Authorization,'Bearer user');assert.equal(first.header['TENANT-ID'],'42')
|
|
assert.deepEqual(first.formData,{dir:'photos'});assert.equal(first.filePath,'local-photo.png')
|
|
const other={url:'https://outside.example/upload',header:{Authorization:'other'}};interceptor.invoke(other)
|
|
assert.deepEqual(other.header,{Authorization:'other'})
|
|
}finally{global.uni=previous}
|
|
})
|