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
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"); |
|
} |