Blob upload using POST

This commit is contained in:
yflory 2025-05-21 15:06:31 +02:00
parent 84dd369c86
commit de46da0e7a
9 changed files with 189 additions and 61 deletions

View File

@ -86,4 +86,28 @@ removeBlock.complete = function (Env, body, cb) {
Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb);
};
// Get an upload cookie
// Get a cookie allowing you to upload to the blobstage of your user
const uploadCookie = Commands.UPLOAD_COOKIE = function (Env, body, cb) {
const { publicKey } = body;
// they must provide a valid public key
if (publicKey && typeof(publicKey) === "string"
&& publicKey.length === 44) {
return cb();
}
cb("INVALID_KEY");
};
uploadCookie.complete = function (Env, body, cb) {
const { publicKey } = body;
const safeKey = Util.escapeKeyCharacters(publicKey);
Env.blobStore.uploadCookie(safeKey, (err, cookie) => {
if (err) { return void cb(err); }
cb(void 0, {cookie});
});
};

View File

@ -69,6 +69,7 @@ const NOAUTH = require("./challenge-commands/base.js");
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK; // Account creation + password change
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
COMMANDS.UPLOAD_COOKIE = NOAUTH.UPLOAD_COOKIE;
const TOTP = require("./challenge-commands/totp.js");
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;

View File

