From a650e3beaafd11e309be0a647e7c9303be22d3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Tue, 21 Nov 2023 16:28:47 +0300 Subject: [PATCH] feat(amazon-s3): add amazon s3 integration --- .../src/apps/amazon-s3/assets/favicon.svg | 34 ++++ .../backend/src/apps/amazon-s3/auth/index.ts | 56 +++++++ .../apps/amazon-s3/auth/is-still-verified.ts | 9 ++ .../apps/amazon-s3/auth/verify-credentials.ts | 9 ++ .../apps/amazon-s3/common/add-auth-header.ts | 153 ++++++++++++++++++ .../apps/amazon-s3/common/get-current-date.ts | 13 ++ .../common/get-current-user.ts | 4 +- .../backend/src/apps/amazon-s3/index.d.ts | 0 packages/backend/src/apps/amazon-s3/index.ts | 16 ++ .../google-drive/auth/is-still-verified.ts | 2 +- .../google-drive/auth/verify-credentials.ts | 2 +- 11 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/apps/amazon-s3/assets/favicon.svg create mode 100644 packages/backend/src/apps/amazon-s3/auth/index.ts create mode 100644 packages/backend/src/apps/amazon-s3/auth/is-still-verified.ts create mode 100644 packages/backend/src/apps/amazon-s3/auth/verify-credentials.ts create mode 100644 packages/backend/src/apps/amazon-s3/common/add-auth-header.ts create mode 100644 packages/backend/src/apps/amazon-s3/common/get-current-date.ts rename packages/backend/src/apps/{google-drive => amazon-s3}/common/get-current-user.ts (54%) create mode 100644 packages/backend/src/apps/amazon-s3/index.d.ts create mode 100644 packages/backend/src/apps/amazon-s3/index.ts diff --git a/packages/backend/src/apps/amazon-s3/assets/favicon.svg b/packages/backend/src/apps/amazon-s3/assets/favicon.svg new file mode 100644 index 00000000..3f63be51 --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/assets/favicon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/backend/src/apps/amazon-s3/auth/index.ts b/packages/backend/src/apps/amazon-s3/auth/index.ts new file mode 100644 index 00000000..02208aeb --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/auth/index.ts @@ -0,0 +1,56 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/amazon-s3/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in AWS, enter the URL above.', + clickToCopy: true, + }, + { + key: 'accessKeyId', + label: 'Access Key ID', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'secretAccessKey', + label: 'Secret Access Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'screenName', + label: 'Screen Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/amazon-s3/auth/is-still-verified.ts b/packages/backend/src/apps/amazon-s3/auth/is-still-verified.ts new file mode 100644 index 00000000..c46fa18b --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const isStillVerified = async ($: IGlobalVariable) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/amazon-s3/auth/verify-credentials.ts b/packages/backend/src/apps/amazon-s3/auth/verify-credentials.ts new file mode 100644 index 00000000..f613d6b4 --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/auth/verify-credentials.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const { data } = await $.http.get('/'); + + console.log('data:', data); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/amazon-s3/common/add-auth-header.ts b/packages/backend/src/apps/amazon-s3/common/add-auth-header.ts new file mode 100644 index 00000000..d38c50ca --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/common/add-auth-header.ts @@ -0,0 +1,153 @@ +import { IJSONObject, TBeforeRequest } from '@automatisch/types'; +import crypto from 'crypto'; +import { getISODate, getYYYYMMDD } from './get-current-date'; + +function hmac(key: string | Buffer, data: string) { + return crypto.createHmac('sha256', key).update(data).digest('hex'); +} + +function hmacWoHex(key: Buffer | string, data: string) { + return crypto.createHmac('sha256', key).update(data).digest(); +} + +function hash(data: string) { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +function prepareCanonicalRequest( + method: string, + path: string, + queryParams: IJSONObject | string, + headers: IJSONObject, + payload: string +) { + const canonicalRequest = [method, encodeURIComponent(path)]; + + // Step 3: Canonical Query String + if (typeof queryParams === 'string') { + canonicalRequest.push(''); + } else { + const sortedQueryParams = Object.keys(queryParams) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + queryParams[key] as string + )}` + ) + .sort(); + canonicalRequest.push(sortedQueryParams.join('&')); + } + + // Step 4: Canonical Headers + const sortedHeaders = Object.keys(headers) + .sort() + .map((key) => `${key.toLowerCase()}:${(headers[key] as string).trim()}`); + + canonicalRequest.push(sortedHeaders.join('\n')); + + // Step 5: Signed Headers + const signedHeaders = Object.keys(headers) + .sort() + .map((key) => key.toLowerCase()) + .join(';'); + canonicalRequest.push(signedHeaders); + + const hashedPayload = hash(payload); + canonicalRequest.push(hashedPayload); + + return canonicalRequest.join('\n'); +} + +function prepareStringToSign( + datetime: string, + credentialScope: string, + hashedCanonicalRequest: string +) { + const stringToSign = [ + 'AWS4-HMAC-SHA256', + datetime, + credentialScope, + hashedCanonicalRequest, + ]; + + return stringToSign.join('\n'); +} + +function calculateSigningKey( + secretKey: string, + date: string, + region: string, + service: string +) { + const dateKey = hmacWoHex('AWS4' + secretKey, date); + const dateRegionKey = hmacWoHex(dateKey, region); + const dateRegionServiceKey = hmacWoHex(dateRegionKey, service); + const signingKey = hmacWoHex(dateRegionServiceKey, 'aws4_request'); + return signingKey; +} + +function createAuthorizationHeader( + accessKey: string, + credentialScope: string, + signedHeaders: string, + signature: string +) { + return `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; +} + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + const accessKeyId = $.auth.data.accessKeyId as string; + const secretAccessKey = $.auth.data.secretAccessKey as string; + const date = getYYYYMMDD(); + const formattedDate = getISODate(); + const region = 'us-east-1'; + const method = 'GET'; + const path = '/'; + const queryParams = ''; + const payload = ''; + const headers = { + Host: 's3.amazonaws.com', + 'X-Amz-Content-Sha256': hash(payload), + 'X-Amz-Date': formattedDate, + }; + const headerKeys = Object.keys(headers) + .sort() + .map((header) => header.toLowerCase()) + .join(';'); + + const canonicalRequest = prepareCanonicalRequest( + method, + path, + queryParams, + headers, + payload + ); + + const stringToSign = prepareStringToSign( + formattedDate, + `${date}/${region}/s3/aws4_request`, + hash(canonicalRequest) + ); + + const signingKey = calculateSigningKey(secretAccessKey, date, region, 's3'); + + const signature = hmac(signingKey, stringToSign); + + const authorizationHeader = createAuthorizationHeader( + accessKeyId, + `${date}/${region}/s3/aws4_request`, + headerKeys, + signature + ); + + if ($.auth.data?.secretAccessKey && $.auth.data?.accessKeyId) { + requestConfig.headers.Authorization = authorizationHeader; + requestConfig.headers['Host'] = 's3.amazonaws.com'; + requestConfig.headers['X-Amz-Content-Sha256'] = hash(payload); + requestConfig.headers['X-Amz-Date'] = formattedDate; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/amazon-s3/common/get-current-date.ts b/packages/backend/src/apps/amazon-s3/common/get-current-date.ts new file mode 100644 index 00000000..dc40c020 --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/common/get-current-date.ts @@ -0,0 +1,13 @@ +export const getYYYYMMDD = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = (today.getMonth() + 1).toString().padStart(2, '0'); + const day = today.getDate().toString().padStart(2, '0'); + + const formattedDate = `${year}${month}${day}`; + return formattedDate; +}; + +export const getISODate = () => { + return new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); +}; diff --git a/packages/backend/src/apps/google-drive/common/get-current-user.ts b/packages/backend/src/apps/amazon-s3/common/get-current-user.ts similarity index 54% rename from packages/backend/src/apps/google-drive/common/get-current-user.ts rename to packages/backend/src/apps/amazon-s3/common/get-current-user.ts index 724fe1ac..b66b81a0 100644 --- a/packages/backend/src/apps/google-drive/common/get-current-user.ts +++ b/packages/backend/src/apps/amazon-s3/common/get-current-user.ts @@ -1,9 +1,7 @@ import { IGlobalVariable } from '@automatisch/types'; const getCurrentUser = async ($: IGlobalVariable) => { - const { data: currentUser } = await $.http.get( - 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' - ); + const { data: currentUser } = await $.http.get('/'); return currentUser; }; diff --git a/packages/backend/src/apps/amazon-s3/index.d.ts b/packages/backend/src/apps/amazon-s3/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/amazon-s3/index.ts b/packages/backend/src/apps/amazon-s3/index.ts new file mode 100644 index 00000000..fe3188eb --- /dev/null +++ b/packages/backend/src/apps/amazon-s3/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; + +export default defineApp({ + name: 'Amazon S3', + key: 'amazon-s3', + baseUrl: '', + apiBaseUrl: 'https://s3.amazonaws.com', + iconUrl: '{BASE_URL}/apps/amazon-s3/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/amazon-s3/connection', + primaryColor: '7B1D13', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, +}); diff --git a/packages/backend/src/apps/google-drive/auth/is-still-verified.ts b/packages/backend/src/apps/google-drive/auth/is-still-verified.ts index c46fa18b..1ae818fa 100644 --- a/packages/backend/src/apps/google-drive/auth/is-still-verified.ts +++ b/packages/backend/src/apps/google-drive/auth/is-still-verified.ts @@ -1,5 +1,5 @@ import { IGlobalVariable } from '@automatisch/types'; -import getCurrentUser from '../common/get-current-user'; +import getCurrentUser from '../../amazon-s3/common/get-current-user'; const isStillVerified = async ($: IGlobalVariable) => { const currentUser = await getCurrentUser($); diff --git a/packages/backend/src/apps/google-drive/auth/verify-credentials.ts b/packages/backend/src/apps/google-drive/auth/verify-credentials.ts index 124e73c1..405332a2 100644 --- a/packages/backend/src/apps/google-drive/auth/verify-credentials.ts +++ b/packages/backend/src/apps/google-drive/auth/verify-credentials.ts @@ -1,5 +1,5 @@ import { IField, IGlobalVariable } from '@automatisch/types'; -import getCurrentUser from '../common/get-current-user'; +import getCurrentUser from '../../amazon-s3/common/get-current-user'; type TUser = { displayName: string;