User-facing desktop application for server.garden
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

279 lines
8.8 KiB

const https = require('https');
const awsClient = require('./vendor/awsClient.js');
module.exports = function(file, getEncryptionInstanceFromPassphrase) {
if (!file) {
return null;
}
const isBackBlaze = file['choose-providers'] && file['choose-providers'].objectStorage == 'backblaze';
const hasBackBlazeKey = file['backblaze-account'] && file['backblaze-account'].secretKey;
const isS3Compatible = file['choose-providers'] && file['choose-providers'].objectStorage == 's3compatible';
const hasS3CompatibleKey = file['s3compatible-account'] && file['s3compatible-account'].secretKey;
let toReturn = null;
if(file['cloud-service-type'] == 'greenhouse') {
throw new Error("greenhouse object storage not implemented");
} else if(isBackBlaze && hasBackBlazeKey) {
toReturn = new BackblazeObjectStorage(file)
} else if(isS3Compatible && hasS3CompatibleKey) {
toReturn = new S3CompatibleObjectStorage(file)
}
if(toReturn && file.passphrase) {
toReturn = new E2EEObjectStorage(toReturn, getEncryptionInstanceFromPassphrase(file.passphrase))
}
return toReturn;
}
const httpsPromise = (method, url, headers, body) => {
if(typeof body == 'object') {
body = JSON.stringify(body);
}
if(typeof body == 'string') {
body = Buffer.from(body);
}
if(body) {
headers['Content-Length'] = body.length;
}
const options = {
method,
headers,
timeout: 5000,
};
return new Promise((resolve, reject) => {
//console.log(`${options.method} ${url}`);
const request = https.request(url, options, (response) => {
//console.log(`GOT ${options.method} ${url}`);
let responseBody = null;
response.on('data', (chunk) => responseBody = responseBody ? Buffer.concat([responseBody, chunk]) : chunk);
response.on('end', () => {
resolve({
response,
responseBody
})
});
});
request.on('error', (err) => {
reject(err)
});
request.on('abort', () => {
reject(new Error(`${options.method} ${url}: aborted`));
});
// request.on('timeout', () => {
// console.log(`TIMEDOUT ${options.method} ${url}`);
// reject(new Error(`${options.method} ${url}: timed out`));
// request.destroy();
// });
if(body) {
request.write(body);
}
request.end();
});
}
function BackblazeObjectStorage(file) {
const b2AuthorizeURL = "https://api.backblazeb2.com/b2api/v2/b2_authorize_account"
let keyId = file['backblaze-account'].keyId;
let secretKey = file['backblaze-account'].secretKey;
let bucketName = file['backblaze-account'].bucketName;
let authorizationToken = null;
let uploadAuthorizationToken = null;
let apiUrl = null;
let uploadUrl = null;
let downloadUrl = null;
let accountId = null;
let bucketId = null;
if (!keyId || !secretKey || !bucketName) {
throw new Error("can't makeBackblazeObjectStorage: username, password, and bucketName are all required.")
}
const login = async () => {
const headers = {
Authorization: 'Basic ' + Buffer.from(keyId + ':' + secretKey).toString('base64')
};
let {response, responseBody} = await httpsPromise("GET", b2AuthorizeURL, headers);
responseBody = responseBody.toString('utf-8')
let responseObject = JSON.parse(responseBody);
if(response.statusCode < 300) {
//const responseObject = JSON.parse(responseBody);
apiUrl = responseObject.apiUrl;
downloadUrl = responseObject.downloadUrl;
accountId = responseObject.accountId;
authorizationToken = responseObject.authorizationToken;
let {response, responseBody} = await httpsPromise(
"GET",
`${apiUrl}/b2api/v2/b2_list_buckets?accountId=${accountId}&bucketName=${bucketName}`,
{ Authorization: authorizationToken }
);
responseBody = responseBody.toString('utf-8');
if(response.statusCode < 300) {
const responseObject = JSON.parse(responseBody);
if(responseObject.buckets.length < 1) {
return {bucketNotFound: true};
} else {
bucketId = responseObject.buckets[0].bucketId;
return {bucketNotFound: false};
}
} else {
throw new Error(`backblaze error on b2_list_buckets: ${responseBody}`)
}
} else {
throw new Error(`backblaze authorize error: ${responseBody}`)
}
};
this.createIfNotExists = async () => {
let ensureLogin = {bucketNotFound: false};
if(!authorizationToken || !apiUrl || !bucketId) {
ensureLogin = await login();
}
if(ensureLogin.bucketNotFound) {
const {response, responseBody} = await httpsPromise(
"POST",
`${apiUrl}/b2api/v2/b2_create_bucket`,
{ Authorization: authorizationToken },
{
bucketName,
accountId,
bucketType: "allPrivate",
lifecycleRules: [
{
daysFromHidingToDeleting: 1,
fileNamePrefix: ""
}
]
}
);
if(response.statusCode < 300) {
const responseObject = JSON.parse(responseBody.toString('utf-8'));
bucketId = responseObject.bucketId;
} else {
throw new Error(`backblaze error on b2_create_bucket: ${responseBody.toString('utf-8')}`)
}
}
};
this.createAccessKeyIfNotExists = (key) => { throw new Error("createAccessKeyIfNotExists not implemented"); };
this.list = async (key) => {
if(!authorizationToken || !apiUrl || !bucketId) {
const result = await login();
if(result.bucketNotFound) {
throw new Error("ERROR_BUCKET_DOES_NOT_EXIST_YET");
}
}
if(key[0] == '/') {
key = key.substring(1);
}
if(key[key.length-1] != '/') {
key = `${key}/`;
}
const url = `${apiUrl}/b2api/v2/b2_list_file_names?bucketId=${bucketId}&delimiter=/${key != "" ? `&prefix=${key}` : ""}`;
const {response, responseBody} = await httpsPromise( "GET", url, { Authorization: authorizationToken } );
//console.log(responseBody.toString('utf-8'));
const responseObject = JSON.parse(responseBody.toString('utf-8'));
if(response.statusCode > 299) {
throw new Error(`Got HTTP ${response.statusCode} from ${url}`)
}
return responseObject.files.map(x => ({
name: `/${x.fileName}`.substring(key.length).replace(/(^\/)|(\/$)/g,''),
isDirectory: x.action == "folder",
lastModified: Number(x.uploadTimestamp)
}));
};
this.get = async (key) => {
if(!authorizationToken || !apiUrl || !bucketId) {
const result = await login();
if(result.bucketNotFound) {
throw new Error("ERROR_BUCKET_DOES_NOT_EXIST_YET");
}
}
if(key[0] == '/') {
key = key.substring(1);
}
const url = `${downloadUrl}/file/${bucketName}/${key}`;
const {response, responseBody} = await httpsPromise( "GET", url, { Authorization: authorizationToken } );
if(response.statusCode == 404) {
return {notFound: true};
}
if(response.statusCode > 299) {
throw new Error(`Got HTTP ${response.statusCode} from ${url}`);
}
const pathParts = key.split('/');
return {
notFound: false,
file: {
name: pathParts[pathParts.length-1],
content: responseBody,
lastModified: Number(response.headers['x-bz-upload-timestamp'])
}
}
};
this.put = (key, content) => { throw new Error("put not implemented"); };
this.delete = (key) => { throw new Error("delete not implemented"); };
}
function E2EEObjectStorage(underlying, encryptionInstance) {
this.createIfNotExists = async () => await underlying.createIfNotExists(key);
this.createAccessKeyIfNotExists = async (key) => await underlying.createAccessKeyIfNotExists(key);
this.list = async (key) => await underlying.list(key);
this.get = async (key, skipDecrypt) => {
const result = await underlying.get(key);
if(result.file && result.file.content && !skipDecrypt) {
const toDecrypt = Buffer.from(result.file.content);
try {
result.file.content = Buffer.from(encryptionInstance.decryptToBytes(toDecrypt));
} catch (err) {
console.log(`get ${key} from object storage: decryption failed, returning unmodified content'`, err)
}
}
return result;
};
this.put = async (key, content) => {
const isByteArray = content instanceof Uint8Array;
if(typeof content == "string") {
content = encryptionInstance.encryptString(content)
} else if(typeof content == "object" && !isByteArray) {
content = encryptionInstance.encryptString(JSON.stringify(content))
} else if (isByteArray) {
content = encryptionInstance.encryptBytes(content)
}
return await underlying.put(key, content)
};
this.delete = async (key) => await underlying.delete(key);
}
function S3CompatibleObjectStorage(file) {
throw new Error("S3CompatibleObjectStorage not implemented");
}