@ -19,6 +19,7 @@ const BlobStore = require("./storage/blob");
const BlockStore = require("./storage/block");
const plugins = require("./plugin-manager");
const gzipStatic = require('connect-gzip-static');
const CPCrypto = require('./crypto');
const DEFAULT_QUERY_TIMEOUT = 5000;
const PID = process.pid;
@ -26,6 +27,8 @@ const PID = process.pid;
let SSOUtils = plugins.SSO && plugins.SSO.utils;
var Env = JSON.parse(process.env.Env);
let blobStore;
let cpcrypto;
Env.plugins = plugins;
const response = Util.response(function (errLabel, info) {
if (!Env.Log) { return; }
@ -70,6 +73,7 @@ const EVENTS = {};
EVENTS.ENV_UPDATE = function (data /*, cb */) {
try {
Env = JSON.parse(data);
Env.blobStore = blobStore;
Env.Log = Log;
Env.plugins = plugins;
Env.sendMessage = sendMessage;
@ -263,7 +267,6 @@ app.use('/ssoauth', (req, res, next) => {
next();
});
app.use('/blob', function (req, res, next) {
/* Head requests are used to check the size of a blob.
Clients can configure a maximum size to download automatically,
@ -772,6 +775,51 @@ app.get('/api/logo', function (req, res) {
});
});
app.post('/upload-blob', Express.json({limit:"500kb"}), (req, res) => {
const { chunk, sig, edPublic } = req.body;
if (!cpcrypto) {
return void res.status(500).send({error: 'NOCRYPTO'});
}
const forbidden = reason => {
return void res.status(403).send({error: reason});
};
try {
// Check signature
const sigu8 = Util.decodeBase64(sig);
const vkey = Util.decodeBase64(edPublic);
const ok = cpcrypto.open(sigu8, vkey);
if (!ok) { return forbidden('INVALID_KEY'); }
const cookie = Util.encodeUTF8(sigu8.subarray(64));
// Check cookie
const safeKey = Util.escapeKeyCharacters(edPublic);
Env.blobStore.checkUploadCookie(safeKey, value => {
if (value !== cookie) {
return forbidden('INVALID_COOKIE');
}
// Upload chunk
Env.blobStore.upload(safeKey, chunk, (err) => {
if (err) {
return res.status(500).send({error: err});
}
// Get new cookie
Env.blobStore.uploadCookie(safeKey, (err, _c) => {
if (err) {
return res.status(500).send({error: err});
}
res.status(200).send({
cookie: _c
});
});
});
});
} catch (e) {
return void res.status(500).send({error: e.message});
}
});
// This endpoint handles authenticated RPCs over HTTP
// via an interactive challenge-response protocol
app.use(Express.json());
@ -779,6 +827,7 @@ app.post('/api/auth', function (req, res, next) {
AuthCommands.handle(Env, req, res, next);
});
app.use(function (req, res /*, next */) {
if (/^(\/favicon\.ico\/|.*\.js\.map|.*\/translations\/.*\.json)/.test(req.url)) {
// ignore common 404s
@ -822,7 +871,10 @@ nThen(function (w) {
getSession: function () {},
}, w(function (err, blob) {
if (err) { return; }
Env.blobStore = blob;
Env.blobStore = blobStore = blob;
}));
CPCrypto.init(w(function (err, crypto) {
cpcrypto = crypto;
}));
}).nThen(function () {
// TODO inform the parent process that this worker is ready

View File

@ -10,6 +10,8 @@ var BlobStore = module.exports;
var nThen = require("nthen");
var Semaphore = require("saferphore");
var Util = require("../common-util");
const Crypto = require('crypto');
const PERMISSIVE = 511;
const readFileBin = require("../stream-file").readFileBin;
@ -275,39 +277,37 @@ var upload = function (Env, safeKey, content, cb) {
try { dec = Buffer.from(content, 'base64'); }
catch (e) { return void cb('DECODE_BUFFER'); }
var len = dec.length;
var session = Env.getSession(safeKey);
if (typeof(session.currentUploadSize) !== 'number' ||
typeof(session.pendingUploadSize) !== 'number') {
// improperly initialized... maybe they didn't check before uploading?
// reject it, just in case
return cb('NOT_READY');
}
if (session.currentUploadSize > session.pendingUploadSize) {
return cb('E_OVER_LIMIT');
}
var path = makeStagePath(Env, safeKey);
Fs.appendFile(path, dec, cb);
};
const getRandomCookie = function () {
return Crypto.randomBytes(16).toString('hex');
};
var uploadCookie = function (Env, safeKey, cb) {
var stagePath = makeStagePath(Env, safeKey);
var cookiePath = stagePath + '.cookie';
const cookie = getRandomCookie();
if (!session.blobstage) {
makeFileStream(stagePath, function (e, stream) {
if (!stream) { return void cb(e); }
var blobstage = session.blobstage = stream;
blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
//Env.incrementBytesWritten(len);
Fse.mkdirp(Path.dirname(cookiePath), PERMISSIVE, function (err) {
if (err && err.code !== 'EEXIST') { return void cb(err); }
Fs.writeFile(cookiePath, cookie, err => {
cb(err, cookie);
});
} else {
session.blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
//Env.incrementBytesWritten(len);
}
});
};
var checkUploadCookie = function (Env, safeKey, cb) {
var stagePath = makeStagePath(Env, safeKey);
var cookiePath = stagePath + '.cookie';
Fs.readFile(cookiePath, function (err, content) {
if (err) { return void cb(); }
let expireTime = +new Date() - (5*60*1000);
Fs.stat(cookiePath, function (err, stats) {
if (stats.mtime < expireTime) { return void cb(); }
cb(content.toString('utf8'));
});
});
};
var closeBlobstage = function (Env, safeKey) {
@ -370,6 +370,10 @@ var upload_complete = function (Env, safeKey, id, cb) {
// FIXME we could just move and handle the EEXISTS instead of the above block
Fse.move(oldPath, newPath, function (e) {
if (e) { return void cb('RENAME_ERR'); }
// clear upload cookie
Fs.unlink(oldPath+'.cookie', function () {});
cb(void 0, id);
});
});
@ -448,6 +452,9 @@ var owned_upload_complete = function (Env, safeKey, id, cb) {
// otherwise it worked...
}));
}).nThen(function () {
// clear upload cookie
Fs.unlink(oldPath+'.cookie', function () {});
// clean up their session when you're done
// call back with the blob id...
cb(void 0, id);
@ -739,6 +746,16 @@ BlobStore.create = function (config, _cb) {
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
upload(Env, safeKey, content, Util.once(Util.mkAsync(cb)));
},
uploadCookie: function (safeKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
uploadCookie(Env, safeKey, Util.once(Util.mkAsync(cb)));
},
checkUploadCookie: function (safeKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
checkUploadCookie(Env, safeKey, Util.once(Util.mkAsync(cb)));
},
cancel: function (safeKey, fileSize, _cb) {
var cb = Util.once(Util.mkAsync(_cb));

View File

@ -484,18 +484,6 @@ const factory = (Sortify, UserObject, ProxyManager,
});
};
Store.uploadChunk = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.send.unauthenticated('UPLOAD', data.chunk, function (e, msg) {
cb({
error: e,
msg: msg
});
});
};
var initTempRpc = (clientId, cb) => {
if (store.rpc) { return void cb(store.rpc); }
var kp = Crypto.Nacl.sign.keyPair();

View File

@ -22,7 +22,6 @@ const factory = AStore => {
GET_PIN_LIMIT: Store.getPinLimit,
CLEAR_OWNED_CHANNEL: Store.clearOwnedChannel,
REMOVE_OWNED_CHANNEL: Store.removeOwnedChannel,
UPLOAD_CHUNK: Store.uploadChunk,
UPLOAD_COMPLETE: Store.uploadComplete,
UPLOAD_STATUS: Store.uploadStatus,
UPLOAD_CANCEL: Store.uploadCancel,

View File

@ -636,13 +636,6 @@ define([
});
};
common.uploadChunk = function (teamId, data, cb) {
postMessage("UPLOAD_CHUNK", {teamId: teamId, chunk: data}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
// ANON RPC
// SFRAME: talk to anon_rpc from the iframe

View File

@ -3,15 +3,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'/file/file-crypto.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/outer/cache-store.js',
'/common/outer/http-command.js',
'/components/chainpad-crypto/crypto.js',
'/components/nthen/index.js',
], function (FileCrypto, Hash, Util, Cache, nThen) {
], function (ApiConfig, FileCrypto, Hash, Util, Cache, ServerCommand, Crypto, nThen) {
var module = {};
module.uploadU8 =function (common, data, cb) {
module.uploadU8 = function (common, data, cb) {
var teamId = data.teamId;
var u8 = data.u8;
var metadata = data.metadata;
@ -27,13 +30,43 @@ define([
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata);
var sendChunk = function (box, cb) {
var enc = Util.encodeBase64(box);
common.uploadChunk(teamId, enc, function (e, msg) {
cb(e, msg);
});
let uploadUrl = '/upload-blob';
let size = 0;
if (ApiConfig.fileHost) {
const origin = new URL(ApiConfig.fileHost).origin;
uploadUrl = origin + uploadUrl;
}
var sendChunk = function (box, cb) {
const enc = Util.encodeBase64(box);
size += enc.length;
const c = Util.decodeUTF8(cookie);
const sig_str = window.nacl.sign(c, keys.secretKey);
const sig = Util.encodeBase64(sig_str);
const body = {
chunk: enc,
sig: sig,
edPublic: keys.edPublic
};
fetch(uploadUrl, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).then(res => res.json())
.then(json => {
if (json?.error) {
return cb(json.error);
}
cookie = json.cookie;
cb();
}).catch(cb);
};
let keys, cookie;
var actual = 0;
var encryptedArr = [];
var again = function (err, box) {
@ -69,6 +102,27 @@ define([
});
};
const startUpload = () => {
common.getAccessKeys(arr => {
const myKeys = arr.find(obj => {
return (!obj.id && !teamId) || +obj.id === +teamId;
});
if (!myKeys) { return void onError('NO_KEYS'); }
keys = {
edPublic: myKeys.edPublic,
publicKey: Util.decodeBase64(myKeys.edPublic),
secretKey: Util.decodeBase64(myKeys.edPrivate)
};
ServerCommand(keys, {
command: 'UPLOAD_COOKIE',
}, (err, data) => {
cookie = data?.cookie;
if (err || !cookie) { return void onError(err || 'NOCOOKIE'); }
next(again);
});
});
};
common.uploadStatus(teamId, estimate, function (e, pending) {
if (e) {
console.error(e);
@ -83,11 +137,11 @@ define([
if (e) {
return void console.error(e);
}
next(again);
startUpload();
});
});
}
next(again);
startUpload();
});
};

File diff suppressed because one or more lines are too long