Skip to content

Commit 9e49baf

Browse files
authored
Multiple improvements (#47)
* Obfuscate key Fixes #46 * Added retry logic Fixes #40 * Tweaking config * Migrate to github actions * Dropped node v10
1 parent d81b395 commit 9e49baf

File tree

13 files changed

+2051
-599
lines changed

13 files changed

+2051
-599
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ aws serverlessrepo create-application-version \
2525
echo "SAM application ${APPLICATION} version ${VERSION} published!"
2626

2727
# tag repo
28-
git config --global user.email "circleci@uvasoftware.com"
29-
git config --global user.name "CircleCI"
30-
git tag -a v"${VERSION}" -m "Release by CircleCI v${VERSION}"
28+
git config --global user.email "ci@uvasoftware.com"
29+
git config --global user.name "Github Actions"
30+
git tag -a v"${VERSION}" -m "Release by Github Actions v${VERSION}"
3131
git push origin v"${VERSION}"
3232

3333
# bumping version

.github/workflows/master.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
on:
2+
push:
3+
branches:
4+
- master
5+
jobs:
6+
deploy:
7+
if: "!contains(github.event.commits[0].message, '[ci skip]')"
8+
name: Deploy
9+
runs-on: ubuntu-latest
10+
container:
11+
image: debian:9
12+
steps:
13+
- uses: actions/checkout@v2
14+
- name: Install dependencies
15+
run: apt-get update -qq && apt-get install -qqy git unzip ssh ca-certificates tar gzip
16+
- name: Deploy
17+
run: bash ./.github/workflows/deploy.sh

.github/workflows/pr.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: PR Builds
2+
on: pull_request
3+
4+
jobs:
5+
build:
6+
runs-on: ubuntu-latest
7+
strategy:
8+
matrix:
9+
node-version: [ 12.x, 14.x ]
10+
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Use Node.js ${{ matrix.node-version }}
14+
uses: actions/setup-node@v1
15+
with:
16+
node-version: ${{ matrix.node-version }}
17+
- run: npm install
18+
- run: npm run build --if-present
19+
- run: npm test
20+
env:
21+
CI: true

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
A Sam-Packaged AWS Lambda client to the [scanii.com](https://scanii.com) content processing engine. For a detailed walk-through of deploying this application see: https://support.scanii.com/article/55-how-do-i-analyze-content-stored-on-amazon-s3.
33

44
## How it works
5-
This is, essentially, a series of lambda functions packaged in a one-click deployable application that configures everything needed so your S3 objects are submitted automatically to scanii’s content analysis [API](https://docs.scanii.com/v2.1/overview.html). Once the content is processed, you can choose from a couple of different actions:
5+
This is, essentially, a series of lambda functions packaged in a one-click deployable application that configures everything needed so your S3 objects are submitted automatically to scanii’s content analysis [API](https://docs.scanii.com/v2.1/overview.html). Once the content is processed, you can choose from a couple of different actions:
66

77
1. Tag the content - this is defaulted to on and adds the following tag to objects processed:
88
1. `ScaniiId` -> the resource id of the processed content
99
2. `ScaniiFindings` -> list of identified findings (content engine dependent)
1010
3. `ScaniiContentType` -> the identified content type of the file processed
11-
2. Delete object with findings - this is defaulted to **off** and will delete S3 objects with findings (such as malware or NSFW content) - for a full list of available content identification see https://support.scanii.com/article/20-content-detection-engines
11+
2. Delete the object with findings - this is defaulted to **off** and will delete S3 objects with findings (such as malware or NSFW content) - for a full list of available content identification see https://support.scanii.com/article/20-content-detection-engines
1212

1313
## Working with the source code
1414
The source code for this application is written using Javascript and requires, at least, nodejs 8 to run. Before getting started we strongly advise you to become familiar with the following technologies:

lib/ag-handler.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ exports.handler = async (event, context, callback) => {
2727
console.log("metadata:", result.metadata);
2828

2929
// now asserting bucket/keys were not tampered with:
30-
assert.ok(result.metadata.signature === utils.generateSignature(result.metadata.bucket.toString(), result.metadata.key.toString()), "invalid signature");
30+
assert.ok(result.metadata.signature === utils.generateSignature(result.metadata.bucket.toString(),
31+
result.metadata.key.toString()), "invalid signature");
3132
console.log('signature check passed for signature', result.metadata.signature);
3233

3334
if (result.error === undefined) {

lib/client.js

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
const VERSION = require('../package.json').version;
2-
const request = require('request');
32
const assert = require('assert');
3+
const axios = require('axios');
4+
const querystring = require('querystring')
45

56
/**
67
* Minimal Scanii API client in javascript (@see https://docs.scanii.com/v2.1/resources.html)
78
*/
89
class ScaniiClient {
9-
constructor(key, secret, endpoint = "api.scanii.com") {
10+
constructor(key, secret, endpoint = "api.scanii.com", maxAttempts, maxAttemptDelay) {
1011
this.key = key;
1112
this.secret = secret;
12-
this.endpoint = endpoint;
13+
this.maxAttempts = maxAttempts;
14+
this.maxAttemptDelay = maxAttemptDelay;
1315
this.userAgent = `scanii-lambda/v${VERSION}`;
16+
this.client = axios.create({
17+
auth: {
18+
username: key, password: secret
19+
},
20+
headers: {
21+
'User-Agent': this.userAgent
22+
},
23+
baseURL: `https://${endpoint}`
24+
});
1425
console.log(`scanii client created using endpoint ${endpoint} and version ${VERSION}`)
1526
}
1627

@@ -23,43 +34,26 @@ class ScaniiClient {
2334
*/
2435

2536
async fetch(location, callback, metadata) {
26-
const options = {
27-
url: `https://${this.endpoint}/v2.1/files/fetch`,
28-
auth: {
29-
'user': this.key,
30-
'pass': this.secret,
31-
},
32-
headers: {
33-
'User-Agent': this.userAgent
34-
},
35-
method: 'POST',
36-
form: {
37-
location: location,
38-
}
39-
};
37+
let data = {
38+
location: location
39+
}
4040

4141
if (callback !== null) {
42-
options.form.callback = callback;
42+
data.callback = callback;
4343
}
44-
4544
if (metadata !== null) {
4645
for (const k in metadata) {
4746
if (metadata.hasOwnProperty(k)) {
48-
options.form[`metadata[${k}]`] = metadata[k];
47+
data[`metadata[${k}]`] = metadata[k];
4948
}
5049
}
5150
}
52-
return new Promise((resolve, reject) => {
53-
request(options, (error, response, body) => {
54-
if (error) {
55-
reject(error)
56-
} else {
57-
assert.ok(response.statusCode === 202, `Invalid response from server, with HTTP code: ${response.statusCode}`);
58-
let file = JSON.parse(body);
59-
console.log(`submit successful with id: ${file.id}`);
60-
resolve({id: file.id, location: response.headers.location});
61-
}
62-
});
51+
52+
return await this._retry(async () => {
53+
const response = await this.client.post('/v2.1/files/fetch', querystring.stringify(data), {headers: {'content-type': 'application/x-www-form-urlencoded'}});
54+
assert.ok(response.status === 202, `Invalid response from server, with HTTP code: ${response.status}`);
55+
console.log(`submit successful with id: ${response.data.id}`);
56+
return ({id: response.data.id, location: response.headers.location});
6357
});
6458
}
6559

@@ -70,32 +64,49 @@ class ScaniiClient {
7064
* @returns {Promise<*>}
7165
*/
7266
async retrieve(id) {
73-
const options = {
74-
url: `https://${this.endpoint}/v2.1/files/${id}`,
75-
auth: {
76-
'user': this.key,
77-
'pass': this.secret,
78-
},
79-
headers: {
80-
'User-Agent': this.userAgent
81-
},
82-
method: 'GET'
83-
};
67+
return await this._retry(async () => {
68+
const response = await this.client.get(`/v2.1/files/fetch/${id}`);
69+
assert.ok(response.status === 200, `Invalid response from server, with HTTP code: ${response.status}`);
70+
let result = JSON.parse(response.data);
71+
console.log(`retrieve successful with id: ${result.id}`);
72+
return result;
73+
});
74+
}
8475

85-
return new Promise((resolve, reject) => {
86-
request(options, (error, response, body) => {
87-
if (error) {
88-
reject(error);
89-
}
90-
assert.ok(response.statusCode === 200, `Invalid response from server, with HTTP code: ${response.statusCode}`);
91-
let result = JSON.parse(body);
92-
console.log(`retrieve successful with id: ${result.id}`);
93-
resolve(result);
76+
/**
77+
* Wraps an async function call around a basic retry logic
78+
* @param func
79+
* @returns {Promise<void>}
80+
* @private
81+
*/
82+
async _retry(func) {
83+
let attempt = 1;
84+
while (attempt <= this.maxAttempts) {
85+
if (attempt > 1) {
86+
const wait = Math.round(Math.random() * this.maxAttemptDelay);
87+
console.log(`retrying is enabled, going to wait ${wait}ms and try again`);
88+
await new Promise(resolve => setTimeout(resolve, wait));
89+
}
9490

95-
});
96-
});
91+
try {
92+
return await func()
93+
} catch (e) {
94+
console.error(e.message);
95+
} finally {
96+
attempt++
97+
}
98+
}
99+
100+
throw new ScaniiError(attempt - 1);
97101
}
102+
}
98103

104+
class ScaniiError extends Error {
105+
constructor(attempts) {
106+
super(`Scanii ERROR, could not get a successful response from service after ${attempts} attempts`);
107+
this.attempts = attempts;
108+
}
99109
}
100110

101111
exports.ScaniiClient = ScaniiClient;
112+
exports.ScaniiError = ScaniiError;

lib/config.js

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,50 @@
1-
CONFIG = {
2-
KEY: null,
3-
SECRET: null,
4-
API_ENDPOINT: "api.scanii.com",
5-
CALLBACK_URL: null,
6-
ACTION_TAG_OBJECT: false,
7-
ACTION_DELETE_OBJECT: false,
8-
};
1+
const CONFIG = {}
92

103
if (process.env.AWS_SAM_LOCAL !== undefined) {
114
console.log("starting...");
125
console.log(process.env);
136
}
147

8+
function defaults() {
9+
CONFIG.KEY = null;
10+
CONFIG.SECRET = null;
11+
CONFIG.API_ENDPOINT = "api.scanii.com";
12+
CONFIG.CALLBACK_URL = null;
13+
CONFIG.ACTION_TAG_OBJECT = false;
14+
CONFIG.ACTION_DELETE_OBJECT = false;
15+
CONFIG.MAX_ATTEMPTS = 10;
16+
CONFIG.MAX_ATTEMPT_DELAY_MSEC = 30_000;
17+
1518
// extracting config overwrites from the environment:
19+
if (process.env.API_KEY) {
20+
CONFIG.KEY = process.env.API_KEY;
21+
}
22+
if (process.env.API_SECRET) {
23+
CONFIG.SECRET = process.env.API_SECRET;
24+
}
1625

17-
if (process.env.API_KEY) {
18-
CONFIG.KEY = process.env.API_KEY;
19-
}
20-
if (process.env.API_SECRET) {
21-
CONFIG.SECRET = process.env.API_SECRET;
22-
}
26+
if (process.env.ACTION_TAG_OBJECT === "true") {
27+
CONFIG.ACTION_TAG_OBJECT = true;
28+
}
2329

24-
if (process.env.ACTION_TAG_OBJECT === "true") {
25-
CONFIG.ACTION_TAG_OBJECT = true;
26-
}
30+
if (process.env.ACTION_DELETE_OBJECT === "true") {
31+
CONFIG.ACTION_DELETE_OBJECT = true;
32+
}
2733

28-
if (process.env.ACTION_DELETE_OBJECT === "true") {
29-
CONFIG.ACTION_DELETE_OBJECT = true;
30-
}
34+
if (process.env.CALLBACK_URL) {
35+
CONFIG.CALLBACK_URL = process.env.CALLBACK_URL;
36+
}
37+
38+
if (process.env.MAX_ATTEMPTS) {
39+
CONFIG.MAX_ATTEMPTS = process.env.MAX_ATTEMPTS;
40+
}
41+
42+
if (process.env.MAX_ATTEMPT_DELAY_MSEC) {
43+
CONFIG.MAX_ATTEMPT_DELAY_MSEC = process.env.MAX_ATTEMPT_DELAY_MSEC;
44+
}
3145

32-
if (process.env.CALLBACK_URL) {
33-
CONFIG.CALLBACK_URL = process.env.CALLBACK_URL;
3446
}
3547

48+
defaults();
49+
exports.defaults = defaults;
3650
exports.CONFIG = CONFIG;

lib/s3-handler.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const CONFIG = require('./config').CONFIG;
55
const utils = require('./utils');
66
const scanii = require('./client');
77
const pkg = require('../package.json');
8+
const crypto = require('crypto');
89

910

1011
/**
@@ -23,7 +24,9 @@ exports.handler = async (event, context, callback) => {
2324
assert.ok(CONFIG.KEY !== null, "api key cannot be null");
2425
assert.ok(CONFIG.SECRET !== null, "api secret cannot be null");
2526

26-
const scaniiClient = new scanii.ScaniiClient(CONFIG.KEY, CONFIG.SECRET, CONFIG.API_ENDPOINT);
27+
const scaniiClient = new scanii.ScaniiClient(CONFIG.KEY, CONFIG.SECRET, CONFIG.API_ENDPOINT,
28+
CONFIG.MAX_ATTEMPTS, CONFIG.MAX_ATTEMPT_DELAY_MSEC);
29+
2730
const S3 = new AWS.S3({apiVersion: '2006-03-01'});
2831

2932
// Get the object from the event and show its content type
@@ -51,11 +54,13 @@ exports.handler = async (event, context, callback) => {
5154
// signing request
5255
const signature = utils.generateSignature(bucket, key);
5356
console.log('using signature ' + signature);
57+
const sha1sum = crypto.createHash('sha1');
5458

59+
// we obfuscate the object key since it can contain sensitive information we don't want stored:
5560
const metadata = {
5661
"signature": signature,
5762
"bucket": bucket,
58-
"key": key
63+
"key-sha1": sha1sum.update(key).digest('hex')
5964
};
6065

6166
const submitResult = await scaniiClient.fetch(url, CONFIG.CALLBACK_URL, metadata);

0 commit comments

Comments
 (0)