├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app.js ├── dist ├── batcher.d.ts ├── batcher.d.ts.map ├── dataUtils.d.ts ├── dataUtils.d.ts.map ├── ensureValidUtils.d.ts ├── ensureValidUtils.d.ts.map ├── formatEventDataAndSave.d.ts ├── formatEventDataAndSave.d.ts.map ├── getRawBody.d.ts ├── getRawBody.d.ts.map ├── governanceRulesManager.d.ts ├── governanceRulesManager.d.ts.map ├── index.d.ts ├── index.d.ts.map ├── moesifConfigManager.d.ts ├── moesifConfigManager.d.ts.map ├── nextjsUtils.d.ts ├── nextjsUtils.d.ts.map ├── outgoing.d.ts ├── outgoing.d.ts.map ├── outgoingRecorder.d.ts └── outgoingRecorder.d.ts.map ├── eslint.config.mjs ├── images └── app_id.png ├── lib ├── batcher.js ├── dataUtils.js ├── ensureValidUtils.js ├── formatEventDataAndSave.js ├── getRawBody.js ├── governanceRulesManager.js ├── index.js ├── moesifConfigManager.js ├── nextjsUtils.js ├── outgoing.js └── outgoingRecorder.js ├── package-lock.json ├── package.json ├── test ├── batcherUnit.js ├── governanceRuleUnit.js ├── mockserver.js ├── outgoingUnit.js ├── outgoingWithMoesif.js ├── outgoingWithMoesifExpress.js ├── testConfig.js ├── testGetIPaddress.js ├── testSendingActions.js ├── testUpdatingEntities.js └── testUtils.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/intellij,node,osx,windows,less 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea 10 | .idea/workspace.xml 11 | .idea/tasks.xml 12 | .idea/dictionaries 13 | .idea/vcs.xml 14 | .idea/jsLibraryMappings.xml 15 | 16 | # Sensitive or high-churn files: 17 | .idea/dataSources.ids 18 | .idea/dataSources.xml 19 | .idea/dataSources.local.xml 20 | .idea/sqlDataSources.xml 21 | .idea/dynamic.xml 22 | .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | .idea/gradle.xml 26 | .idea/libraries 27 | 28 | # Mongo Explorer plugin: 29 | .idea/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | ### Intellij Patch ### 52 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 53 | 54 | # *.iml 55 | # modules.xml 56 | # .idea/misc.xml 57 | # *.ipr 58 | 59 | 60 | ### Node ### 61 | # Logs 62 | logs 63 | *.log 64 | npm-debug.log* 65 | 66 | # Runtime data 67 | pids 68 | *.pid 69 | *.seed 70 | *.pid.lock 71 | 72 | # Directory for instrumented libs generated by jscoverage/JSCover 73 | lib-cov 74 | 75 | # Coverage directory used by tools like istanbul 76 | coverage 77 | 78 | # nyc test coverage 79 | .nyc_output 80 | 81 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 82 | .grunt 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (http://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules 92 | jspm_packages 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | 104 | ### OSX ### 105 | *.DS_Store 106 | .AppleDouble 107 | .LSOverride 108 | 109 | # Icon must end with two \r 110 | Icon 111 | 112 | 113 | # Thumbnails 114 | ._* 115 | 116 | # Files that might appear in the root of a volume 117 | .DocumentRevisions-V100 118 | .fseventsd 119 | .Spotlight-V100 120 | .TemporaryItems 121 | .Trashes 122 | .VolumeIcon.icns 123 | .com.apple.timemachine.donotpresent 124 | 125 | # Directories potentially created on remote AFP share 126 | .AppleDB 127 | .AppleDesktop 128 | Network Trash Folder 129 | Temporary Items 130 | .apdisk 131 | 132 | 133 | ### Windows ### 134 | # Windows image file caches 135 | Thumbs.db 136 | ehthumbs.db 137 | 138 | # Folder config file 139 | Desktop.ini 140 | 141 | # Recycle Bin used on file shares 142 | $RECYCLE.BIN/ 143 | 144 | # Windows Installer files 145 | *.cab 146 | *.msi 147 | *.msm 148 | *.msp 149 | 150 | # Windows shortcuts 151 | *.lnk 152 | 153 | 154 | ### Less ### 155 | *.css 156 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "arrowParens": "always", 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Moesif, Inc 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Apache License 17 | Version 2.0, January 2004 18 | http://www.apache.org/licenses/ 19 | 20 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 21 | 22 | 1. Definitions. 23 | 24 | "License" shall mean the terms and conditions for use, reproduction, 25 | and distribution as defined by Sections 1 through 9 of this document. 26 | 27 | "Licensor" shall mean the copyright owner or entity authorized by 28 | the copyright owner that is granting the License. 29 | 30 | "Legal Entity" shall mean the union of the acting entity and all 31 | other entities that control, are controlled by, or are under common 32 | control with that entity. For the purposes of this definition, 33 | "control" means (i) the power, direct or indirect, to cause the 34 | direction or management of such entity, whether by contract or 35 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 36 | outstanding shares, or (iii) beneficial ownership of such entity. 37 | 38 | "You" (or "Your") shall mean an individual or Legal Entity 39 | exercising permissions granted by this License. 40 | 41 | "Source" form shall mean the preferred form for making modifications, 42 | including but not limited to software source code, documentation 43 | source, and configuration files. 44 | 45 | "Object" form shall mean any form resulting from mechanical 46 | transformation or translation of a Source form, including but 47 | not limited to compiled object code, generated documentation, 48 | and conversions to other media types. 49 | 50 | "Work" shall mean the work of authorship, whether in Source or 51 | Object form, made available under the License, as indicated by a 52 | copyright notice that is included in or attached to the work 53 | (an example is provided in the Appendix below). 54 | 55 | "Derivative Works" shall mean any work, whether in Source or Object 56 | form, that is based on (or derived from) the Work and for which the 57 | editorial revisions, annotations, elaborations, or other modifications 58 | represent, as a whole, an original work of authorship. For the purposes 59 | of this License, Derivative Works shall not include works that remain 60 | separable from, or merely link (or bind by name) to the interfaces of, 61 | the Work and Derivative Works thereof. 62 | 63 | "Contribution" shall mean any work of authorship, including 64 | the original version of the Work and any modifications or additions 65 | to that Work or Derivative Works thereof, that is intentionally 66 | submitted to Licensor for inclusion in the Work by the copyright owner 67 | or by an individual or Legal Entity authorized to submit on behalf of 68 | the copyright owner. For the purposes of this definition, "submitted" 69 | means any form of electronic, verbal, or written communication sent 70 | to the Licensor or its representatives, including but not limited to 71 | communication on electronic mailing lists, source code control systems, 72 | and issue tracking systems that are managed by, or on behalf of, the 73 | Licensor for the purpose of discussing and improving the Work, but 74 | excluding communication that is conspicuously marked or otherwise 75 | designated in writing by the copyright owner as "Not a Contribution." 76 | 77 | "Contributor" shall mean Licensor and any individual or Legal Entity 78 | on behalf of whom a Contribution has been received by Licensor and 79 | subsequently incorporated within the Work. 80 | 81 | 2. Grant of Copyright License. Subject to the terms and conditions of 82 | this License, each Contributor hereby grants to You a perpetual, 83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 84 | copyright license to reproduce, prepare Derivative Works of, 85 | publicly display, publicly perform, sublicense, and distribute the 86 | Work and such Derivative Works in Source or Object form. 87 | 88 | 3. Grant of Patent License. Subject to the terms and conditions of 89 | this License, each Contributor hereby grants to You a perpetual, 90 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 91 | (except as stated in this section) patent license to make, have made, 92 | use, offer to sell, sell, import, and otherwise transfer the Work, 93 | where such license applies only to those patent claims licensable 94 | by such Contributor that are necessarily infringed by their 95 | Contribution(s) alone or by combination of their Contribution(s) 96 | with the Work to which such Contribution(s) was submitted. If You 97 | institute patent litigation against any entity (including a 98 | cross-claim or counterclaim in a lawsuit) alleging that the Work 99 | or a Contribution incorporated within the Work constitutes direct 100 | or contributory patent infringement, then any patent licenses 101 | granted to You under this License for that Work shall terminate 102 | as of the date such litigation is filed. 103 | 104 | 4. Redistribution. You may reproduce and distribute copies of the 105 | Work or Derivative Works thereof in any medium, with or without 106 | modifications, and in Source or Object form, provided that You 107 | meet the following conditions: 108 | 109 | (a) You must give any other recipients of the Work or 110 | Derivative Works a copy of this License; and 111 | 112 | (b) You must cause any modified files to carry prominent notices 113 | stating that You changed the files; and 114 | 115 | (c) You must retain, in the Source form of any Derivative Works 116 | that You distribute, all copyright, patent, trademark, and 117 | attribution notices from the Source form of the Work, 118 | excluding those notices that do not pertain to any part of 119 | the Derivative Works; and 120 | 121 | (d) If the Work includes a "NOTICE" text file as part of its 122 | distribution, then any Derivative Works that You distribute must 123 | include a readable copy of the attribution notices contained 124 | within such NOTICE file, excluding those notices that do not 125 | pertain to any part of the Derivative Works, in at least one 126 | of the following places: within a NOTICE text file distributed 127 | as part of the Derivative Works; within the Source form or 128 | documentation, if provided along with the Derivative Works; or, 129 | within a display generated by the Derivative Works, if and 130 | wherever such third-party notices normally appear. The contents 131 | of the NOTICE file are for informational purposes only and 132 | do not modify the License. You may add Your own attribution 133 | notices within Derivative Works that You distribute, alongside 134 | or as an addendum to the NOTICE text from the Work, provided 135 | that such additional attribution notices cannot be construed 136 | as modifying the License. 137 | 138 | You may add Your own copyright statement to Your modifications and 139 | may provide additional or different license terms and conditions 140 | for use, reproduction, or distribution of Your modifications, or 141 | for any such Derivative Works as a whole, provided Your use, 142 | reproduction, and distribution of the Work otherwise complies with 143 | the conditions stated in this License. 144 | 145 | 5. Submission of Contributions. Unless You explicitly state otherwise, 146 | any Contribution intentionally submitted for inclusion in the Work 147 | by You to the Licensor shall be under the terms and conditions of 148 | this License, without any additional terms or conditions. 149 | Notwithstanding the above, nothing herein shall supersede or modify 150 | the terms of any separate license agreement you may have executed 151 | with Licensor regarding such Contributions. 152 | 153 | 6. Trademarks. This License does not grant permission to use the trade 154 | names, trademarks, service marks, or product names of the Licensor, 155 | except as required for reasonable and customary use in describing the 156 | origin of the Work and reproducing the content of the NOTICE file. 157 | 158 | 7. Disclaimer of Warranty. Unless required by applicable law or 159 | agreed to in writing, Licensor provides the Work (and each 160 | Contributor provides its Contributions) on an "AS IS" BASIS, 161 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 162 | implied, including, without limitation, any warranties or conditions 163 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 164 | PARTICULAR PURPOSE. You are solely responsible for determining the 165 | appropriateness of using or redistributing the Work and assume any 166 | risks associated with Your exercise of permissions under this License. 167 | 168 | 8. Limitation of Liability. In no event and under no legal theory, 169 | whether in tort (including negligence), contract, or otherwise, 170 | unless required by applicable law (such as deliberate and grossly 171 | negligent acts) or agreed to in writing, shall any Contributor be 172 | liable to You for damages, including any direct, indirect, special, 173 | incidental, or consequential damages of any character arising as a 174 | result of this License or out of the use or inability to use the 175 | Work (including but not limited to damages for loss of goodwill, 176 | work stoppage, computer failure or malfunction, or any and all 177 | other commercial damages or losses), even if such Contributor 178 | has been advised of the possibility of such damages. 179 | 180 | 9. Accepting Warranty or Additional Liability. While redistributing 181 | the Work or Derivative Works thereof, You may choose to offer, 182 | and charge a fee for, acceptance of support, warranty, indemnity, 183 | or other liability obligations and/or rights consistent with this 184 | License. However, in accepting such obligations, You may act only 185 | on Your own behalf and on Your sole responsibility, not on behalf 186 | of any other Contributor, and only if You agree to indemnify, 187 | defend, and hold each Contributor harmless for any liability 188 | incurred by, or claims asserted against, such Contributor by reason 189 | of your accepting any such warranty or additional liability. 190 | 191 | END OF TERMS AND CONDITIONS 192 | 193 | APPENDIX: How to apply the Apache License to your work. 194 | 195 | To apply the Apache License to your work, attach the following 196 | boilerplate notice, with the fields enclosed by brackets "[]" 197 | replaced with your own identifying information. (Don't include 198 | the brackets!) The text should be enclosed in the appropriate 199 | comment syntax for the file format. We also recommend that a 200 | file or class name and description of purpose be included on the 201 | same "printed page" as the copyright notice for easier 202 | identification within third-party archives. 203 | 204 | Copyright [yyyy] [name of copyright owner] 205 | 206 | Licensed under the Apache License, Version 2.0 (the "License"); 207 | you may not use this file except in compliance with the License. 208 | You may obtain a copy of the License at 209 | 210 | http://www.apache.org/licenses/LICENSE-2.0 211 | 212 | Unless required by applicable law or agreed to in writing, software 213 | distributed under the License is distributed on an "AS IS" BASIS, 214 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 215 | See the License for the specific language governing permissions and 216 | limitations under the License. 217 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xingheng on 10/16/16. 3 | * This file is a simple test app. 4 | */ 5 | 6 | var express = require('express'); 7 | var app = express(); 8 | 9 | var moesif = require('./lib'); 10 | 11 | const APPLICATION_ID = 'YOUR_MOESIF_APPLICATION_ID'; 12 | 13 | var moesifMiddleWare = moesif({ applicationId: APPLICATION_ID }); 14 | 15 | app.use(moesifMiddleWare); 16 | app.use(express.json()); 17 | 18 | app.get('/', function (req, res) { 19 | res.json({ a: 'abc' }); 20 | }); 21 | 22 | app.get('/abc', function (req, res) { 23 | res.json({ abc: 'abcefg' }); 24 | }); 25 | 26 | const server = app.listen(0, () => { 27 | console.log(`Example app listening on port ${server.address().port}`); 28 | }); 29 | -------------------------------------------------------------------------------- /dist/batcher.d.ts: -------------------------------------------------------------------------------- 1 | export = createBatcher; 2 | declare function createBatcher(handleBatch: any, maxSize: any, maxTime: any): { 3 | dataArray: any[]; 4 | /** @type {any} */ 5 | add: any; 6 | /** @type {any} */ 7 | flush: any; 8 | }; 9 | //# sourceMappingURL=batcher.d.ts.map -------------------------------------------------------------------------------- /dist/batcher.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../lib/batcher.js"],"names":[],"mappings":";AAAA;;IAMI,kBAAkB;SAAP,GAAG;IAYd,kBAAkB;WAAP,GAAG;EAcjB"} -------------------------------------------------------------------------------- /dist/dataUtils.d.ts: -------------------------------------------------------------------------------- 1 | declare function _getUrlFromRequestOptions(options: any, request: any): string; 2 | declare function _getEventModelFromRequestAndResponse(requestOptions: any, request: any, requestTime: any, requestBody: any, response: any, responseTime: any, responseBody: any): { 3 | request: { 4 | verb: any; 5 | uri: string; 6 | headers: any; 7 | time: any; 8 | transferEncoding: string; 9 | body: any; 10 | }; 11 | response: { 12 | time: any; 13 | status: any; 14 | headers: any; 15 | transferEncoding: string; 16 | body: any; 17 | }; 18 | }; 19 | declare function _safeJsonParse(body: any): { 20 | body: any; 21 | transferEncoding: string; 22 | }; 23 | declare function _startWithJson(body: any): boolean; 24 | declare function _bodyToBase64(body: any): any; 25 | declare function _hashSensitive(jsonBody: any, debug: any): any; 26 | export function logMessage(debug: any, functionName: any, message: any, details: any): void; 27 | export function timeTookInSeconds(startTime: any, endTime: any): string; 28 | export function isJsonHeader(msg: any): boolean; 29 | export function appendChunk(buf: any, chunk: any): any; 30 | export function computeBodySize(body: any): number; 31 | export function totalChunkLength(chunk1: any, chunk2: any): any; 32 | export function ensureToString(id: any): any; 33 | export function getReqHeaders(req: any): any; 34 | export function generateUUIDv4(): string; 35 | export { _getUrlFromRequestOptions as getUrlFromRequestOptions, _getEventModelFromRequestAndResponse as getEventModelFromRequestAndResponse, _safeJsonParse as safeJsonParse, _startWithJson as startWithJson, _bodyToBase64 as bodyToBase64, _hashSensitive as hashSensitive }; 36 | //# sourceMappingURL=dataUtils.d.ts.map -------------------------------------------------------------------------------- /dist/dataUtils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"dataUtils.d.ts","sourceRoot":"","sources":["../lib/dataUtils.js"],"names":[],"mappings":"AA8FA,+EA2CC;AA0GD;;;;;;;;;;;;;;;;EAqDC;AAxHD;;;EAsCC;AAED,oDAeC;AA5FD,+CAaC;AA1HD,gEA8DC;AArFD,4FAiBC;AAED,wEAEC;AA8QD,gDAcC;AA2BD,uDA8BC;AA9CD,mDAcC;AAkCD,gEAIC;AAED,6CAcC;AAED,6CAOC;AAED,yCAUC"} -------------------------------------------------------------------------------- /dist/ensureValidUtils.d.ts: -------------------------------------------------------------------------------- 1 | export function ensureValidOptions(options: any): void; 2 | export function ensureValidLogData(logData: any): void; 3 | export function ensureValidUserModel(userModel: any): void; 4 | export function ensureValidUsersBatchModel(usersBatchModel: any): void; 5 | export function ensureValidCompanyModel(companyModel: any): void; 6 | export function ensureValidCompaniesBatchModel(companiesBatchModel: any): void; 7 | //# sourceMappingURL=ensureValidUtils.d.ts.map -------------------------------------------------------------------------------- /dist/ensureValidUtils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ensureValidUtils.d.ts","sourceRoot":"","sources":["../lib/ensureValidUtils.js"],"names":[],"mappings":"AAIA,uDAmDC;AAED,uDAoCC;AAED,2DAIC;AAED,uEAMC;AAED,iEAIC;AAED,+EAMC"} -------------------------------------------------------------------------------- /dist/formatEventDataAndSave.d.ts: -------------------------------------------------------------------------------- 1 | export = formatEventDataAndSave; 2 | declare function formatEventDataAndSave(responseBodyBuffer: any, req: any, res: any, options: any, saveEvent: any): void; 3 | //# sourceMappingURL=formatEventDataAndSave.d.ts.map -------------------------------------------------------------------------------- /dist/formatEventDataAndSave.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"formatEventDataAndSave.d.ts","sourceRoot":"","sources":["../lib/formatEventDataAndSave.js"],"names":[],"mappings":";AAoFA,yHA8IC"} -------------------------------------------------------------------------------- /dist/getRawBody.d.ts: -------------------------------------------------------------------------------- 1 | export = getRawBody; 2 | /** 3 | * Get the raw body of a stream (typically HTTP). 4 | * 5 | * @param {object} stream 6 | * @param {object|string|function} [options] 7 | * @param {function} [callback] 8 | * @public 9 | */ 10 | declare function getRawBody(stream: object, options?: object | string | Function, callback?: Function): void | Promise; 11 | //# sourceMappingURL=getRawBody.d.ts.map -------------------------------------------------------------------------------- /dist/getRawBody.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"getRawBody.d.ts","sourceRoot":"","sources":["../lib/getRawBody.js"],"names":[],"mappings":";AA0DA;;;;;;;GAOG;AAEH,oCANW,MAAM,YACN,MAAM,GAAC,MAAM,WAAS,4CAuDhC"} -------------------------------------------------------------------------------- /dist/governanceRulesManager.d.ts: -------------------------------------------------------------------------------- 1 | declare const _exports: GovernanceRulesManager; 2 | export = _exports; 3 | /** 4 | * 5 | * @type Class 6 | * 7 | * */ 8 | declare function GovernanceRulesManager(): void; 9 | declare class GovernanceRulesManager { 10 | _lastUpdate: number; 11 | setLogger(logger: any): void; 12 | _logger: any; 13 | log(message: any, details: any): void; 14 | hasRules(): boolean; 15 | shouldFetch(): boolean; 16 | tryGetRules(): Promise; 17 | _cacheRules(rules: any): void; 18 | regexRules: any; 19 | userRulesHashByRuleId: {}; 20 | companyRulesHashByRuleId: {}; 21 | unidentifiedUserRules: any; 22 | unidentifiedCompanyRules: any; 23 | _getApplicableRegexRules(requestFields: any, requestBody: any, requestHeaders: any): any; 24 | _getApplicableUnidentifiedUserRules(requestFields: any, requestBody: any, requestHeaders: any): any; 25 | _getApplicableUnidentifiedCompanyRules(requestFields: any, requestBody: any, requestHeaders: any): any; 26 | _getApplicableUserRules(configUserRulesValues: any, requestFields: any, requestBody: any, requestHeaders: any): any[]; 27 | _getApplicableCompanyRules(configCompanyRulesValues: any, requestFields: any, requestBody: any, requestHeaders: any): any[]; 28 | applyRuleList(applicableRules: any, responseHolder: any, configRuleValues: any): any; 29 | governInternal(config: any, userId: any, companyId: any, requestFields: any, requestBody: any, requestHeaders: any, originalUrl: any): { 30 | status: any; 31 | headers: {}; 32 | body: any; 33 | blocked_by: any; 34 | }; 35 | governRequestNextJs(config: any, userId: any, companyId: any, requestBody: any, requestHeaders: any, originalUrl: any, originalIp: any, originalMethod: any): { 36 | status: any; 37 | headers: {}; 38 | body: any; 39 | blocked_by: any; 40 | }; 41 | governRequest(config: any, userId: any, companyId: any, request: any): { 42 | status: any; 43 | headers: {}; 44 | body: any; 45 | blocked_by: any; 46 | }; 47 | } 48 | //# sourceMappingURL=governanceRulesManager.d.ts.map -------------------------------------------------------------------------------- /dist/governanceRulesManager.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"governanceRulesManager.d.ts","sourceRoot":"","sources":["../lib/governanceRulesManager.js"],"names":[],"mappings":";;AA0MA;;;;KAIK;AACL,gDAEC;;IADC,oBAAoB;IAGtB,6BAEC;IADC,aAAqB;IAGvB,sCAIC;IAED,oBAEC;IAED,uBAIC;IAED,4BAyCC;IAED,8BA8BC;IA5BC,gBAEE;IACF,0BAA+B;IAC/B,6BAAkC;IAiBlC,2BAEE;IAEF,8BAEE;IAGJ,yFAYC;IAED,oGAYC;IAED,uGAYC;IAED,sHAkEC;IAED,4HAiEC;IAED,qFAiCC;IAED;;;;;MAwEC;IAED;;;;;MA0BC;IAED;;;;;MAqBC"} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export = makeMoesifMiddleware; 2 | /** 3 | * @typedef {Object} MoesifOptions 4 | * @property {string} applicationId 5 | * @property {(req: object, res: object) => string | undefined | null} [identifyUser] 6 | * @property {(req: object, res: object) => string | undefined | null} [identifyCompany] 7 | * @property {(req: object, res: object) => string | undefined | null} [getSessionToken] 8 | * @property {(req: object, res: object) => string | undefined | null} [getApiVersion] 9 | * @property {(req: object, res: object) => object | undefined | null} [getMetadata] 10 | * @property {(req: object, res: object) => boolean | undefined | null | any} [skip] 11 | * @property {(eventModel: object) => object} [maskContent] 12 | * @property {boolean} [logBody] - default true 13 | * @property {boolean} [debug] 14 | * @property {boolean} [noAutoHideSensitive] 15 | * @property {(error: object) => any} [callback] 16 | * @property {boolean} [disableBatching] 17 | * @property {number} [batchSize] - default 200 18 | * @property {number} [batchMaxTime] - default 2000 19 | * @property {string} [baseUri] - switch to another collector endpoint when using proxy 20 | * @property {number} [retry] - must be between 0 to 3 if provided. 21 | * @property {number} [requestMaxBodySize] - default 100000 22 | * @property {number} [responseMaxBodySize] - default 100000 23 | * @property {number} [maxOutgoingTimeout] - default 30000 24 | * @property {boolean} [isNextJsAppRouter] - default false 25 | */ 26 | /** 27 | * @param {MoesifOptions} options 28 | */ 29 | declare function makeMoesifMiddleware(options: MoesifOptions): { 30 | (arg1: object, arg2?: any, arg3?: any): any; 31 | /** 32 | * @param {object} userModel - https://www.moesif.com/docs/api?javascript--nodejs#update-a-user 33 | * @param {function} [cb] 34 | */ 35 | updateUser(userModel: object, cb?: Function): Promise; 36 | /** 37 | * @param {object[]} usersBatchModel 38 | * @param {function} [cb] 39 | */ 40 | updateUsersBatch(usersBatchModel: object[], cb?: Function): Promise; 41 | /** 42 | * @param {object} companyModel - https://www.moesif.com/docs/api?javascript--nodejs#companies 43 | * @param {function} [cb] 44 | */ 45 | updateCompany(companyModel: object, cb?: Function): Promise; 46 | /** 47 | * @param {object[]} companiesBatchModel 48 | * @param {function} [cb] 49 | */ 50 | updateCompaniesBatch(companiesBatchModel: object[], cb?: Function): Promise; 51 | updateSubscription(subscriptionModel: any, cb: any): Promise; 52 | updateSubscriptionsBatch(subscriptionBatchModel: any, cb: any): Promise; 53 | startCaptureOutgoing(): void; 54 | }; 55 | declare namespace makeMoesifMiddleware { 56 | export { MoesifOptions }; 57 | } 58 | type MoesifOptions = { 59 | applicationId: string; 60 | identifyUser?: (req: object, res: object) => string | undefined | null; 61 | identifyCompany?: (req: object, res: object) => string | undefined | null; 62 | getSessionToken?: (req: object, res: object) => string | undefined | null; 63 | getApiVersion?: (req: object, res: object) => string | undefined | null; 64 | getMetadata?: (req: object, res: object) => object | undefined | null; 65 | skip?: (req: object, res: object) => boolean | undefined | null | any; 66 | maskContent?: (eventModel: object) => object; 67 | /** 68 | * - default true 69 | */ 70 | logBody?: boolean; 71 | debug?: boolean; 72 | noAutoHideSensitive?: boolean; 73 | callback?: (error: object) => any; 74 | disableBatching?: boolean; 75 | /** 76 | * - default 200 77 | */ 78 | batchSize?: number; 79 | /** 80 | * - default 2000 81 | */ 82 | batchMaxTime?: number; 83 | /** 84 | * - switch to another collector endpoint when using proxy 85 | */ 86 | baseUri?: string; 87 | /** 88 | * - must be between 0 to 3 if provided. 89 | */ 90 | retry?: number; 91 | /** 92 | * - default 100000 93 | */ 94 | requestMaxBodySize?: number; 95 | /** 96 | * - default 100000 97 | */ 98 | responseMaxBodySize?: number; 99 | /** 100 | * - default 30000 101 | */ 102 | maxOutgoingTimeout?: number; 103 | /** 104 | * - default false 105 | */ 106 | isNextJsAppRouter?: boolean; 107 | }; 108 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.js"],"names":[],"mappings":";AA6DA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;GAEG;AACH,+CAFY,aAAa;WAwNZ,MAAM,SACN,GAAG,SACH,GAAG;IAoWd;;;OAGG;0BAFQ,MAAM;IAuBjB;;;OAGG;sCAFQ,MAAM,EAAE;IA0BnB;;;OAGG;gCAFQ,MAAM;IAuBjB;;;OAGG;8CAFQ,MAAM,EAAE;;;;EAyFpB;;;;;mBA3vBa,MAAM;yBACA,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;4BACjD,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;4BACjD,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;0BACjD,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;wBACjD,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;iBACjD,MAAM,OAAO,MAAM,KAAK,OAAO,GAAG,SAAS,GAAG,IAAI,GAAG,GAAG;+BACjD,MAAM,KAAK,MAAM;;;;cAC9B,OAAO;YACP,OAAO;0BACP,OAAO;uBACC,MAAM,KAAK,GAAG;sBACtB,OAAO;;;;gBACP,MAAM;;;;mBACN,MAAM;;;;cACN,MAAM;;;;YACN,MAAM;;;;yBACN,MAAM;;;;0BACN,MAAM;;;;yBACN,MAAM;;;;wBACN,OAAO"} -------------------------------------------------------------------------------- /dist/moesifConfigManager.d.ts: -------------------------------------------------------------------------------- 1 | declare const _exports: MoesifConfigManager; 2 | export = _exports; 3 | declare function MoesifConfigManager(): void; 4 | declare class MoesifConfigManager { 5 | _lastConfigUpdate: number; 6 | hasConfig(): boolean; 7 | shouldFetchConfig(): boolean; 8 | tryGetConfig(): void; 9 | _loadingConfig: boolean; 10 | _getSampleRate(userId: any, companyId: any): any; 11 | shouldSend(userId: any, companyId: any): boolean; 12 | tryUpdateHash(response: any): void; 13 | _lastSeenHash: any; 14 | } 15 | //# sourceMappingURL=moesifConfigManager.d.ts.map -------------------------------------------------------------------------------- /dist/moesifConfigManager.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"moesifConfigManager.d.ts","sourceRoot":"","sources":["../lib/moesifConfigManager.js"],"names":[],"mappings":";;AAgBA,6CAEC;;IADC,0BAA0B;IAG5B,qBAEC;IAED,6BAQC;IAED,qBAyBC;IAtBG,wBAA0B;IAwB9B,iDAoBC;IAED,iDAGC;IAED,mCAIC;IAFG,mBAAkD"} -------------------------------------------------------------------------------- /dist/nextjsUtils.d.ts: -------------------------------------------------------------------------------- 1 | export function extractNextJsEventDataAndSave({ request, requestTime, response, responseTime, options, saveEvent, blockedBy, }: { 2 | request: any; 3 | requestTime: any; 4 | response: any; 5 | responseTime: any; 6 | options: any; 7 | saveEvent: any; 8 | blockedBy: any; 9 | }): Promise; 10 | //# sourceMappingURL=nextjsUtils.d.ts.map -------------------------------------------------------------------------------- /dist/nextjsUtils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"nextjsUtils.d.ts","sourceRoot":"","sources":["../lib/nextjsUtils.js"],"names":[],"mappings":"AA+EA;;;;;;;;iBAqEC"} -------------------------------------------------------------------------------- /dist/outgoing.d.ts: -------------------------------------------------------------------------------- 1 | export = _patch; 2 | declare function _patch(recorder: any, logger: any, moesifOptions: any): () => void; 3 | //# sourceMappingURL=outgoing.d.ts.map -------------------------------------------------------------------------------- /dist/outgoing.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"outgoing.d.ts","sourceRoot":"","sources":["../lib/outgoing.js"],"names":[],"mappings":";AAuQA,oFAmDC"} -------------------------------------------------------------------------------- /dist/outgoingRecorder.d.ts: -------------------------------------------------------------------------------- 1 | export = _createOutgoingRecorder; 2 | declare function _createOutgoingRecorder(saveEvent: any, moesifOptions: any, logger: any): (capturedData: any) => void; 3 | //# sourceMappingURL=outgoingRecorder.d.ts.map -------------------------------------------------------------------------------- /dist/outgoingRecorder.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"outgoingRecorder.d.ts","sourceRoot":"","sources":["../lib/outgoingRecorder.js"],"names":[],"mappings":";AAmCA,uHAsFC"} -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | 5 | export default [ 6 | {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, 7 | {languageOptions: { globals: globals.node }}, 8 | pluginJs.configs.recommended, 9 | ]; 10 | -------------------------------------------------------------------------------- /images/app_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moesif/moesif-nodejs/64f6a823d1784a4431a93f62dac8eff179776d9c/images/app_id.png -------------------------------------------------------------------------------- /lib/batcher.js: -------------------------------------------------------------------------------- 1 | function createBatcher(handleBatch, maxSize, maxTime) { 2 | return { 3 | dataArray: [], 4 | // using closure, so no need to keep as part of the object. 5 | // maxSize: maxSize, 6 | // maxTime: maxTime, 7 | /** @type {any} */ 8 | add: function (data) { 9 | this.dataArray.push(data); 10 | if (this.dataArray.length >= maxSize) { 11 | this.flush(); 12 | } else if (maxTime && this.dataArray.length === 1) { 13 | var self = this; 14 | this._timeout = setTimeout(function () { 15 | self.flush(); 16 | }, maxTime); 17 | } 18 | }, 19 | /** @type {any} */ 20 | flush: function () { 21 | // note, in case the handleBatch is a 22 | // delayed function, then it swaps before 23 | // sending the current data. 24 | clearTimeout(this._timeout); 25 | this._lastFlush = Date.now(); 26 | var currentDataArray = this.dataArray; 27 | this.dataArray = []; 28 | setTimeout(function () { 29 | handleBatch(currentDataArray); 30 | }, 10); 31 | }, 32 | }; 33 | } 34 | 35 | module.exports = createBatcher; 36 | -------------------------------------------------------------------------------- /lib/dataUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | var hash = require('crypto-js/md5'); 5 | var isCreditCard = require('card-validator'); 6 | var assign = require('lodash/assign'); 7 | 8 | var logMessage = function (debug, functionName, message, details) { 9 | if (debug) { 10 | var finalMessage = message; 11 | try { 12 | if (details && debug !== 'instrumentation') { 13 | if (Buffer.isBuffer(details) || typeof details === 'string') { 14 | finalMessage = message + '\n' + details; 15 | } else if (details.stack && details.message) { 16 | finalMessage = message + '\n' + details.stack; 17 | } else if (typeof details === 'object') { 18 | finalMessage = message + '\n' + JSON.stringify(details); 19 | } 20 | } 21 | } catch (err) { 22 | } 23 | console.log('MOESIF: [' + functionName + '] ' + finalMessage); 24 | } 25 | }; 26 | 27 | var timeTookInSeconds = function (startTime, endTime) { 28 | return (endTime - startTime) / 1000.0 + ' seconds'; 29 | }; 30 | 31 | function _hashSensitive(jsonBody, debug) { 32 | if (jsonBody === null) return jsonBody; 33 | 34 | if (Array.isArray(jsonBody)) { 35 | return jsonBody.map(function (item) { 36 | var itemType = typeof item; 37 | 38 | if (itemType === 'number' || itemType === 'string') { 39 | var creditCardCheck = isCreditCard.number('' + item); 40 | if (creditCardCheck.isValid) { 41 | logMessage( 42 | debug, 43 | 'hashSensitive', 44 | 'looks like a credit card, performing hash.' 45 | ); 46 | return hash(item).toString(); 47 | } 48 | } 49 | 50 | return _hashSensitive(item, debug); 51 | }); 52 | } 53 | 54 | if (typeof jsonBody === 'object') { 55 | var returnObject = {}; 56 | 57 | Object.keys(jsonBody).forEach(function (key) { 58 | var innerVal = jsonBody[key]; 59 | var innerValType = typeof innerVal; 60 | 61 | if ( 62 | key.toLowerCase().indexOf('password') !== -1 && 63 | typeof innerVal === 'string' 64 | ) { 65 | logMessage( 66 | debug, 67 | 'hashSensitive', 68 | 'key is password, so hashing the value.' 69 | ); 70 | returnObject[key] = hash(jsonBody[key]).toString(); 71 | } else if (innerValType === 'number' || innerValType === 'string') { 72 | var creditCardCheck = isCreditCard.number('' + innerVal); 73 | if (creditCardCheck.isValid) { 74 | logMessage( 75 | debug, 76 | 'hashSensitive', 77 | 'a field looks like credit card, performing hash.' 78 | ); 79 | returnObject[key] = hash(jsonBody[key]).toString(); 80 | } else { 81 | returnObject[key] = _hashSensitive(innerVal, debug); 82 | } 83 | } else { 84 | // recursive test for every value. 85 | returnObject[key] = _hashSensitive(innerVal, debug); 86 | } 87 | }); 88 | 89 | return returnObject; 90 | } 91 | 92 | return jsonBody; 93 | } 94 | 95 | function _getUrlFromRequestOptions(options, request) { 96 | if (typeof options === 'string') { 97 | options = url.parse(options); 98 | } else { 99 | // Avoid modifying the original options object. 100 | let originalOptions = options; 101 | options = {}; 102 | if (originalOptions) { 103 | Object.keys(originalOptions).forEach((key) => { 104 | options[key] = originalOptions[key]; 105 | }); 106 | } 107 | } 108 | 109 | // Oddly, url.format ignores path and only uses pathname and search, 110 | // so create them from the path, if path was specified 111 | if (options.path) { 112 | var parsedQuery = url.parse(options.path); 113 | options.pathname = parsedQuery.pathname; 114 | options.search = parsedQuery.search; 115 | } 116 | 117 | // Simiarly, url.format ignores hostname and port if host is specified, 118 | // even if host doesn't have the port, but http.request does not work 119 | // this way. It will use the port if one is not specified in host, 120 | // effectively treating host as hostname, but will use the port specified 121 | // in host if it exists. 122 | if (options.host && options.port) { 123 | // Force a protocol so it will parse the host as the host, not path. 124 | // It is discarded and not used, so it doesn't matter if it doesn't match 125 | var parsedHost = url.parse('http://' + options.host); 126 | if (!parsedHost.port && options.port) { 127 | options.hostname = options.host; 128 | delete options.host; 129 | } 130 | } 131 | 132 | // Mix in default values used by http.request and others 133 | options.protocol = 134 | options.protocol || (request.agent && request.agent.protocol) || undefined; 135 | options.hostname = options.hostname || 'localhost'; 136 | 137 | return url.format(options); 138 | } 139 | 140 | function _bodyToBase64(body) { 141 | if (!body) { 142 | return body; 143 | } 144 | if (Buffer.isBuffer(body)) { 145 | return body.toString('base64'); 146 | } else if (typeof body === 'string') { 147 | return Buffer.from(body).toString('base64'); 148 | } else if (typeof body.toString === 'function') { 149 | return Buffer.from(body.toString()).toString('base64'); 150 | } else { 151 | return ''; 152 | } 153 | } 154 | 155 | function isPlainObject(value) { 156 | if (Object.prototype.toString.call(value) !== '[object Object]') { 157 | return false; 158 | } 159 | const prototype = Object.getPrototypeOf(value); 160 | return prototype === null || prototype === Object.prototype; 161 | } 162 | 163 | function isPlainObjectOrPrimitive(value) { 164 | if (isPlainObject(value)) { 165 | return true; 166 | } 167 | const type = typeof value; 168 | return ( 169 | type === 'number' || 170 | type === 'boolean' || 171 | type === 'string' || 172 | value === null || 173 | value === undefined 174 | ); 175 | } 176 | 177 | function _safeJsonParse(body) { 178 | try { 179 | var type = typeof body; 180 | if (!Buffer.isBuffer(body) && type === 'object') { 181 | if (isPlainObject(body)) { 182 | return { 183 | body: body, 184 | transferEncoding: undefined, 185 | }; 186 | } 187 | if ( 188 | Array.isArray(body) && 189 | body.every && 190 | body.every(isPlainObjectOrPrimitive) 191 | ) { 192 | return { 193 | body: body, 194 | transferEncoding: undefined, 195 | }; 196 | } 197 | 198 | // in case of non POJO 199 | return { 200 | body: JSON.parse(JSON.stringify(body)), 201 | transferEncoding: undefined, 202 | }; 203 | } 204 | 205 | return { 206 | body: JSON.parse(body.toString()), 207 | transferEncoding: undefined, 208 | }; 209 | } catch (e) { 210 | return { 211 | body: _bodyToBase64(body), 212 | transferEncoding: 'base64', 213 | }; 214 | } 215 | } 216 | 217 | function _startWithJson(body) { 218 | var str; 219 | if (body && Buffer.isBuffer(body)) { 220 | str = body.slice(0, 1).toString('ascii'); 221 | } else { 222 | str = body; 223 | } 224 | 225 | if (str && typeof str === 'string') { 226 | var newStr = str.trim(); 227 | if (newStr.startsWith('{') || newStr.startsWith('[')) { 228 | return true; 229 | } 230 | } 231 | return true; 232 | } 233 | 234 | function getRequestHeaders(requestOptions, request) { 235 | if (request && request.getHeaders) { 236 | return request.getHeaders(); 237 | } 238 | if (requestOptions.headers) { 239 | return requestOptions.headers; 240 | } 241 | return {}; 242 | } 243 | 244 | function _getEventModelFromRequestAndResponse( 245 | requestOptions, 246 | request, 247 | requestTime, 248 | requestBody, 249 | response, 250 | responseTime, 251 | responseBody 252 | ) { 253 | var logData = {}; 254 | logData.request = {}; 255 | 256 | logData.request.verb = 257 | typeof requestOptions === 'string' ? 'GET' : requestOptions.method || 'GET'; 258 | logData.request.uri = _getUrlFromRequestOptions(requestOptions, request); 259 | 260 | logData.request.headers = getRequestHeaders(requestOptions, request); 261 | logData.request.time = requestTime; 262 | 263 | if (requestBody) { 264 | var isReqBodyMaybeJson = _startWithJson(requestBody); 265 | 266 | if (isReqBodyMaybeJson) { 267 | var parsedReqBody = _safeJsonParse(requestBody); 268 | 269 | logData.request.transferEncoding = parsedReqBody.transferEncoding; 270 | logData.request.body = parsedReqBody.body; 271 | } else { 272 | logData.request.transferEncoding = 'base64'; 273 | logData.request.body = _bodyToBase64(requestBody); 274 | } 275 | } 276 | 277 | logData.response = {}; 278 | logData.response.time = responseTime; 279 | logData.response.status = (response && (response.statusCode || response.status)) || 599; 280 | logData.response.headers = assign({}, (response && response.headers) || {}); 281 | 282 | if (responseBody) { 283 | var isResBodyMaybeJson = _startWithJson(responseBody); 284 | 285 | if (isResBodyMaybeJson) { 286 | var parsedResBody = _safeJsonParse(responseBody); 287 | 288 | logData.response.transferEncoding = parsedResBody.transferEncoding; 289 | logData.response.body = parsedResBody.body; 290 | } else { 291 | logData.response.transferEncoding = 'base64'; 292 | logData.response.body = _bodyToBase64(responseBody); 293 | } 294 | } 295 | 296 | return logData; 297 | } 298 | 299 | function isJsonHeader(msg) { 300 | if (msg) { 301 | var headers = msg.headers || msg._moHeaders; 302 | if (headers['content-encoding']) { 303 | return false; 304 | } 305 | if ( 306 | headers['content-type'] && 307 | headers['content-type'].indexOf('json') >= 0 308 | ) { 309 | return true; 310 | } 311 | } 312 | return false; 313 | } 314 | 315 | function approximateObjectSize(obj) { 316 | try { 317 | const str = JSON.stringify(obj); 318 | return str.length; 319 | } catch (err) { 320 | return 0; 321 | } 322 | } 323 | 324 | function computeBodySize(body) { 325 | if (body === null || body === undefined) { 326 | return 0; 327 | } 328 | if (typeof body === 'string') { 329 | return body.length; 330 | } 331 | if (Buffer.isBuffer(body)) { 332 | return body.length; 333 | } 334 | if (typeof body === 'object') { 335 | return approximateObjectSize(body); 336 | } 337 | return 0; 338 | } 339 | 340 | function appendChunk(buf, chunk) { 341 | if (chunk) { 342 | if (Buffer.isBuffer(chunk)) { 343 | try { 344 | return buf ? Buffer.concat([buf, chunk]) : Buffer.from(chunk); 345 | } catch (err) { 346 | return buf; 347 | } 348 | } else if (typeof chunk === 'string') { 349 | try { 350 | return buf 351 | ? Buffer.concat([buf, Buffer.from(chunk)]) 352 | : Buffer.from(chunk); 353 | } catch (err) { 354 | return buf; 355 | } 356 | } else if (typeof chunk === 'object' || Array.isArray(chunk)) { 357 | try { 358 | return buf 359 | ? Buffer.concat([buf, Buffer.from(JSON.stringify(chunk))]) 360 | : Buffer.from(JSON.stringify(chunk)); 361 | } catch (err) { 362 | return buf; 363 | } 364 | } else { 365 | console.error('body chunk is not a Buffer or String.'); 366 | return buf; 367 | } 368 | } 369 | return buf; 370 | } 371 | 372 | function totalChunkLength(chunk1, chunk2) { 373 | var length1 = chunk1 ? chunk1.length || 0 : 0; 374 | var length2 = chunk2 ? chunk2.length || 0 : 0; 375 | return length1 + length2; 376 | } 377 | 378 | function ensureToString(id) { 379 | if (typeof id === 'number') { 380 | return String(id); 381 | } 382 | if (typeof id === 'string') { 383 | return id; 384 | } 385 | if (id === null || id === undefined) { 386 | return id; 387 | } 388 | if (typeof id === 'object') { 389 | return String(id); 390 | } 391 | return id; 392 | } 393 | 394 | function getReqHeaders(req) { 395 | if (req.headers) { 396 | return req.headers; 397 | } else if (req.getHeaders) { 398 | return req.getHeaders() || {}; 399 | } 400 | return {}; 401 | } 402 | 403 | function generateUUIDv4() { 404 | let timeNow = new Date().getTime(); // Current time in milliseconds 405 | let timeRandom = timeNow + Math.random(); // Combine time and random number 406 | 407 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 408 | let r = (timeRandom + Math.random() * 16) % 16 | 0; // Mix in timeRandom for extra entropy 409 | timeRandom = Math.floor(timeRandom / 16); 410 | let v = c === 'x' ? r : (r & 0x3 | 0x8); // Handle the fixed bits for UUIDv4 411 | return v.toString(16); 412 | }); 413 | } 414 | 415 | module.exports = { 416 | getUrlFromRequestOptions: _getUrlFromRequestOptions, 417 | getEventModelFromRequestAndResponse: _getEventModelFromRequestAndResponse, 418 | safeJsonParse: _safeJsonParse, 419 | startWithJson: _startWithJson, 420 | bodyToBase64: _bodyToBase64, 421 | hashSensitive: _hashSensitive, 422 | logMessage: logMessage, 423 | timeTookInSeconds: timeTookInSeconds, 424 | isJsonHeader: isJsonHeader, 425 | appendChunk: appendChunk, 426 | computeBodySize: computeBodySize, 427 | totalChunkLength: totalChunkLength, 428 | ensureToString: ensureToString, 429 | getReqHeaders: getReqHeaders, 430 | generateUUIDv4: generateUUIDv4, 431 | }; 432 | -------------------------------------------------------------------------------- /lib/ensureValidUtils.js: -------------------------------------------------------------------------------- 1 | 2 | var isFunction = require('lodash/isFunction'); 3 | var isNumber = require('lodash/isNumber'); 4 | 5 | function ensureValidOptions(options) { 6 | if (!options) throw new Error('options are required by moesif-nodejs middleware'); 7 | if (!options.applicationId || typeof options.applicationId !== 'string') { 8 | throw new Error( 9 | 'A moesif application id is required. Please obtain it through your settings at www.moesif.com' 10 | ); 11 | } 12 | if (options.applicationId.length < 50) { 13 | throw new Error( 14 | 'A moesif application id is required. The format of the moesif application id provided does not look correct. Please obtain it through your settings at www.moesif.com' 15 | ); 16 | } 17 | if (options.identifyUser && !isFunction(options.identifyUser)) { 18 | throw new Error('identifyUser should be a function'); 19 | } 20 | if (options.identifyCompany && !isFunction(options.identifyCompany)) { 21 | throw new Error('identifyCompany should be a function'); 22 | } 23 | if (options.getMetadata && !isFunction(options.getMetadata)) { 24 | throw new Error('getMetadata should be a function'); 25 | } 26 | if (options.getSessionToken && !isFunction(options.getSessionToken)) { 27 | throw new Error('getSessionToken should be a function'); 28 | } 29 | if (options.getTags && !isFunction(options.getTags)) { 30 | throw new Error('getTags should be a function'); 31 | } 32 | if (options.getApiVersion && !isFunction(options.getApiVersion)) { 33 | throw new Error('getApiVersion should be a function'); 34 | } 35 | if (options.maskContent && !isFunction(options.maskContent)) { 36 | throw new Error('maskContent should be a function'); 37 | } 38 | if (options.skip && !isFunction(options.skip)) { 39 | throw new Error('skip should be a function'); 40 | } 41 | if (options.retry && (!isNumber(options.retry) || options.retry > 3 || options.retry < 0)) { 42 | throw new Error('If retry is set, it must be a number between 0 to 3.'); 43 | } 44 | if (options.batchSize && (!isNumber(options.batchSize) || options.batchSize <= 1)) { 45 | throw new Error('batchSize must be a number greater than or equal to 1'); 46 | } 47 | if (options.batchMaxTime && (!isNumber(options.batchMaxTime) || options.batchMaxTime <= 500)) { 48 | throw new Error('batchMaxTime must be greater than 500 milliseonds'); 49 | } 50 | if (options.requestMaxBodySize && (!isNumber(options.requestMaxBodySize) || options.requestMaxBodySize < 0)) { 51 | throw new Error('requestMaxBodySize must be a number greater than 0'); 52 | } 53 | if (options.responseMaxBodySize && (!isNumber(options.responseMaxBodySize) || options.responseMaxBodySize < 0)) { 54 | throw new Error('responseMaxBodySize must be a number greater than 0'); 55 | } 56 | } 57 | 58 | function ensureValidLogData(logData) { 59 | if (!logData.request) { 60 | throw new Error( 61 | 'For Moesif events, request and response objects are required. Please check your maskContent function do not remove this' 62 | ); 63 | } else { 64 | if (!logData.request.time) { 65 | throw new Error( 66 | 'For Moesif events, request time is required. Please check your maskContent function do not remove this' 67 | ); 68 | } 69 | if (!logData.request.verb) { 70 | throw new Error( 71 | 'For Moesif events, request verb is required. Please check your maskContent function do not remove this' 72 | ); 73 | } 74 | if (!logData.request.uri) { 75 | throw new Error( 76 | 'For Moesif events, request uri is required. Please check your maskContent function do not remove this' 77 | ); 78 | } 79 | } 80 | if (!logData.response) { 81 | throw new Error( 82 | 'For Moesif events, request and response objects are required. Please check your maskContent function do not remove this' 83 | ); 84 | } else { 85 | // if (!logData.response.body) { 86 | // throw new Error('for log events, response body objects is required but can be empty object'); 87 | // } 88 | if (!logData.request.time) { 89 | throw new Error( 90 | 'For Moesif events, response time is required. The middleware should populate it automatically. Please check your maskContent function do not remove this' 91 | ); 92 | } 93 | } 94 | } 95 | 96 | function ensureValidUserModel(userModel) { 97 | if (!userModel.userId) { 98 | throw new Error('To update a user, a userId field is required'); 99 | } 100 | } 101 | 102 | function ensureValidUsersBatchModel(usersBatchModel) { 103 | for (let userModel of usersBatchModel) { 104 | if (!userModel.userId) { 105 | throw new Error('To update a user, a userId field is required'); 106 | } 107 | } 108 | } 109 | 110 | function ensureValidCompanyModel(companyModel) { 111 | if (!companyModel.companyId) { 112 | throw new Error('To update a company, a companyId field is required'); 113 | } 114 | } 115 | 116 | function ensureValidCompaniesBatchModel(companiesBatchModel) { 117 | for (let companyModel of companiesBatchModel) { 118 | if (!companyModel.companyId) { 119 | throw new Error('To update a company, a companyId field is required'); 120 | } 121 | } 122 | } 123 | 124 | function ensureValidActionModel(actionModel) { 125 | if (!actionModel.actionName) { 126 | throw new Error('To send an Action, the actionName field is required'); 127 | } 128 | if (!(actionModel.request && actionModel.request.uri)) { 129 | throw new Error('To send an Action, the request and request.uri fields are required'); 130 | } 131 | } 132 | 133 | function ensureValidActionsBatchModel(actionsBatchModel) { 134 | for (let actionModel of actionsBatchModel) { 135 | if (!actionModel.actionName) { 136 | throw new Error('To send an Action, the actionName field is required'); 137 | } 138 | if (!(actionModel.request && actionModel.request.uri)) { 139 | throw new Error('To send an Action, the request and request.uri fields are required'); 140 | } 141 | } 142 | } 143 | 144 | module.exports = { 145 | ensureValidOptions: ensureValidOptions, 146 | ensureValidLogData: ensureValidLogData, 147 | ensureValidUserModel: ensureValidUserModel, 148 | ensureValidUsersBatchModel: ensureValidUsersBatchModel, 149 | ensureValidCompanyModel: ensureValidCompanyModel, 150 | ensureValidCompaniesBatchModel: ensureValidCompaniesBatchModel, 151 | ensureValidActionModel: ensureValidActionModel, 152 | ensureValidActionsBatchModel: ensureValidActionsBatchModel 153 | }; 154 | -------------------------------------------------------------------------------- /lib/formatEventDataAndSave.js: -------------------------------------------------------------------------------- 1 | var dataUtils = require('./dataUtils'); 2 | var ensureValidUtils = require('./ensureValidUtils'); 3 | var requestIp = require('request-ip'); 4 | 5 | var logMessage = dataUtils.logMessage; 6 | var hashSensitive = dataUtils.hashSensitive; 7 | var bodyToBase64 = dataUtils.bodyToBase64; 8 | var startWithJson = dataUtils.startWithJson; 9 | var timeTookInSeconds = dataUtils.timeTookInSeconds; 10 | var safeJsonParse = dataUtils.safeJsonParse; 11 | var isJsonHeader = dataUtils.isJsonHeader; 12 | var computeBodySize = dataUtils.computeBodySize; 13 | var ensureToString = dataUtils.ensureToString; 14 | 15 | const TRANSACTION_ID_HEADER = 'x-moesif-transaction-id'; 16 | 17 | var ensureValidLogData = ensureValidUtils.ensureValidLogData; 18 | 19 | function decodeHeaders(header) { 20 | try { 21 | var keyVal = header.split('\r\n'); 22 | 23 | // Remove Request Line or Status Line 24 | keyVal.shift(); 25 | 26 | var obj = {}; 27 | var i; 28 | for (i in keyVal) { 29 | keyVal[i] = keyVal[i].split(':', 2); 30 | if (keyVal[i].length != 2) { 31 | continue; 32 | } 33 | obj[keyVal[i][0].trim()] = keyVal[i][1].trim(); 34 | } 35 | return obj; 36 | } catch (err) { 37 | return {}; 38 | } 39 | } 40 | 41 | function safeGetResponseHeaders(res) { 42 | try { 43 | if (res.getHeaders) { 44 | return res.getHeaders(); 45 | } 46 | try { 47 | // access ._headers will result in exception 48 | // in some versions fo node. 49 | // so must be in try block. 50 | if (res._headers) { 51 | return res._headers; 52 | } 53 | } catch (err) { 54 | } 55 | return res.headers || decodeHeaders(res._header); 56 | } catch(err) { 57 | return {}; 58 | } 59 | } 60 | 61 | // req getters that require trust proxy fn 62 | // protocol (not used). 63 | // ips (not used) 64 | // ip (not used) 65 | // subdomains (not used) 66 | // hostname 67 | // secure (used) 68 | 69 | function safeGetHostname(req) { 70 | try { 71 | return req.hostname; 72 | } catch(err) { 73 | return (req.headers && req.headers['x-forwarded-host']) || 'localhost'; 74 | } 75 | } 76 | 77 | function safeGetReqSecure(req) { 78 | try { 79 | return req.secure; 80 | } catch(err) { 81 | return false; 82 | } 83 | } 84 | 85 | function formatEventDataAndSave(responseBodyBuffer, req, res, options, saveEvent) { 86 | logMessage(options.debug, 'formatEventDataAndSave', 'reqUrl=' + req.originalUrl); 87 | logMessage(options.debug, 'formatEventDataAndSave', 'responseBodyBuffer=', responseBodyBuffer); 88 | 89 | var logData = {}; 90 | logData.request = {}; 91 | logData.request.verb = req.method; 92 | var protocol = 93 | (req.connection && req.connection.encrypted) || safeGetReqSecure(req) ? 'https://' : 'http://'; 94 | 95 | var host = req.headers.host || safeGetHostname(req); 96 | logData.request.uri = protocol + host + (req.originalUrl || req.url); 97 | logData.request.headers = req.headers; 98 | 99 | if (options.logBody) { 100 | var parseRequestBodyStartTime = Date.now(); 101 | const requestBody = req.body || req._moRawBody; 102 | // requestBody could be string or json object. 103 | 104 | const requestBodySize = computeBodySize(requestBody); 105 | 106 | if (requestBodySize > options.requestMaxBodySize) { 107 | logMessage(options.debug, 'formatEventDataAndSave', 'requestBodySize ' + requestBodySize + ' bigger than requestMaxBodySize ' + options.requestMaxBodySize, requestBody); 108 | 109 | logData.request.body = { 110 | msg: 'request.body.length exceeded options requestMaxBodySize of ' + options.requestMaxBodySize 111 | }; 112 | } else if (requestBody) { 113 | logMessage(options.debug, 'formatEventDataAndSave', 'processing req.body'); 114 | var isReqBodyMaybeJson = isJsonHeader(req) || startWithJson(requestBody); 115 | 116 | if (isReqBodyMaybeJson) { 117 | var parseRequestBodyAsJsonStartTime = Date.now(); 118 | var parsedReqBody = safeJsonParse(requestBody); 119 | 120 | logData.request.transferEncoding = parsedReqBody.transferEncoding; 121 | logData.request.body = parsedReqBody.body; 122 | var parseRequestBodyAsJsonEndTime = Date.now(); 123 | logMessage(options.debug, 'parseRequestBodyAsJson took time ', timeTookInSeconds(parseRequestBodyAsJsonStartTime, parseRequestBodyAsJsonEndTime)); 124 | } else { 125 | var parseRequestBodyAsBase64StartTime = Date.now(); 126 | logData.request.transferEncoding = 'base64'; 127 | logData.request.body = bodyToBase64(requestBody); 128 | var parseRequestBodyAsBase64EndTime = Date.now(); 129 | logMessage(options.debug, 'parseRequestBodyAsBase64 took time ', timeTookInSeconds(parseRequestBodyAsBase64StartTime, parseRequestBodyAsBase64EndTime)); 130 | } 131 | } 132 | 133 | var parseRequestBodyEndTime = Date.now(); 134 | logMessage(options.debug, 'parseRequestBody took time ', timeTookInSeconds(parseRequestBodyStartTime, parseRequestBodyEndTime)); 135 | } 136 | 137 | logData.request.ipAddress = requestIp.getClientIp(req); 138 | 139 | logData.request.time = req._startTime; 140 | 141 | logData.response = {}; 142 | logData.response.status = res.statusCode ? res.statusCode : 599; 143 | res._moHeaders = safeGetResponseHeaders(res); 144 | logData.response.headers = res._moHeaders; 145 | logData.response.time = res._endTime; 146 | // if _mo_blocked_by not exist, it will be undefined anyways. 147 | logData.blockedBy = res._mo_blocked_by; 148 | 149 | if (options.logBody) { 150 | if (res._mo_blocked_by) { 151 | // blocked body is always json 152 | logData.response.body = res._mo_blocked_body; 153 | } else if (responseBodyBuffer) { 154 | logMessage(options.debug, 'formatEventDataAndSave', 'processing responseBodyBuffer'); 155 | if (responseBodyBuffer.length < options.responseMaxBodySize) { 156 | if (isJsonHeader(res) || startWithJson(responseBodyBuffer)) { 157 | var parsedResBody = safeJsonParse(responseBodyBuffer); 158 | logData.response.transferEncoding = parsedResBody.transferEncoding; 159 | logData.response.body = parsedResBody.body; 160 | } else { 161 | logData.response.transferEncoding = 'base64'; 162 | logData.response.body = bodyToBase64(responseBodyBuffer); 163 | } 164 | } else { 165 | logData.response.body = { 166 | msg: 'response.body.length exceeded options responseMaxBodySize of ' + options.responseMaxBodySize 167 | } 168 | } 169 | } 170 | } 171 | 172 | logMessage(options.debug, 'formatEventDataAndSave', 'created data', logData); 173 | 174 | logData = options.maskContent(logData); 175 | 176 | var identifyUserStartTime = Date.now(); 177 | logData.userId = ensureToString(options.identifyUser(req, res)); 178 | var identifyUserEndTime = Date.now(); 179 | logMessage(options.debug, 'identifyUser took time ', timeTookInSeconds(identifyUserStartTime, identifyUserEndTime)); 180 | 181 | var identifyCompanyStartTime = Date.now(); 182 | logData.companyId = ensureToString(options.identifyCompany(req, res)); 183 | var identifyCompanyEndTime = Date.now(); 184 | logMessage(options.debug, 'identifyCompany took time ', timeTookInSeconds(identifyCompanyStartTime, identifyCompanyEndTime)); 185 | 186 | logData.sessionToken = options.getSessionToken(req, res); 187 | logData.tags = options.getTags(req, res); 188 | logData.request.apiVersion = options.getApiVersion(req, res); 189 | logData.metadata = options.getMetadata(req, res); 190 | 191 | // Set API direction 192 | logData.direction = "Incoming" 193 | 194 | logMessage(options.debug, 'formatEventDataAndSave', 'applied options to data=', logData); 195 | 196 | var ensureValidLogDataStartTime = Date.now(); 197 | ensureValidLogData(logData); 198 | var ensureValidLogDataEndTime = Date.now(); 199 | logMessage(options.debug, 'ensureValidLogData took time ', timeTookInSeconds(ensureValidLogDataStartTime, ensureValidLogDataEndTime)); 200 | 201 | // This is fire and forget, we don't want logging to hold up the request so don't wait for the callback 202 | if (!options.skip(req, res)) { 203 | logMessage(options.debug, 'formatEventDataAndSave', 'queue data to send to moesif'); 204 | 205 | if (!options.noAutoHideSensitive) { 206 | var noAutoHideSensitiveStartTime = Date.now(); 207 | // autoHide 208 | try { 209 | logData.request.headers = hashSensitive(logData.request.headers, options.debug); 210 | logData.request.body = hashSensitive(logData.request.body, options.debug); 211 | logData.response.headers = hashSensitive(logData.response.headers, options.debug); 212 | logData.response.body = hashSensitive(logData.response.body, options.debug); 213 | } catch (err) { 214 | logMessage(options.debug, 'formatEventDataAndSave', 'error on hashSensitive err=' + err); 215 | } 216 | var noAutoHideSensitiveEndTime = Date.now(); 217 | logMessage(options.debug, 'noAutoHideSensitive took time ', timeTookInSeconds(noAutoHideSensitiveStartTime, noAutoHideSensitiveEndTime)); 218 | } 219 | 220 | // Add Transaction Id to Event Request Model 221 | if (logData.response.headers[TRANSACTION_ID_HEADER]) { 222 | logData.request.headers[TRANSACTION_ID_HEADER] = logData.response.headers[TRANSACTION_ID_HEADER]; 223 | } 224 | 225 | saveEvent(logData); 226 | } 227 | } 228 | 229 | module.exports = formatEventDataAndSave; 230 | -------------------------------------------------------------------------------- /lib/getRawBody.js: -------------------------------------------------------------------------------- 1 | /* 2 | * raw-body modified from npm raw-body package. 3 | * Moesif do not need to halt and unpipe the stream if there is an error. 4 | * 5 | * Copyright(c) 2013-2014 Jonathan Ong 6 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 7 | * MIT Licensed 8 | */ 9 | 10 | 'use strict' 11 | 12 | /** 13 | * Module dependencies. 14 | * @private 15 | */ 16 | 17 | var bytes = require('bytes') 18 | var createError = require('http-errors') 19 | var iconv = require('iconv-lite') 20 | 21 | /** 22 | * Module exports. 23 | * @public 24 | */ 25 | 26 | module.exports = getRawBody 27 | 28 | /** 29 | * Module variables. 30 | * @private 31 | */ 32 | 33 | var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: / 34 | 35 | /** 36 | * Get the decoder for a given encoding. 37 | * 38 | * @param {string} encoding 39 | * @private 40 | */ 41 | 42 | function getDecoder (encoding) { 43 | if (!encoding) return null 44 | 45 | try { 46 | return iconv.getDecoder(encoding) 47 | } catch (e) { 48 | // error getting decoder 49 | if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e 50 | 51 | // the encoding was not found 52 | throw createError(415, 'specified encoding unsupported', { 53 | encoding: encoding, 54 | type: 'encoding.unsupported' 55 | }) 56 | } 57 | } 58 | 59 | /** 60 | * Get the raw body of a stream (typically HTTP). 61 | * 62 | * @param {object} stream 63 | * @param {object|string|function} [options] 64 | * @param {function} [callback] 65 | * @public 66 | */ 67 | 68 | function getRawBody (stream, options, callback) { 69 | var done = callback 70 | var opts = options || {} 71 | 72 | if (options === true || typeof options === 'string') { 73 | // short cut for encoding 74 | opts = { 75 | encoding: options 76 | } 77 | } 78 | 79 | if (typeof options === 'function') { 80 | done = options 81 | opts = {} 82 | } 83 | 84 | // validate callback is a function, if provided 85 | if (done !== undefined && typeof done !== 'function') { 86 | throw new TypeError('argument callback must be a function') 87 | } 88 | 89 | // require the callback without promises 90 | if (!done && !global.Promise) { 91 | throw new TypeError('argument callback is required') 92 | } 93 | 94 | // get encoding 95 | var encoding = opts.encoding !== true 96 | ? opts.encoding 97 | : 'utf-8' 98 | 99 | // convert the limit to an integer 100 | var limit = bytes.parse(opts.limit) 101 | 102 | // convert the expected length to an integer 103 | var length = opts.length != null && !isNaN(opts.length) 104 | ? parseInt(opts.length, 10) 105 | : null 106 | 107 | if (done) { 108 | // classic callback style 109 | return readStream(stream, encoding, length, limit, done) 110 | } 111 | 112 | return new Promise(function executor (resolve, reject) { 113 | readStream(stream, encoding, length, limit, function onRead (err, buf) { 114 | if (err) return reject(err) 115 | resolve(buf) 116 | }) 117 | }) 118 | } 119 | 120 | // /** 121 | // * Halt a stream. 122 | // * 123 | // * @param {Object} stream 124 | // * @private 125 | // */ 126 | 127 | // function halt (stream) { 128 | // // unpipe everything from the stream 129 | // unpipe(stream) 130 | 131 | // // pause stream 132 | // if (typeof stream.pause === 'function') { 133 | // stream.pause() 134 | // } 135 | // } 136 | 137 | /** 138 | * Read the data from the stream. 139 | * 140 | * @param {object} stream 141 | * @param {string} encoding 142 | * @param {number} length 143 | * @param {number} limit 144 | * @param {function} callback 145 | * @public 146 | */ 147 | 148 | function readStream (stream, encoding, length, limit, callback) { 149 | var complete = false 150 | var sync = true 151 | 152 | // check the length and limit options. 153 | // note: we intentionally leave the stream paused, 154 | // so users should handle the stream themselves. 155 | if (limit !== null && length !== null && length > limit) { 156 | return done(createError(413, 'request entity too large', { 157 | expected: length, 158 | length: length, 159 | limit: limit, 160 | type: 'entity.too.large' 161 | })) 162 | } 163 | 164 | // streams1: assert request encoding is buffer. 165 | // streams2+: assert the stream encoding is buffer. 166 | // stream._decoder: streams1 167 | // state.encoding: streams2 168 | // state.decoder: streams2, specifically < 0.10.6 169 | var state = stream._readableState 170 | if (stream._decoder || (state && (state.encoding || state.decoder))) { 171 | // developer error 172 | return done(createError(500, 'stream encoding should not be set', { 173 | type: 'stream.encoding.set' 174 | })) 175 | } 176 | 177 | var received = 0 178 | var decoder 179 | 180 | try { 181 | decoder = getDecoder(encoding) 182 | } catch (err) { 183 | return done(err) 184 | } 185 | 186 | var buffer = decoder 187 | ? '' 188 | : [] 189 | 190 | // attach listeners 191 | stream.on('aborted', onAborted) 192 | stream.on('close', cleanup) 193 | stream.on('data', onData) 194 | stream.on('end', onEnd) 195 | stream.on('error', onEnd) 196 | 197 | // mark sync section complete 198 | sync = false 199 | 200 | function done () { 201 | var args = new Array(arguments.length) 202 | 203 | // copy arguments 204 | for (var i = 0; i < args.length; i++) { 205 | args[i] = arguments[i] 206 | } 207 | 208 | // mark complete 209 | complete = true 210 | 211 | if (sync) { 212 | process.nextTick(invokeCallback) 213 | } else { 214 | invokeCallback() 215 | } 216 | 217 | function invokeCallback () { 218 | cleanup() 219 | 220 | // if (args[0]) { 221 | // // halt the stream on error 222 | // halt(stream) 223 | // } 224 | 225 | callback.apply(null, args) 226 | } 227 | } 228 | 229 | function onAborted () { 230 | if (complete) return 231 | 232 | done(createError(400, 'request aborted', { 233 | code: 'ECONNABORTED', 234 | expected: length, 235 | length: length, 236 | received: received, 237 | type: 'request.aborted' 238 | })) 239 | } 240 | 241 | function onData (chunk) { 242 | if (complete) return 243 | 244 | received += chunk.length 245 | 246 | if (limit !== null && received > limit) { 247 | done(createError(413, 'request entity too large', { 248 | limit: limit, 249 | received: received, 250 | type: 'entity.too.large' 251 | })) 252 | } else if (decoder) { 253 | buffer += decoder.write(chunk) 254 | } else { 255 | buffer.push(chunk) 256 | } 257 | } 258 | 259 | function onEnd (err) { 260 | if (complete) return 261 | if (err) return done(err) 262 | 263 | // Moesif: We really don't care about if length does not match. 264 | // if (length !== null && received !== length) { 265 | // // done(createError(400, 'request size did not match content length', { 266 | // // expected: length, 267 | // // length: length, 268 | // // received: received, 269 | // // type: 'request.size.invalid' 270 | // // })) 271 | // } else { 272 | var string = decoder 273 | ? buffer + (decoder.end() || '') 274 | : Buffer.concat(buffer) 275 | done(null, string) 276 | // } 277 | } 278 | 279 | function cleanup () { 280 | buffer = null 281 | 282 | stream.removeListener('aborted', onAborted) 283 | stream.removeListener('data', onData) 284 | stream.removeListener('end', onEnd) 285 | stream.removeListener('error', onEnd) 286 | stream.removeListener('close', cleanup) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /lib/governanceRulesManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Governance Rules Manager is responsible for fetching governance rules 3 | * and figure out if rules needs to be applied and apply the rules 4 | * 5 | * This is done by ensuring the x-moesif-config-etag doesn't change. 6 | */ 7 | 8 | var safeGet = require('lodash/get'); 9 | var isNil = require('lodash/isNil'); 10 | var assign = require('lodash/assign'); 11 | var requestIp = require('request-ip'); 12 | var dataUtils = require('./dataUtils'); 13 | 14 | var safeJsonParse = dataUtils.safeJsonParse; 15 | var getReqHeaders = dataUtils.getReqHeaders; 16 | 17 | var moesifController = require('moesifapi').ApiController; 18 | 19 | const CONFIG_UPDATE_DELAY = 60000; // 1 minutes 20 | const HASH_HEADER = 'x-moesif-config-etag'; 21 | 22 | function now() { 23 | return new Date().getTime(); 24 | } 25 | 26 | const RULE_TYPES = { 27 | USER: 'user', 28 | COMPANY: 'company', 29 | REGEX: 'regex', 30 | }; 31 | 32 | function prepareFieldValues(request, requestBody) { 33 | return { 34 | 'request.verb': request.method, 35 | 'request.ip': requestIp.getClientIp(request), 36 | 'request.route': request.originalUrl || request.url, 37 | 'request.body.operationName': safeGet(requestBody, 'operationName'), 38 | }; 39 | } 40 | 41 | function prepareRequestBody(request) { 42 | if (request.body) { 43 | if (typeof request.body === 'object') { 44 | return request.body; 45 | } 46 | if (typeof request.body === 'string') { 47 | return safeJsonParse(request.body); 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | function getFieldValueForPath(path, requestFields, requestBody, requestHeaders) { 55 | if (path && path.indexOf('request.body.') === 0 && requestBody) { 56 | const bodyKey = path.replace('request.body.', ''); 57 | return requestBody[bodyKey]; 58 | } else if (path && path.indexOf('request.headers.') === 0 && requestHeaders) { 59 | const headerKey = path.replace('request.headers.', ''); 60 | return requestHeaders[headerKey] || requestHeaders[headerKey.toLowerCase()]; 61 | } else if (path && requestFields) { 62 | return requestFields[path]; 63 | } 64 | return ''; 65 | } 66 | 67 | function doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders) { 68 | if (!regexConfig || regexConfig.length <= 0 || !Array.isArray(regexConfig)) { 69 | // means customer do not care about regex match and only cohort match. 70 | return true; 71 | } 72 | 73 | const arrayToOr = regexConfig.map(function (oneGroupOfConditions) { 74 | const conditions = oneGroupOfConditions.conditions || []; 75 | 76 | return conditions.reduce(function (andSoFar, currentCondition) { 77 | if (!andSoFar) return false; 78 | 79 | const path = currentCondition.path; 80 | 81 | const fieldValue = getFieldValueForPath(path, requestFields, requestBody, requestHeaders); 82 | 83 | try { 84 | const regex = new RegExp(currentCondition.value); 85 | return regex.test(fieldValue); 86 | } catch (err) { 87 | return false; 88 | } 89 | }, true); 90 | }); 91 | 92 | return arrayToOr.reduce(function (sofar, curr) { 93 | return sofar || curr; 94 | }, false); 95 | } 96 | 97 | function replaceVariableNameWithValueWithDefault(inputString, variables) { 98 | // This regular expression matches patterns like {{VARIABLE_NAME}} or {{VARIABLE_NAME|DEFAULT_VALUE}} 99 | const variablePattern = /\{\{([^\{\}|]+)(\|([^}]+))?\}\}/g; 100 | 101 | return inputString.replace(variablePattern, (match, variableName, _, defaultValue) => { 102 | // Check if the variableName exists in the variables object and is not null/undefined 103 | if ( 104 | variables.hasOwnProperty(variableName) && 105 | variables[variableName] !== null && 106 | variables[variableName] !== undefined 107 | ) { 108 | // Replace with the variable value from the variables object 109 | return variables[variableName]; 110 | } else { 111 | // If the variable is not provided, use the default value (if available) 112 | // If defaultValue is undefined (i.e., not provided in the template), this will return an empty string 113 | return defaultValue || 'UNKNOWN'; 114 | } 115 | }); 116 | } 117 | // // Example usage: 118 | // const inputString = "This string has {{VARIABLE_NAME}} and {{MISSING_VARIABLE|default value}} to be {{foo.bar|default name}} replaced {{no_default.field}}."; 119 | 120 | // // Suppose VARIABLE_NAME is provided, but MISSING_VARIABLE is not 121 | // const variables = { 122 | // VARIABLE_NAME: "some value", 123 | // 'foo.bar': 'nihao' 124 | // // MISSING_VARIABLE is not provided, so its default value from the template should be used 125 | // // no_default.field is not provided, the template have no default either, so it becomes "UNKNOWN" 126 | // }; 127 | 128 | // const result = replaceVariableNameWithValueWithDefault(inputString, variables); 129 | // console.log(result): 130 | // // This string has some value and default value to be nihao replaced UNKNOWN. 131 | 132 | function recursivelyReplaceValues(tempObjectOrVal, mergeTagValues, ruleVariables) { 133 | if (!ruleVariables || ruleVariables.length <= 0) { 134 | return tempObjectOrVal; 135 | } 136 | 137 | if (typeof tempObjectOrVal === 'string') { 138 | let tempString = tempObjectOrVal; 139 | const variablesAndValues = {}; 140 | ruleVariables.forEach(function (ruleVar) { 141 | const varName = ruleVar.name; 142 | const replacementValue = safeGet(mergeTagValues, varName); 143 | 144 | if (replacementValue) { 145 | variablesAndValues[varName] = replacementValue; 146 | } 147 | }); 148 | 149 | const replacedString = replaceVariableNameWithValueWithDefault(tempString, variablesAndValues); 150 | return replacedString; 151 | } 152 | 153 | if (isNil(tempObjectOrVal)) { 154 | return tempObjectOrVal; 155 | } 156 | 157 | if (Array.isArray(tempObjectOrVal)) { 158 | return tempObjectOrVal.map(function (val) { 159 | return recursivelyReplaceValues(val, mergeTagValues, ruleVariables); 160 | }); 161 | } 162 | 163 | if (typeof tempObjectOrVal === 'object') { 164 | const tempReturnValue = {}; 165 | Object.entries(tempObjectOrVal).forEach(function ([key, val]) { 166 | tempReturnValue[key] = recursivelyReplaceValues(val, mergeTagValues, ruleVariables); 167 | }); 168 | 169 | return tempReturnValue; 170 | } 171 | return tempObjectOrVal; 172 | } 173 | 174 | function modifyResponseForOneRule(rule, responseHolder, mergeTagValues) { 175 | // headers are merge add to existing 176 | const ruleVariables = rule.variables; 177 | 178 | const ruleHeaders = safeGet(rule, 'response.headers'); 179 | if (ruleHeaders) { 180 | const valueReplacedHeaders = recursivelyReplaceValues( 181 | ruleHeaders, 182 | mergeTagValues, 183 | ruleVariables 184 | ); 185 | responseHolder.headers = assign(responseHolder.headers, valueReplacedHeaders); 186 | } 187 | 188 | if (rule.block) { 189 | // also need to set this in case it is missing. 190 | // blocked body is always json 191 | responseHolder.headers['Content-Type'] = 'application/json'; 192 | // in case of rule block, we replace the status and body. 193 | const ruleResBody = safeGet(rule, 'response.body'); 194 | const replacedBody = recursivelyReplaceValues(ruleResBody, mergeTagValues, ruleVariables); 195 | responseHolder.body = replacedBody; 196 | responseHolder.status = safeGet(rule, 'response.status'); 197 | responseHolder.blocked_by = rule._id; 198 | } 199 | 200 | return responseHolder; 201 | } 202 | 203 | /** 204 | * 205 | * @type Class 206 | * 207 | * */ 208 | function GovernanceRulesManager() { 209 | this._lastUpdate = 0; 210 | } 211 | 212 | GovernanceRulesManager.prototype.setLogger = function (logger) { 213 | this._logger = logger; 214 | }; 215 | 216 | GovernanceRulesManager.prototype.log = function (message, details) { 217 | if (this._logger) { 218 | this._logger(message, details); 219 | } 220 | }; 221 | 222 | GovernanceRulesManager.prototype.hasRules = function () { 223 | return Boolean(this._rules && this._rules.length > 0); 224 | }; 225 | 226 | GovernanceRulesManager.prototype.shouldFetch = function () { 227 | // wait to reload the config, since different collector instances 228 | // might have different versions of the config 229 | return !this._rules || now() - this._lastUpdate > CONFIG_UPDATE_DELAY; 230 | }; 231 | 232 | GovernanceRulesManager.prototype.tryGetRules = function () { 233 | var self = this; 234 | 235 | return new Promise(function (resolve, reject) { 236 | if (!self._loading && self.shouldFetch()) { 237 | // only send one config request at a time 238 | self._loading = true; 239 | self.log('loading rules'); 240 | moesifController.getRules(function (err, response, event) { 241 | self._loading = false; 242 | // prevent keep calling. 243 | self._rules = []; 244 | if (err) { 245 | self.log('load gov rules failed' + err.toString()); 246 | // we resolve anyways and move on. 247 | // it will be retried again. 248 | resolve(); 249 | } 250 | 251 | if (response && response.statusCode === 200) { 252 | self._configHash = event.response.headers[HASH_HEADER]; 253 | try { 254 | self._rules = response.body; 255 | if (Array.isArray(self._rules)) { 256 | self.log('obtained ' + self._rules.length + 'rules'); 257 | self._cacheRules(self._rules); 258 | } else { 259 | self.log('unexpected: rules from server is not array', self._rules); 260 | } 261 | self._lastUpdate = now(); 262 | resolve(self._rules); 263 | } catch (e) { 264 | self.log('moesif-nodejs: error parsing rules ' + e.toString()); 265 | } 266 | } 267 | }); 268 | } else { 269 | self.log('skip loading rules, already loaded recently'); 270 | resolve(self._rules); 271 | } 272 | }); 273 | }; 274 | 275 | GovernanceRulesManager.prototype._cacheRules = function (rules) { 276 | var self = this; 277 | this.regexRules = rules.filter(function (item) { 278 | return item.type === RULE_TYPES.REGEX; 279 | }); 280 | this.userRulesHashByRuleId = {}; 281 | this.companyRulesHashByRuleId = {}; 282 | 283 | rules.forEach(function (rule) { 284 | switch (rule.type) { 285 | case RULE_TYPES.COMPANY: 286 | self.companyRulesHashByRuleId[rule._id] = rule; 287 | break; 288 | case RULE_TYPES.USER: 289 | self.userRulesHashByRuleId[rule._id] = rule; 290 | break; 291 | case RULE_TYPES.REGEX: 292 | break; 293 | default: 294 | break; 295 | } 296 | }); 297 | 298 | this.unidentifiedUserRules = rules.filter(function (rule) { 299 | return rule.type === RULE_TYPES.USER && rule.applied_to_unidentified; 300 | }); 301 | 302 | this.unidentifiedCompanyRules = rules.filter(function (rule) { 303 | return rule.type === RULE_TYPES.COMPANY && rule.applied_to_unidentified; 304 | }); 305 | }; 306 | 307 | GovernanceRulesManager.prototype._getApplicableRegexRules = function ( 308 | requestFields, 309 | requestBody, 310 | requestHeaders 311 | ) { 312 | if (this.regexRules) { 313 | return this.regexRules.filter((rule) => { 314 | const regexConfig = rule.regex_config; 315 | return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders); 316 | }); 317 | } 318 | return []; 319 | }; 320 | 321 | GovernanceRulesManager.prototype._getApplicableUnidentifiedUserRules = function ( 322 | requestFields, 323 | requestBody, 324 | requestHeaders 325 | ) { 326 | if (this.unidentifiedUserRules) { 327 | return this.unidentifiedUserRules.filter((rule) => { 328 | const regexConfig = rule.regex_config; 329 | return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders); 330 | }); 331 | } 332 | return []; 333 | }; 334 | 335 | GovernanceRulesManager.prototype._getApplicableUnidentifiedCompanyRules = function ( 336 | requestFields, 337 | requestBody, 338 | requestHeaders 339 | ) { 340 | if (this.unidentifiedCompanyRules) { 341 | return this.unidentifiedCompanyRules.filter((rule) => { 342 | const regexConfig = rule.regex_config; 343 | return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders); 344 | }); 345 | } 346 | return []; 347 | }; 348 | 349 | GovernanceRulesManager.prototype._getApplicableUserRules = function ( 350 | configUserRulesValues, 351 | requestFields, 352 | requestBody, 353 | requestHeaders 354 | ) { 355 | const self = this; 356 | 357 | const applicableRules = []; 358 | const rulesThatUserIsInCohortHash = {}; 359 | 360 | const userRulesHashByRuleId = this.userRulesHashByRuleId; 361 | 362 | // handle if user is in cohort. 363 | // if user is in a rule's cohort, the data is from config_rule_rules_values 364 | if (Array.isArray(configUserRulesValues) && configUserRulesValues.length > 0) { 365 | configUserRulesValues.forEach(function (entry) { 366 | const ruleId = entry.rules; 367 | 368 | // cache the fact current user is in the cohort of this rule. 369 | rulesThatUserIsInCohortHash[ruleId] = true; 370 | 371 | const foundRule = userRulesHashByRuleId[ruleId]; 372 | if (!foundRule) { 373 | // skip not found, but shouldn't be the case here. 374 | self.log('rule not found for rule id from config' + ruleId); 375 | return; 376 | } 377 | 378 | const regexMatched = doesRegexConfigMatch( 379 | foundRule.regex_config, 380 | requestFields, 381 | requestBody, 382 | requestHeaders 383 | ); 384 | 385 | if (!regexMatched) { 386 | // skipping because regex didn't not match. 387 | return; 388 | } 389 | 390 | if (foundRule.applied_to === 'not_matching') { 391 | // skipping because rule is apply to those not in cohort. 392 | return; 393 | } else { 394 | applicableRules.push(foundRule); 395 | } 396 | }); 397 | } 398 | 399 | // handle if rule is not matching and user is not in the cohort. 400 | Object.values(userRulesHashByRuleId).forEach((rule) => { 401 | if (rule.applied_to === 'not_matching' && !rulesThatUserIsInCohortHash[rule._id]) { 402 | const regexMatched = doesRegexConfigMatch( 403 | rule.regex_config, 404 | requestFields, 405 | requestBody, 406 | requestHeaders 407 | ); 408 | if (regexMatched) { 409 | applicableRules.push(rule); 410 | } 411 | } 412 | }); 413 | 414 | return applicableRules; 415 | }; 416 | 417 | GovernanceRulesManager.prototype._getApplicableCompanyRules = function ( 418 | configCompanyRulesValues, 419 | requestFields, 420 | requestBody, 421 | requestHeaders 422 | ) { 423 | const applicableRules = []; 424 | const rulesThatCompanyIsInCohortHash = {}; 425 | const self = this; 426 | 427 | const rulesHashByRuleId = this.companyRulesHashByRuleId; 428 | 429 | // handle if company is in cohort. 430 | // if company is in a rule's cohort, the data is from config_rules_values 431 | if (Array.isArray(configCompanyRulesValues) && configCompanyRulesValues.length > 0) { 432 | configCompanyRulesValues.forEach(function (entry) { 433 | const ruleId = entry.rules; 434 | 435 | // cache the fact current company is in the cohort of this rule. 436 | rulesThatCompanyIsInCohortHash[ruleId] = true; 437 | 438 | const foundRule = rulesHashByRuleId[ruleId]; 439 | if (!foundRule) { 440 | // skip not found, but shouldn't be the case here. 441 | self.log('rule not found for rule id from config' + ruleId); 442 | return; 443 | } 444 | 445 | const regexMatched = doesRegexConfigMatch( 446 | foundRule.regex_config, 447 | requestFields, 448 | requestBody, 449 | requestHeaders 450 | ); 451 | 452 | if (!regexMatched) { 453 | // skipping because regex didn't not match. 454 | return; 455 | } 456 | 457 | if (foundRule.applied_to === 'not_matching') { 458 | // skipping because rule is apply to those not in cohort. 459 | return; 460 | } else { 461 | applicableRules.push(foundRule); 462 | } 463 | }); 464 | } 465 | 466 | // company is not in cohort, and if rule is not matching we apply the rule. 467 | Object.values(rulesHashByRuleId).forEach((rule) => { 468 | if (rule.applied_to === 'not_matching' && !rulesThatCompanyIsInCohortHash[rule._id]) { 469 | const regexMatched = doesRegexConfigMatch( 470 | rule.regex_config, 471 | requestFields, 472 | requestBody, 473 | requestHeaders 474 | ); 475 | if (regexMatched) { 476 | applicableRules.push(rule); 477 | } 478 | } 479 | }); 480 | 481 | return applicableRules; 482 | }; 483 | 484 | GovernanceRulesManager.prototype.applyRuleList = function ( 485 | applicableRules, 486 | responseHolder, 487 | configRuleValues 488 | ) { 489 | const self = this; 490 | if (!applicableRules || !Array.isArray(applicableRules) || applicableRules.length <= 0) { 491 | return responseHolder; 492 | } 493 | 494 | return applicableRules.reduce(function (prevResponseHolder, currentRule) { 495 | const ruleValuePair = (configRuleValues || []).find( 496 | (ruleValuePair) => ruleValuePair.rules === currentRule._id 497 | ); 498 | const mergeTagValues = ruleValuePair && ruleValuePair.values; 499 | try { 500 | self.log('modify file response for one rule, prev responseHolder ', { 501 | prevResponseHolder, 502 | currentRule, 503 | mergeTagValues, 504 | }); 505 | const resultResponseHolder = modifyResponseForOneRule( 506 | currentRule, 507 | prevResponseHolder, 508 | mergeTagValues 509 | ); 510 | self.log('finished modify response', { resultResponseHolder }); 511 | return resultResponseHolder; 512 | } catch (err) { 513 | self.log('error applying rule ' + currentRule._id + ' ' + err.toString()); 514 | return prevResponseHolder; 515 | } 516 | }, responseHolder); 517 | }; 518 | 519 | GovernanceRulesManager.prototype.governInternal = function ( 520 | config, 521 | userId, 522 | companyId, 523 | requestFields, 524 | requestBody, 525 | requestHeaders, 526 | originalUrl 527 | ) { 528 | // start with null for everything except for headers with empty hash that can accumulate values. 529 | let responseHolder = { 530 | status: null, 531 | headers: {}, 532 | body: null, 533 | blocked_by: null, 534 | }; 535 | 536 | try { 537 | // apply in reverse order of priority will results in highest priority rules is final rule applied. 538 | // highest to lowest priority are: user rules, company rules, and regex rules. 539 | const applicableRegexRules = this._getApplicableRegexRules( 540 | requestFields, 541 | requestBody, 542 | requestHeaders 543 | ); 544 | responseHolder = this.applyRuleList(applicableRegexRules, responseHolder); 545 | 546 | if (isNil(companyId)) { 547 | const anonCompanyRules = this._getApplicableUnidentifiedCompanyRules( 548 | requestFields, 549 | requestBody, 550 | requestHeaders 551 | ); 552 | responseHolder = this.applyRuleList(anonCompanyRules, responseHolder); 553 | } else { 554 | const configCompanyRulesValues = safeGet(safeGet(config, 'company_rules'), companyId); 555 | const idCompanyRules = this._getApplicableCompanyRules( 556 | configCompanyRulesValues, 557 | requestFields, 558 | requestBody, 559 | requestHeaders 560 | ); 561 | responseHolder = this.applyRuleList(idCompanyRules, responseHolder, configCompanyRulesValues); 562 | } 563 | 564 | if (isNil(userId)) { 565 | const anonUserRules = this._getApplicableUnidentifiedUserRules( 566 | requestFields, 567 | requestBody, 568 | requestHeaders 569 | ); 570 | responseHolder = this.applyRuleList(anonUserRules, responseHolder); 571 | } else { 572 | const configUserRulesValues = safeGet(safeGet(config, 'user_rules'), userId); 573 | const idUserRules = this._getApplicableUserRules( 574 | configUserRulesValues, 575 | requestFields, 576 | requestBody, 577 | requestHeaders 578 | ); 579 | responseHolder = this.applyRuleList(idUserRules, responseHolder, configUserRulesValues); 580 | } 581 | } catch (err) { 582 | this.log('error trying to govern request ' + err.toString, { 583 | url: originalUrl, 584 | userId, 585 | companyId, 586 | }); 587 | } 588 | this.log('govern results', responseHolder); 589 | 590 | return responseHolder; 591 | }; 592 | 593 | GovernanceRulesManager.prototype.governRequestNextJs = function ( 594 | config, 595 | userId, 596 | companyId, 597 | requestBody, 598 | requestHeaders, 599 | originalUrl, 600 | originalIp, 601 | originalMethod 602 | ) { 603 | const requestFields = { 604 | 'request.verb': originalMethod, 605 | 'request.ip': originalIp, 606 | 'request.route': originalUrl, 607 | 'request.body.operationName': safeGet(requestBody, 'operationName'), 608 | }; 609 | 610 | return this.governInternal( 611 | config, 612 | userId, 613 | companyId, 614 | requestFields, 615 | requestBody, 616 | requestHeaders, 617 | originalUrl 618 | ); 619 | }; 620 | 621 | GovernanceRulesManager.prototype.governRequest = function (config, userId, companyId, request) { 622 | const requestBody = prepareRequestBody(request); 623 | const requestFields = prepareFieldValues(request, requestBody); 624 | const requestHeaders = getReqHeaders(request); 625 | this.log('preparing to govern', { 626 | requestBody, 627 | requestFields, 628 | userId, 629 | companyId, 630 | requestHeaders, 631 | }); 632 | 633 | return this.governInternal( 634 | config, 635 | userId, 636 | companyId, 637 | requestFields, 638 | requestBody, 639 | requestHeaders, 640 | request && request.originalUrl 641 | ); 642 | }; 643 | 644 | module.exports = new GovernanceRulesManager(); 645 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xingheng on 10/16/16. 3 | */ 4 | 5 | var isNil = require('lodash/isNil'); 6 | var moesifapi = require('moesifapi'); 7 | var EventModel = moesifapi.EventModel; 8 | var UserModel = moesifapi.UserModel; 9 | var CompanyModel = moesifapi.CompanyModel; 10 | var SubscriptionModel = moesifapi.SubscriptionModel; 11 | var ActionModel = moesifapi.ActionModel; 12 | 13 | var dataUtils = require('./dataUtils'); 14 | var patch = require('./outgoing'); 15 | var createOutgoingRecorder = require('./outgoingRecorder'); 16 | var createBatcher = require('./batcher'); 17 | var moesifConfigManager = require('./moesifConfigManager'); 18 | var uuid4 = require('uuid4'); 19 | var unparsed = require('koa-body/unparsed.js'); 20 | var ensureValidUtils = require('./ensureValidUtils'); 21 | var formatEventDataAndSave = require('./formatEventDataAndSave'); 22 | var governanceRulesManager = require('./governanceRulesManager'); 23 | const { extractNextJsEventDataAndSave } = require('./nextjsUtils'); 24 | 25 | // express converts headers to lowercase 26 | const TRANSACTION_ID_HEADER = 'x-moesif-transaction-id'; 27 | 28 | var logMessage = dataUtils.logMessage; 29 | var timeTookInSeconds = dataUtils.timeTookInSeconds; 30 | var appendChunk = dataUtils.appendChunk; 31 | var totalChunkLength = dataUtils.totalChunkLength; 32 | var ensureToString = dataUtils.ensureToString; 33 | var getReqHeaders = dataUtils.getReqHeaders; 34 | 35 | var ensureValidOptions = ensureValidUtils.ensureValidOptions; 36 | var ensureValidUserModel = ensureValidUtils.ensureValidUserModel; 37 | var ensureValidUsersBatchModel = ensureValidUtils.ensureValidUsersBatchModel; 38 | var ensureValidCompanyModel = ensureValidUtils.ensureValidCompanyModel; 39 | var ensureValidCompaniesBatchModel = ensureValidUtils.ensureValidCompaniesBatchModel; 40 | var ensureValidActionModel = ensureValidUtils.ensureValidActionModel; 41 | var ensureValidActionsBatchModel = ensureValidUtils.ensureValidActionsBatchModel; 42 | 43 | // default option utility functions. 44 | 45 | var noop = function () {}; // implicitly return undefined 46 | 47 | var defaultSkip = function (req, res) { 48 | return false; 49 | }; 50 | 51 | var defaultIdentifyUser = function (req, res) { 52 | if (req) { 53 | // Express Default User Id 54 | if (req.user) { 55 | return req.user.id; 56 | } 57 | // Koa Default User Id 58 | if (req.state && req.state.user) { 59 | return req.state.user.sub || req.state.user.id; 60 | } 61 | } 62 | return undefined; 63 | }; 64 | 65 | /** 66 | * @typedef {Object} MoesifOptions 67 | * @property {string} applicationId 68 | * @property {(req: object, res: object) => string | undefined | null} [identifyUser] 69 | * @property {(req: object, res: object) => string | undefined | null} [identifyCompany] 70 | * @property {(req: object, res: object) => string | undefined | null} [getSessionToken] 71 | * @property {(req: object, res: object) => string | undefined | null} [getApiVersion] 72 | * @property {(req: object, res: object) => object | undefined | null} [getMetadata] 73 | * @property {(req: object, res: object) => boolean | undefined | null | any} [skip] 74 | * @property {(eventModel: object) => object} [maskContent] 75 | * @property {boolean} [logBody] - default true 76 | * @property {boolean} [debug] 77 | * @property {boolean} [noAutoHideSensitive] 78 | * @property {(error: object) => any} [callback] 79 | * @property {boolean} [disableBatching] 80 | * @property {number} [batchSize] - default 200 81 | * @property {number} [batchMaxTime] - default 2000 82 | * @property {string} [baseUri] - switch to another collector endpoint when using proxy 83 | * @property {number} [retry] - must be between 0 to 3 if provided. 84 | * @property {number} [requestMaxBodySize] - default 100000 85 | * @property {number} [responseMaxBodySize] - default 100000 86 | * @property {number} [maxOutgoingTimeout] - default 30000 87 | * @property {boolean} [isNextJsAppRouter] - default false 88 | */ 89 | 90 | /** 91 | * @param {MoesifOptions} options 92 | */ 93 | function makeMoesifMiddleware(options) { 94 | logMessage(options.debug, 'moesifInitiator', 'start'); 95 | 96 | var ensureValidOptionsStartTime = Date.now(); 97 | 98 | ensureValidOptions(options); 99 | 100 | var ensureValidOptionsEndTime = Date.now(); 101 | 102 | logMessage( 103 | options.debug, 104 | 'ensureValidOptions took time ', 105 | timeTookInSeconds(ensureValidOptionsStartTime, ensureValidOptionsEndTime) 106 | ); 107 | 108 | // config moesifapi 109 | var config = moesifapi.configuration; 110 | /** 111 | * @type {string} 112 | */ 113 | config.ApplicationId = options.applicationId || options.ApplicationId; 114 | config.UserAgent = 'moesif-nodejs/' + '3.9.1'; 115 | config.BaseUri = options.baseUri || options.BaseUri || config.BaseUri; 116 | // default retry to 1. 117 | config.retry = isNil(options.retry) ? 1 : options.retry; 118 | var moesifController = moesifapi.ApiController; 119 | 120 | var logGovernance = function (message, details) { 121 | logMessage(options.debug, 'governance', message, details); 122 | }; 123 | governanceRulesManager.setLogger(logGovernance); 124 | moesifConfigManager.tryGetConfig(); 125 | governanceRulesManager.tryGetRules(); 126 | 127 | /** 128 | * @type {function} 129 | */ 130 | options.identifyUser = options.identifyUser || defaultIdentifyUser; 131 | 132 | /** 133 | * @type {function} 134 | */ 135 | options.identifyCompany = options.identifyCompany || noop; 136 | 137 | // function to add custom metadata (must be an object that can be converted to JSON) 138 | /** 139 | * @type {function} 140 | */ 141 | options.getMetadata = options.getMetadata || noop; 142 | 143 | // function to add custom session token (must be a string) 144 | options.getSessionToken = options.getSessionToken || noop; 145 | 146 | // function to allow adding of custom tags (this is decprecated - getMetadata should be used instead) 147 | options.getTags = options.getTags || noop; 148 | 149 | // function to declare the api version used for the request 150 | /** 151 | * @type {function} 152 | */ 153 | options.getApiVersion = options.getApiVersion || noop; 154 | 155 | // logBody option 156 | var logBody = true; 157 | if (typeof options.logBody !== 'undefined' && options.logBody !== null) { 158 | logBody = Boolean(options.logBody); 159 | } 160 | /** 161 | * @type {function} 162 | */ 163 | options.logBody = logBody; 164 | 165 | // function that allows removal of certain, unwanted fields, before it will be sent to moesif 166 | options.maskContent = 167 | options.maskContent || 168 | function (eventData) { 169 | return eventData; 170 | }; 171 | 172 | // function where conditions can be declared, when a request should be skipped and not be tracked by moesif 173 | options.skip = options.skip || defaultSkip; 174 | 175 | var batcher = null; 176 | 177 | options.batchSize = options.batchSize || 200; 178 | options.batchMaxTime = options.batchMaxTime || 2000; 179 | options.requestMaxBodySize = options.requestMaxBodySize || 100000; 180 | options.responseMaxBodySize = options.responseMaxBodySize || 100000; 181 | 182 | options.maxOutgoingTimeout = options.maxOutgoingTimeout || 30000; 183 | 184 | if (options.disableBatching) { 185 | batcher = null; 186 | } else { 187 | batcher = createBatcher( 188 | function (eventArray) { 189 | // start log time batcher took staring here. 190 | var batcherStartTime = Date.now(); 191 | moesifController.createEventsBatch( 192 | eventArray.map(function (logData) { 193 | return new EventModel(logData); 194 | }), 195 | function (err, response) { 196 | var batcherEndTime = Date.now(); 197 | logMessage( 198 | options.debug, 199 | 'createBatcher took time ', 200 | timeTookInSeconds(batcherStartTime, batcherEndTime) 201 | ); 202 | if (err) { 203 | logMessage(options.debug, 'saveEventsBatch', 'moesif API failed with error: ', err); 204 | if (options.callback) { 205 | options.callback(err, eventArray); 206 | } 207 | } else { 208 | moesifConfigManager.tryUpdateHash(response); 209 | 210 | logMessage( 211 | options.debug, 212 | 'saveEventsBatch', 213 | 'moesif API succeeded with batchSize ' + eventArray.length 214 | ); 215 | if (options.callback) { 216 | options.callback(null, eventArray); 217 | } 218 | } 219 | } 220 | ); 221 | }, 222 | options.batchSize, 223 | options.batchMaxTime 224 | ); 225 | } 226 | 227 | var trySaveEventLocal = function (eventData) { 228 | var trySaveEventLocalStartTime = Date.now(); 229 | var tryGetConfigStartTime = Date.now(); 230 | moesifConfigManager.tryGetConfig(); 231 | governanceRulesManager.tryGetRules(); 232 | 233 | var tryGetConfigEndTime = Date.now(); 234 | logMessage( 235 | options.debug, 236 | 'tryGetConfig took time ', 237 | timeTookInSeconds(tryGetConfigStartTime, tryGetConfigEndTime) 238 | ); 239 | 240 | if ( 241 | moesifConfigManager.shouldSend( 242 | eventData && eventData.userId, 243 | eventData && eventData.companyId 244 | ) 245 | ) { 246 | var getSampleRateStartTime = Date.now(); 247 | let sampleRate = moesifConfigManager._getSampleRate( 248 | eventData && eventData.userId, 249 | eventData && eventData.companyId 250 | ); 251 | var getSampleRateEndTime = Date.now(); 252 | logMessage( 253 | options.debug, 254 | 'getSampleRate took time ', 255 | timeTookInSeconds(getSampleRateStartTime, getSampleRateEndTime) 256 | ); 257 | eventData.weight = sampleRate === 0 ? 1 : Math.floor(100 / sampleRate); 258 | if (batcher) { 259 | var eventAddedToTheBatchStartTime = Date.now(); 260 | batcher.add(eventData); 261 | var eventAddedToTheBatchEndTime = Date.now(); 262 | logMessage( 263 | options.debug, 264 | 'eventAddedToTheBatch took time ', 265 | timeTookInSeconds(eventAddedToTheBatchStartTime, eventAddedToTheBatchEndTime) 266 | ); 267 | } else { 268 | var sendEventStartTime = Date.now(); 269 | var sendEventEndTime; 270 | moesifController.createEvent(new EventModel(eventData), function (err) { 271 | logMessage(options.debug, 'saveEvent', 'moesif API callback err=' + err); 272 | if (err) { 273 | logMessage(options.debug, 'saveEvent', 'moesif API failed with error.'); 274 | if (options.callback) { 275 | options.callback(err, eventData); 276 | } 277 | sendEventEndTime = Date.now(); 278 | logMessage( 279 | options.debug, 280 | 'sendSingleEvent took time ', 281 | timeTookInSeconds(sendEventStartTime, sendEventEndTime) 282 | ); 283 | } else { 284 | logMessage(options.debug, 'saveEvent', 'moesif API succeeded'); 285 | if (options.callback) { 286 | options.callback(null, eventData); 287 | } 288 | sendEventEndTime = Date.now(); 289 | logMessage( 290 | options.debug, 291 | 'sendSingleEvent took time ', 292 | timeTookInSeconds(sendEventStartTime, sendEventEndTime) 293 | ); 294 | } 295 | }); 296 | } 297 | } 298 | var trySaveEventLocalEndTime = Date.now(); 299 | logMessage( 300 | options.debug, 301 | 'trySaveEventLocal took time ', 302 | timeTookInSeconds(trySaveEventLocalStartTime, trySaveEventLocalEndTime) 303 | ); 304 | }; 305 | 306 | /** 307 | * @param {object} arg1 - the middleware arguments may vary depends framework 308 | * @param {any} [arg2] 309 | * @param {any} [arg3] 310 | */ 311 | let moesifMiddleware = function (arg1, arg2, arg3) { 312 | var req = arg1; 313 | var res = arg2; 314 | var next = arg3; 315 | logMessage(options.debug, 'moesifMiddleware', 'start'); 316 | var middleWareStartTime = Date.now(); 317 | 318 | var koaContext = null; 319 | // If Koa context, use correct arguments 320 | if (arg1.req && arg1.res && arg1.state && arg1.app) { 321 | logMessage(options.debug, 'moesifMiddleware', 'Using Koa context'); 322 | koaContext = arg1; 323 | req = koaContext.req; 324 | 325 | // capture request body in case of Koa and in case req body is already set. 326 | req.body = req.body ? req.body : koaContext.request && koaContext.request.body; 327 | 328 | req.state = koaContext.state; 329 | res = koaContext.res; 330 | next = arg3 || arg2; 331 | } 332 | 333 | req._startTime = new Date(); 334 | 335 | if (options.skip(req, res)) { 336 | logMessage(options.debug, 'moesifMiddleware', 'skipped ' + req.originalUrl); 337 | if (next) { 338 | return next(); 339 | } 340 | } 341 | 342 | // declare getRawBodyPromise here so in scope. 343 | var getRawBodyPromise; 344 | var rawReqDataFromEventEmitter; 345 | var dataEventTracked = false; 346 | 347 | var reqHeaders = getReqHeaders(req); 348 | 349 | // determines if the request is or isn't a multipart/form-data "file" type 350 | function isMultiPartUpload() { 351 | const contentTypeHeader = reqHeaders && reqHeaders['content-type']; 352 | if (!contentTypeHeader) { 353 | return false; 354 | } else if (contentTypeHeader.indexOf('multipart/form-data') >= 0) { 355 | return true; 356 | } 357 | return false; 358 | } 359 | 360 | var multiPartUpload = isMultiPartUpload(); 361 | 362 | if ( 363 | options.logBody && 364 | !req.body && 365 | reqHeaders && 366 | reqHeaders['content-type'] && 367 | reqHeaders['content-length'] && 368 | parseInt(reqHeaders['content-length']) > 0 && 369 | //if the request is "multipart/form-data" file type, we do not attempt to capture the body, otherwise we capture it 370 | !multiPartUpload 371 | ) { 372 | // this will attempt to capture body in case body parser or some other body reader is used. 373 | // by instrumenting the "data" event. 374 | // notes: in its source code: readable stream pipe (incase of proxy) will also trigger "data" event 375 | req._mo_on = req.on; 376 | req.on = function (evt, handler) { 377 | var passedOnFunction = handler; 378 | if (evt === 'data' && !dataEventTracked) { 379 | logMessage(options.debug, 'patched on', 'instrument on data event'); 380 | dataEventTracked = true; 381 | passedOnFunction = function (chs) { 382 | logMessage(options.debug, 'req data event', 'chunks=', chs); 383 | if (totalChunkLength(rawReqDataFromEventEmitter, chs) < options.requestMaxBodySize) { 384 | rawReqDataFromEventEmitter = appendChunk(rawReqDataFromEventEmitter, chs); 385 | } else { 386 | rawReqDataFromEventEmitter = 387 | '{ "msg": "request body size exceeded options requestMaxBodySize" }'; 388 | } 389 | handler(chs); 390 | }; 391 | } 392 | return req._mo_on(evt, passedOnFunction); 393 | }; 394 | 395 | // this is used if no one ever ever read request data after response ended already. 396 | getRawBodyPromise = function () { 397 | return new Promise(function (resolve, reject) { 398 | logMessage(options.debug, 'getRawBodyPromise executor', 'started'); 399 | var total; 400 | 401 | if (!req.readable) { 402 | resolve(total); 403 | } 404 | req._mo_on('data', function (chs) { 405 | if (totalChunkLength(total, chs) < options.requestMaxBodySize) { 406 | total = appendChunk(total, chs); 407 | } else { 408 | total = '{ "msg": "request body size exceeded options requestMaxBodySize" }'; 409 | } 410 | }); 411 | req._mo_on('error', function (err) { 412 | logMessage(options.debug, 'getRawBodyPromise executor', 'error reading request body'); 413 | resolve('{ "msg": "error reading request body"}'); 414 | }); 415 | req._mo_on('end', function () { 416 | resolve(total); 417 | }); 418 | // a fail safe to always exit 419 | setTimeout(function () { 420 | resolve(total); 421 | }, 1000); 422 | }); 423 | }; 424 | } 425 | 426 | // Manage to get information from the response too, just like Connect.logger does: 427 | res._mo_write = res.write; 428 | var resBodyBuf; 429 | var resBodyBufLimitedExceeded; 430 | 431 | var responseWriteAppendChunkStartTime = Date.now(); 432 | 433 | if (options.logBody) { 434 | // we only need to patch res.write if we are logBody 435 | res.write = function (chunk, encoding, callback) { 436 | logMessage(options.debug, 'response write', 'append chunk=' + chunk); 437 | if ( 438 | !resBodyBufLimitedExceeded && 439 | totalChunkLength(resBodyBuf, chunk) < options.responseMaxBodySize 440 | ) { 441 | resBodyBuf = appendChunk(resBodyBuf, chunk); 442 | } else { 443 | resBodyBufLimitedExceeded = true; 444 | } 445 | res._mo_write(chunk, encoding, callback); 446 | }; 447 | } 448 | var responseWriteAppendChunkEndTime = Date.now(); 449 | logMessage( 450 | options.debug, 451 | 'responseWriteAppendChunk took time ', 452 | timeTookInSeconds(responseWriteAppendChunkStartTime, responseWriteAppendChunkEndTime) 453 | ); 454 | 455 | // Manage to get information from the response too, just like Connect.logger does: 456 | if (!res._mo_end) { 457 | logMessage( 458 | options.debug, 459 | 'moesifMiddleware', 460 | '_mo_end is not defined so saving original end.' 461 | ); 462 | res._mo_end = res.end; 463 | } else { 464 | logMessage( 465 | options.debug, 466 | 'moesifMiddleware', 467 | '_mo_end is already defined. Did you attach moesif express twice?' 468 | ); 469 | } 470 | 471 | // Add TransactionId to the response send to the client 472 | var addTxIdToResponseStartTime = Date.now(); 473 | let disableTransactionId = options.disableTransactionId ? options.disableTransactionId : false; 474 | if (!disableTransactionId) { 475 | let txId = reqHeaders[TRANSACTION_ID_HEADER] || dataUtils.generateUUIDv4(); 476 | // Use setHeader() instead of set() so it works with plain http-module and Express 477 | res.setHeader(TRANSACTION_ID_HEADER, txId); 478 | } 479 | var addTxIdToResponseEndTime = Date.now(); 480 | logMessage( 481 | options.debug, 482 | 'addTxIdToResponse took time ', 483 | timeTookInSeconds(addTxIdToResponseStartTime, addTxIdToResponseEndTime) 484 | ); 485 | 486 | res.end = function (chunk, encoding, callback) { 487 | var finalBuf = resBodyBuf; 488 | 489 | if (chunk && typeof chunk !== 'function' && options.logBody) { 490 | logMessage(options.debug, 'response end', 'append chunk', chunk); 491 | if ( 492 | !resBodyBufLimitedExceeded && 493 | totalChunkLength(resBodyBuf, chunk) < options.responseMaxBodySize 494 | ) { 495 | finalBuf = appendChunk(resBodyBuf, chunk); 496 | } else { 497 | finalBuf = '{ "msg": "response.body.length exceeded options responseMaxBodySize of "}'; 498 | } 499 | } 500 | 501 | res._mo_end(chunk, encoding, callback); 502 | 503 | res._endTime = new Date(); 504 | 505 | try { 506 | // if req.body does not exist by koaContext exists try to extract body 507 | if (!req.body && koaContext && options.logBody) { 508 | try { 509 | logMessage(options.debug, 'moesifMiddleware', 'try to get koa unparsed body'); 510 | req.body = 511 | koaContext.request && (koaContext.request.body || koaContext.request.body[unparsed]); 512 | } catch (err) { 513 | logMessage( 514 | options.debug, 515 | 'moesifMiddleware', 516 | 'try to get koa unparsed body failed: ' + err 517 | ); 518 | } 519 | } 520 | 521 | if (!req.body && rawReqDataFromEventEmitter && options.logBody) { 522 | logMessage( 523 | options.debug, 524 | 'moesifMiddleware', 525 | 'rawReqDatFromEventEmitter exists, getting body from it' 526 | ); 527 | req._moRawBody = rawReqDataFromEventEmitter; 528 | } 529 | 530 | // if req body or rawReqBody still does not exists but we can getRawBodyPromise. 531 | if (!req.body && !req._moRawBody && getRawBodyPromise && options.logBody) { 532 | logMessage( 533 | options.debug, 534 | 'moesifMiddleware', 535 | 'req have no body attached and we have handle on getRawBodyPromise' 536 | ); 537 | // at this point, the response already ended. 538 | // if no one read the request body, we can consume the stream. 539 | getRawBodyPromise() 540 | .then((str) => { 541 | logMessage( 542 | options.debug, 543 | 'getRawBodyPromise', 544 | 'successful. append request object with raw body: ' + (str && str.length) 545 | ); 546 | req._moRawBody = str; 547 | return req; 548 | }) 549 | .then(() => { 550 | var logEventAfterGettingRawBodyStartTime = Date.now(); 551 | formatEventDataAndSave(finalBuf, req, res, options, trySaveEventLocal); 552 | var logEventAfterGettingRawBodyEndTime = Date.now(); 553 | logMessage( 554 | options.debug, 555 | 'logEventAfterGettingRawBody took time ', 556 | timeTookInSeconds( 557 | logEventAfterGettingRawBodyStartTime, 558 | logEventAfterGettingRawBodyEndTime 559 | ) 560 | ); 561 | }) 562 | .catch((err) => { 563 | logMessage(options.debug, 'getRawBodyPromise', 'error getting rawbody' + err); 564 | }); 565 | } else { 566 | // this covers three use cases: 567 | // case 1: options.logBody is false. request body doesn't matter. 568 | // case 2: request.body is already attached to req.body 569 | // case 3: request.body doesn't exist anyways. 570 | var logEventWithoutGettingRawBodyStartTime = Date.now(); 571 | formatEventDataAndSave(finalBuf, req, res, options, trySaveEventLocal); 572 | var logEventWithoutGettingRawBodyEndTime = Date.now(); 573 | logMessage( 574 | options.debug, 575 | 'logEventWithoutGettingRawBody took time ', 576 | timeTookInSeconds( 577 | logEventWithoutGettingRawBodyStartTime, 578 | logEventWithoutGettingRawBodyEndTime 579 | ) 580 | ); 581 | } 582 | } catch (err) { 583 | logMessage(options.debug, 'moesifMiddleware', 'error occurred during log event: ' + err); 584 | logMessage(options.debug, 'moesifMiddleware', 'stack trace \n' + err.stack); 585 | if (options.callback) { 586 | options.callback(err); 587 | } 588 | } 589 | //end of patched res.end function 590 | }; 591 | 592 | if (governanceRulesManager.hasRules()) { 593 | var governedResponseHolder = governanceRulesManager.governRequest( 594 | moesifConfigManager._config, 595 | // this may cause identifyUser and identifyCompany to be called twice, 596 | // but this should be ok, but in order to block for governance rule 597 | // we have to trigger this earlier in the stream before response might be ready 598 | ensureToString(options.identifyUser(req, res)), 599 | ensureToString(options.identifyCompany(req, res)), 600 | req 601 | ); 602 | // always add the headers if exists in case of non blocking rules that 603 | // just add headers. 604 | if (governedResponseHolder.headers) { 605 | Object.entries(governedResponseHolder.headers).forEach(function (entry) { 606 | var headerKey = entry[0]; 607 | var headerVal = entry[1]; 608 | res.setHeader(headerKey, headerVal); 609 | }); 610 | } 611 | 612 | if (governedResponseHolder.blocked_by) { 613 | res._mo_blocked_by = governedResponseHolder.blocked_by; 614 | res._mo_blocked_body = governedResponseHolder.body; 615 | 616 | res.statusCode = governedResponseHolder.status; 617 | res.end(JSON.stringify(governedResponseHolder.body)); 618 | } 619 | } 620 | 621 | var middleWareEndTime = Date.now(); 622 | logMessage(options.debug, 'moesifMiddleware', 'finished, pass on to next().'); 623 | logMessage( 624 | options.debug, 625 | 'moesifMiddleware took time ', 626 | timeTookInSeconds(middleWareStartTime, middleWareEndTime) 627 | ); 628 | 629 | // do not trigger next in middleware chain if it is already blocked. 630 | if (next && !res._mo_blocked_by) { 631 | return next(); 632 | } 633 | }; 634 | 635 | if (options.isNextJsAppRouter) { 636 | // this is special handler for nextJS 637 | moesifMiddleware = function (handler) { 638 | return async function (request, context) { 639 | // if we need to log requestBody we need to clone it. 640 | const requestTime = new Date().toISOString(); 641 | let requestForLogging = options?.logBody ? request.clone() : request; 642 | 643 | const response = await handler(request, context); 644 | 645 | if (!options.disableTransactionId) { 646 | let txId = request.headers.get(TRANSACTION_ID_HEADER) || dataUtils.generateUUIDv4(); 647 | response.headers.set(TRANSACTION_ID_HEADER, txId); 648 | } 649 | 650 | let responseForLogging = options?.logBody ? response.clone() : response; 651 | const responseTime = new Date().toISOString(); 652 | extractNextJsEventDataAndSave({ 653 | request: requestForLogging, 654 | requestTime, 655 | response: responseForLogging, 656 | responseTime, 657 | options, 658 | saveEvent: trySaveEventLocal 659 | }); 660 | 661 | return response; 662 | }; 663 | }; 664 | } 665 | /** 666 | * @param {object} userModel - https://www.moesif.com/docs/api?javascript--nodejs#update-a-user 667 | * @param {function} [cb] 668 | */ 669 | moesifMiddleware.updateUser = function (userModel, cb) { 670 | var user = new UserModel(userModel); 671 | logMessage(options.debug, 'updateUser', 'convertedUserObject=', user); 672 | ensureValidUserModel(user); 673 | logMessage(options.debug, 'updateUser', 'userModel valid'); 674 | if (cb) { 675 | moesifController.updateUser(user, cb); 676 | } else { 677 | return new Promise(function (resolve, reject) { 678 | moesifController.updateUser(user, function (err, response) { 679 | if (err) { 680 | reject(err); 681 | } else { 682 | resolve(response); 683 | } 684 | }); 685 | }); 686 | } 687 | }; 688 | 689 | /** 690 | * @param {object[]} usersBatchModel 691 | * @param {function} [cb] 692 | */ 693 | moesifMiddleware.updateUsersBatch = function (usersBatchModel, cb) { 694 | var usersBatch = []; 695 | for (var userModel of usersBatchModel) { 696 | usersBatch.push(new UserModel(userModel)); 697 | } 698 | logMessage(options.debug, 'updateUsersBatch', 'convertedUserArray=', usersBatch); 699 | ensureValidUsersBatchModel(usersBatch); 700 | logMessage(options.debug, 'updateUsersBatch', 'usersBatchModel valid'); 701 | if (cb) { 702 | moesifController.updateUsersBatch(usersBatch, cb); 703 | } else { 704 | return new Promise(function (resolve, reject) { 705 | moesifController.updateUsersBatch(usersBatch, function (err, response) { 706 | if (err) { 707 | reject(err); 708 | } else { 709 | resolve(response); 710 | } 711 | }); 712 | }); 713 | } 714 | }; 715 | 716 | /** 717 | * @param {object} companyModel - https://www.moesif.com/docs/api?javascript--nodejs#companies 718 | * @param {function} [cb] 719 | */ 720 | moesifMiddleware.updateCompany = function (companyModel, cb) { 721 | var company = new CompanyModel(companyModel); 722 | logMessage(options.debug, 'updateCompany', 'convertedCompany=', company); 723 | ensureValidCompanyModel(company); 724 | logMessage(options.debug, 'updateCompany', 'companyModel valid'); 725 | if (cb) { 726 | moesifController.updateCompany(company, cb); 727 | } else { 728 | return new Promise(function (resolve, reject) { 729 | moesifController.updateCompany(company, function (err, response) { 730 | if (err) { 731 | reject(err); 732 | } else { 733 | resolve(response); 734 | } 735 | }); 736 | }); 737 | } 738 | }; 739 | 740 | /** 741 | * @param {object[]} companiesBatchModel 742 | * @param {function} [cb] 743 | */ 744 | moesifMiddleware.updateCompaniesBatch = function (companiesBatchModel, cb) { 745 | var companiesBatch = []; 746 | for (var companyModel of companiesBatchModel) { 747 | companiesBatch.push(new CompanyModel(companyModel)); 748 | } 749 | logMessage(options.debug, 'updateCompaniesBatch', 'convertedCompaniesArray=', companiesBatch); 750 | ensureValidCompaniesBatchModel(companiesBatch); 751 | logMessage(options.debug, 'updateCompaniesBatch', 'companiesBatchModel valid'); 752 | 753 | if (cb) { 754 | moesifController.updateCompaniesBatch(companiesBatch, cb); 755 | } else { 756 | return new Promise(function (resolve, reject) { 757 | moesifController.updateCompaniesBatch(companiesBatch, function (err, response) { 758 | if (err) { 759 | reject(err); 760 | } else { 761 | resolve(response); 762 | } 763 | }); 764 | }); 765 | } 766 | }; 767 | 768 | moesifMiddleware.updateSubscription = function (subscriptionModel, cb) { 769 | var subscription = new SubscriptionModel(subscriptionModel); 770 | logMessage(options.debug, 'updateSubscription', 'convertedSubscription=', subscription); 771 | if (cb) { 772 | moesifController.updateSubscription(subscription, cb); 773 | } else { 774 | return new Promise(function (resolve, reject) { 775 | moesifController.updateSubscription(subscription, function (err, response) { 776 | if (err) { 777 | reject(err); 778 | } else { 779 | resolve(response); 780 | } 781 | }); 782 | }); 783 | } 784 | }; 785 | 786 | moesifMiddleware.updateSubscriptionsBatch = function (subscriptionBatchModel, cb) { 787 | var subscriptionsBatch = []; 788 | for (var subscriptionModel of subscriptionBatchModel) { 789 | subscriptionsBatch.push(new SubscriptionModel(subscriptionModel)); 790 | } 791 | logMessage( 792 | options.debug, 793 | 'updateSubscriptionsBatch', 794 | 'convertedSubscriptionsArray=', 795 | subscriptionsBatch 796 | ); 797 | if (cb) { 798 | moesifController.updateSubscriptionsBatch(subscriptionsBatch, cb); 799 | } else { 800 | return new Promise(function (resolve, reject) { 801 | moesifController.updateSubscriptionsBatch(subscriptionsBatch, function (err, response) { 802 | if (err) { 803 | reject(err); 804 | } else { 805 | resolve(response); 806 | } 807 | }); 808 | }); 809 | } 810 | }; 811 | 812 | /** 813 | * @param {object} actionModel - https://www.moesif.com/docs/api?javascript--nodejs#track-a-custom-action 814 | * @param {function} [cb] 815 | */ 816 | moesifMiddleware.sendAction = function (actionModel, cb) { 817 | var action = new ActionModel(actionModel); 818 | logMessage(options.debug, 'sendAction', 'convertedActionObject=', action); 819 | ensureValidActionModel(action); 820 | logMessage(options.debug, 'sendAction', 'actionModel valid'); 821 | if (cb) { 822 | moesifController.sendAction(action, cb); 823 | } else { 824 | return new Promise(function (resolve, reject) { 825 | moesifController.sendAction(action, function (err, response) { 826 | if (err) { 827 | reject(err); 828 | } else { 829 | resolve(response); 830 | } 831 | }); 832 | }); 833 | } 834 | }; 835 | 836 | /** 837 | * @param {object[]} actionsBatchModel 838 | * @param {function} [cb] 839 | */ 840 | moesifMiddleware.sendActionsBatch = function (actionsBatchModel, cb) { 841 | var actionsBatch = []; 842 | for (let action of actionsBatchModel) { 843 | actionsBatch.push(new ActionModel(action)); 844 | } 845 | 846 | logMessage(options.debug, 'sendActionsBatch', 'convertedActionArray=', actionsBatch); 847 | ensureValidActionsBatchModel(actionsBatchModel); 848 | logMessage(options.debug, 'sendActionsBatch', 'actionsBatchModel valid'); 849 | 850 | if (cb) { 851 | moesifController.sendActionsBatch(actionsBatch, cb); 852 | } else { 853 | return new Promise(function (resolve, reject) { 854 | moesifController.sendActionsBatch(actionsBatch, function (err, response) { 855 | if (err) { 856 | reject(err); 857 | } else { 858 | resolve(response); 859 | } 860 | }); 861 | }); 862 | } 863 | }; 864 | 865 | moesifMiddleware.startCaptureOutgoing = function () { 866 | if (moesifMiddleware._mo_patch) { 867 | logMessage( 868 | options.debug, 869 | 'startCaptureOutgoing', 870 | 'already started capturing outgoing requests.' 871 | ); 872 | } else { 873 | function patchLogger(text, jsonObject) { 874 | logMessage(options.debug, 'outgoing capture', text, jsonObject); 875 | } 876 | var recorder = createOutgoingRecorder(trySaveEventLocal, options, patchLogger); 877 | moesifMiddleware._mo_patch = patch(recorder, patchLogger, options); 878 | } 879 | }; 880 | 881 | logMessage(options.debug, 'moesifInitiator', 'returning moesifMiddleware Function'); 882 | return moesifMiddleware; 883 | } 884 | 885 | module.exports = makeMoesifMiddleware; 886 | -------------------------------------------------------------------------------- /lib/moesifConfigManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MoesifConfigManager is responsible for fetching and ensuring 3 | * the config for our api appId is up to date. 4 | * 5 | * This is done by ensuring the x-moesif-config-etag doesn't change. 6 | */ 7 | 8 | var moesifController = require('moesifapi').ApiController; 9 | 10 | const CONFIG_UPDATE_DELAY = 60000; // 1 minutes 11 | const HASH_HEADER = 'x-moesif-config-etag'; 12 | 13 | function now() { 14 | return new Date().getTime(); 15 | } 16 | 17 | function MoesifConfigManager() { 18 | this._lastConfigUpdate = 0; 19 | } 20 | 21 | MoesifConfigManager.prototype.hasConfig = function () { 22 | return Boolean(this._config); 23 | }; 24 | 25 | MoesifConfigManager.prototype.shouldFetchConfig = function () { 26 | // wait to reload the config, since different collector instances 27 | // might have different versions of the config 28 | return ( 29 | !this._config || 30 | this._lastSeenHash !== this._configHash || 31 | now() - this._lastConfigUpdate > CONFIG_UPDATE_DELAY 32 | ); 33 | }; 34 | 35 | MoesifConfigManager.prototype.tryGetConfig = function () { 36 | if (!this._loadingConfig && this.shouldFetchConfig()) { 37 | // only send one config request at a time 38 | this._loadingConfig = true; 39 | 40 | var that = this; 41 | 42 | moesifController.getAppConfig(function (err, __, event) { 43 | that._loadingConfig = false; 44 | if (event && event.response && event.response.statusCode === 200) { 45 | that._configHash = event.response.headers[HASH_HEADER]; 46 | var responseBody = event.response.body; 47 | try { 48 | if (typeof responseBody === 'string') { 49 | that._config = JSON.parse(responseBody); 50 | } else { 51 | that._config = responseBody; 52 | } 53 | that._lastConfigUpdate = now(); 54 | } catch (e) { 55 | console.warn('moesif-nodejs: error parsing config'); 56 | } 57 | } 58 | }); 59 | } 60 | }; 61 | 62 | MoesifConfigManager.prototype._getSampleRate = function (userId, companyId) { 63 | if (!this._config) return 100; 64 | 65 | if ( 66 | userId && 67 | this._config.user_sample_rate && 68 | typeof this._config.user_sample_rate[userId] === 'number' 69 | ) { 70 | return this._config.user_sample_rate[userId]; 71 | } 72 | 73 | if ( 74 | companyId && 75 | this._config.company_sample_rate && 76 | typeof this._config.company_sample_rate[companyId] === 'number' 77 | ) { 78 | return this._config.company_sample_rate[companyId]; 79 | } 80 | 81 | return typeof this._config.sample_rate === 'number' ? this._config.sample_rate : 100; 82 | }; 83 | 84 | MoesifConfigManager.prototype.shouldSend = function (userId, companyId) { 85 | const random = Math.random() * 100; 86 | return random <= this._getSampleRate(userId, companyId); 87 | }; 88 | 89 | MoesifConfigManager.prototype.tryUpdateHash = function (response) { 90 | if (response && response.headers && response.headers[HASH_HEADER]) { 91 | this._lastSeenHash = response.headers[HASH_HEADER]; 92 | } 93 | }; 94 | 95 | module.exports = new MoesifConfigManager(); 96 | -------------------------------------------------------------------------------- /lib/nextjsUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { bodyToBase64, ensureToString, logMessage, hashSensitive } = require('./dataUtils'); 3 | 4 | const TRANSACTION_ID_HEADER = 'x-moesif-transaction-id'; 5 | 6 | function getNextJsFullUrl(request) { 7 | 8 | const url = request.url; 9 | 10 | if (url && url.indexOf('http') === 0) { 11 | return url; 12 | } 13 | 14 | const protocol = request.headers.get('x-forwarded-proto') || 'http'; 15 | 16 | // Get the host 17 | const host = request.headers.get('host'); 18 | 19 | // Get the full URL 20 | const fullUrl = `${protocol}://${host}${request.url}`; 21 | 22 | return fullUrl; 23 | } 24 | 25 | async function safeGetNextJsBody(clonedObj, options) { 26 | try { 27 | const body = await clonedObj.json(); 28 | logMessage(options.debug, 'safeGetNExtBody got body', body); 29 | 30 | return { 31 | body: body, 32 | }; 33 | } catch (error) { 34 | // Attempt to read the body as text if it's not JSON 35 | logMessage(options.debug, 'safeGetNextBody Not JSON', error); 36 | 37 | try { 38 | const textBody = await clonedObj.text(); 39 | if (!textBody) { 40 | return {}; 41 | } 42 | 43 | logMessage(options.debug, 'safeGetNextBody got text body', textBody); 44 | 45 | return { 46 | body: bodyToBase64(textBody), 47 | transferEncoding: 'base64', 48 | }; 49 | } catch (textError) { 50 | // we can not get body. so just move on. 51 | logMessage(options.debug, 'text exract error', textError); 52 | return {}; 53 | } 54 | } 55 | } 56 | 57 | function getNextJsIp(request) { 58 | try { 59 | const xForwardedFor = request.headers.get('x-forwarded-for'); 60 | const clientIp = xForwardedFor 61 | ? xForwardedFor.split(',')[0].trim() 62 | : request.headers.get('x-real-ip') || request.connection.remoteAddress; 63 | 64 | return clientIp; 65 | } catch (err) { 66 | return null; 67 | } 68 | } 69 | 70 | function getNextJsHeaders(rawHeadersObject) { 71 | const entries = Array.from(rawHeadersObject.entries()); 72 | const result = {}; 73 | entries.forEach((item) => { 74 | result[item[0]] = item[1]; 75 | }); 76 | 77 | return result; 78 | } 79 | 80 | async function extractNextJsEventDataAndSave({ 81 | request, 82 | requestTime, 83 | response, 84 | responseTime, 85 | options, 86 | saveEvent, 87 | blockedBy, 88 | }) { 89 | if (options.skip(request, response)) { 90 | logMessage(options.debug, 'skipped logging to moesif due to skip', request.url); 91 | return; 92 | } 93 | 94 | let logData = { 95 | blockedBy, 96 | userId: ensureToString(options.identifyUser(request, response)), 97 | companyId: ensureToString(options.identifyUser(request, response)), 98 | metadata: options.getMetadata(request, response), 99 | sessionToken: options.getSessionToken(request, response), 100 | }; 101 | 102 | logData.request = { 103 | ipAddress: getNextJsIp(request), 104 | time: requestTime, 105 | uri: getNextJsFullUrl(request), 106 | verb: request.method, 107 | headers: getNextJsHeaders(request.headers), 108 | }; 109 | logData.request.verb = request.method; 110 | 111 | logData.response = { 112 | time: responseTime, 113 | headers: getNextJsHeaders(response.headers), 114 | status: response.status, 115 | }; 116 | 117 | if (options.logBody) { 118 | const requestBodyInfo = await safeGetNextJsBody(request, options); 119 | logData.request.body = requestBodyInfo.body; 120 | logData.request.transferEncoding = requestBodyInfo.transferEncoding; 121 | const responseBodyInfo = await safeGetNextJsBody(response, options); 122 | logData.response.body = responseBodyInfo.body; 123 | logData.response.transferEncoding = requestBodyInfo.transferEncoding; 124 | } 125 | 126 | if (!options.noAutoHideSensitive) { 127 | var noAutoHideSensitiveStartTime = Date.now(); 128 | // autoHide 129 | try { 130 | logData.request.headers = hashSensitive(logData.request.headers, options.debug); 131 | logData.request.body = hashSensitive(logData.request.body, options.debug); 132 | logData.response.headers = hashSensitive(logData.response.headers, options.debug); 133 | logData.response.body = hashSensitive(logData.response.body, options.debug); 134 | } catch (err) { 135 | logMessage(options.debug, 'formatEventDataAndSave', 'error on hashSensitive err=' + err); 136 | } 137 | var noAutoHideSensitiveEndTime = Date.now(); 138 | } 139 | if (logData.response.headers[TRANSACTION_ID_HEADER]) { 140 | logData.request.headers[TRANSACTION_ID_HEADER] = 141 | logData.response.headers[TRANSACTION_ID_HEADER]; 142 | } 143 | 144 | logMessage(options.debug, `extractNextJsEventDataAndSave`, `finished formatting nextjs log data and sending to moesif`); 145 | 146 | logData = options.maskContent(logData); 147 | 148 | return saveEvent(logData); 149 | } 150 | 151 | module.exports = { 152 | extractNextJsEventDataAndSave, 153 | }; 154 | -------------------------------------------------------------------------------- /lib/outgoing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var dataUtils = require('./dataUtils'); 6 | var util = require('util'); 7 | var nodeUrl = require('url'); 8 | 9 | var getEventModelFromRequestAndResponse = dataUtils.getEventModelFromRequestAndResponse; 10 | var appendChunk = dataUtils.appendChunk; 11 | 12 | function isMoesif(request, requestOptions) { 13 | if (typeof requestOptions === 'string') { 14 | if(requestOptions.includes('moesif.net')) return true; 15 | } 16 | if (request && typeof request.getHeader === 'function') { 17 | if (request.getHeader('X-Moesif-SDK') || request.getHeader('X-Moesif-Application-Id')) 18 | return true; 19 | } 20 | 21 | if (requestOptions && requestOptions.host && typeof requestOptions.host === 'string') { 22 | if (requestOptions.host.includes('moesif.net')) return true; 23 | } 24 | 25 | if (requestOptions && requestOptions.headers) { 26 | if (requestOptions.headers['X-Moesif-SDK'] || requestOptions.headers['X-Moesif-Application-Id']) 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | // based on https://github.com/nodejs/node/blob/0324529e0fa234b8102c1a6a1cde19c76a6fff82/lib/internal/url.js#L1406 33 | function urlToHttpOptions(url) { 34 | const options = { 35 | protocol: url.protocol, 36 | hostname: 37 | typeof url.hostname === 'string' && url.hostname.indexOf('[') === 0 38 | ? url.hostname.slice(1, -1) 39 | : url.hostname, 40 | hash: url.hash, 41 | search: url.search, 42 | pathname: url.pathname, 43 | path: `${url.pathname || ""}${url.search || ""}`, 44 | href: url.href 45 | }; 46 | if (url.port !== '') { 47 | options.port = Number(url.port); 48 | } 49 | if (url.username || url.password) { 50 | options.auth = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`; 51 | } 52 | return options; 53 | } 54 | 55 | 56 | // handle these scenarios 57 | // http.request(options) 58 | // http.request(urlString, options); 59 | // http.request(urlString); (simple get). 60 | // http.request(URLObject, options); 61 | // http.request(URLObject); (simple get). 62 | // below is based on official nodejs code for http 63 | function standardizeRequestOption(input, options, cb) { 64 | if (typeof input === 'string') { 65 | const urlStr = input; 66 | input = urlToHttpOptions(new nodeUrl.URL(urlStr)); 67 | } else if (input instanceof nodeUrl.URL) { 68 | // url.URL instance 69 | input = urlToHttpOptions(input); 70 | } else { 71 | cb = options; 72 | options = input; 73 | input = null; 74 | } 75 | 76 | if (typeof options === 'function') { 77 | cb = options; 78 | options = input || {}; 79 | } else { 80 | options = Object.assign(input || {}, options); 81 | } 82 | 83 | return options; 84 | } 85 | 86 | function track(requestOptions, request, recorder, logger, moesifOptions) { 87 | if (isMoesif(request, requestOptions)) { 88 | logger('skip capturing requests to moesif itself'); 89 | return; 90 | } 91 | 92 | var startTime = new Date(); 93 | 94 | var originalRequestWrite = request.write; 95 | var requestBody = null; 96 | var finished = false; 97 | var debugString = requestOptions; 98 | if (typeof requestOptions === 'object' && requestOptions); { 99 | debugString = (requestOptions.hostname || requestOptions.host) + (requestOptions.path || requestOptions.pathname); 100 | logger('initiating capturing of outing ' + util.inspect(requestOptions)); 101 | } 102 | 103 | request.write = function(chunk, encoding, callback) { 104 | var writeReturnValue = originalRequestWrite.call(request, chunk, encoding, callback); 105 | logger("write outgoing request body for " + debugString + chunk); 106 | requestBody = appendChunk(requestBody, chunk); 107 | return writeReturnValue; 108 | }; 109 | 110 | var originalRequestEnd = request.end; 111 | request.end = function(chunk, encoding, callback) { 112 | var endReturnValue = originalRequestEnd.call(request, chunk, encoding, callback); 113 | logger('end outgoing request body for ' + debugString + chunk); 114 | requestBody = appendChunk(requestBody, chunk); 115 | return endReturnValue; 116 | }; 117 | 118 | request.on("response", function (res) { 119 | var responseBody = null; 120 | logger("on response triggered in moesif " + debugString); 121 | var endTime = new Date(); // this will most likely be overriden. 122 | 123 | if (moesifOptions && moesifOptions.outgoingPatch) { 124 | var myStream = res; 125 | var dataEventTracked = false; 126 | var endEventTracked = false; 127 | myStream._mo_on = myStream.on; 128 | 129 | myStream.on = function (evt, handler) { 130 | var passOnHandler = handler; 131 | if (evt === "data" && !dataEventTracked) { 132 | logger("tracking outgoing response Data Event " + debugString); 133 | dataEventTracked = true; 134 | passOnHandler = function (chs) { 135 | logger( 136 | "outgoing response Data handler received for " + 137 | debugString + 138 | " " + 139 | chs 140 | ); 141 | responseBody = appendChunk(responseBody, chs); 142 | // always update end time in case end event is not triggered. 143 | endTime = new Date(); 144 | return handler(chs); 145 | }; 146 | } else if (evt === "end" && !endEventTracked) { 147 | logger("tracking outgoing response End event " + debugString); 148 | endEventTracked = true; 149 | passOnHandler = function (chs) { 150 | logger("outgoing response End handler" + debugString); 151 | endTime = new Date(); 152 | 153 | if (!finished) { 154 | finished = true; 155 | recorder( 156 | getEventModelFromRequestAndResponse( 157 | requestOptions, 158 | request, 159 | startTime, 160 | requestBody, 161 | res, 162 | endTime, 163 | responseBody 164 | ) 165 | ); 166 | } 167 | 168 | return handler(chs); 169 | }; 170 | } 171 | return myStream._mo_on(evt, passOnHandler); 172 | }; 173 | } else { 174 | res.on('data', function(d) { 175 | logger('outgoing data received', d); 176 | responseBody = appendChunk(responseBody, d); 177 | }); 178 | 179 | // only triggered when an event is aborted, 180 | // at this point, since "error" on request 181 | // isn't started. I need to count on this abort to 182 | // let me know the end point. 183 | res.on('abort', function() { 184 | logger('on abort is triggered in response'); 185 | logger('raw responsebody from out going API call is'); 186 | logger(responseBody); 187 | finished = true; 188 | recorder( 189 | getEventModelFromRequestAndResponse( 190 | requestOptions, 191 | request, 192 | startTime, 193 | requestBody, 194 | res, 195 | endTime, 196 | responseBody 197 | ) 198 | ); 199 | }); 200 | 201 | res.on('end', function() { 202 | var endTime = new Date(); 203 | logger('outgoing response end event for outgoing call'); 204 | logger(responseBody); 205 | finished = true; 206 | recorder( 207 | getEventModelFromRequestAndResponse( 208 | requestOptions, 209 | request, 210 | startTime, 211 | requestBody, 212 | res, 213 | endTime, 214 | responseBody 215 | ) 216 | ); 217 | }); 218 | } 219 | }); 220 | 221 | // if req.abort() is called before request connection started. 222 | // 'error' on request is always triggered at somepoint. 223 | // but if req.abort() is called have response object already exists, 224 | // then "error" on request is not triggered. 225 | 226 | request.on('error', function(error) { 227 | logger('on error for outgoing request ' + debugString, error); 228 | finished = true; 229 | var endTime = new Date(); 230 | recorder( 231 | getEventModelFromRequestAndResponse( 232 | requestOptions, 233 | request, 234 | startTime, 235 | requestBody, 236 | null, 237 | endTime, 238 | null 239 | ) 240 | ); 241 | }); 242 | 243 | // fail safe if not finished 244 | setTimeout(() => { 245 | if (!finished) { 246 | logger('outbound request longer than 2 second, timing out. log what we have.' + debugString); 247 | finished = true; 248 | var endTime = new Date(); 249 | recorder( 250 | getEventModelFromRequestAndResponse( 251 | requestOptions, 252 | request, 253 | startTime, 254 | requestBody, 255 | null, 256 | endTime, 257 | null 258 | ) 259 | ); 260 | } 261 | }, moesifOptions.maxOutgoingTimeout || 30000); 262 | } 263 | 264 | function _patch(recorder, logger, moesifOptions) { 265 | var originalGet = http.get; 266 | var originalHttpsGet = https.get; 267 | 268 | var originalRequest = http.request; 269 | var originalHttpsRequest = https.request; 270 | 271 | // On node >= v0.11.12 and < 9.0 (excluding 8.9.0) https.request just calls http.request (with additional options). 272 | // On node < 0.11.12, 8.9.0, and 9.0 > https.request is handled separately 273 | // Patch both and leave add a _mo_tracked flag to prevent double tracking. 274 | 275 | http.request = function(options, ...requestArgs) { 276 | var request = originalRequest.call(http, options, ...requestArgs); 277 | if (!request._mo_tracked) { 278 | request._mo_tracked = true; 279 | var requestOptions = standardizeRequestOption(options, ...requestArgs); 280 | track(requestOptions, request, recorder, logger, moesifOptions); 281 | } 282 | return request; 283 | }; 284 | 285 | https.request = function(options, ...requestArgs) { 286 | var request = originalHttpsRequest.call(https, options, ...requestArgs); 287 | if (!request._mo_tracked) { 288 | request._mo_tracked = true; 289 | var requestOptions = standardizeRequestOption(options, ...requestArgs); 290 | track(requestOptions, request, recorder, logger, moesifOptions); 291 | } 292 | return request; 293 | }; 294 | 295 | http.get = function(options, ...requestArgs) { 296 | var request = http.request.call(http, options, ...requestArgs); 297 | request.end(); 298 | return request; 299 | }; 300 | 301 | https.get = function(options, ...requestArgs) { 302 | var request = https.request.call(https, options, ...requestArgs); 303 | request.end(); 304 | return request; 305 | }; 306 | 307 | function _unpatch() { 308 | http.request = originalRequest; 309 | https.request = originalHttpsRequest; 310 | http.get = originalGet; 311 | https.get = originalHttpsGet; 312 | } 313 | 314 | return _unpatch; 315 | } 316 | 317 | module.exports = _patch; 318 | -------------------------------------------------------------------------------- /lib/outgoingRecorder.js: -------------------------------------------------------------------------------- 1 | var assign = require('lodash/assign'); 2 | var dataUtils = require('./dataUtils'); 3 | 4 | var hashSensitive = dataUtils.hashSensitive; 5 | 6 | function createMockIncomingRequestResponse(logData) { 7 | var getHeader = function(name) { 8 | var lowerCaseName = typeof name === 'string' && name.toLowerCase(); 9 | return this.headers[name] || this.headers[lowerCaseName]; 10 | }; 11 | var req = { 12 | _mo_mocked: true, 13 | headers: logData.request.headers || {}, 14 | method: logData.request.verb, 15 | url: logData.request.uri, 16 | getHeader: getHeader, 17 | get: getHeader, 18 | body: logData.request.body 19 | }; 20 | 21 | var res = { 22 | _mo_mocked: true, 23 | headers: logData.response.headers || {}, 24 | statusCode: logData.response.status, 25 | getHeader: getHeader, 26 | get: getHeader, 27 | body: logData.response.body 28 | }; 29 | 30 | return { 31 | request: req, 32 | response: res 33 | }; 34 | } 35 | 36 | function _createOutgoingRecorder(saveEvent, moesifOptions, logger) { 37 | return function(capturedData) { 38 | 39 | // Already have more comprehensive short circuit upstream. 40 | // so comment below check. 41 | // if (capturedData.request.uri && capturedData.request.uri.includes('moesif.net')) { 42 | // // skip if it is moesif. 43 | // logger('request skipped since it is moesif'); 44 | // return; 45 | // } 46 | 47 | // apply moesif options: 48 | 49 | // we do this to make the outging request and response look like signature of 50 | // incoming request and responses, so that the moesif express options (which are designed for incoming request) 51 | // can be called. 52 | // and put everything in try block, just in case. 53 | var mock = createMockIncomingRequestResponse(capturedData); 54 | 55 | var logData = assign({}, capturedData); 56 | 57 | if (!moesifOptions.skip(mock.request, mock.response)) { 58 | if (!moesifOptions.noAutoHideSensitive) { 59 | // autoHide 60 | try { 61 | logData.request.headers = hashSensitive(logData.request.headers, moesifOptions.debug); 62 | logData.request.body = hashSensitive(logData.request.body, moesifOptions.debug); 63 | logData.response.headers = hashSensitive(logData.response.headers, moesifOptions.debug); 64 | logData.response.body = hashSensitive(logData.response.body, moesifOptions.debug); 65 | } catch (err) { 66 | logger('error on hashSensitive err=' + err); 67 | } 68 | } 69 | 70 | logData = moesifOptions.maskContent(logData); 71 | 72 | try { 73 | logData.userId = moesifOptions.identifyUser(mock.request, mock.response); 74 | } catch (err) { 75 | logger('error identify user:' + err); 76 | } 77 | 78 | try { 79 | logData.companyId = moesifOptions.identifyCompany(mock.request, mock.response); 80 | } catch (err) { 81 | logger('error identifying company:' + err); 82 | } 83 | 84 | try { 85 | logData.sessionToken = moesifOptions.getSessionToken(mock.request, mock.response); 86 | } catch (err) { 87 | logger('error getSessionToken' + err); 88 | } 89 | 90 | try { 91 | logData.tags = moesifOptions.getTags(mock.request, mock.response); 92 | } catch (err) { 93 | logger('error getTags' + err); 94 | } 95 | 96 | try { 97 | logData.request.apiVersion = moesifOptions.getApiVersion(mock.request, mock.response); 98 | } catch (err) { 99 | logger('error getApiVersion' + err); 100 | } 101 | 102 | try { 103 | logData.metadata = moesifOptions.getMetadata(mock.request, mock.response); 104 | } catch (err) { 105 | logger('error adding metadata:' + err); 106 | } 107 | 108 | // logBody option 109 | if (!moesifOptions.logBody) { 110 | logData.request.body = null; 111 | logData.response.body = null; 112 | } 113 | 114 | // Set API direction 115 | logData.direction = "Outgoing" 116 | 117 | logger('queueing outgoing event to be sent to Moesif', logData); 118 | 119 | saveEvent(logData); 120 | } 121 | }; 122 | } 123 | 124 | module.exports = _createOutgoingRecorder; 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moesif-nodejs", 3 | "version": "3.9.1", 4 | "description": "Monitoring agent to log API calls to Moesif for deep API analytics", 5 | "main": "lib/index.js", 6 | "typings": "dist/index.d.ts", 7 | "keywords": [ 8 | "moesif", 9 | "api", 10 | "gateway", 11 | "express", 12 | "api", 13 | "express", 14 | "debug", 15 | "logging", 16 | "monitoring", 17 | "trace", 18 | "analytics", 19 | "graphql", 20 | "rpc" 21 | ], 22 | "author": { 23 | "name": "Xing Wang", 24 | "email": "xing@moesif.com", 25 | "url": "https://www.moesif.com" 26 | }, 27 | "license": "Apache-2.0", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/Moesif/moesif-nodejs" 31 | }, 32 | "dependencies": { 33 | "bytes": "^3.1.1", 34 | "card-validator": "^8.1.1", 35 | "content-type": "^1.0.4", 36 | "crypto-js": "^4.1.1", 37 | "http-errors": "^2.0.0", 38 | "iconv-lite": "^0.6.3", 39 | "koa-body": "^4.2.0", 40 | "lodash": "^4.17.19", 41 | "moesifapi": ">=3.1.2", 42 | "raw-body": "^2.4.2", 43 | "request-ip": "^3.3.0", 44 | "uuid4": "^2.0.2" 45 | }, 46 | "devDependencies": { 47 | "@eslint/js": "^9.8.0", 48 | "@types/node": "^18.15.11", 49 | "assert": "^2.0.0", 50 | "blanket": "^1.2.3", 51 | "chai": "^4.3.10", 52 | "eslint": "^9.8.0", 53 | "express": "^5.0.0", 54 | "express-unless": "^1.0.0", 55 | "globals": "^15.9.0", 56 | "mocha": "^9.2.1", 57 | "node-mocks-http": "^1.11.0", 58 | "promise": "^8.1.0", 59 | "should": "^13.2.3", 60 | "travis-cov": "^0.2.5", 61 | "typescript": "^5.0.3", 62 | "util": "^0.12.4" 63 | }, 64 | "scripts": { 65 | "test": "node_modules/.bin/mocha --reporter spec", 66 | "build:types": "npx tsc" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/batcherUnit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var creatBatcher = require('../lib/batcher'); 4 | 5 | 6 | var RUN_TEST = false; 7 | 8 | if (RUN_TEST) { 9 | describe('unit test for batcher module', function() { 10 | this.timeout(10000); 11 | 12 | it('simple batch triggered by size', function(done) { 13 | var batcher = creatBatcher(function(dataArray) { 14 | console.log(dataArray); 15 | assert(dataArray.length === 3); 16 | done(); 17 | }, 3, 10000); 18 | 19 | batcher.add('2'); 20 | batcher.add('1352'); 21 | batcher.add('523'); 22 | batcher.add('523423'); 23 | }); // end of it 24 | 25 | it('simple batch triggered bu maxtime.', function(done) { 26 | var batcher = creatBatcher(function(dataArray) { 27 | console.log(dataArray); 28 | assert(dataArray.length === 1); 29 | done(); 30 | }, 3, 1000); 31 | 32 | batcher.add('2'); 33 | }); // end of it 34 | 35 | 36 | 37 | it('batch triggered by size 3 times, and then triggered by time', function(done) { 38 | var triggerCount = 0; 39 | var batcher = creatBatcher(function(dataArray) { 40 | console.log('batcher triggered:' + triggerCount); 41 | console.log(dataArray); 42 | 43 | if (triggerCount === 0) { 44 | assert(dataArray.length === 3); 45 | } 46 | if (triggerCount === 1) { 47 | assert(dataArray.length === 3); 48 | } 49 | if (triggerCount === 2) { 50 | assert(dataArray.length === 1); 51 | done(); 52 | } 53 | triggerCount = triggerCount + 1; 54 | }, 3, 1000); 55 | 56 | batcher.add('1'); 57 | batcher.add('2'); 58 | batcher.add('3'); 59 | batcher.add('4'); 60 | batcher.add('5'); 61 | batcher.add('6'); 62 | batcher.add('7'); 63 | }); // end of it 64 | 65 | it('batch triggered by time 2 times, and then triggered by size', function(done) { 66 | var triggerCount = 0; 67 | 68 | var startTime = Date.now(); 69 | 70 | var batcher = creatBatcher(function(dataArray) { 71 | console.log('batcher triggered:' + triggerCount); 72 | console.log(dataArray); 73 | console.log('from now'); 74 | console.log(Date.now() - startTime); 75 | 76 | if (triggerCount === 0) { 77 | assert(dataArray.length === 2); 78 | } 79 | if (triggerCount === 1) { 80 | assert(dataArray.length === 1); 81 | } 82 | if (triggerCount === 2) { 83 | assert(dataArray.length === 3); 84 | done(); 85 | } 86 | triggerCount = triggerCount + 1; 87 | }, 3, 1000); 88 | 89 | 90 | batcher.add('1'); 91 | batcher.add('2'); 92 | 93 | setTimeout(() => { 94 | batcher.add('3'); 95 | }, 2000); 96 | 97 | setTimeout(() => { 98 | batcher.add('4'); 99 | batcher.add('5'); 100 | batcher.add('6'); 101 | }, 4000); 102 | }); // end of it 103 | 104 | }); // end of describe 105 | 106 | } // end of if(RUN_TEST) 107 | -------------------------------------------------------------------------------- /test/governanceRuleUnit.js: -------------------------------------------------------------------------------- 1 | var moesifapi = require('moesifapi'); 2 | var assert = require('assert'); 3 | var moesifConfigManager = require('../lib/moesifConfigManager'); 4 | var governanceRulesManager = require('../lib/governanceRulesManager'); 5 | 6 | var RUN_TEST = false; 7 | 8 | if (RUN_TEST) { 9 | describe('governance rules unit test', function () { 10 | var config = moesifapi.configuration; 11 | config.ApplicationId = 'Your Moesif Applicaiton Id'; 12 | 13 | it('can load rules and verify cached correctly', function () { 14 | return governanceRulesManager.tryGetRules().then((result) => { 15 | console.log(JSON.stringify(result, null, ' ')); 16 | console.log(JSON.stringify(governanceRulesManager.userRulesHashByRuleId, null, ' ')); 17 | console.log(JSON.stringify(governanceRulesManager.unidentifiedCompanyRules, null, ' ')); 18 | }); 19 | }); 20 | 21 | it('get applicable user rules for unidentifed user', function () { 22 | var requestFields = { 23 | 'request.verb': 'GET', 24 | 'request.ip_address': '125.2.3.2', 25 | 'request.route': '', 26 | 'request.body.operationName': 'operator name', 27 | }; 28 | var requestBody = { 29 | subject: 'should_block', 30 | }; 31 | 32 | var applicableRules = governanceRulesManager._getApplicableRegexRules( 33 | requestFields, 34 | requestBody 35 | ); 36 | console.log( 37 | 'applicableRules : ' + applicableRules.length + ' ' + JSON.stringify(applicableRules) 38 | ); 39 | assert(applicableRules.length === 1, 'expected 1 rule to match for regex rule'); 40 | }); 41 | 42 | it('can get applicable rules for identified user who is in cohort rule', function () { 43 | var requestFields = { 44 | 'request.route': 'test/no_italy', 45 | }; 46 | 47 | var requestBody = { 48 | subject: 'should_block', 49 | }; 50 | 51 | var userId = 'rome1'; 52 | // https://www.moesif.com/wrap/app/88:210-660:387/governance-rule/64a783a3e7d62b036d16006e 53 | 54 | var config_user_rules_values = [ 55 | { 56 | rules: '64a783a3e7d62b036d16006e', 57 | values: { 58 | 0: 'rome', 59 | 1: 'some value for 1', 60 | 2: 'some value for 2', 61 | }, 62 | }, 63 | ]; 64 | 65 | var applicableRules = governanceRulesManager._getApplicableUserRules( 66 | config_user_rules_values, 67 | requestFields, 68 | requestBody 69 | ); 70 | console.log( 71 | 'applicableRules : ' + applicableRules.length + ' ' + JSON.stringify(applicableRules) 72 | ); 73 | assert(applicableRules.length === 1, 'expected 1 rule to match for applicable user rules'); 74 | }); 75 | 76 | it('get applicable users rules to a user in cohort but rule is not in cohort', function () { 77 | var requestFields = { 78 | 'request.route': 'hello/canada', 79 | }; 80 | 81 | var requestBody = { 82 | from_location: 'canada', 83 | }; 84 | 85 | var userId = 'vancouver'; 86 | 87 | // https://www.moesif.com/wrap/app/88:210-660:387/governance-rule/64a783a43660b60f7c766a06 88 | var config_user_rules_values = [ 89 | { 90 | rules: '64a783a43660b60f7c766rando', 91 | values: { 92 | 0: 'city', 93 | 1: 'some value for 1', 94 | 2: 'some value for 2', 95 | }, 96 | }, 97 | ]; 98 | 99 | var applicableRules = governanceRulesManager._getApplicableUserRules( 100 | config_user_rules_values, 101 | requestFields, 102 | requestBody 103 | ); 104 | console.log( 105 | 'applicableRules : ' + applicableRules.length + ' ' + JSON.stringify(applicableRules) 106 | ); 107 | assert( 108 | applicableRules.length === 1, 109 | 'expected 1 rule to match for user in cohort rule is not in cohort' 110 | ); 111 | }); 112 | 113 | it('can apply multiple rules', function () { 114 | var requestFields = { 115 | 'request.route': 'hello/canada', 116 | }; 117 | var requestBody = { 118 | from_location: 'cairo', 119 | }; 120 | 121 | var applicableRules = governanceRulesManager._getApplicableUserRules( 122 | null, 123 | requestFields, 124 | requestBody 125 | ); 126 | console.log( 127 | 'applicableRules : ' + applicableRules.length + ' ' + JSON.stringify(applicableRules) 128 | ); 129 | assert( 130 | applicableRules.length === 2, 131 | 'expected 2 rules for user not in cohort, regex should match 2 rules' 132 | ); 133 | 134 | var responseHolder = { 135 | headers: {}, 136 | }; 137 | 138 | var newResponseHolder = governanceRulesManager.applyRuleList( 139 | applicableRules, 140 | responseHolder, 141 | null 142 | ); 143 | 144 | console.log(JSON.stringify(newResponseHolder, null, ' ')); 145 | 146 | assert(!!newResponseHolder.blocked_by, 'blocked by should exists'); 147 | }); 148 | }); // end describe 149 | } 150 | -------------------------------------------------------------------------------- /test/mockserver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xingheng on 10/13/16. 3 | */ 4 | 5 | var assert = require('assert'); 6 | var util = require('util'); 7 | 8 | var mocks = require('node-mocks-http'); 9 | var Promise = require('promise/lib/es6-extensions'); 10 | var should = require('should'); 11 | var expect = require('chai').expect; 12 | var extend = require('lodash/extend'); 13 | var moesif = require('../lib'); 14 | 15 | // replace with an moesif application id token to test.. 16 | var TEST_API_SECRET_KEY = 'Your Moesif Application Id'; 17 | var RUN_TEST = false; 18 | 19 | function mockReq(reqMock) { 20 | var reqSpec = extend( 21 | { 22 | method: 'GET', 23 | url: '/hello', 24 | hostname: 'localhost:3000', 25 | protocol: 'http', 26 | headers: { 27 | header1: 'value 1' 28 | }, 29 | ip: '127.0.0.1', 30 | query: { 31 | val: '1' 32 | }, 33 | params: { 34 | id: 20 35 | } 36 | }, 37 | reqMock 38 | ); 39 | 40 | return mocks.createRequest(reqSpec); 41 | } 42 | 43 | function mockRes() { 44 | var res = mocks.createResponse(); 45 | res.status(200); 46 | return res; 47 | } 48 | 49 | function loggerTestHelper(providedOptions, moesifOptions) { 50 | var options = extend( 51 | { 52 | loggerOptions: null, 53 | req: null, 54 | res: null, 55 | next: function(req, res, next) { 56 | res.end('{ "msg": "ok."}'); 57 | } 58 | }, 59 | providedOptions 60 | ); 61 | 62 | var req = mockReq(options.req); 63 | // console.log('mocked req ' + JSON.stringify(req)); 64 | var res = extend(mockRes(), options.res); 65 | 66 | return new Promise(function(resolve, reject) { 67 | var moesifMiddleWareOptions = extend( 68 | { 69 | applicationId: TEST_API_SECRET_KEY, 70 | logBody: true, 71 | callback: function(err, logData) { 72 | if (err) { 73 | reject(err); 74 | } else { 75 | resolve(logData); 76 | } 77 | } 78 | }, 79 | moesifOptions 80 | ); 81 | 82 | var middleware = moesif(moesifMiddleWareOptions); 83 | 84 | middleware(req, res, function(_req, _res, next) { 85 | options.next(req, res, next); 86 | // resolve(result); 87 | }); 88 | }); 89 | } 90 | 91 | if (RUN_TEST) { 92 | describe('moesif-nodejs', function() { 93 | describe('fail cases', function() { 94 | it('throw an error when not provided an application id.', function() { 95 | expect(function() { 96 | moesif({}); 97 | }).to.throw(Error); 98 | }); 99 | 100 | it('throw an error when identifyUser is not a function', function() { 101 | expect(function() { 102 | moesif({ 103 | applicationId: TEST_API_SECRET_KEY, 104 | identifyUser: 'abc' 105 | }); 106 | }).to.throw(Error); 107 | }); 108 | }); 109 | 110 | describe('success cases', function() { 111 | this.timeout(3000); 112 | 113 | it('middleware should be function that takes 3 arguments', function() { 114 | expect(moesif({ applicationId: TEST_API_SECRET_KEY }).length).to.equal(3); 115 | }); 116 | 117 | it('test one successful submission without body', function(done) { 118 | function next(req, res, next) { 119 | res.end(); 120 | } 121 | 122 | var testHelperOptions = { 123 | next: next, 124 | req: { 125 | url: '/testnobody' 126 | } 127 | }; 128 | loggerTestHelper(testHelperOptions) 129 | .then(function(result) { 130 | // console.log('inside callback of loggerTesthelper'); 131 | // console.log(JSON.stringify(result)); 132 | expect(result[0].response).to.exist; 133 | expect(result[0].request).to.exist; 134 | done(); 135 | // console.log('result in tester is:' + JSON.stringify(result, null, ' ')); 136 | }) 137 | .catch(function(err) { 138 | done(err); 139 | }); 140 | }); 141 | 142 | it('test moesif with body', function(done) { 143 | function next(req, res, next) { 144 | res.end('{"bodycontent1": "bodycontent1"}'); 145 | } 146 | 147 | var testHelperOptions = { 148 | next: next, 149 | req: { 150 | body: {}, 151 | url: '/testwithbody' 152 | } 153 | }; 154 | 155 | loggerTestHelper(testHelperOptions) 156 | .then(function(result) { 157 | expect(result[0].response.body.bodycontent1).to.equal('bodycontent1'); 158 | done(); 159 | }) 160 | .catch(function(err) { 161 | done(err); 162 | }); 163 | }); 164 | 165 | it('test moesif with identifyUser function', function(done) { 166 | function next(req, res, next) { 167 | res.end('{"test": "test moesif with identifyUser function"}'); 168 | } 169 | 170 | var testHelperOptions = { 171 | next: next, 172 | req: { 173 | body: {}, 174 | url: '/testwithidentifyuser' 175 | } 176 | }; 177 | 178 | var testMoesifOptions = { 179 | applicationId: TEST_API_SECRET_KEY, 180 | identifyUser: function(_req, _res) { 181 | return 'abc'; 182 | } 183 | }; 184 | 185 | loggerTestHelper(testHelperOptions, testMoesifOptions) 186 | .then(function(result) { 187 | expect(result[0].userId).to.equal('abc'); 188 | done(); 189 | }) 190 | .catch(function(err) { 191 | done(err); 192 | }); 193 | }); 194 | 195 | it('test moesif with maskContent function', function(done) { 196 | function next(req, res, next) { 197 | res.end('{"test": "test moesif with maskContent function"}'); 198 | } 199 | 200 | var testHelperOptions = { 201 | next: next, 202 | req: { 203 | headers: { 204 | header1: 'value 1', 205 | header2: 'value 2', 206 | header3: 'value 3' 207 | }, 208 | body: { requestbody1: 'requestbody1' }, 209 | url: '/testwithmaskcontent' 210 | } 211 | }; 212 | 213 | var testMoesifOptions = { 214 | applicationId: TEST_API_SECRET_KEY, 215 | maskContent: function(_logData) { 216 | var maskedLogData = extend({}, _logData); 217 | maskedLogData.request.headers.header1 = undefined; 218 | return maskedLogData; 219 | } 220 | }; 221 | 222 | loggerTestHelper(testHelperOptions, testMoesifOptions) 223 | .then(function(result) { 224 | expect(result[0].request.headers.header1).to.not.exist; 225 | expect(result[0].request.headers.header2).to.equal('value 2'); 226 | done(); 227 | }) 228 | .catch(function(err) { 229 | done(err); 230 | }); 231 | }); 232 | 233 | it('test moesif with html body', function(done) { 234 | function next(req, res, next) { 235 | res.end('

