├── .gitignore ├── .nvmrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── proxy │ ├── authentication.js │ ├── authentication.js.map │ ├── index.js │ └── index.js.map └── sshenabler │ ├── index.js │ ├── index.js.map │ ├── package-lock.json │ └── package.json ├── documentation ├── application-menu.png ├── sap-cf-proxy.png └── sap-cf-proxy.pptx ├── mta.yaml ├── package-lock.json ├── package.json ├── socks-proxy ├── pom.xml ├── socks-proxy.iml ├── src │ └── main │ │ └── java │ │ ├── ConnectivitySocks5ProxySocket.java │ │ ├── DBSocketFactory.java │ │ ├── ProxyServer.java │ │ └── StartSocksProxy.java └── start-proxy.sh ├── start-proxy.sh ├── start-ssh-tunnel.sh ├── test └── ping.http ├── ts ├── proxy │ ├── authentication.ts │ └── index.ts └── sshenabler │ ├── index.ts │ └── package.json ├── tsconfig.json └── xs-security.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | gen/ 3 | .gen/ 4 | node_modules/ 5 | target/ 6 | *.db 7 | .DS_Store 8 | _out 9 | 10 | default-env*.json 11 | default-services*.json 12 | 13 | /mta_archives/ 14 | /mta-op* 15 | Makefile* 16 | 17 | .env -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch via NPM", 9 | "request": "launch", 10 | "runtimeArgs": ["run-script", "start"], 11 | "runtimeExecutable": "npm", 12 | "skipFiles": ["/**"], 13 | "type": "pwa-node" 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "proxy", 19 | "program": "${workspaceFolder}/ts/proxy/index.ts", 20 | "preLaunchTask": "tsc: build - tsconfig.json", 21 | "outFiles": ["${workspaceFolder}/dist/proxy/**/*.js"], 22 | "env": { 23 | "PORT": "5050" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.compile.nullAnalysis.mode": "disabled", 3 | "SAP HANA Database Explorer.displaySapWebAnalyticsStartupNotification": false, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 0.0.2 (2023-08-25) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * include package-lock.json ([bc79079](https://njames.github.com/njames/sap-cf-proxy/commit/bc79079e087386e138a397dd4b33913c03f09e48)) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE DERIVED BEER-WARE LICENSE" (Revision 1): 4 | * You can do whatever you want with this stuff. When you like it, just buy 5 | * Joachim Van Praet (@vanprjo) or Gregor Wolf (@gregorwolf) a beer when 6 | * you see one of them. 7 | * 8 | * Inspired by the official: https://fedoraproject.org/wiki/Licensing/Beerware 9 | * 10 | * "THE BEER-WARE LICENSE" (Revision 42): 11 | * wrote this file. As long as you retain this notice you 12 | * can do whatever you want with this stuff. If we meet some day, and you think 13 | * this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp 14 | * ---------------------------------------------------------------------------- 15 | */ 16 | 17 | --- 18 | 19 | 20 | Apache License 21 | Version 2.0, January 2004 22 | http://www.apache.org/licenses/ 23 | 24 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 25 | 26 | 1. Definitions. 27 | 28 | "License" shall mean the terms and conditions for use, reproduction, 29 | and distribution as defined by Sections 1 through 9 of this document. 30 | 31 | "Licensor" shall mean the copyright owner or entity authorized by 32 | the copyright owner that is granting the License. 33 | 34 | "Legal Entity" shall mean the union of the acting entity and all 35 | other entities that control, are controlled by, or are under common 36 | control with that entity. For the purposes of this definition, 37 | "control" means (i) the power, direct or indirect, to cause the 38 | direction or management of such entity, whether by contract or 39 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 40 | outstanding shares, or (iii) beneficial ownership of such entity. 41 | 42 | "You" (or "Your") shall mean an individual or Legal Entity 43 | exercising permissions granted by this License. 44 | 45 | "Source" form shall mean the preferred form for making modifications, 46 | including but not limited to software source code, documentation 47 | source, and configuration files. 48 | 49 | "Object" form shall mean any form resulting from mechanical 50 | transformation or translation of a Source form, including but 51 | not limited to compiled object code, generated documentation, 52 | and conversions to other media types. 53 | 54 | "Work" shall mean the work of authorship, whether in Source or 55 | Object form, made available under the License, as indicated by a 56 | copyright notice that is included in or attached to the work 57 | (an example is provided in the Appendix below). 58 | 59 | "Derivative Works" shall mean any work, whether in Source or Object 60 | form, that is based on (or derived from) the Work and for which the 61 | editorial revisions, annotations, elaborations, or other modifications 62 | represent, as a whole, an original work of authorship. For the purposes 63 | of this License, Derivative Works shall not include works that remain 64 | separable from, or merely link (or bind by name) to the interfaces of, 65 | the Work and Derivative Works thereof. 66 | 67 | "Contribution" shall mean any work of authorship, including 68 | the original version of the Work and any modifications or additions 69 | to that Work or Derivative Works thereof, that is intentionally 70 | submitted to Licensor for inclusion in the Work by the copyright owner 71 | or by an individual or Legal Entity authorized to submit on behalf of 72 | the copyright owner. For the purposes of this definition, "submitted" 73 | means any form of electronic, verbal, or written communication sent 74 | to the Licensor or its representatives, including but not limited to 75 | communication on electronic mailing lists, source code control systems, 76 | and issue tracking systems that are managed by, or on behalf of, the 77 | Licensor for the purpose of discussing and improving the Work, but 78 | excluding communication that is conspicuously marked or otherwise 79 | designated in writing by the copyright owner as "Not a Contribution." 80 | 81 | "Contributor" shall mean Licensor and any individual or Legal Entity 82 | on behalf of whom a Contribution has been received by Licensor and 83 | subsequently incorporated within the Work. 84 | 85 | 2. Grant of Copyright License. Subject to the terms and conditions of 86 | this License, each Contributor hereby grants to You a perpetual, 87 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 88 | copyright license to reproduce, prepare Derivative Works of, 89 | publicly display, publicly perform, sublicense, and distribute the 90 | Work and such Derivative Works in Source or Object form. 91 | 92 | 3. Grant of Patent License. Subject to the terms and conditions of 93 | this License, each Contributor hereby grants to You a perpetual, 94 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 95 | (except as stated in this section) patent license to make, have made, 96 | use, offer to sell, sell, import, and otherwise transfer the Work, 97 | where such license applies only to those patent claims licensable 98 | by such Contributor that are necessarily infringed by their 99 | Contribution(s) alone or by combination of their Contribution(s) 100 | with the Work to which such Contribution(s) was submitted. If You 101 | institute patent litigation against any entity (including a 102 | cross-claim or counterclaim in a lawsuit) alleging that the Work 103 | or a Contribution incorporated within the Work constitutes direct 104 | or contributory patent infringement, then any patent licenses 105 | granted to You under this License for that Work shall terminate 106 | as of the date such litigation is filed. 107 | 108 | 4. Redistribution. You may reproduce and distribute copies of the 109 | Work or Derivative Works thereof in any medium, with or without 110 | modifications, and in Source or Object form, provided that You 111 | meet the following conditions: 112 | 113 | (a) You must give any other recipients of the Work or 114 | Derivative Works a copy of this License; and 115 | 116 | (b) You must cause any modified files to carry prominent notices 117 | stating that You changed the files; and 118 | 119 | (c) You must retain, in the Source form of any Derivative Works 120 | that You distribute, all copyright, patent, trademark, and 121 | attribution notices from the Source form of the Work, 122 | excluding those notices that do not pertain to any part of 123 | the Derivative Works; and 124 | 125 | (d) If the Work includes a "NOTICE" text file as part of its 126 | distribution, then any Derivative Works that You distribute must 127 | include a readable copy of the attribution notices contained 128 | within such NOTICE file, excluding those notices that do not 129 | pertain to any part of the Derivative Works, in at least one 130 | of the following places: within a NOTICE text file distributed 131 | as part of the Derivative Works; within the Source form or 132 | documentation, if provided along with the Derivative Works; or, 133 | within a display generated by the Derivative Works, if and 134 | wherever such third-party notices normally appear. The contents 135 | of the NOTICE file are for informational purposes only and 136 | do not modify the License. You may add Your own attribution 137 | notices within Derivative Works that You distribute, alongside 138 | or as an addendum to the NOTICE text from the Work, provided 139 | that such additional attribution notices cannot be construed 140 | as modifying the License. 141 | 142 | You may add Your own copyright statement to Your modifications and 143 | may provide additional or different license terms and conditions 144 | for use, reproduction, or distribution of Your modifications, or 145 | for any such Derivative Works as a whole, provided Your use, 146 | reproduction, and distribution of the Work otherwise complies with 147 | the conditions stated in this License. 148 | 149 | 5. Submission of Contributions. Unless You explicitly state otherwise, 150 | any Contribution intentionally submitted for inclusion in the Work 151 | by You to the Licensor shall be under the terms and conditions of 152 | this License, without any additional terms or conditions. 153 | Notwithstanding the above, nothing herein shall supersede or modify 154 | the terms of any separate license agreement you may have executed 155 | with Licensor regarding such Contributions. 156 | 157 | 6. Trademarks. This License does not grant permission to use the trade 158 | names, trademarks, service marks, or product names of the Licensor, 159 | except as required for reasonable and customary use in describing the 160 | origin of the Work and reproducing the content of the NOTICE file. 161 | 162 | 7. Disclaimer of Warranty. Unless required by applicable law or 163 | agreed to in writing, Licensor provides the Work (and each 164 | Contributor provides its Contributions) on an "AS IS" BASIS, 165 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 166 | implied, including, without limitation, any warranties or conditions 167 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 168 | PARTICULAR PURPOSE. You are solely responsible for determining the 169 | appropriateness of using or redistributing the Work and assume any 170 | risks associated with Your exercise of permissions under this License. 171 | 172 | 8. Limitation of Liability. In no event and under no legal theory, 173 | whether in tort (including negligence), contract, or otherwise, 174 | unless required by applicable law (such as deliberate and grossly 175 | negligent acts) or agreed to in writing, shall any Contributor be 176 | liable to You for damages, including any direct, indirect, special, 177 | incidental, or consequential damages of any character arising as a 178 | result of this License or out of the use or inability to use the 179 | Work (including but not limited to damages for loss of goodwill, 180 | work stoppage, computer failure or malfunction, or any and all 181 | other commercial damages or losses), even if such Contributor 182 | has been advised of the possibility of such damages. 183 | 184 | 9. Accepting Warranty or Additional Liability. While redistributing 185 | the Work or Derivative Works thereof, You may choose to offer, 186 | and charge a fee for, acceptance of support, warranty, indemnity, 187 | or other liability obligations and/or rights consistent with this 188 | License. However, in accepting such obligations, You may act only 189 | on Your own behalf and on Your sole responsibility, not on behalf 190 | of any other Contributor, and only if You agree to indemnify, 191 | defend, and hold each Contributor harmless for any liability 192 | incurred by, or claims asserted against, such Contributor by reason 193 | of your accepting any such warranty or additional liability. 194 | 195 | END OF TERMS AND CONDITIONS 196 | 197 | APPENDIX: How to apply the Apache License to your work. 198 | 199 | To apply the Apache License to your work, attach the following 200 | boilerplate notice, with the fields enclosed by brackets "[]" 201 | replaced with your own identifying information. (Don't include 202 | the brackets!) The text should be enclosed in the appropriate 203 | comment syntax for the file format. We also recommend that a 204 | file or class name and description of purpose be included on the 205 | same "printed page" as the copyright notice for easier 206 | identification within third-party archives. 207 | 208 | Copyright [yyyy] [name of copyright owner] 209 | 210 | Licensed under the Apache License, Version 2.0 (the "License"); 211 | you may not use this file except in compliance with the License. 212 | You may obtain a copy of the License at 213 | 214 | http://www.apache.org/licenses/LICENSE-2.0 215 | 216 | Unless required by applicable law or agreed to in writing, software 217 | distributed under the License is distributed on an "AS IS" BASIS, 218 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 219 | See the License for the specific language governing permissions and 220 | limitations under the License. 221 | 222 | ------------------------------------------------------------------------------ 223 | APIs 224 | 225 | This project may include APIs to SAP or third party products or services. The use of these APIs, products and services may be subject to additional agreements. In no event shall the application of the Apache Software License, v.2 to this project grant any rights in or to these APIs, products or services that would alter, expand, be inconsistent with, or supersede any terms of these additional agreements. “API” means application programming interfaces, as well as their respective specifications and implementing code that allows other software products to communicate with or call on SAP or third party products or services (for example, SAP Enterprise Services, BAPIs, Idocs, RFCs and ABAP calls or other user exits) and may be made available through SAP or third party products, SDKs, documentation or other media. 226 | 227 | ------------------------------------------------------------------------------ 228 | SUBCOMPONENTS 229 | 230 | This project includes the following subcomponents that are subject to separate license terms. 231 | Your use of these subcomponents is subject to the separate license terms applicable to 232 | each subcomponent. 233 | 234 | Component: deploy.sh 235 | Licensor: Domenic Denicola 236 | Website: https://gist.github.com/domenic/ec8b0fc8ab45f39403dd/e445116166c79d7ac35eb38a5d348d546f3d1620 237 | License: MIT License 238 | = 2018 239 | = Domenic Denicola 240 | 241 | ------------------------------------------------------------------------------ 242 | 243 | The MIT License (MIT) 244 | 245 | Copyright 246 | 247 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 248 | 249 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 250 | 251 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 252 | 253 | ------------------------------------------------------------------------------ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sap-cf-proxy - proxy to all destinations in SAP BTP Cloud Foundry subaccount 2 | 3 | ## Why? 4 | 5 | You might ask why do I need this project? The answer is whenever you want to work with on-premise destinations or a system connected through a Cloud Connector and you don't have a VPN connection which would allow you to connect directly. But please note that you don't need this project when you're working with SAP Business Application Studio and you have a Cloud Connector connected to the subaccount BAS is running in. 6 | 7 | ## Limitations 8 | 9 | The authentication of the SAP BTP Cloud Foundry subaccount must be configured to use SAP ID or SAP Identity Authentication. For SAP Identity Authentication you must have the credentials of a local user. 10 | 11 | ## How does it work 12 | 13 | ![sap-cf-proxy Architecture](documentation/sap-cf-proxy.png) 14 | 15 | This proxy makes use of the possibility to connect via ssh into an app (sshenabler) that is running inside the SAP BTP Cloud Foundry environment. From there the so-called connectivity proxy is reachable. This proxy is provided by an instance of the Connectivity Service. On your development computer you have to start two programs: 16 | 17 | - the script that establishes the ssh tunnel and forwards requests from the local port 20003 and 20004 to connectivityproxy.internal.cf.eu10.hana.ondemand.com:20003 / 20004 18 | - the reverse proxy that by default listens on port 5050 and acts as your local endpoint for calls that should be forwarded to the on premise system 19 | 20 | When your local app that you develop e.g. a CAP Application using an external service, a SAPUI5 App or a REST Client Script sends a request to the proxy, it will either use the username / password provided as an basic authentication header or via the environment variables USERNAME / PASSWORD and use that to authenticate on the XSUAA service using the password grant type. In return a JWT is retrieved. This token is sent in the backend request and is translated to the X.509 certificate used for principal propagation in the Cloud Connector. The local reverse proxy sends request for an on premise destination to local port that are forwarded via the ssh tunnel to the proxy in the BTP. 21 | 22 | When you want to directly connect to e.g. a database running on-premise (Sybase ASE, Postgres, HANA) then this repository provides a proxy that tunnels TCP requests through to the database. 23 | 24 | ## Prerequisites 25 | 26 | You are logged into your SAP BTP Cloud Foundry subaccount [via the cf CLI](https://blogs.sap.com/2021/04/21/connecting-from-sap-business-application-studio-to-sap-btp-cloud-foundry-environment/), you have the [Multitarget Build Tool](https://sap.github.io/cloud-mta-build-tool/download/) (mbt) installed ,and you have installed the [MultiApps CLI Plugin](https://help.sap.com/docs/btp/sap-business-technology-platform/install-multiapps-cli-plugin-in-cloud-foundry-environment?locale=en-US). If you’re going to use the SOCKS5 proxy (Forward TCP data), you’ll need to have Maven installed. 27 | 28 | ## Installation 29 | 30 | Clone the repository to a directory of your choice: 31 | 32 | ```bash 33 | git clone git@github.com:jowavp/sap-cf-proxy.git 34 | ``` 35 | 36 | ## Setup 37 | 38 | Run the following commands in a terminal window in the `sap-cf-proxy` directory : 39 | 40 | ```bash 41 | npm install 42 | npm run build:mta 43 | npm run deploy:cf 44 | npm run enable-ssh 45 | ``` 46 | 47 | ### Connectivity Proxy 48 | 49 | > By default the script `start-sshtunnel` is configured to forward the requests to: 50 | > `'connectivityproxy.internal.cf.eu10.hana.ondemand.com:20003'` . 51 | 52 | Open the SAP BTP Cockpit and navigate to the details of the deployed app _sshenabler_. Open the Environment Variables of this app. They can be found by opening the cloud foundry space in the correct sub-account. Click on the deployed application `sshenabler` and click on the `Environment Variables` in the left-hand menu. 53 | 54 | ![Application menu](./documentation/application-menu.png) 55 | 56 | Copy the content of the textbox _System Provided_ to a file named _default-env.json_ in the root folder of this project. 57 | Only the `"VCAP_SERVICES"` section is required. **Do not add** the `"VCAP_APPLICATION"` section. 58 | 59 | The hostname or IP address you are looking for is named `onpremise_proxy_host` in the `VCAP_SERVICES` Environment Variable of the `sshenabler` application. 60 | 61 | Update the following section in `package.json` with the hostname or IP address: 62 | 63 | ```json 64 | "config":{ 65 | "proxy": "connectivityproxy.internal.cf.eu10.hana.ondemand.com" 66 | }, 67 | ``` 68 | 69 | Once this configuration is complete you can now run: 70 | 71 | ```bash 72 | npm run start:sshtunnel 73 | ``` 74 | 75 | The result should be something like: 76 | 77 | ```bash 78 | 79 | > sap-cf-proxy@0.0.2 start:sshtunnel 80 | > cf ssh sshenabler -L 20003:$npm_package_config_proxy:20003 -L 20004:$npm_package_config_proxy:20004 81 | 82 | vcap@:~$ 83 | ``` 84 | 85 | and you will be able to enter commands on the server if you wish. 86 | 87 | ### Forward HTTP requests (CAP, UI5) 88 | 89 | Once you have created the local _default-env.json_ as described above, in another terminal window of the sap-cf-proxy app run: 90 | 91 | ```bash 92 | npm start 93 | ``` 94 | 95 | ### Forward TCP data (Database connection) 96 | 97 | Go to the SAP BTP Cockpit and open the details of the deployed app _sshenabler_. Navigate there to the Environment Variables. Copy the content of the textbox _System Provided_ to a local file called _default-env.json_ in the directory _socks-proxy_. 98 | 99 | Then, run the following commands in another terminal window. Make sure to change ON_PREMISE_HOST and ON_PREMISE_PORT to values that you see in the Cloud Connector configuration. To specify a particular cloud connector, set the variable CLOUD_CONNECTOR_LOCATION_ID. 100 | 101 | ```bash 102 | cd socks-proxy 103 | 104 | # Set Environment 105 | export ON_PREMISE_HOST=hostnameOfOnPremiseSystem \ 106 | ON_PREMISE_PORT=portOfOnPremiseSystem \ 107 | CLOUD_CONNECTOR_LOCATION_ID=cloudConnectorLocationId \ 108 | VCAP_SERVICES=$(jq -c '.VCAP_SERVICES' default-env.json) 109 | 110 | # Start the proxy 111 | mvn compile exec:java -Dexec.mainClass="StartSocksProxy" 112 | ``` 113 | 114 | Now you can open your preferred database tool or also your application that uses JDBC to connect and just point it to _localhost:5050_. 115 | 116 | ## Proxy Configuration (valid for HTTP request forwarding) 117 | 118 | Following properties can be configured in a .env file in the root folder of the project. 119 | 120 | | Property | Description | Defaul value | 121 | | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------- | 122 | | PORT | Proxy port | 5050 | 123 | | DESTINATION_PROPERTY_NAME | Header property to define the target destination | X-SAP-BTP-destination | 124 | | DEFAULT_DESTINATION | If no target destination is set in the request to the proxy we use this destination nama. | SAP_ABAP_BACKEND | 125 | | CFPROXY_HOST | Host where the port-forwarding is running to the CF proxy | 127.0.0.1 | 126 | | CFPROXY_POST | Port that is forwarded to the CF proxy | 20003 | 127 | | USERNAME | Username in cloud foundry, if no user is set you have to send an authorization header. | 128 | | PASSWORD | Password in cloud foundry, if no password is set you have to send an authorization header. | 129 | 130 | ## Testing 131 | 132 | Using the VS Code Extension [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) you can execute the test in test/ping.http. Before you can run the test you have to create a .env file in the test folder with the following (of course adjusted) content: 133 | 134 | ```env 135 | sapcpproxy=http://localhost:5050 136 | sapid_username="" 137 | sapid_password="" 138 | sapclient= 139 | ``` 140 | 141 | Connect a Cloud Connector to your Subaccount and create the following destinations: 142 | 143 | - SAP_ABAP_BACKEND_NO_AUTH 144 | - SAP_ABAP_BACKEND_BASIC_AUTH 145 | 146 | Then run the test. The result should be: 147 | 148 | `Server reached.` 149 | 150 | in both cases. 151 | 152 | ## Troubleshooting 153 | 154 | If you start the sshtunnel with `npm run start:sshtunnel` and get the following error: 155 | 156 | ```bash 157 | Error opening SSH connection: You are not authorized to perform the requested action. 158 | ``` 159 | 160 | then you need to: 161 | 162 | ```bash 163 | cf enable-ssh sshenabler; 164 | cf restart sshenabler; 165 | ``` 166 | 167 | Other troubleshooting is [available](https://blogs.sap.com/2018/09/12/ssh-into-cloudfoundry-trouble/) 168 | 169 | ## update modules @todo 170 | 171 | Currently the following warning are emmited on `npm i` 172 | 173 | The deprecated libraries need to be updated. 174 | 175 | ```txt 176 | npm WARN deprecated @types/pino@7.0.5: This is a stub types definition. pino provides its own type definitions, so you do not need this installed. 177 | npm WARN deprecated babel-preset-es2015@6.24.1: 🙌 Thanks for using Babel: we recommend using babel-preset-env now: please read https://babeljs.io/env to update! 178 | npm WARN deprecated @sap-cloud-sdk/analytics@1.54.2: 1.x is no longer maintained. 179 | npm WARN deprecated @sap-cloud-sdk/util@1.54.2: 1.x is no longer maintained. 180 | npm WARN deprecated @sap-cloud-sdk/core@1.54.2: Version 1 of SAP Cloud SDK is no longer maintained. Check the upgrade guide for switching to version 2: https://sap.github.io/cloud-sdk/docs/js/guides/upgrade-to-version-2. 181 | npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. 182 | 183 | 184 | 185 | ``` 186 | -------------------------------------------------------------------------------- /dist/proxy/authentication.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | exports.getAuthenticationType = exports.basicToJWT = void 0; 30 | const xsenv = __importStar(require("@sap/xsenv")); 31 | const axios_1 = __importDefault(require("axios")); 32 | const config = { 33 | timeout: Number(process.env.TIMEOUT_JWT) || 3600, // 3600 - 60 Minutes 34 | }; 35 | var jwtTokenCache = {}; 36 | const basicToJWT = async (authorization) => { 37 | xsenv.loadEnv(); 38 | // check if a xsuaa is linked to this project. 39 | const { xsuaa } = xsenv.getServices({ 40 | xsuaa: { 41 | tag: "xsuaa", 42 | }, 43 | }); 44 | if (!xsuaa) { 45 | throw `No xsuaa service found`; 46 | } 47 | if ("string" === typeof authorization) { 48 | authorization = decodeBA(authorization); 49 | } 50 | let jwtToken; 51 | if (!jwtTokenCache || 52 | !jwtTokenCache[authorization.username] || 53 | new Date().getTime() - config.timeout * 1000 > 54 | jwtTokenCache[authorization.username].timeout) { 55 | jwtToken = await fetchToken(xsuaa, authorization); 56 | jwtTokenCache[authorization.username] = { 57 | jwtToken: jwtToken, 58 | timeout: new Date().getTime(), 59 | }; 60 | } 61 | jwtToken = jwtTokenCache[authorization.username].jwtToken; 62 | return jwtToken; 63 | }; 64 | exports.basicToJWT = basicToJWT; 65 | function convertScope(scope) { 66 | if (!scope) 67 | return null; 68 | return scope 69 | .split(" ") 70 | .map((sc) => sc.split(":")) 71 | .reduce((acc, [key, value]) => { 72 | acc[key] = value; 73 | return acc; 74 | }, {}); 75 | } 76 | const getAuthenticationType = (authorization) => { 77 | return /bearer/gim.test(authorization) 78 | ? "bearer" 79 | : /basic/gim.test(authorization) 80 | ? "basic" 81 | : "none"; 82 | }; 83 | exports.getAuthenticationType = getAuthenticationType; 84 | const fetchToken = async (xsuaa, credentials) => { 85 | const tokenBaseUrl = `${xsuaa.url}`; 86 | const token = (await (0, axios_1.default)({ 87 | url: `${tokenBaseUrl}/oauth/token`, 88 | method: "POST", 89 | responseType: "json", 90 | data: `client_id=${encodeURIComponent(xsuaa.clientid)}&grant_type=password&response_type=token&username=${encodeURIComponent(credentials.username)}&password=${encodeURIComponent(credentials.password)}`, 91 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 92 | auth: { 93 | username: xsuaa.clientid, 94 | password: xsuaa.clientsecret, 95 | }, 96 | })).data; 97 | return token; 98 | }; 99 | function decodeBA(authorization) { 100 | const [type, userpwd] = authorization.split(" "); // Split on a space, the original auth looks like "Basic Y2hhcmxlczoxMjM0NQ==" and we need the 2nd part 101 | const buf = Buffer.from(userpwd, "base64"); // create a buffer and tell it the data coming in is base64 102 | const [username, password] = buf.toString().split(":"); 103 | return { 104 | username, 105 | password, 106 | }; 107 | } 108 | //# sourceMappingURL=authentication.js.map -------------------------------------------------------------------------------- /dist/proxy/authentication.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"authentication.js","sourceRoot":"","sources":["../../ts/proxy/authentication.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/proxy/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | const http_1 = __importDefault(require("http")); 30 | const http_proxy_1 = __importDefault(require("http-proxy")); 31 | const pino_1 = __importDefault(require("pino")); 32 | const dotenv_1 = __importDefault(require("dotenv")); 33 | const core_1 = require("@sap-cloud-sdk/core"); 34 | const xsenv = __importStar(require("@sap/xsenv")); 35 | const authentication_1 = require("./authentication"); 36 | //Load default-env.json file automatically from the beginning 37 | xsenv.loadEnv(); 38 | dotenv_1.default.config(); 39 | const logger = (0, pino_1.default)({ 40 | level: process.env.LOG_LEVEL || "info", 41 | transport: { 42 | target: "pino-pretty", 43 | options: { 44 | colorize: true, 45 | }, 46 | }, 47 | }); 48 | const proxy = http_proxy_1.default.createProxyServer({ 49 | secure: false, 50 | }); 51 | var destinationCache = {}; 52 | const config = { 53 | timeout: Number(process.env.TIMEOUT_DESTINATION) || 3600, // 3600 - 60 Minutes 54 | proxyport: process.env.PORT || 5050, 55 | defaultDestination: process.env.DEFAULT_DESTINATION || "SAP_ABAP_BACKEND", 56 | destinationPropertyName: (process.env.DESTINATION_PROPERTY_NAME || "X-SAP-BTP-destination").toLowerCase(), 57 | cfproxy: { 58 | host: process.env.CFPROXY_HOST || "127.0.0.1", 59 | port: parseInt(process.env.CFPROXY_PORT || "20003"), 60 | }, 61 | credentials: process.env.USERNAME && process.env.PASSWORD 62 | ? { 63 | username: process.env.USERNAME, 64 | password: process.env.PASSWORD, 65 | } 66 | : undefined, 67 | }; 68 | proxy.on("proxyReq", function (proxyReq, req, res, options) { 69 | var _a; 70 | //TODO: do we need another way to send the headers to this function? 71 | //@ts-ignore 72 | const newHeaders = ((_a = options.target) === null || _a === void 0 ? void 0 : _a.headers) || {}; 73 | Object.entries(newHeaders).forEach(function ([key, value]) { 74 | proxyReq.setHeader(key, value); 75 | }); 76 | }); 77 | const server = http_1.default.createServer(async (req, res) => { 78 | const authorization = req.headers.authorization || ""; 79 | const authenticationType = (0, authentication_1.getAuthenticationType)(authorization); 80 | let authorizationHeader; 81 | let jwtToken = { 82 | token_type: "", 83 | access_token: "", 84 | }; 85 | if (authenticationType === "basic") { 86 | jwtToken = await (0, authentication_1.basicToJWT)(authorization); 87 | authorizationHeader = `${jwtToken.token_type} ${jwtToken.access_token}`; 88 | } 89 | if (authenticationType === "bearer") { 90 | jwtToken.token_type = "bearer"; 91 | jwtToken.access_token = authorization.split(" ")[1]; 92 | authorizationHeader = authorization; 93 | } 94 | if (authenticationType === "none" && config.credentials) { 95 | jwtToken = await (0, authentication_1.basicToJWT)(config.credentials); 96 | authorizationHeader = `${jwtToken.token_type} ${jwtToken.access_token}`; 97 | } 98 | // read the destination name 99 | const destinationName = [req.headers[config.destinationPropertyName]].flat()[0] || 100 | config.defaultDestination; 101 | logger.info(`Request entered the building: proxy to ${destinationName}`); 102 | let sdkDestination; 103 | // read the destination on cloud foundry 104 | try { 105 | if (!destinationCache || 106 | !destinationCache[destinationName] || 107 | new Date().getTime() - config.timeout * 1000 > 108 | destinationCache[destinationName].timeout) { 109 | let options = {}; 110 | if (jwtToken.access_token !== "") { 111 | options.userJwt = jwtToken.access_token; 112 | } 113 | sdkDestination = await (0, core_1.getDestination)(destinationName, options); 114 | if (sdkDestination === null) { 115 | throw Error(`Connection ${destinationName} not found`); 116 | } 117 | logger.info(`Cache destination ${destinationName}`); 118 | destinationCache[destinationName] = { 119 | destination: sdkDestination, 120 | timeout: new Date().getTime(), 121 | }; 122 | } 123 | sdkDestination = destinationCache[destinationName].destination; 124 | logger.info(`Forwarding this request to ${sdkDestination.url}`); 125 | let target = new URL(sdkDestination.url); 126 | target.headers = { 127 | host: target.host, 128 | }; 129 | // 130 | if (sdkDestination.authentication === "BasicAuthentication") { 131 | req.headers.authorization = 132 | "Basic " + 133 | Buffer.from(`${sdkDestination.username}:${sdkDestination.password}`, "ascii").toString("base64"); 134 | } 135 | if (sdkDestination.authentication === "OAuth2ClientCredentials") { 136 | if (!sdkDestination.authTokens) { 137 | throw new Error(`No token retrieved for destination ${destinationName}`); 138 | } 139 | const clientCredentialsToken = sdkDestination.authTokens[0].value; 140 | target.headers = { 141 | ...target.headers, 142 | Authorization: `Bearer ${clientCredentialsToken}`, 143 | }; 144 | delete req.headers.authorization; 145 | } 146 | if (sdkDestination.authTokens && 147 | sdkDestination.authTokens[0] && 148 | !sdkDestination.authTokens[0].error) { 149 | if (sdkDestination.authTokens[0].error) { 150 | throw new Error(sdkDestination.authTokens[0].error); 151 | } 152 | target.headers = { 153 | ...target.headers, 154 | Authorization: `${sdkDestination.authTokens[0].type} ${sdkDestination.authTokens[0].value}`, 155 | }; 156 | delete req.headers.authorization; 157 | } 158 | // 159 | if (sdkDestination.proxyType.toLowerCase() === "onpremise") { 160 | logger.info(`This is an on premise request. Let's send it over the SSH tunnel.`); 161 | target = { 162 | headers: { 163 | ...target.headers, 164 | }, 165 | protocol: sdkDestination.proxyConfiguration.protocol, 166 | host: config.cfproxy.host, 167 | port: config.cfproxy.port, 168 | }; 169 | if (sdkDestination.cloudConnectorLocationId) { 170 | target.headers["SAP-Connectivity-SCC-Location_ID"] = 171 | sdkDestination.cloudConnectorLocationId; 172 | } 173 | if (sdkDestination.proxyConfiguration) { 174 | req.headers = { 175 | ...req.headers, 176 | ...sdkDestination.proxyConfiguration.headers, 177 | }; 178 | } 179 | if (sdkDestination.authentication === "PrincipalPropagation") { 180 | req.headers["SAP-Connectivity-Authentication"] = authorizationHeader; 181 | delete req.headers.authorization; 182 | } 183 | } 184 | proxy.web(req, res, { target }); 185 | } 186 | catch (error) { 187 | logger.error(JSON.stringify(error)); 188 | } 189 | }); 190 | logger.info(`proxy listening on port : ${config.proxyport}`); 191 | server.listen(config.proxyport); 192 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/proxy/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../ts/proxy/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/sshenabler/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | // just a dummy http server to create a SSH tunnel to serve onpremise destinations. 7 | const http_1 = __importDefault(require("http")); 8 | const port = process.env.PORT || 3000; 9 | const server = http_1.default.createServer((req, res) => { 10 | res.statusCode = 200; 11 | res.setHeader("Content-Type", "text/plain"); 12 | res.end(`Hi, I'm just a lazy service to enable local SSH tunnels.`); 13 | }); 14 | server.listen(port, () => { 15 | console.log(`Server is running on port ${port}!`); 16 | }); 17 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/sshenabler/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../ts/sshenabler/index.ts"],"names":[],"mappings":";;;;;AAAA,mFAAmF;AACnF,gDAAwB;AAExB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,MAAM,MAAM,GAAG,cAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC5C,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;IACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAC5C,GAAG,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;AACtE,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,GAAG,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /dist/sshenabler/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sshenabler", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sshenabler", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "http": "0.0.1-security" 12 | }, 13 | "engines": { 14 | "node": "^18" 15 | } 16 | }, 17 | "node_modules/http": { 18 | "version": "0.0.1-security", 19 | "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", 20 | "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dist/sshenabler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sshenabler", 3 | "version": "0.0.1", 4 | "engines": { 5 | "node": "^18" 6 | }, 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "dependencies": { 11 | "http": "0.0.1-security" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /documentation/application-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowavp/sap-cf-proxy/ba782660dfd0e4f4ac7b8a6c899e5fe66f320364/documentation/application-menu.png -------------------------------------------------------------------------------- /documentation/sap-cf-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowavp/sap-cf-proxy/ba782660dfd0e4f4ac7b8a6c899e5fe66f320364/documentation/sap-cf-proxy.png -------------------------------------------------------------------------------- /documentation/sap-cf-proxy.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jowavp/sap-cf-proxy/ba782660dfd0e4f4ac7b8a6c899e5fe66f320364/documentation/sap-cf-proxy.pptx -------------------------------------------------------------------------------- /mta.yaml: -------------------------------------------------------------------------------- 1 | ID: sshenabler 2 | _schema-version: 3.2.0 3 | description: Dummy application to create an SSH tunnel to serve destinations 4 | version: 0.0.2 5 | parameters: 6 | enable-parallel-deployments: true 7 | build-parameters: 8 | before-all: 9 | - builder: custom 10 | commands: 11 | - npm install 12 | - npm run build 13 | 14 | modules: 15 | - name: sshenabler 16 | type: nodejs 17 | path: dist/sshenabler 18 | build-parameters: 19 | builder: custom 20 | commands: 21 | - npm install 22 | parameters: 23 | disk-quota: 256M 24 | memory: 128M 25 | requires: 26 | - name: destination_service 27 | - name: uaa_service 28 | - name: connectivity_service 29 | resources: 30 | - name: destination_service 31 | type: destination 32 | parameters: 33 | service-plan: lite 34 | service: destination 35 | - name: connectivity_service 36 | type: connectivity 37 | - name: uaa_service 38 | type: org.cloudfoundry.managed-service 39 | parameters: 40 | path: ./xs-security.json 41 | service-plan: application 42 | service: xsuaa 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap-cf-proxy", 3 | "version": "0.1.0", 4 | "description": "Proxy all destinations in SAP BTP Cloud foundry account", 5 | "main": "index.js", 6 | "engines": { 7 | "node": "^20" 8 | }, 9 | "config": { 10 | "proxy": "connectivityproxy.internal.cf.eu10.hana.ondemand.com" 11 | }, 12 | "scripts": { 13 | "start": "node dist/proxy/index.js", 14 | "build": "run-s build:tsc build:sshenabler", 15 | "build:tsc": "tsc", 16 | "build:sshenabler": "copyfiles -f ts/sshenabler/*.json dist/sshenabler", 17 | "build:mta": "mbt build", 18 | "release": "commit-and-tag-version", 19 | "release:minor": "commit-and-tag-version --minor", 20 | "deploy:cf": "cross-var cf deploy mta_archives/sshenabler_$npm_package_version.mtar", 21 | "enable-ssh": "cf enable-ssh sshenabler && cf restart sshenabler", 22 | "start:sshtunnel": "cf ssh sshenabler -L 20003:$npm_package_config_proxy:20003 -L 20004:$npm_package_config_proxy:20004" 23 | }, 24 | "author": "Joachim Van Praet", 25 | "license": "Apache-2.0", 26 | "dependencies": { 27 | "@sap-cloud-sdk/core": "^1.54.2", 28 | "@sap/xsenv": "^5.2.0", 29 | "axios": "^1.7.4", 30 | "client-oauth2": "^4.3.3", 31 | "dotenv": "^16.4.5", 32 | "http": "0.0.1-security", 33 | "http-proxy": "^1.18.1", 34 | "pino": "^8.7.0", 35 | "sap-cf-destconn": "0.0.38", 36 | "typescript": "^5.5.4" 37 | }, 38 | "devDependencies": { 39 | "@types/http-proxy": "^1.17.15", 40 | "@types/node": "^20", 41 | "@types/pino": "^7.0.5", 42 | "@types/sap__xsenv": "^3.3.2", 43 | "copyfiles": "^2.4.1", 44 | "cross-var": "^1.1.0", 45 | "npm-run-all": "^4.1.5", 46 | "pino-pretty": "^9.4.1", 47 | "commit-and-tag-version": "^11.3.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /socks-proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.1 9 | 10 | 11 | sap-cf-socks-proxy 12 | socks-proxy 13 | 0.0.1 14 | socks-proxy 15 | 16 | 8 17 | 18 | 19 | 20 | net.sourceforge.jtds 21 | jtds 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-test 31 | test 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-cloudfoundry-connector 38 | 2.0.9.RELEASE 39 | 40 | 41 | 42 | 43 | org.springframework.cloud 44 | spring-cloud-spring-service-connector 45 | 2.0.9.RELEASE 46 | 47 | 48 | 49 | 50 | org.json 51 | json 52 | 20210307 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /socks-proxy/socks-proxy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /socks-proxy/src/main/java/ConnectivitySocks5ProxySocket.java: -------------------------------------------------------------------------------- 1 | import org.json.JSONArray; 2 | import org.json.JSONException; 3 | import org.json.JSONObject; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.net.InetSocketAddress; 10 | import java.net.Socket; 11 | import java.net.SocketAddress; 12 | import java.net.SocketException; 13 | import java.nio.ByteBuffer; 14 | import java.util.Base64; 15 | 16 | public class ConnectivitySocks5ProxySocket extends Socket { 17 | 18 | private static final byte SOCKS5_VERSION = 0x05; 19 | private static final byte SOCKS5_JWT_AUTHENTICATION_METHOD = (byte) 0x80; 20 | private static final byte SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION = 0x01; 21 | private static final byte SOCKS5_COMMAND_CONNECT_BYTE = 0x01; 22 | private static final byte SOCKS5_COMMAND_REQUEST_RESERVED_BYTE = 0x00; 23 | private static final byte SOCKS5_COMMAND_ADDRESS_TYPE_IPv4_BYTE = 0x01; 24 | private static final byte SOCKS5_COMMAND_ADDRESS_TYPE_DOMAIN_BYTE = 0x03; 25 | private static final byte SOCKS5_AUTHENTICATION_METHODS_COUNT = 0x01; 26 | private static final int SOCKS5_JWT_AUTHENTICATION_METHOD_UNSIGNED_VALUE = 0x80 & 0xFF; 27 | private static final byte SOCKS5_AUTHENTICATION_SUCCESS_BYTE = 0x00; 28 | 29 | private static final String SOCKS5_PROXY_HOST_PROPERTY = "onpremise_proxy_host"; 30 | private static final String SOCKS5_PROXY_PORT_PROPERTY = "onpremise_socks5_proxy_port"; 31 | 32 | private final String jwtToken; 33 | private final String sccLocationId; 34 | private final boolean useSSHTunnel; 35 | 36 | public ConnectivitySocks5ProxySocket(String jwtToken4ConnectivityService, String sccLocationId, boolean useSSHTunnel) { 37 | this.jwtToken = jwtToken4ConnectivityService; 38 | this.sccLocationId = sccLocationId != null ? Base64.getEncoder().encodeToString(sccLocationId.getBytes()) : ""; 39 | this.useSSHTunnel = useSSHTunnel; 40 | } 41 | 42 | protected InetSocketAddress getProxyAddress() { 43 | try { 44 | if (!useSSHTunnel) { 45 | JSONObject credentials = extractEnvironmentCredentials(); 46 | String proxyHost = credentials.getString(SOCKS5_PROXY_HOST_PROPERTY); 47 | int proxyPort = Integer.parseInt(credentials.getString(SOCKS5_PROXY_PORT_PROPERTY)); 48 | return new InetSocketAddress(proxyHost, proxyPort); 49 | } else { 50 | return new InetSocketAddress("localhost", 20004); 51 | } 52 | } catch (JSONException ex) { 53 | throw new IllegalStateException("Unable to extract the SOCKS5 proxy host and port", ex); 54 | } 55 | } 56 | 57 | private JSONObject extractEnvironmentCredentials() throws JSONException { 58 | JSONObject jsonObj = new JSONObject(System.getenv("VCAP_SERVICES")); 59 | JSONArray jsonArr = jsonObj.getJSONArray("connectivity"); 60 | return jsonArr.getJSONObject(0).getJSONObject("credentials"); 61 | } 62 | 63 | @Override 64 | public void connect(SocketAddress endpoint, int timeout) throws IOException { 65 | super.connect(getProxyAddress(), timeout); 66 | OutputStream outputStream = getOutputStream(); 67 | executeSOCKS5InitialRequest(outputStream); 68 | executeSOCKS5AuthenticationRequest(outputStream); 69 | executeSOCKS5ConnectRequest(outputStream, (InetSocketAddress) endpoint); 70 | } 71 | 72 | private void executeSOCKS5InitialRequest(OutputStream outputStream) throws IOException { 73 | byte[] initialRequest = createInitialSOCKS5Request(); 74 | outputStream.write(initialRequest); 75 | assertServerInitialResponse(); 76 | } 77 | 78 | private byte[] createInitialSOCKS5Request() throws IOException { 79 | try (ByteArrayOutputStream byteArraysStream = new ByteArrayOutputStream()) { 80 | byteArraysStream.write(SOCKS5_VERSION); 81 | byteArraysStream.write(SOCKS5_AUTHENTICATION_METHODS_COUNT); 82 | byteArraysStream.write(SOCKS5_JWT_AUTHENTICATION_METHOD); 83 | return byteArraysStream.toByteArray(); 84 | } 85 | } 86 | 87 | private void assertServerInitialResponse() throws IOException { 88 | InputStream inputStream = getInputStream(); 89 | 90 | int versionByte = inputStream.read(); 91 | if (SOCKS5_VERSION != versionByte) { 92 | throw new SocketException(String.format("Unsupported SOCKS version - expected %s, but received %s", SOCKS5_VERSION, versionByte)); 93 | } 94 | 95 | int authenticationMethodValue = inputStream.read(); 96 | if (SOCKS5_JWT_AUTHENTICATION_METHOD_UNSIGNED_VALUE != authenticationMethodValue) { 97 | throw new SocketException(String.format("Unsupported authentication method value - expected %s, but received %s", 98 | SOCKS5_JWT_AUTHENTICATION_METHOD_UNSIGNED_VALUE, authenticationMethodValue)); 99 | } 100 | } 101 | 102 | private void executeSOCKS5AuthenticationRequest(OutputStream outputStream) throws IOException { 103 | byte[] authenticationRequest = createJWTAuthenticationRequest(); 104 | outputStream.write(authenticationRequest); 105 | 106 | assertAuthenticationResponse(); 107 | } 108 | 109 | private byte[] createJWTAuthenticationRequest() throws IOException { 110 | try (ByteArrayOutputStream byteArraysStream = new ByteArrayOutputStream()) { 111 | byteArraysStream.write(SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION); 112 | byteArraysStream.write(ByteBuffer.allocate(4).putInt(jwtToken.getBytes().length).array()); 113 | byteArraysStream.write(jwtToken.getBytes()); 114 | byteArraysStream.write(ByteBuffer.allocate(1).put((byte) sccLocationId.getBytes().length).array()); 115 | byteArraysStream.write(sccLocationId.getBytes()); 116 | return byteArraysStream.toByteArray(); 117 | } 118 | } 119 | 120 | private void assertAuthenticationResponse() throws IOException { 121 | InputStream inputStream = getInputStream(); 122 | 123 | int authenticationMethodVersion = inputStream.read(); 124 | if (SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION != authenticationMethodVersion) { 125 | throw new SocketException(String.format("Unsupported authentication method version - expected %s, but received %s", 126 | SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION, authenticationMethodVersion)); 127 | } 128 | 129 | int authenticationStatus = inputStream.read(); 130 | if (SOCKS5_AUTHENTICATION_SUCCESS_BYTE != authenticationStatus) { 131 | throw new SocketException("Authentication failed!"); 132 | } 133 | } 134 | 135 | private void executeSOCKS5ConnectRequest(OutputStream outputStream, InetSocketAddress endpoint) throws IOException { 136 | byte[] commandRequest = createConnectCommandRequest(endpoint); 137 | outputStream.write(commandRequest); 138 | 139 | assertConnectCommandResponse(); 140 | } 141 | 142 | private byte[] createConnectCommandRequest(InetSocketAddress endpoint) throws IOException { 143 | String host = endpoint.getHostName(); 144 | int port = endpoint.getPort(); 145 | try (ByteArrayOutputStream byteArraysStream = new ByteArrayOutputStream()) { 146 | byteArraysStream.write(SOCKS5_VERSION); 147 | byteArraysStream.write(SOCKS5_COMMAND_CONNECT_BYTE); 148 | byteArraysStream.write(SOCKS5_COMMAND_REQUEST_RESERVED_BYTE); 149 | byte[] hostToIPv4 = parseHostToIPv4(host); 150 | if (hostToIPv4 != null) { 151 | byteArraysStream.write(SOCKS5_COMMAND_ADDRESS_TYPE_IPv4_BYTE); 152 | byteArraysStream.write(hostToIPv4); 153 | } else { 154 | byteArraysStream.write(SOCKS5_COMMAND_ADDRESS_TYPE_DOMAIN_BYTE); 155 | byteArraysStream.write(ByteBuffer.allocate(1).put((byte) host.getBytes().length).array()); 156 | byteArraysStream.write(host.getBytes()); 157 | } 158 | byteArraysStream.write(ByteBuffer.allocate(2).putShort((short) port).array()); 159 | return byteArraysStream.toByteArray(); 160 | } 161 | } 162 | 163 | private void assertConnectCommandResponse() throws IOException { 164 | InputStream inputStream = getInputStream(); 165 | 166 | int versionByte = inputStream.read(); 167 | if (SOCKS5_VERSION != versionByte) { 168 | throw new SocketException(String.format("Unsupported SOCKS version - expected %s, but received %s", SOCKS5_VERSION, versionByte)); 169 | } 170 | 171 | int connectStatusByte = inputStream.read(); 172 | assertConnectStatus(connectStatusByte); 173 | 174 | readRemainingCommandResponseBytes(inputStream); 175 | } 176 | 177 | private void assertConnectStatus(int commandConnectStatus) throws IOException { 178 | if (commandConnectStatus == 0) { 179 | return; 180 | } 181 | 182 | String commandConnectStatusTranslation; 183 | switch (commandConnectStatus) { 184 | case 1: 185 | commandConnectStatusTranslation = "FAILURE"; 186 | break; 187 | case 2: 188 | commandConnectStatusTranslation = "FORBIDDEN"; 189 | break; 190 | case 3: 191 | commandConnectStatusTranslation = "NETWORK_UNREACHABLE"; 192 | break; 193 | case 4: 194 | commandConnectStatusTranslation = "HOST_UNREACHABLE"; 195 | break; 196 | case 5: 197 | commandConnectStatusTranslation = "CONNECTION_REFUSED"; 198 | break; 199 | case 6: 200 | commandConnectStatusTranslation = "TTL_EXPIRED"; 201 | break; 202 | case 7: 203 | commandConnectStatusTranslation = "COMMAND_UNSUPPORTED"; 204 | break; 205 | case 8: 206 | commandConnectStatusTranslation = "ADDRESS_UNSUPPORTED"; 207 | break; 208 | default: 209 | commandConnectStatusTranslation = "UNKNOWN"; 210 | break; 211 | } 212 | throw new SocketException("SOCKS5 command failed with status: " + commandConnectStatusTranslation); 213 | } 214 | 215 | private byte[] parseHostToIPv4(String hostName) { 216 | byte[] parsedHostName = null; 217 | String[] virtualHostOctets = hostName.split("\\.", -1); 218 | int octetsCount = virtualHostOctets.length; 219 | if (octetsCount == 4) { 220 | try { 221 | byte[] ipOctets = new byte[octetsCount]; 222 | for (int i = 0; i < octetsCount; i++) { 223 | int currentOctet = Integer.parseInt(virtualHostOctets[i]); 224 | if ((currentOctet < 0) || (currentOctet > 255)) { 225 | throw new IllegalArgumentException(String.format("Provided octet %s is not in the range of [0-255]", currentOctet)); 226 | } 227 | ipOctets[i] = (byte) currentOctet; 228 | } 229 | parsedHostName = ipOctets; 230 | } catch (IllegalArgumentException ex) { 231 | return null; 232 | } 233 | } 234 | 235 | return parsedHostName; 236 | } 237 | 238 | private void readRemainingCommandResponseBytes(InputStream inputStream) throws IOException { 239 | inputStream.read(); // skipping over SOCKS5 reserved byte 240 | int addressTypeByte = inputStream.read(); 241 | if (SOCKS5_COMMAND_ADDRESS_TYPE_IPv4_BYTE == addressTypeByte) { 242 | for (int i = 0; i < 6; i++) { 243 | inputStream.read(); 244 | } 245 | } else if (SOCKS5_COMMAND_ADDRESS_TYPE_DOMAIN_BYTE == addressTypeByte) { 246 | int domainNameLength = inputStream.read(); 247 | int portBytes = 2; 248 | inputStream.read(new byte[domainNameLength + portBytes], 0, domainNameLength + portBytes); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /socks-proxy/src/main/java/DBSocketFactory.java: -------------------------------------------------------------------------------- 1 | import org.json.JSONArray; 2 | import org.json.JSONException; 3 | import org.json.JSONObject; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | import javax.net.SocketFactory; 10 | import java.io.IOException; 11 | import java.net.InetAddress; 12 | import java.net.Socket; 13 | 14 | public class DBSocketFactory extends SocketFactory { 15 | 16 | private final boolean useSSHTunnel; 17 | private final String locationId; 18 | 19 | public DBSocketFactory(String cloudConnectorLocationId, boolean useSSHTunnel) { 20 | super(); 21 | this.useSSHTunnel = useSSHTunnel; 22 | this.locationId = cloudConnectorLocationId; 23 | } 24 | 25 | @Override 26 | public Socket createSocket() throws IOException { 27 | JSONObject credentials = extractEnvironmentCredentials(); 28 | String client_id = credentials.getString("clientid"); 29 | String client_secret = credentials.getString("clientsecret"); 30 | String tokenUrl = credentials.getString("url") + "/oauth/token?grant_type=client_credentials"; 31 | 32 | RestTemplate template = new RestTemplate(); 33 | HttpHeaders headers = new HttpHeaders(); 34 | headers.setBasicAuth(client_id, client_secret); 35 | HttpEntity entity = new HttpEntity(headers); 36 | 37 | String response = template.exchange(tokenUrl,HttpMethod.GET, entity, String.class).getBody(); 38 | 39 | JSONObject obj = new JSONObject(response); 40 | return new ConnectivitySocks5ProxySocket(obj.getString("access_token"), locationId, useSSHTunnel); 41 | } 42 | 43 | @Override 44 | public Socket createSocket(String arg0, int arg1) { 45 | throw new UnsupportedOperationException(); 46 | } 47 | 48 | @Override 49 | public Socket createSocket(InetAddress arg0, int arg1) { 50 | throw new UnsupportedOperationException(); 51 | } 52 | 53 | @Override 54 | public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3) { 55 | throw new UnsupportedOperationException(); 56 | } 57 | 58 | @Override 59 | public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) { 60 | throw new UnsupportedOperationException(); 61 | } 62 | 63 | 64 | private JSONObject extractEnvironmentCredentials() throws JSONException { 65 | JSONObject jsonObj = new JSONObject(System.getenv("VCAP_SERVICES")); 66 | JSONArray jsonArr = jsonObj.getJSONArray("connectivity"); 67 | return jsonArr.getJSONObject(0).getJSONObject("credentials"); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /socks-proxy/src/main/java/ProxyServer.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import java.io.InputStream; 3 | import java.io.OutputStream; 4 | import java.net.InetSocketAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | import java.util.logging.Logger; 8 | 9 | public class ProxyServer { 10 | private static final Logger LOGGER = Logger.getLogger(ProxyServer.class.getName()); 11 | 12 | private final ServerSocket serverSocket; 13 | private final String hostname; 14 | private final int port; 15 | private final String cloudConnectorLocationId; 16 | private final boolean useSSHTunnel; 17 | 18 | public ProxyServer(int port, String hostnameToReach, int portToReach, String cloudConnectorLocationId, boolean useSSHTunnel) throws IOException { 19 | this.serverSocket = new ServerSocket(port); 20 | this.hostname = hostnameToReach; 21 | this.port = portToReach; 22 | this.cloudConnectorLocationId = cloudConnectorLocationId; 23 | this.useSSHTunnel = useSSHTunnel; 24 | LOGGER.info("Initialized SOCKS5 proxy server listening on 127.0.0.1:" + port + " " + (useSSHTunnel ? 25 | "by using the SSH tunnel on localhost:20004": "by not using the SSH tunnel")); 26 | } 27 | 28 | public void run() throws IOException { 29 | while (true) { 30 | try { 31 | Socket client = serverSocket.accept(); 32 | // Handle each connection in a new thread 33 | new Thread(() -> handleClientConnection(client)).start(); 34 | } catch (IOException e) { 35 | LOGGER.throwing("ProxyServer", "run", e); 36 | } 37 | } 38 | } 39 | 40 | private void handleClientConnection(Socket client) { 41 | final byte[] request = new byte[1024]; 42 | final byte[] reply = new byte[4096]; 43 | 44 | try (Socket socketToProxy = new DBSocketFactory(cloudConnectorLocationId, useSSHTunnel).createSocket()) { 45 | socketToProxy.connect(InetSocketAddress.createUnresolved(hostname, port)); 46 | 47 | final InputStream streamFromServer = socketToProxy.getInputStream(); 48 | final OutputStream streamToServer = socketToProxy.getOutputStream(); 49 | 50 | final InputStream streamFromClient = client.getInputStream(); 51 | final OutputStream streamToClient = client.getOutputStream(); 52 | 53 | // Create a thread to handle client to server 54 | Thread clientToServer = new Thread(() -> { 55 | try { 56 | int bytesRead; 57 | while (!client.isClosed() && (bytesRead = streamFromClient.read(request)) != -1) { 58 | streamToServer.write(request, 0, bytesRead); 59 | streamToServer.flush(); 60 | } 61 | } catch (IOException e) { 62 | LOGGER.throwing("ProxyServer", "clientToServer", e); 63 | } 64 | }); 65 | 66 | // Create a thread to handle server to client 67 | Thread serverToClient = new Thread(() -> { 68 | try { 69 | int bytesRead; 70 | while ((bytesRead = streamFromServer.read(reply)) != -1) { 71 | streamToClient.write(reply, 0, bytesRead); 72 | streamToClient.flush(); 73 | } 74 | } catch (IOException e) { 75 | LOGGER.throwing("ProxyServer", "serverToClient", e); 76 | } 77 | }); 78 | 79 | clientToServer.start(); 80 | serverToClient.start(); 81 | 82 | clientToServer.join(); 83 | serverToClient.join(); 84 | 85 | } catch (IOException | InterruptedException e) { 86 | LOGGER.throwing("ProxyServer", "handleClientConnection", e); 87 | } finally { 88 | try { 89 | // Ensure the client socket is closed and threads can exit 90 | if (client != null) client.close(); 91 | LOGGER.info("Client disconnected, cleaning up"); 92 | } catch (IOException e) { 93 | LOGGER.throwing("ProxyServer", "handleClientConnection", e); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /socks-proxy/src/main/java/StartSocksProxy.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import java.util.logging.Logger; 3 | 4 | public class StartSocksProxy { 5 | static Logger LOGGER = Logger.getLogger(StartSocksProxy.class.getName()); 6 | 7 | public static void main(String[] args) throws IOException { 8 | String onPremiseHost = System.getenv("ON_PREMISE_HOST"); 9 | int onPremisePort = System.getenv("ON_PREMISE_PORT") != null ? Integer.parseInt(System.getenv("ON_PREMISE_PORT")) : 0; 10 | String cloudConnectorLocationId = System.getenv("CLOUD_CONNECTOR_LOCATION_ID"); 11 | // default port 5050 12 | int port = (System.getenv("PORT") != null) ? Integer.parseInt(System.getenv("PORT")) : 5050; 13 | // default to using SSH tunnel 14 | boolean useSSHTunnel = (System.getenv("USE_SSH_TUNNEL") == null) || Boolean.parseBoolean(System.getenv("USE_SSH_TUNNEL")); 15 | 16 | if (onPremiseHost != null && onPremisePort != 0) { 17 | LOGGER.info("Starting up local SOCKS5 proxy to " + onPremiseHost + ":" + onPremisePort); 18 | ProxyServer proxy = new ProxyServer(port, onPremiseHost, onPremisePort, cloudConnectorLocationId, useSSHTunnel); 19 | proxy.run(); 20 | } else { 21 | LOGGER.severe("Could not find ON_PREMISE_HOST and/or ON_PREMISE_PORT defined in any valid way"); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /socks-proxy/start-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $(grep -v '^#' .env | xargs) 3 | VCAP_SERVICES=$(jq '.VCAP_SERVICES' default-env.json|jq -c .) mvn compile exec:java -Dexec.mainClass="StartSocksProxy" 4 | -------------------------------------------------------------------------------- /start-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | until npm start; do 3 | echo "Proxy crashed with exit code $?. Respawning…" >&2 4 | sleep 1 5 | done 6 | -------------------------------------------------------------------------------- /start-ssh-tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | until npm run start:sshtunnel; do 3 | echo "SSH-tunnel crashed with exit code $?. Respawning…" >&2 4 | sleep 1 5 | done 6 | -------------------------------------------------------------------------------- /test/ping.http: -------------------------------------------------------------------------------- 1 | ### Test public endpoind using a no authentication destination via sap-cf-proxy 2 | GET {{$dotenv sapcpproxy}}/sap/public/ping 3 | X-SAP-BTP-destination: SAP_ABAP_BACKEND_NO_AUTH 4 | Authorization: Basic {{$dotenv sapid_username}}:{{$dotenv sapid_password}} 5 | 6 | ### Test authenticated endpoint using a basic auth destination via sap-cf-proxy 7 | GET {{$dotenv sapcpproxy}}/sap/bc/ping 8 | X-SAP-BTP-destination: SAP_ABAP_BACKEND_BASIC_AUTH 9 | Accept-Language: en 10 | Authorization: Basic {{$dotenv sapid_username}}:{{$dotenv sapid_password}} 11 | 12 | ### Test authenticated endpoint using a principal propagation destination via sap-cf-proxy 13 | GET {{$dotenv sapcpproxy}}/sap/bc/ping 14 | ?sap-client={{$dotenv sapclient}} 15 | X-SAP-BTP-destination: SAP_ABAP_BACKEND 16 | Accept-Language: en 17 | Authorization: Basic {{$dotenv sapid_username}}:{{$dotenv sapid_password}} 18 | 19 | 20 | ### Test OAuth2ClientCredentials destination (Microsoft Graph) 21 | GET {{$dotenv sapcpproxy}}/v1.0/users 22 | X-SAP-BTP-destination: MicrosoftGraph 23 | Authorization: Basic {{$dotenv sapid_username}}:{{$dotenv sapid_password}} 24 | -------------------------------------------------------------------------------- /ts/proxy/authentication.ts: -------------------------------------------------------------------------------- 1 | import * as xsenv from "@sap/xsenv"; 2 | import axios from "axios"; 3 | import { IHTTPDestinationConfiguration } from "sap-cf-destconn"; 4 | import internal from "stream"; 5 | 6 | type IXSUAA = { 7 | apiurl: string; 8 | clientid: string; 9 | clientsecret: string; 10 | identityzone: string; 11 | identityzoneid: string; 12 | sburl: string; 13 | subaccountid: string; 14 | tenantid: string; 15 | tenantmode: string; 16 | uaadomain: string; 17 | url: string; 18 | verificationkey: string; 19 | xsappname: string; 20 | zoneid: string; 21 | }; 22 | 23 | type ICredentials = { 24 | username: string; 25 | password: string; 26 | }; 27 | 28 | type IToken = { 29 | access_token: string; 30 | token_type: string; 31 | id_token: string; 32 | refresh_token: string; 33 | expires_in: number; 34 | scope: string; 35 | jti: string; 36 | }; 37 | 38 | const config = { 39 | timeout: Number(process.env.TIMEOUT_JWT) || 3600, // 3600 - 60 Minutes 40 | }; 41 | 42 | type IjwtTokenCache = { 43 | [authorization: string]: { 44 | timeout: number; 45 | jwtToken: any; 46 | }; 47 | }; 48 | 49 | var jwtTokenCache: IjwtTokenCache = {}; 50 | 51 | export const basicToJWT: any = async (authorization: string | ICredentials) => { 52 | xsenv.loadEnv(); 53 | // check if a xsuaa is linked to this project. 54 | const { xsuaa } = xsenv.getServices({ 55 | xsuaa: { 56 | tag: "xsuaa", 57 | }, 58 | }); 59 | 60 | if (!xsuaa) { 61 | throw `No xsuaa service found`; 62 | } 63 | if ("string" === typeof authorization) { 64 | authorization = decodeBA(authorization); 65 | } 66 | let jwtToken: IToken; 67 | if ( 68 | !jwtTokenCache || 69 | !jwtTokenCache[authorization.username] || 70 | new Date().getTime() - config.timeout * 1000 > 71 | jwtTokenCache[authorization.username].timeout 72 | ) { 73 | jwtToken = await fetchToken(xsuaa, authorization); 74 | jwtTokenCache[authorization.username] = { 75 | jwtToken: jwtToken, 76 | timeout: new Date().getTime(), 77 | }; 78 | } 79 | jwtToken = jwtTokenCache[authorization.username].jwtToken; 80 | return jwtToken; 81 | }; 82 | 83 | function convertScope(scope?: String) { 84 | if (!scope) return null; 85 | return scope 86 | .split(" ") 87 | .map((sc) => sc.split(":")) 88 | .reduce<{ [key: string]: string }>((acc, [key, value]) => { 89 | acc[key] = value; 90 | return acc; 91 | }, {}); 92 | } 93 | 94 | export const getAuthenticationType = (authorization: string) => { 95 | return /bearer/gim.test(authorization) 96 | ? "bearer" 97 | : /basic/gim.test(authorization) 98 | ? "basic" 99 | : "none"; 100 | }; 101 | 102 | const fetchToken: any = async (xsuaa: IXSUAA, credentials: ICredentials) => { 103 | const tokenBaseUrl = `${xsuaa.url}`; 104 | const token = ( 105 | await axios({ 106 | url: `${tokenBaseUrl}/oauth/token`, 107 | method: "POST", 108 | responseType: "json", 109 | data: `client_id=${encodeURIComponent( 110 | xsuaa.clientid 111 | )}&grant_type=password&response_type=token&username=${encodeURIComponent( 112 | credentials.username 113 | )}&password=${encodeURIComponent(credentials.password)}`, 114 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 115 | auth: { 116 | username: xsuaa.clientid, 117 | password: xsuaa.clientsecret, 118 | }, 119 | }) 120 | ).data; 121 | 122 | return token; 123 | }; 124 | 125 | function decodeBA(authorization: string) { 126 | const [type, userpwd] = authorization.split(" "); // Split on a space, the original auth looks like "Basic Y2hhcmxlczoxMjM0NQ==" and we need the 2nd part 127 | 128 | const buf = Buffer.from(userpwd, "base64"); // create a buffer and tell it the data coming in is base64 129 | const [username, password] = buf.toString().split(":"); 130 | 131 | return { 132 | username, 133 | password, 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /ts/proxy/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import httpProxy from "http-proxy"; 3 | import pino from "pino"; 4 | import dotenv from "dotenv"; 5 | import { 6 | getDestination, 7 | Destination, 8 | DestinationOptions, 9 | } from "@sap-cloud-sdk/core"; 10 | import * as xsenv from "@sap/xsenv"; 11 | 12 | import { basicToJWT, getAuthenticationType } from "./authentication"; 13 | 14 | type IAuthenticationType = "bearer" | "basic" | "none"; 15 | type IDestinationCache = { 16 | [destination: string]: { 17 | timeout: number; 18 | destination: Destination; 19 | }; 20 | }; 21 | //Load default-env.json file automatically from the beginning 22 | xsenv.loadEnv(); 23 | dotenv.config(); 24 | 25 | const logger = pino({ 26 | level: process.env.LOG_LEVEL || "info", 27 | transport: { 28 | target: "pino-pretty", 29 | options: { 30 | colorize: true, 31 | }, 32 | }, 33 | }); 34 | 35 | const proxy = httpProxy.createProxyServer({ 36 | secure: false, 37 | }); 38 | 39 | var destinationCache: IDestinationCache = {}; 40 | 41 | const config = { 42 | timeout: Number(process.env.TIMEOUT_DESTINATION) || 3600, // 3600 - 60 Minutes 43 | proxyport: process.env.PORT || 5050, 44 | defaultDestination: process.env.DEFAULT_DESTINATION || "SAP_ABAP_BACKEND", 45 | destinationPropertyName: ( 46 | process.env.DESTINATION_PROPERTY_NAME || "X-SAP-BTP-destination" 47 | ).toLowerCase(), 48 | cfproxy: { 49 | host: process.env.CFPROXY_HOST || "127.0.0.1", 50 | port: parseInt(process.env.CFPROXY_PORT || "20003"), 51 | }, 52 | credentials: 53 | process.env.USERNAME && process.env.PASSWORD 54 | ? { 55 | username: process.env.USERNAME, 56 | password: process.env.PASSWORD, 57 | } 58 | : undefined, 59 | }; 60 | 61 | proxy.on("proxyReq", function (proxyReq, req, res, options) { 62 | //TODO: do we need another way to send the headers to this function? 63 | //@ts-ignore 64 | const newHeaders = options.target?.headers || {}; 65 | 66 | Object.entries(newHeaders).forEach(function ([key, value]) { 67 | proxyReq.setHeader(key, value); 68 | }); 69 | }); 70 | 71 | const server = http.createServer(async (req, res) => { 72 | const authorization = req.headers.authorization || ""; 73 | const authenticationType = getAuthenticationType(authorization); 74 | 75 | let authorizationHeader; 76 | let jwtToken = { 77 | token_type: "", 78 | access_token: "", 79 | }; 80 | if (authenticationType === "basic") { 81 | jwtToken = await basicToJWT(authorization); 82 | authorizationHeader = `${jwtToken.token_type} ${jwtToken.access_token}`; 83 | } 84 | if (authenticationType === "bearer") { 85 | jwtToken.token_type = "bearer"; 86 | jwtToken.access_token = authorization.split(" ")[1]; 87 | authorizationHeader = authorization; 88 | } 89 | if (authenticationType === "none" && config.credentials) { 90 | jwtToken = await basicToJWT(config.credentials); 91 | authorizationHeader = `${jwtToken.token_type} ${jwtToken.access_token}`; 92 | } 93 | 94 | // read the destination name 95 | const destinationName = 96 | [req.headers[config.destinationPropertyName]].flat()[0] || 97 | config.defaultDestination; 98 | logger.info(`Request entered the building: proxy to ${destinationName}`); 99 | let sdkDestination; 100 | // read the destination on cloud foundry 101 | try { 102 | if ( 103 | !destinationCache || 104 | !destinationCache[destinationName] || 105 | new Date().getTime() - config.timeout * 1000 > 106 | destinationCache[destinationName].timeout 107 | ) { 108 | let options: DestinationOptions = {}; 109 | if (jwtToken.access_token !== "") { 110 | options.userJwt = jwtToken.access_token; 111 | } 112 | sdkDestination = await getDestination(destinationName, options); 113 | if (sdkDestination === null) { 114 | throw Error(`Connection ${destinationName} not found`); 115 | } 116 | logger.info(`Cache destination ${destinationName}`); 117 | destinationCache[destinationName] = { 118 | destination: sdkDestination, 119 | timeout: new Date().getTime(), 120 | }; 121 | } 122 | sdkDestination = destinationCache[destinationName].destination; 123 | 124 | logger.info(`Forwarding this request to ${sdkDestination.url}`); 125 | let target: any = new URL(sdkDestination.url); 126 | target.headers = { 127 | host: target.host, 128 | }; 129 | 130 | // 131 | if (sdkDestination.authentication === "BasicAuthentication") { 132 | req.headers.authorization = 133 | "Basic " + 134 | Buffer.from( 135 | `${sdkDestination.username}:${sdkDestination.password}`, 136 | "ascii" 137 | ).toString("base64"); 138 | } 139 | 140 | if (sdkDestination.authentication === "OAuth2ClientCredentials") { 141 | if (!sdkDestination.authTokens) { 142 | throw new Error( 143 | `No token retrieved for destination ${destinationName}` 144 | ); 145 | } 146 | const clientCredentialsToken = sdkDestination.authTokens[0].value; 147 | target.headers = { 148 | ...target.headers, 149 | Authorization: `Bearer ${clientCredentialsToken}`, 150 | }; 151 | delete req.headers.authorization; 152 | } 153 | 154 | if ( 155 | sdkDestination.authTokens && 156 | sdkDestination.authTokens[0] && 157 | !sdkDestination.authTokens[0].error 158 | ) { 159 | if (sdkDestination.authTokens[0].error) { 160 | throw new Error(sdkDestination.authTokens[0].error); 161 | } 162 | target.headers = { 163 | ...target.headers, 164 | Authorization: `${sdkDestination.authTokens[0].type} ${sdkDestination.authTokens[0].value}`, 165 | }; 166 | delete req.headers.authorization; 167 | } 168 | // 169 | 170 | if (sdkDestination.proxyType!.toLowerCase() === "onpremise") { 171 | logger.info( 172 | `This is an on premise request. Let's send it over the SSH tunnel.` 173 | ); 174 | 175 | target = { 176 | headers: { 177 | ...target.headers, 178 | }, 179 | protocol: sdkDestination.proxyConfiguration!.protocol, 180 | host: config.cfproxy.host, 181 | port: config.cfproxy.port, 182 | }; 183 | if (sdkDestination.cloudConnectorLocationId) { 184 | target.headers["SAP-Connectivity-SCC-Location_ID"] = 185 | sdkDestination.cloudConnectorLocationId; 186 | } 187 | 188 | if (sdkDestination.proxyConfiguration) { 189 | req.headers = { 190 | ...req.headers, 191 | ...sdkDestination.proxyConfiguration!.headers, 192 | }; 193 | } 194 | 195 | if (sdkDestination.authentication === "PrincipalPropagation") { 196 | req.headers["SAP-Connectivity-Authentication"] = authorizationHeader; 197 | delete req.headers.authorization; 198 | } 199 | } 200 | 201 | proxy.web(req, res, { target }); 202 | } catch (error) { 203 | logger.error(JSON.stringify(error)); 204 | } 205 | }); 206 | 207 | logger.info(`proxy listening on port : ${config.proxyport}`); 208 | server.listen(config.proxyport); 209 | -------------------------------------------------------------------------------- /ts/sshenabler/index.ts: -------------------------------------------------------------------------------- 1 | // just a dummy http server to create a SSH tunnel to serve onpremise destinations. 2 | import http from "http"; 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | const server = http.createServer((req, res) => { 7 | res.statusCode = 200; 8 | res.setHeader("Content-Type", "text/plain"); 9 | res.end(`Hi, I'm just a lazy service to enable local SSH tunnels.`); 10 | }); 11 | 12 | server.listen(port, () => { 13 | console.log(`Server is running on port ${port}!`); 14 | }); 15 | -------------------------------------------------------------------------------- /ts/sshenabler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sshenabler", 3 | "version": "0.0.1", 4 | "engines": { 5 | "node": "^18" 6 | }, 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "dependencies": { 11 | "http": "0.0.1-security" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./ts", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /xs-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "xsappname": "sshenabler", 3 | "tenant-mode": "dedicated", 4 | "description": "SSH Enabler", 5 | "scopes": [ ], 6 | "role-templates": [ ], 7 | "oauth2-configuration": { 8 | "redirect-uris": [ 9 | "https://*.cfapps.eu10.hana.ondemand.com/**", 10 | "http://localhost:*/**" 11 | ] 12 | } 13 | } 14 | --------------------------------------------------------------------------------