Add verified administrator controls and upload abuse limits
This commit is contained in:
@@ -90,7 +90,7 @@ try {
|
||||
const database=new Database('/data/database/hop.sqlite'), db=drizzle(database);
|
||||
const oldId=randomUUID(), nextId=randomUUID();
|
||||
db.insert(users).values({id:oldId,email:'previous@docker.hop.test',displayName:'previous',passwordHash,status:'active',verifiedAt:now,createdAt:now}).run();
|
||||
db.insert(users).values({id:nextId,email:'verified@docker.hop.test',displayName:'verified',passwordHash,status:'pending',createdAt:now}).run();
|
||||
db.insert(users).values({id:nextId,email:'hello@noname.company',displayName:'verified',passwordHash,status:'pending',createdAt:now}).run();
|
||||
db.insert(accountTokens).values({id:randomUUID(),userId:nextId,purpose:'verify',tokenHash:hash(token),createdAt:now,expiresAt:now+60000}).run();
|
||||
database.close();
|
||||
const sessionResponse=await fetch(base+'/api/guest/session',{method:'POST',headers:{Origin:'https://hop.test',Cookie:guestCookie,'Content-Type':'application/json'},body:'{}'});
|
||||
@@ -112,6 +112,14 @@ try {
|
||||
`),
|
||||
);
|
||||
}
|
||||
if (verifiedAccount) {
|
||||
const saved = JSON.parse(
|
||||
node(
|
||||
`const base='http://127.0.0.1:${port}', cookie=${JSON.stringify(verifiedAccount.cookie)};const session=await(await fetch(base+'/api/guest/session',{method:'POST',headers:{Origin:'https://hop.test',Cookie:cookie,'Content-Type':'application/json'},body:'{}'})).json();const r=await fetch(base+'/api/admin/settings',{method:'PATCH',headers:{Origin:'https://hop.test',Cookie:cookie,'X-CSRF-Token':session.csrfToken,'Content-Type':'application/json'},body:JSON.stringify({revision:0,maxConcurrentUploads:3,blockedCountries:['CN']})});if(!r.ok)throw Error(await r.text());console.log(JSON.stringify(await r.json()));`,
|
||||
),
|
||||
);
|
||||
assert.equal(saved.maxConcurrentUploads, 3);
|
||||
}
|
||||
const fileState = JSON.parse(
|
||||
node(
|
||||
`import {io} from 'socket.io-client';import {randomUUID} from 'node:crypto';const base='http://127.0.0.1:${port}';const headers={Origin:'https://hop.test','Content-Type':'application/json'};async function guest(){const r=await fetch(base+'/api/guest/session',{method:'POST',headers,body:'{}'});return {cookie:r.headers.get('set-cookie').split(';')[0],...await r.json()};}const a=await guest(),b=await guest();async function call(g,path,body,key){const r=await fetch(base+path,{method:'POST',headers:{...headers,Cookie:g.cookie,'X-CSRF-Token':g.csrfToken,...(key?{'Idempotency-Key':key}:{})},body:JSON.stringify(body)});if(!r.ok)throw Error(await r.text());return r.json();}const sockets=[];for(const g of [a,b]){const socket=io(base,{transports:['websocket'],auth:{csrfToken:g.csrfToken},extraHeaders:{Origin:'https://hop.test',Cookie:g.cookie},reconnection:false});sockets.push(socket);await new Promise((resolve,reject)=>{socket.once('connect',resolve);socket.once('connect_error',reject);});}const invite=await call(a,'/api/spaces',{});await call(b,'/api/spaces/join',{code:invite.code});const bytes=Buffer.from('Docker file persistence');const transfer=await call(a,'/api/transfer-requests',{targetDeviceId:b.device.id,kind:'file',byteSize:bytes.length,count:1,files:[{name:'docker.txt',size:bytes.length,type:'text/plain'}]},randomUUID());await call(b,'/api/transfer-requests/'+transfer.requestId+'/accept',{},randomUUID());const form=new FormData();form.append('file',new Blob([bytes]),'docker.txt');const uploaded=await fetch(base+'/api/uploads?requestId='+transfer.requestId+'&fileIndex=0',{method:'POST',headers:{Origin:'https://hop.test',Cookie:a.cookie,'X-CSRF-Token':a.csrfToken,'Idempotency-Key':randomUUID()},body:form});if(!uploaded.ok)throw Error(await uploaded.text());const list=await fetch(base+'/api/device/items',{headers:{Cookie:b.cookie}});const item=(await list.json()).items[0];const original=${JSON.stringify(' Docker 텍스트 日本語 😀\r\n 공백과 줄바꿈\n')};const message=await call(a,'/api/transfer-requests',{targetDeviceId:b.device.id,kind:'text',count:1,byteSize:Buffer.byteLength(original)},randomUUID());await call(b,'/api/transfer-requests/'+message.requestId+'/accept',{},randomUUID());await call(a,'/api/transfers/'+message.requestId+'/complete',{text:original},randomUUID());const receivedText=await fetch(base+'/api/device/items',{headers:{Cookie:b.cookie}});const textItem=(await receivedText.json()).items.find(i=>i.kind==='text');if(textItem.text!==original)throw Error('text content changed');const {createHash}=await import('node:crypto');const chunkSize=8*1024*1024,big=Buffer.alloc(chunkSize+7,120),digest=v=>createHash('sha256').update(v).digest();const fingerprint=digest(Buffer.concat([digest(big.subarray(0,chunkSize)),digest(big.subarray(chunkSize))])).toString('hex');const pending=await call(a,'/api/transfer-requests',{targetDeviceId:b.device.id,kind:'file',byteSize:big.length,count:1,files:[{name:'resume.bin',size:big.length,type:'application/octet-stream'}]},randomUUID());await call(b,'/api/transfer-requests/'+pending.requestId+'/accept',{},randomUUID());const staged=await call(a,'/api/uploads/resumable',{requestId:pending.requestId,fileIndex:0,fingerprint});const first=await fetch(base+'/api/uploads/resumable/'+staged.uploadId,{method:'PUT',headers:{Origin:'https://hop.test',Cookie:a.cookie,'X-CSRF-Token':a.csrfToken,'Content-Type':'application/octet-stream','Upload-Offset':'0'},body:big.subarray(0,chunkSize)});if(!first.ok)throw Error(await first.text());for(const socket of sockets)socket.disconnect();console.log(JSON.stringify({cookie:b.cookie,accessId:item.accessId,text:{accessId:textItem.accessId,value:original},pending:{uploadId:staged.uploadId,cookie:a.cookie,csrf:a.csrfToken}}));`,
|
||||
@@ -135,6 +143,14 @@ try {
|
||||
assert.equal(restored.passwordMinLength, 8);
|
||||
assert.equal(restored.account.id, verifiedAccount.userId);
|
||||
assert.equal(restored.account.verified, true);
|
||||
assert.equal(restored.account.administrator, true);
|
||||
const settings = JSON.parse(
|
||||
node(
|
||||
`const r=await fetch('http://127.0.0.1:${port}/api/admin/summary',{headers:{Cookie:${JSON.stringify(verifiedAccount.cookie)}}});if(!r.ok)throw Error(await r.text());console.log(JSON.stringify((await r.json()).settings));`,
|
||||
),
|
||||
);
|
||||
assert.equal(settings.maxConcurrentUploads, 3);
|
||||
assert.deepEqual(settings.blockedCountries, ['CN']);
|
||||
}
|
||||
const preservedText = JSON.parse(
|
||||
node(
|
||||
@@ -208,7 +224,7 @@ try {
|
||||
}
|
||||
assert.equal(docker('inspect', '--format', '{{json .NetworkSettings.Ports}}', name), '{}');
|
||||
console.log(
|
||||
`PASS: production Docker startup, HTTPS Secure session cookie, migration, anonymous session/name/file and Unicode text persistence and partial chunk resume after restart, SQLite integrity, no host ports published.${bindUploads ? ' Bind uploads visible on host, custom NAS UID ownership, manual host deletion reconciliation.' : ''}${verifiedAccount ? ' Verified account auto-login, protected cookie, previous session revocation, one-use token and session persistence after restart.' : ''}`,
|
||||
`PASS: production Docker startup, HTTPS Secure session cookie, migration, anonymous session/name/file and Unicode text persistence and partial chunk resume after restart, SQLite integrity, no host ports published.${bindUploads ? ' Bind uploads visible on host, custom NAS UID ownership, manual host deletion reconciliation.' : ''}${verifiedAccount ? ' Verified account auto-login, protected cookie, previous session revocation, one-use token, admin role and persisted admin controls after restart.' : ''}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(execFileSync('docker', ['logs', name], { encoding: 'utf8', stdio: 'inherit' }));
|
||||
|
||||
Reference in New Issue
Block a user