response body

response body is html

'); 236 | } 237 | 238 | var testHelperOptions = { 239 | next: next, 240 | req: { 241 | headers: { 242 | header1: 'value 1', 243 | header2: 'value 2', 244 | header3: 'value 3' 245 | }, 246 | body: '

request body

request body is html

', 247 | url: '/testwithhtmlbody' 248 | } 249 | }; 250 | 251 | var testMoesifOptions = { 252 | applicationId: TEST_API_SECRET_KEY 253 | }; 254 | 255 | loggerTestHelper(testHelperOptions, testMoesifOptions) 256 | .then(function(result) { 257 | expect(result[0].request.transferEncoding).to.equal('base64'); 258 | expect(result[0].response.transferEncoding).to.equal('base64'); 259 | done(); 260 | }) 261 | .catch(function(err) { 262 | done(err); 263 | }); 264 | }); 265 | 266 | it('test moesif with malformed json', function(done) { 267 | function next(req, res, next) { 268 | res.end('{[abcd: '); 269 | } 270 | 271 | var testHelperOptions = { 272 | next: next, 273 | req: { 274 | headers: { 275 | 'Content-Type': 'application/json', 276 | header2: 'value 2', 277 | header3: 'value 3' 278 | }, 279 | body: '{"body1": "body1val"}', 280 | url: '/malformedbody' 281 | } 282 | }; 283 | 284 | var testMoesifOptions = { 285 | applicationId: TEST_API_SECRET_KEY, 286 | maskContent: function(_logData) { 287 | var maskedLogData = extend({}, _logData); 288 | maskedLogData.request.headers.header1 = undefined; 289 | return maskedLogData; 290 | } 291 | }; 292 | 293 | loggerTestHelper(testHelperOptions, testMoesifOptions) 294 | .then(function(result) { 295 | done(); 296 | }) 297 | .catch(function(err) { 298 | done(err); 299 | }); 300 | }); 301 | 302 | it('should be able to update user profile to Moesif.', function(done) { 303 | var moesifMiddleware = moesif({ applicationId: TEST_API_SECRET_KEY }); 304 | 305 | moesifMiddleware.updateUser( 306 | { 307 | userId: '12345', 308 | companyId: '67890', 309 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' }, 310 | campaign: { utmSource: 'Newsletter', utmMedium: 'Email'} 311 | }, 312 | function(error, response, context) { 313 | expect(context.response.statusCode).to.equal(201); 314 | if (error) done(error); 315 | else done(); 316 | } 317 | ); 318 | }); 319 | 320 | it('should be able to update user profiles in batch to Moesif.', function(done) { 321 | var moesifMiddleware = moesif({ applicationId: TEST_API_SECRET_KEY }); 322 | 323 | var users = [] 324 | 325 | users.push({ 326 | userId: '12345', 327 | companyId: '67890', 328 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' } 329 | }) 330 | 331 | users.push({ 332 | userId: '1234', 333 | companyId: '6789', 334 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' } 335 | }) 336 | 337 | moesifMiddleware.updateUsersBatch(users, 338 | function(error, response, context) { 339 | expect(context.response.statusCode).to.equal(201); 340 | if (error) done(error); 341 | else done(); 342 | } 343 | ); 344 | }); 345 | 346 | it('should be able to update company profiles to Moesif.', function(done) { 347 | var moesifMiddleware = moesif({ applicationId: TEST_API_SECRET_KEY }); 348 | 349 | moesifMiddleware.updateCompany({ 350 | companyId: '12345', 351 | companyDomain: 'acmeinc.com', 352 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' }, 353 | campaign: { utmSource: 'Adwords', utmMedium: 'Twitter'} 354 | }, 355 | function(error, response, context) { 356 | expect(context.response.statusCode).to.equal(201); 357 | if (error) done(error); 358 | else done(); 359 | } 360 | ); 361 | }); 362 | 363 | it('should be able to update company profiles in batch to Moesif.', function(done) { 364 | var moesifMiddleware = moesif({ applicationId: TEST_API_SECRET_KEY }); 365 | 366 | var companies = [] 367 | 368 | companies.push({ 369 | companyId: '12345', 370 | companyDomain: 'nowhere.com', 371 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' } 372 | }) 373 | 374 | companies.push({ 375 | companyId: '1234', 376 | companyDomain: 'acmeinc.com', 377 | metadata: { email: 'abc@email.com', name: 'abcdef', image: '123' } 378 | }) 379 | 380 | moesifMiddleware.updateCompaniesBatch(companies, 381 | function(error, response, context) { 382 | expect(context.response.statusCode).to.equal(201); 383 | if (error) done(error); 384 | else done(); 385 | } 386 | ); 387 | }); 388 | }); 389 | }); 390 | } 391 | -------------------------------------------------------------------------------- /test/outgoingUnit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var https = require('https'); 4 | var patch = require('../lib/outgoing'); 5 | 6 | var RUN_TEST = false; 7 | 8 | if (RUN_TEST) { 9 | describe('unit test capture outgoing http requests', function() { 10 | before(function() { 11 | var logger = function(str) { 12 | console.log('[logger]: ' + str); 13 | }; 14 | var recorder = function(logData) { 15 | console.log('recorder is called'); 16 | console.log(JSON.stringify(logData, null, ' ')); 17 | }; 18 | patch(recorder, logger); 19 | }); 20 | 21 | it('test simple http get request is captured', function(done) { 22 | https.get({ 23 | host: 'jsonplaceholder.typicode.com', 24 | path: '/posts/1' 25 | }, function (res) { 26 | var body = ''; 27 | res.on('data', function(d) { 28 | body += d; 29 | }); 30 | 31 | res.on('end', function() { 32 | var parsed = JSON.parse(body); 33 | console.log(parsed); 34 | setTimeout(function () { 35 | // I need make sure the 36 | // recorder's end is called 37 | // before this ends. 38 | done(); 39 | }, 500); 40 | }); 41 | }) 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/outgoingWithMoesif.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var https = require('https'); 4 | var moesifapi = require('moesifapi'); 5 | var patch = require('../lib/outgoing'); 6 | var createOutgoingRecorder = require('../lib/outgoingRecorder'); 7 | 8 | var RUN_TEST = false; 9 | 10 | if (RUN_TEST) { 11 | describe('test capture using actual moesif api', function() { 12 | this.timeout(9000); 13 | 14 | before(function() { 15 | var config = moesifapi.configuration; 16 | config.ApplicationId = ''; 17 | // config.BaseUri = options.baseUri || options.BaseUri || config.BaseUri; 18 | var moesifController = moesifapi.ApiController; 19 | var logger = function(text) { 20 | console.log('[test logger]:' + text); 21 | }; 22 | 23 | var options = {}; 24 | 25 | options.identifyUser = 26 | options.identifyUser || 27 | function() { 28 | return undefined; 29 | }; 30 | 31 | options.identifyCompany = 32 | options.identifyCompany || 33 | function() { 34 | return undefined; 35 | }; 36 | 37 | options.logBody = true; 38 | 39 | options.getMetadata = 40 | options.getMetadata || 41 | function(req, res) { 42 | console.log('test get metadata is called'); 43 | console.log(JSON.stringify(req.headers)); 44 | console.log(JSON.stringify(res.headers)); 45 | console.log(res.getHeader('Date')); 46 | console.log(res.getHeader('date')); 47 | return undefined; 48 | }; 49 | 50 | options.getSessionToken = 51 | options.getSessionToken || 52 | function() { 53 | return undefined; 54 | }; 55 | options.getTags = 56 | options.getTags || 57 | function() { 58 | return undefined; 59 | }; 60 | options.getApiVersion = 61 | options.getApiVersion || 62 | function() { 63 | return '123,523'; 64 | }; 65 | options.maskContent = 66 | options.maskContent || 67 | function(eventData) { 68 | return eventData; 69 | }; 70 | 71 | options.skip = 72 | options.skip || 73 | function(req, res) { 74 | return false; 75 | }; 76 | 77 | var trySaveEventLocal = function(eventData) { 78 | moesifController.createEvent(new moesifapi.EventModel(eventData), function(err) { 79 | console.log('moesif API callback err=' + err); 80 | if (err) { 81 | console.log('moesif API failed with error.'); 82 | if (options.callback) { 83 | options.callback(err, eventData); 84 | } 85 | } else { 86 | console.log('moesif API succeeded'); 87 | if (options.callback) { 88 | options.callback(null, eventData); 89 | } 90 | } 91 | }); 92 | }; 93 | 94 | var recorder = createOutgoingRecorder(trySaveEventLocal, options, logger); 95 | 96 | var unpatch = patch(recorder, logger); 97 | console.log('patched successfully, return value of patch is'); 98 | console.log(unpatch); 99 | }); 100 | 101 | it('test a non json string body', function(done) { 102 | var req = http.request( 103 | { 104 | method: 'POST', 105 | host: 'jsonplaceholder.typicode.com', 106 | path: '/posts' 107 | }, 108 | function(res) { 109 | var body = ''; 110 | res.on('data', function(d) { 111 | body += d; 112 | }); 113 | 114 | res.on('end', function() { 115 | var parsed = JSON.parse(body); 116 | console.log(parsed); 117 | setTimeout(function() { 118 | // I need make sure the 119 | // recorder's end is triggered 120 | // before this ends. 121 | done(); 122 | }, 500); 123 | }); 124 | } 125 | ); 126 | 127 | req.write('not a json'); 128 | 129 | req.end(); 130 | }); 131 | 132 | // end of describe 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /test/outgoingWithMoesifExpress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var https = require('https'); 4 | var patch = require('../lib/outgoing'); 5 | var moesif = require('../lib'); 6 | 7 | var RUN_TEST = false; 8 | 9 | if (RUN_TEST) { 10 | describe('test capture using actual moesif express attached api', function() { 11 | this.timeout(9000); 12 | 13 | before(function() { 14 | var options = { 15 | debug: false, 16 | applicationId:'Your Moesif Application Id' 17 | }; 18 | // function to identify user. 19 | options.identifyUser = 20 | options.identifyUser || 21 | function() { 22 | return undefined; 23 | }; 24 | 25 | options.logBody = true; 26 | 27 | options.getMetadata = 28 | options.getMetadata || 29 | function(req, res) { 30 | return undefined; 31 | }; 32 | 33 | options.getSessionToken = 34 | options.getSessionToken || 35 | function() { 36 | return undefined; 37 | }; 38 | options.getTags = 39 | options.getTags || 40 | function() { 41 | return undefined; 42 | }; 43 | options.getApiVersion = 44 | options.getApiVersion || 45 | function() { 46 | return '123,523'; 47 | }; 48 | options.maskContent = 49 | options.maskContent || 50 | function(eventData) { 51 | return eventData; 52 | }; 53 | 54 | options.skip = 55 | options.skip || 56 | function(req, res) { 57 | return false; 58 | }; 59 | 60 | var middleware = moesif(options); 61 | middleware.startCaptureOutgoing(); 62 | }); 63 | 64 | it('test simple http get request is captured', function(done) { 65 | https.get( 66 | { 67 | host: 'jsonplaceholder.typicode.com', 68 | path: '/posts/1' 69 | }, 70 | function(res) { 71 | var body = ''; 72 | res.on('data', function(d) { 73 | body += d; 74 | }); 75 | 76 | res.on('end', function() { 77 | var parsed = JSON.parse(body); 78 | console.log(parsed); 79 | setTimeout(function() { 80 | // I need make sure the 81 | // recorder's end is called 82 | // before this ends. 83 | done(); 84 | }, 2000); 85 | }); 86 | } 87 | ); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/testConfig.js: -------------------------------------------------------------------------------- 1 | var moesifapi = require('moesifapi'); 2 | var assert = require('assert'); 3 | var moesifConfigManager = require('../lib/moesifConfigManager'); 4 | 5 | 6 | var RUN_TEST = false; 7 | 8 | if (RUN_TEST) { 9 | describe('moesif config manager tests', function () { 10 | var config = moesifapi.configuration; 11 | 12 | config.ApplicationId = 'Application Id'; 13 | 14 | it('can get moesif config manager', function (done) { 15 | moesifConfigManager.tryGetConfig(); 16 | setTimeout(() => { 17 | console.log('got config back'); 18 | console.log(JSON.stringify(moesifConfigManager._config, null, ' ')); 19 | assert( 20 | typeof moesifConfigManager._config === 'object', 21 | 'we should have app config back as object' 22 | ); 23 | done(); 24 | }, 1000); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/testGetIPaddress.js: -------------------------------------------------------------------------------- 1 | var requestIp = require('request-ip'); 2 | var assert = require('assert'); 3 | 4 | var RUN_TEST = false; 5 | 6 | if (RUN_TEST) { 7 | describe('Test the isolated case', function() { 8 | var fakeRequest = { 9 | headers: { 10 | 'x-forwarded-for': '20.56.20.20, 234.134.211.173' 11 | } 12 | } 13 | console.log('test fake requst'); 14 | 15 | const result = requestIp.getClientIp(fakeRequest); 16 | console.log(result); 17 | assert(result === '20.56.20.20', 'ip address should match first one'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/testSendingActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var moesifapi = require('moesifapi'); 4 | var creatBatcher = require('../lib/batcher'); 5 | var moesif = require('../lib/index'); 6 | const { expect } = require('chai'); 7 | const crypto = require('crypto'); 8 | 9 | var RUN_TEST = true; 10 | 11 | if (RUN_TEST) { 12 | describe('unit tests for sending actions', function () { 13 | this.timeout(10000); 14 | 15 | var middleWare = moesif({ 16 | applicationId: 17 | '', 18 | debug: true, 19 | // debug: 'instrumentation', 20 | }); 21 | 22 | it('send a single valid action', async function () { 23 | const actionName = "Clicked 'Sign up'"; 24 | const actionMetadata = { 25 | button_label: 'Get Started', 26 | sign_up_method: 'Google SSO', 27 | }; 28 | const actionReqContext = { 29 | uri: 'https://api.acmeinc.com/get-started/', 30 | ipAddress: '199.2.232.2', 31 | }; 32 | const actionModel = { 33 | actionName: actionName, 34 | metadata: actionMetadata, 35 | request: actionReqContext, 36 | }; 37 | middleWare.sendAction(actionModel, function (err, resp) { 38 | if (err) { 39 | console.log(err); 40 | } else { 41 | } 42 | done(); 43 | }); 44 | }); 45 | 46 | it('send a batch of valid actions', async function () { 47 | var req_contextA = { 48 | time: new Date(), 49 | uri: 'https://api.acmeinc.com/items/reviews/', 50 | ipAddress: '69.48.220.123', 51 | userAgentString: 52 | 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', 53 | }; 54 | 55 | var req_contextB = { 56 | time: new Date(), 57 | uri: 'https://api.acmeinc.com/pricing/', 58 | ipAddress: '61.48.220.126', 59 | userAgentString: 'PostmanRuntime/7.26.5', 60 | }; 61 | 62 | // Define the actions. 63 | var actions = [ 64 | { 65 | transactionId: crypto.randomUUID(), 66 | actionName: 'Clicked Sign Up', 67 | sessionToken: '23abf0owekfmcn4u3qypxg09w4d8ayrcdx8nu2ng]s98y18cx98q3yhwmnhcfx43f', 68 | userId: crypto.randomInt(1, 1001), 69 | companyId: crypto.randomInt(1, 1001), 70 | metadata: { 71 | email: 'alex@acmeinc.com', 72 | button_label: 'Get Started', 73 | sign_up_method: 'Google SSO', 74 | }, 75 | request: req_contextA, 76 | }, 77 | 78 | { 79 | transactionId: crypto.randomUUID(), 80 | actionName: 'Viewed pricing', 81 | sessionToken: '23jdf0owejfmbn4u3qypxg09w4d8ayrxdx8nu2ng]s98y18cx98q3yhwmnhcfx43f', 82 | userId: crypto.randomInt(1, 1001), 83 | companyId: crypto.randomInt(1, 1001), 84 | metadata: { 85 | email: 'kim@acmeinc.com', 86 | button_label: 'See pricing', 87 | sign_up_method: 'Google SSO', 88 | }, 89 | request: req_contextB, 90 | }, 91 | ]; 92 | 93 | middleWare.sendActionsBatch(actions, function (err, resp) { 94 | if (err) { 95 | console.log(err); 96 | } else { 97 | } 98 | done(); 99 | }); 100 | }); 101 | 102 | it('throws error when sending a single action with invalid request field', async function () { 103 | const actionName = "Clicked 'Sign up'"; 104 | const actionMetadata = { 105 | button_label: 'Get Started', 106 | sign_up_method: 'Google SSO', 107 | }; 108 | // Request context empty that should throw an error 109 | const actionReqContext = {}; 110 | const actionModel = { 111 | actionName: actionName, 112 | metadata: actionMetadata, 113 | request: actionReqContext, 114 | }; 115 | try { 116 | await middleWare.sendAction(actionModel, () => {}); 117 | } catch (err) { 118 | expect(err).to.be.instanceOf(Error); 119 | expect(err.message).to.eql( 120 | 'To send an Action, the request and request.uri fields are required' 121 | ); 122 | } 123 | }); 124 | 125 | it('throws error when sending a batch of actions with invalid request field', async function () { 126 | // Define the actions. 127 | var actions = [ 128 | { 129 | transactionId: crypto.randomUUID(), 130 | actionName: 'Clicked Sign Up', 131 | sessionToken: '23abf0owekfmcn4u3qypxg09w4d8ayrcdx8nu2ng]s98y18cx98q3yhwmnhcfx43f', 132 | userId: crypto.randomInt(1, 1001), 133 | companyId: crypto.randomInt(1, 1001), 134 | metadata: { 135 | email: 'alex@acmeinc.com', 136 | button_label: 'Get Started', 137 | sign_up_method: 'Google SSO', 138 | }, 139 | // Missing requried uri field 140 | request: { 141 | time: Date.now(), 142 | ipAddress: '12.48.120.123', 143 | }, 144 | }, 145 | 146 | { 147 | transactionId: crypto.randomUUID(), 148 | actionName: 'Viewed pricing', 149 | sessionToken: '23jdf0owejfmbn4u3qypxg09w4d8ayrxdx8nu2ng]s98y18cx98q3yhwmnhcfx43f', 150 | userId: crypto.randomInt(1, 1001), 151 | companyId: crypto.randomInt(1, 1001), 152 | metadata: { 153 | email: 'kim@acmeinc.com', 154 | button_label: 'See pricing', 155 | sign_up_method: 'Google SSO', 156 | }, 157 | // Missing requried uri field 158 | request: { 159 | time: Date.now(), 160 | ipAddress: '12.48.220.123', 161 | }, 162 | }, 163 | ]; 164 | try { 165 | await middleWare.sendActionsBatch(actions, () => {}); 166 | } catch (err) { 167 | expect(err).to.be.instanceOf(Error); 168 | expect(err.message).to.eql( 169 | 'To send an Action, the request and request.uri fields are required' 170 | ); 171 | } 172 | }); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /test/testUpdatingEntities.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var assert = require("assert"); 3 | var moesifapi = require("moesifapi"); 4 | var creatBatcher = require("../lib/batcher"); 5 | var moesif = require("../lib/index"); 6 | 7 | var CompanyModel = moesifapi.CompanyModel; 8 | 9 | var RUN_TEST = true; 10 | 11 | if (RUN_TEST) { 12 | describe("unit tests for updating companies or users", function () { 13 | this.timeout(10000); 14 | 15 | var middleWare = moesif({ 16 | applicationId: "Your Moesif Application ID", 17 | debug: true 18 | // debug: 'instrumentation', 19 | }); 20 | 21 | it("verify toJSON converts camelCase to snake_case for Company predefined fields", function () { 22 | const camelCasedCompany = { 23 | companyId: "randomId" + Math.random(), 24 | ipAddress: "199.2.232.2", 25 | companyDomain: "hello.com" 26 | }; 27 | 28 | const companyModel = new CompanyModel(camelCasedCompany); 29 | 30 | const resultOfToJSON = companyModel.toJSON(); 31 | console.log(JSON.stringify(resultOfToJSON)); 32 | // console of companyModel 33 | console.log(JSON.stringify(companyModel)); 34 | }); // end of it 35 | 36 | it("update single company", function (done) { 37 | const singleCamelCasedCompany = { 38 | companyId: "randomId" + Math.random(), 39 | ipAddress: "199.2.232.2", 40 | companyDomain: "hello.com" 41 | }; 42 | 43 | middleWare.updateCompany( 44 | singleCamelCasedCompany, 45 | function (err, success) { 46 | if (err) { 47 | console.log(err); 48 | } else { 49 | } 50 | done(); 51 | } 52 | ); 53 | }); // end of it 54 | 55 | it("update single company promise", function () { 56 | const singleCamelCasedCompany = { 57 | companyId: "randomId" + Math.random(), 58 | ipAddress: "199.2.232.2", 59 | companyDomain: "hello.com" 60 | }; 61 | 62 | return middleWare.updateCompany(singleCamelCasedCompany); 63 | }); // end of it 64 | 65 | it("update company batch", function (done) { 66 | const batchCamelCasedCompany = [ 67 | { 68 | companyId: "randomId" + Math.random(), 69 | ipAddress: "199.2.232.2", 70 | companyDomain: "twitch.com", 71 | metadata: { 72 | name: "dude" 73 | } 74 | }, 75 | { 76 | companyId: "randomId" + Math.random(), 77 | ipAddress: "199.2.232.2", 78 | companyDomain: "stuff.com", 79 | metadata: { 80 | name: "stuff" 81 | } 82 | } 83 | ]; 84 | 85 | middleWare.updateCompaniesBatch( 86 | batchCamelCasedCompany, 87 | function (err, success) { 88 | if (err) { 89 | console.log(err); 90 | } else { 91 | } 92 | done(); 93 | } 94 | ); 95 | }); // end of it 96 | 97 | it("update company batch promise", function () { 98 | const batchCamelCasedCompany = [ 99 | { 100 | companyId: "randomId" + Math.random(), 101 | ipAddress: "199.2.232.2", 102 | companyDomain: "twitch.com", 103 | metadata: { 104 | name: "dude" 105 | } 106 | }, 107 | { 108 | companyId: "randomId" + Math.random(), 109 | ipAddress: "199.2.232.2", 110 | companyDomain: "stuff.com", 111 | metadata: { 112 | name: "stuff" 113 | } 114 | } 115 | ]; 116 | 117 | return middleWare.updateCompaniesBatch(batchCamelCasedCompany); 118 | }); // end of it 119 | 120 | it("update single user", function (done) { 121 | const singleCamelCasedUser = { 122 | userId: "userId" + Math.random(), 123 | ipAddress: "199.2.232.2", 124 | companyId: "helloThere" 125 | }; 126 | 127 | middleWare.updateUser(singleCamelCasedUser, function (err, success) { 128 | if (err) { 129 | console.log(err); 130 | } else { 131 | } 132 | done(); 133 | }); 134 | }); // end of it 135 | 136 | it("update single user promise", function () { 137 | const singleCamelCasedUser = { 138 | userId: "userId" + Math.random(), 139 | ipAddress: "199.2.232.2", 140 | companyId: "helloThere" 141 | }; 142 | 143 | return middleWare.updateUser(singleCamelCasedUser); 144 | }); // end of it 145 | 146 | 147 | 148 | it("update user batch", function (done) { 149 | const camelCasedUsersArray = [ 150 | { 151 | userId: "userId" + Math.random(), 152 | ipAddress: "199.2.232.2", 153 | companyId: "helloThere" 154 | }, 155 | { 156 | userId: "userId" + Math.random(), 157 | ipAddress: "199.2.232.2", 158 | companyId: "helloThere", 159 | metadata: { 160 | name: "you", 161 | first_name: "hello" 162 | } 163 | } 164 | ]; 165 | 166 | middleWare.updateUsersBatch( 167 | camelCasedUsersArray, 168 | function (err, success) { 169 | if (err) { 170 | console.log(err); 171 | } else { 172 | } 173 | done(); 174 | } 175 | ); 176 | }); // end of it 177 | 178 | it("update user batch promise", function () { 179 | const camelCasedUsersArray = [ 180 | { 181 | userId: "userId" + Math.random(), 182 | ipAddress: "199.2.232.2", 183 | companyId: "helloThere" 184 | }, 185 | { 186 | userId: "userId" + Math.random(), 187 | ipAddress: "199.2.232.2", 188 | companyId: "helloThere", 189 | metadata: { 190 | name: "you", 191 | first_name: "hello" 192 | } 193 | } 194 | ]; 195 | 196 | return middleWare.updateUsersBatch(camelCasedUsersArray); 197 | }); // end of it 198 | 199 | 200 | it("update single subscription promise", function () { 201 | var date = new Date(); 202 | const singleCamelCasedSubscription = { 203 | subscriptionId: "subscriptionId" + Math.random(), 204 | companyId: 'random' + Math.random(), 205 | currentPeriodStart: new Date(date.setMonth(date.getMonth() - 2)).toISOString(), 206 | currentPeriodEnd: new Date(date.setMonth(date.getMonth() + 2)).toISOString(), 207 | status: 'active', 208 | metadata: { 209 | random: 'abc' 210 | } 211 | }; 212 | 213 | return middleWare.updateSubscription(singleCamelCasedSubscription); 214 | }); // 215 | 216 | it("update batch subscription promise", function () { 217 | var date = new Date(); 218 | const batchCamelCasedSubscriptions = [ 219 | { 220 | subscriptionId: 'subscriptionId' + Math.random(), 221 | companyId: 'random' + Math.random(), 222 | currentPeriodStart: new Date(date.setMonth(date.getMonth() - 2)).toISOString(), 223 | currentPeriodEnd: new Date(date.setMonth(date.getMonth() + 2)).toISOString(), 224 | status: 'active', 225 | metadata: { 226 | random: 'abc', 227 | }, 228 | }, 229 | { 230 | subscriptionId: 'subscriptionId' + Math.random(), 231 | companyId: 'random' + Math.random(), 232 | currentPeriodStart: new Date(date.setMonth(date.getMonth() - 3)).toISOString(), 233 | currentPeriodEnd: new Date(date.setMonth(date.getMonth() + 4)).toISOString(), 234 | status: 'active', 235 | metadata: { 236 | random: 'abc2', 237 | }, 238 | }, 239 | ]; 240 | 241 | return middleWare.updateSubscriptionsBatch(batchCamelCasedSubscriptions); 242 | }); // end of it 243 | 244 | }); // end of describe 245 | } // end of if(RUN_TEST) 246 | -------------------------------------------------------------------------------- /test/testUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var https = require('https'); 4 | var dataUtils = require('../lib/dataUtils'); 5 | var assert = require('assert'); 6 | 7 | var RUN_TEST = false; 8 | 9 | if (RUN_TEST) { 10 | describe('test data utils', function () { 11 | it('test simple hash sensitive with passwords', function (done) { 12 | const testData = { 13 | blah: '123421', 14 | stuff: [ 15 | { 16 | password1: '12342DIOSDLDS' 17 | }, 18 | { 19 | password2: 'Adsfdsadf23432431234123A' 20 | } 21 | ], 22 | pass: '12341241' 23 | }; 24 | 25 | const hashedValue = dataUtils.hashSensitive(testData); 26 | console.log(testData); 27 | console.log(hashedValue); 28 | 29 | assert(hashedValue.stuff[0].password1 !== testData.stuff[0].password1); 30 | assert(hashedValue.stuff[1].password2 !== testData.stuff[1].password2); 31 | assert(hashedValue.pass === testData.pass); 32 | done(); 33 | }); // end of test simp 34 | 35 | it('test computeBodySize', function () { 36 | const body = { 37 | random: '22505296759' 38 | }; 39 | 40 | console.log('size of json: ' + dataUtils.computeBodySize(body)); 41 | }); 42 | 43 | it('test safeJsonParse', function () { 44 | var nonPlainObject = new Map(); 45 | nonPlainObject.set('a', 1); 46 | nonPlainObject.set('b', 2); 47 | 48 | var nonPlainObject2 = { 49 | abc: 'foo', 50 | stuff: function () { 51 | console.log('hello'); 52 | } 53 | } 54 | 55 | var arrayWithNonPlainObjects = [{}, new Map(), { abc: 12 }, function() { console.log('hello'); } ]; 56 | 57 | var function2 = function() { 58 | console.log('helloworld'); 59 | } 60 | 61 | var arrayOfPlainObjects = [{ a: 1234}, { b: 'abc'}]; 62 | console.log('test non plain object'); 63 | console.log(JSON.stringify(dataUtils.safeJsonParse(nonPlainObject))); 64 | console.log('test plain object with function as property'); 65 | console.log(JSON.stringify(dataUtils.safeJsonParse(nonPlainObject2))); 66 | console.log('test array with non plain objects'); 67 | console.log(JSON.stringify(dataUtils.safeJsonParse(arrayWithNonPlainObjects))); 68 | console.log('test array with all plain objects'); 69 | console.log(JSON.stringify(dataUtils.safeJsonParse(arrayOfPlainObjects))); 70 | console.log('test number'); 71 | console.log(JSON.stringify(dataUtils.safeJsonParse(123432))); 72 | console.log('test boolean'); 73 | console.log(JSON.stringify(dataUtils.safeJsonParse(true))); 74 | console.log('test array of numbers'); 75 | console.log(JSON.stringify(dataUtils.safeJsonParse([1, 2, 3, 4, 5]))); 76 | console.log('test null'); 77 | console.log(JSON.stringify(dataUtils.safeJsonParse(null))); 78 | console.log('test undefined'); 79 | console.log(JSON.stringify(dataUtils.safeJsonParse(undefined))); 80 | console.log('test function'); 81 | console.log(JSON.stringify(dataUtils.safeJsonParse(function2))); 82 | }); 83 | }); // end of describe 84 | } 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | // Tells TypeScript to read JS files, as 6 | // normally they are ignored as source files 7 | "allowJs": true, 8 | // Generate d.ts files 9 | "declaration": true, 10 | // This compiler run should 11 | // only output d.ts files 12 | "emitDeclarationOnly": true, 13 | // Types should go into this directory. 14 | // Removing this would place the .d.ts files 15 | // next to the .js files 16 | "outDir": "dist", 17 | // go to js file when using IDE functions like 18 | // "Go to Definition" in VSCode 19 | "declarationMap": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------