Skip to content

Commit 33d334f

Browse files
committed
Support private S3 buckets
1 parent c842373 commit 33d334f

File tree

6 files changed

+151
-4
lines changed

6 files changed

+151
-4
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Options include:
8585
- `--target=0.4.0`: Pass the target node or node-webkit version to compile against
8686
- `--target_arch=ia32`: Pass the target arch and override the host `arch`. Any value that is [supported by Node.js](https://nodejs.org/api/os.html#osarch) is valid.
8787
- `--target_platform=win32`: Pass the target platform and override the host `platform`. Valid values are `linux`, `darwin`, `win32`, `sunos`, `freebsd`, `openbsd`, and `aix`.
88+
- `--acl=<acl>`: Set the S3 ACL when publishing binaries (e.g., `public-read`, `private`). Overrides the `binary.acl` setting in package.json.
8889

8990
Both `--build-from-source` and `--fallback-to-build` can be passed alone or they can provide values. You can pass `--fallback-to-build=false` to override the option as declared in package.json. In addition to being able to pass `--build-from-source` you can also pass `--build-from-source=myapp` where `myapp` is the name of your module.
9091

@@ -185,6 +186,33 @@ Your S3 server region.
185186

186187
Set `s3ForcePathStyle` to true if the endpoint url should not be prefixed with the bucket name. If false (default), the server endpoint would be constructed as `bucket_name.your_server.com`.
187188

189+
###### acl
190+
191+
The S3 Access Control List (ACL) to apply when publishing binaries. Defaults to `'public-read'` for backward compatibility. Common values include:
192+
193+
- `public-read` - (default) Binary is publicly accessible by anyone
194+
- `private` - Binary requires AWS credentials to download
195+
- `authenticated-read` - Any authenticated AWS user can download
196+
- `bucket-owner-read` - Bucket owner gets READ access
197+
- `bucket-owner-full-control` - Bucket owner gets FULL_CONTROL access
198+
199+
**For private binaries:**
200+
- Users installing your package will need AWS credentials configured (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables)
201+
- The `aws-sdk` package must be available at install time
202+
- If authentication fails, node-pre-gyp will fall back to building from source (if `--fallback-to-build` is specified)
203+
204+
You can also specify the ACL via command-line flag: `node-pre-gyp publish --acl=private`
205+
206+
Example for private binaries:
207+
```json
208+
"binary": {
209+
"module_name": "your_module",
210+
"module_path": "./lib/binding/",
211+
"host": "https://your-bucket.s3.us-east-1.amazonaws.com",
212+
"acl": "private"
213+
}
214+
```
215+
188216
##### The `binary` object has optional properties
189217

190218
###### remote_path

lib/install.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const log = require('./util/log.js');
1010
const existsAsync = fs.exists || path.exists;
1111
const versioning = require('./util/versioning.js');
1212
const napi = require('./util/napi.js');
13+
const s3_setup = require('./util/s3_setup.js');
14+
const url = require('url');
1315
// for fetching binaries
1416
const fetch = require('node-fetch');
1517
const tar = require('tar');
@@ -23,6 +25,65 @@ try {
2325
// do nothing
2426
}
2527

28+
function place_binary_authenticated(opts, targetDir, callback) {
29+
log.info('install', 'Attempting authenticated S3 download');
30+
31+
// Check if AWS credentials are available
32+
if (!process.env.AWS_ACCESS_KEY_ID && !process.env.AWS_SECRET_ACCESS_KEY) {
33+
const err = new Error('Binary is private but AWS credentials not found. Please configure AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or use --fallback-to-build to compile from source.');
34+
err.statusCode = 403;
35+
return callback(err);
36+
}
37+
38+
try {
39+
const config = s3_setup.detect(opts);
40+
const s3 = s3_setup.get_s3(config);
41+
const key_name = url.resolve(config.prefix, opts.package_name);
42+
43+
log.info('install', 'Downloading from S3:', config.bucket, key_name);
44+
45+
const s3_opts = {
46+
Bucket: config.bucket,
47+
Key: key_name
48+
};
49+
50+
s3.getObject(s3_opts, (err, data) => {
51+
if (err) {
52+
log.error('install', 'Authenticated S3 download failed:', err.message);
53+
return callback(err);
54+
}
55+
56+
log.info('install', 'Authenticated download successful, extracting...');
57+
58+
const { Readable } = require('stream');
59+
const dataStream = Readable.from(data.Body);
60+
61+
let extractions = 0;
62+
const countExtractions = (entry) => {
63+
extractions += 1;
64+
log.info('install', `unpacking ${entry.path}`);
65+
};
66+
67+
dataStream.pipe(extract(targetDir, countExtractions))
68+
.on('error', (e) => {
69+
callback(e);
70+
})
71+
.on('close', () => {
72+
log.info('install', `extracted file count: ${extractions}`);
73+
callback();
74+
});
75+
});
76+
} catch (e) {
77+
if (e.code === 'MODULE_NOT_FOUND' && e.message.includes('aws-sdk')) {
78+
const err = new Error('Binary is private and requires aws-sdk for authenticated download. Please run: npm install aws-sdk');
79+
err.statusCode = 403;
80+
return callback(err);
81+
}
82+
log.error('install', 'Error setting up authenticated download:', e.message);
83+
callback(e);
84+
}
85+
}
86+
2687
function place_binary(uri, targetDir, opts, callback) {
2788
log.log('GET', uri);
2889

@@ -63,6 +124,11 @@ function place_binary(uri, targetDir, opts, callback) {
63124
fetch(sanitized, { agent })
64125
.then((res) => {
65126
if (!res.ok) {
127+
// If we get 403 Forbidden, the binary might be private - try authenticated download
128+
if (res.status === 403) {
129+
log.info('install', 'Received 403 Forbidden - attempting authenticated download');
130+
return place_binary_authenticated(opts, targetDir, callback);
131+
}
66132
throw new Error(`response status ${res.status} ${res.statusText} on ${sanitized}`);
67133
}
68134
const dataStream = res.body;

lib/node-pre-gyp.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ proto.configDefs = {
9898
debug: Boolean, // 'build'
9999
directory: String, // bin
100100
proxy: String, // 'install'
101-
loglevel: String // everywhere
101+
loglevel: String, // everywhere
102+
acl: String // 'publish' - S3 ACL for published binaries
102103
};
103104

104105
/**

lib/publish.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ function publish(gyp, argv, callback) {
4343
// the object does not already exist
4444
log.info('publish', 'Preparing to put object');
4545
const s3_put_opts = {
46-
ACL: 'public-read',
46+
ACL: opts.acl,
4747
Body: fs.createReadStream(tarball),
4848
Key: key_name,
4949
Bucket: config.bucket
5050
};
51-
log.info('publish', 'Putting object', s3_put_opts.ACL, s3_put_opts.Bucket, s3_put_opts.Key);
51+
log.info('publish', 'Putting object with ACL:', s3_put_opts.ACL);
52+
log.info('publish', 'Bucket:', s3_put_opts.Bucket, 'Key:', s3_put_opts.Key);
5253
try {
5354
s3.putObject(s3_put_opts, (err2, resp) => {
5455
log.info('publish', 'returned from putting object');

lib/util/versioning.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ module.exports.evaluate = function(package_json, options, napi_build_version) {
307307
toolset: options.toolset || '', // address https://github.com/mapbox/node-pre-gyp/issues/119
308308
bucket: package_json.binary.bucket,
309309
region: package_json.binary.region,
310-
s3ForcePathStyle: package_json.binary.s3ForcePathStyle || false
310+
s3ForcePathStyle: package_json.binary.s3ForcePathStyle || false,
311+
acl: options.acl || package_json.binary.acl || 'public-read'
311312
};
312313
// support host mirror with npm config `--{module_name}_binary_host_mirror`
313314
// e.g.: https://github.com/node-inspector/v8-profiler/blob/master/package.json#L25

test/versioning.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,56 @@ test('should throw when custom node target is not found in abi_crosswalk file',
100100
}
101101
});
102102

103+
test('should default ACL to public-read when not specified', (t) => {
104+
const mock_package_json = {
105+
'name': 'test',
106+
'main': 'test.js',
107+
'version': '0.1.0',
108+
'binary': {
109+
'module_name': 'test',
110+
'module_path': './lib/binding/',
111+
'host': 'https://some-bucket.s3.us-east-1.amazonaws.com'
112+
}
113+
};
114+
const opts = versioning.evaluate(mock_package_json, {});
115+
t.equal(opts.acl, 'public-read', 'ACL should default to public-read');
116+
t.end();
117+
});
118+
119+
test('should use ACL from package.json binary.acl', (t) => {
120+
const mock_package_json = {
121+
'name': 'test',
122+
'main': 'test.js',
123+
'version': '0.1.0',
124+
'binary': {
125+
'module_name': 'test',
126+
'module_path': './lib/binding/',
127+
'host': 'https://some-bucket.s3.us-east-1.amazonaws.com',
128+
'acl': 'private'
129+
}
130+
};
131+
const opts = versioning.evaluate(mock_package_json, {});
132+
t.equal(opts.acl, 'private', 'ACL should be read from package.json binary.acl');
133+
t.end();
134+
});
135+
136+
test('should allow CLI flag to override package.json ACL', (t) => {
137+
const mock_package_json = {
138+
'name': 'test',
139+
'main': 'test.js',
140+
'version': '0.1.0',
141+
'binary': {
142+
'module_name': 'test',
143+
'module_path': './lib/binding/',
144+
'host': 'https://some-bucket.s3.us-east-1.amazonaws.com',
145+
'acl': 'private'
146+
}
147+
};
148+
const opts = versioning.evaluate(mock_package_json, { acl: 'authenticated-read' });
149+
t.equal(opts.acl, 'authenticated-read', 'CLI flag should override package.json ACL');
150+
t.end();
151+
});
152+
103153
test('should throw when custom node target is not semver', (t) => {
104154
try {
105155
versioning.get_runtime_abi('node', '1.2.3.4');

0 commit comments

Comments
 (0)