Skip to content

Commit ebc9dbc

Browse files
authored
Merge pull request #8812 from romayalon/romy-response-headers-additions
S3 | GET/HEAD Object | Add Response Headers Support
2 parents c055ec3 + 9071a54 commit ebc9dbc

File tree

5 files changed

+156
-4
lines changed

5 files changed

+156
-4
lines changed

src/endpoint/s3/ops/s3_get_object.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async function get_object(req, res) {
4848
throw new S3Error(S3Error.InvalidObjectState);
4949
}
5050
}
51-
51+
http_utils.set_response_headers_from_request(req, res);
5252
const obj_size = object_md.size;
5353
const params = {
5454
object_md,

src/endpoint/s3/ops/s3_head_object.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ async function head_object(req, res) {
2828

2929
s3_utils.set_response_object_md(res, object_md);
3030
s3_utils.set_encryption_response_headers(req, res, object_md.encryption);
31+
http_utils.set_response_headers_from_request(req, res);
3132
}
3233

3334
module.exports = {

src/test/unit_tests/test_nsfs_integration.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ const tmp_fs_root = path.join(TMP_PATH, 'test_bucket_namespace_fs');
6060
// on Containerized - new_buckets_path is the directory
6161
const new_bucket_path_param = get_new_buckets_path_by_test_env(tmp_fs_root, '/');
6262

63+
// response headers
64+
const response_content_disposition = 'attachment';
65+
const response_content_language = 'hebrew';
66+
const response_content_type = 'application/json';
67+
const response_cache_control = 'no-cache';
68+
const response_expires = new Date();
69+
response_expires.setMilliseconds(0);
70+
6371
// currently will pass only when running locally
6472
mocha.describe('bucket operations - namespace_fs', function() {
6573
const nsr = 'nsr';
@@ -2242,6 +2250,115 @@ mocha.describe('Presigned URL tests', function() {
22422250
const expected_err = new S3Error(S3Error.AuthorizationQueryParametersError);
22432251
await assert_throws_async(fetchData(invalid_expiry_presigned_url), expected_err.message);
22442252
});
2253+
2254+
it('get-object - fetch valid presigned URL - 604800 seconds - epoch expiry - should return object data + return response headers', async () => {
2255+
const response_queries = {
2256+
ResponseContentDisposition: response_content_disposition,
2257+
ResponseContentLanguage: response_content_language,
2258+
ResponseContentType: response_content_type,
2259+
ResponseCacheControl: response_cache_control,
2260+
ResponseExpires: response_expires
2261+
};
2262+
const presigned_url_params_with_response_headers = { ...presigned_url_params, response_queries };
2263+
const url_with_response_headers = cloud_utils.get_signed_url(presigned_url_params_with_response_headers, 604800);
2264+
const headers = await fetchHeaders(url_with_response_headers);
2265+
assert.equal(headers.get('content-disposition'), response_content_disposition);
2266+
assert.equal(headers.get('content-language'), response_content_language);
2267+
assert.equal(headers.get('content-type'), response_content_type);
2268+
assert.equal(headers.get('cache-control'), response_cache_control);
2269+
assert.deepStrictEqual(headers.get('expires'), response_expires.toUTCString());
2270+
});
2271+
2272+
it('head-object - fetch valid presigned URL - 604800 seconds - epoch expiry - should return object data + return response headers', async () => {
2273+
const response_queries = {
2274+
ResponseContentDisposition: response_content_disposition,
2275+
ResponseContentLanguage: response_content_language,
2276+
ResponseContentType: response_content_type,
2277+
ResponseCacheControl: response_cache_control,
2278+
ResponseExpires: response_expires
2279+
};
2280+
const presigned_url_params_with_response_headers = { ...presigned_url_params, response_queries };
2281+
const url_with_response_headers = cloud_utils.get_signed_url(presigned_url_params_with_response_headers, 604800, 'headObject');
2282+
const headers = await fetchHeaders(url_with_response_headers, { method: 'HEAD' });
2283+
assert.equal(headers.get('content-disposition'), response_content_disposition);
2284+
assert.equal(headers.get('content-language'), response_content_language);
2285+
assert.equal(headers.get('content-type'), response_content_type);
2286+
assert.equal(headers.get('cache-control'), response_cache_control);
2287+
assert.deepStrictEqual(headers.get('expires'), response_expires.toUTCString());
2288+
});
2289+
});
2290+
2291+
mocha.describe('response headers test - regular request', function() {
2292+
this.timeout(50000); // eslint-disable-line no-invalid-this
2293+
const nsr = 'response_headers_nsr';
2294+
const account_name = 'response_header_account';
2295+
const fs_path = path.join(TMP_PATH, 'response_header_tests/');
2296+
const response_header_bucket = 'response-headerbucket';
2297+
const response_header_object = 'response-header-object.txt';
2298+
const response_header_body = 'response_header_body';
2299+
let s3_client;
2300+
let access_key;
2301+
let secret_key;
2302+
CORETEST_ENDPOINT = coretest.get_http_address();
2303+
2304+
mocha.before(async function() {
2305+
await fs_utils.create_fresh_path(fs_path);
2306+
await rpc_client.pool.create_namespace_resource({ name: nsr, nsfs_config: { fs_root_path: fs_path } });
2307+
const new_buckets_path = is_nc_coretest ? fs_path : '/';
2308+
const nsfs_account_config = {
2309+
uid: process.getuid(), gid: process.getgid(), new_buckets_path, nsfs_only: true
2310+
};
2311+
const account_params = { ...new_account_params, email: `${account_name}@noobaa.io`, name: account_name, default_resource: nsr, nsfs_account_config };
2312+
const res = await rpc_client.account.create_account(account_params);
2313+
access_key = res.access_keys[0].access_key;
2314+
secret_key = res.access_keys[0].secret_key;
2315+
s3_client = generate_s3_client(access_key.unwrap(), secret_key.unwrap(), CORETEST_ENDPOINT);
2316+
await s3_client.createBucket({ Bucket: response_header_bucket });
2317+
await s3_client.putObject({ Bucket: response_header_bucket, Key: response_header_object, Body: response_header_body });
2318+
});
2319+
2320+
mocha.after(async function() {
2321+
if (!is_nc_coretest) return;
2322+
await s3_client.deleteObject({ Bucket: response_header_bucket, Key: response_header_object });
2323+
await s3_client.deleteBucket({ Bucket: response_header_bucket });
2324+
await rpc_client.account.delete_account({ email: `${account_name}@noobaa.io` });
2325+
await fs_utils.folder_delete(fs_path);
2326+
});
2327+
2328+
it('get-object - response headers', async () => {
2329+
const res = await s3_client.getObject({
2330+
Bucket: response_header_bucket,
2331+
Key: response_header_object,
2332+
ResponseContentDisposition: response_content_disposition,
2333+
ResponseContentLanguage: response_content_language,
2334+
ResponseContentType: response_content_type,
2335+
ResponseCacheControl: response_cache_control,
2336+
ResponseExpires: response_expires,
2337+
});
2338+
assert.equal(res.ContentDisposition, response_content_disposition);
2339+
assert.equal(res.ContentLanguage, response_content_language);
2340+
assert.equal(res.ContentType, response_content_type);
2341+
assert.equal(res.CacheControl, response_cache_control);
2342+
assert.deepStrictEqual(res.Expires, response_expires);
2343+
});
2344+
2345+
it('head-object - response headers', async () => {
2346+
const res = await s3_client.headObject({
2347+
Bucket: response_header_bucket,
2348+
Key: response_header_object,
2349+
ResponseContentDisposition: response_content_disposition,
2350+
ResponseContentLanguage: response_content_language,
2351+
ResponseContentType: response_content_type,
2352+
ResponseCacheControl: response_cache_control,
2353+
ResponseExpires: response_expires,
2354+
});
2355+
assert.equal(res.ContentDisposition, response_content_disposition);
2356+
assert.equal(res.ContentLanguage, response_content_language);
2357+
assert.equal(res.ContentType, response_content_type);
2358+
assert.equal(res.CacheControl, response_cache_control);
2359+
assert.deepStrictEqual(res.Expires, response_expires);
2360+
});
2361+
22452362
});
22462363

22472364
async function fetchData(presigned_url) {
@@ -2257,3 +2374,17 @@ async function fetchData(presigned_url) {
22572374
data = await response.text();
22582375
return data.trim();
22592376
}
2377+
2378+
async function fetchHeaders(presigned_url, options) {
2379+
const response = await fetch(presigned_url, { ...options, agent: new http.Agent({ keepAlive: false }) });
2380+
let data;
2381+
if (!response.ok) {
2382+
data = (await response.text()).trim();
2383+
const err_json = (await http_utils.parse_xml_to_js(data)).Error;
2384+
const err = new Error(err_json.Message);
2385+
err.code = err_json.Code;
2386+
throw err;
2387+
}
2388+
return response.headers;
2389+
}
2390+

src/util/cloud_utils.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ async function generate_aws_sts_creds(params, roleSessionName) {
5858
);
5959
}
6060

61-
function get_signed_url(params, expiry = 604800) {
61+
function get_signed_url(params, expiry = 604800, custom_operation = 'getObject') {
62+
const op = custom_operation;
6263
const s3 = new AWS.S3({
6364
endpoint: params.endpoint,
6465
credentials: {
@@ -76,12 +77,14 @@ function get_signed_url(params, expiry = 604800) {
7677
agent: http_utils.get_unsecured_agent(params.endpoint)
7778
}
7879
});
80+
const response_queries = params.response_queries || {};
7981
return s3.getSignedUrl(
80-
'getObject', {
82+
op, {
8183
Bucket: params.bucket.unwrap(),
8284
Key: params.key,
8385
VersionId: params.version_id,
84-
Expires: expiry
86+
Expires: expiry,
87+
...response_queries
8588
}
8689
);
8790
}

src/util/http_utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/* eslint-disable no-control-regex */
44

55
const _ = require('lodash');
6+
const util = require('util');
67
const ip_module = require('ip');
78
const net = require('net');
89
const url = require('url');
@@ -908,6 +909,21 @@ function handle_server_error(err) {
908909
process.exit(1);
909910
}
910911

912+
/**
913+
* set_response_headers_from_request sets the response headers based on the request headers
914+
* gap - response-content-encoding needs to be added with a more complex logic
915+
* @param {http.IncomingMessage} req
916+
* @param {http.ServerResponse} res
917+
*/
918+
function set_response_headers_from_request(req, res) {
919+
dbg.log2(`set_response_headers_from_request req.query ${util.inspect(req.query)}`);
920+
if (req.query['response-cache-control']) res.setHeader('Cache-Control', req.query['response-cache-control']);
921+
if (req.query['response-content-disposition']) res.setHeader('Content-Disposition', req.query['response-content-disposition']);
922+
if (req.query['response-content-language']) res.setHeader('Content-Language', req.query['response-content-language']);
923+
if (req.query['response-content-type']) res.setHeader('Content-Type', req.query['response-content-type']);
924+
if (req.query['response-expires']) res.setHeader('Expires', req.query['response-expires']);
925+
}
926+
911927
exports.parse_url_query = parse_url_query;
912928
exports.parse_client_ip = parse_client_ip;
913929
exports.get_md_conditions = get_md_conditions;
@@ -944,3 +960,4 @@ exports.CONTENT_TYPE_APP_OCTET_STREAM = CONTENT_TYPE_APP_OCTET_STREAM;
944960
exports.CONTENT_TYPE_APP_JSON = CONTENT_TYPE_APP_JSON;
945961
exports.CONTENT_TYPE_APP_XML = CONTENT_TYPE_APP_XML;
946962
exports.CONTENT_TYPE_APP_FORM_URLENCODED = CONTENT_TYPE_APP_FORM_URLENCODED;
963+
exports.set_response_headers_from_request = set_response_headers_from_request;

0 commit comments

Comments
 (0)