Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
laszlo_kajan2
Active Participant
2,343

Goal


Complement the Java example on the SAP help page 'Consuming the Connectivity Service / Using the TCP Protocol for Cloud Applications' with a Node.js example.

Keywords: 'how to use the SAP BTP CF connectivity service SOCKS5, TCP proxy from a Node.js application?', 'how to reach an on-premises TCP service from a BTP CF Node.js application?'

Node.js example


This example provides a SOCKS5 client implementation that uses the connectivity service available in the Business Technology Platform (BTP) Cloud Foundry (CF) environment.

The code defines a connection utility module 'btp-cf-socks5-proxy-utils', with type information using JSDoc annotations. Thanks to the JSDoc typing, the module provides TypeScript checks and inline hints when used in the Business Application Studio (BAS):
// @ts-check
'use strict';
/** @type {import('assert/strict')} */
const assert = require('assert').strict;
const SocksClient = require('socks').SocksClient;

let btpCfSocks5ProxyUtils = {

// See the implementation of assertAuthenticationResponse() at Using the TCP Protocol for Cloud Applications | https://help.sap.com/viewer/cca91383641e40ffbe03bdc78f00f681/Cloud/en-US/cd1583775afa43f0bb9ec69d9db...
SOCKS5_AUTHENTICATION_SUCCESS_BYTE: 0x00,
SOCKS5_CUSTOM_RESP_SIZE: 2,
SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION: 0x01,

/**
* @typedef {object} ConnectionOptions
* @prop {string} [cc_location=""] - cloud connector location, optional
* @prop {string} conn_svc_token - JWT token obtained via client_credentials grant for bound connectivity service
* @prop {string} remote_host - proxy destination host
* @prop {number} remote_port - proxy destination port
* @prop {string} onpremise_proxy_host - SOCKS5 proxy host
* @prop {number | string} onpremise_socks5_proxy_port - SOCKS5 proxy port
*/

/**
* @typedef {import('socks/typings/common/constants').SocksClientEstablishedEvent} SocksClientEstablishedEvent
*/

/**
* Creates a new SOCKS connection.
* @param {ConnectionOptions} opts
* @returns {Promise<SocksClientEstablishedEvent>}
*/
createConnection: function (opts) {
const ccLocation = opts.cc_location || "";

/**
* @type {import('socks').SocksClientOptions}
*/
const options = {
proxy: {
host: opts.onpremise_proxy_host,
port: typeof opts.onpremise_socks5_proxy_port === 'number' ? opts.onpremise_socks5_proxy_port : parseInt(opts.onpremise_socks5_proxy_port, 10),
type: 5, // Proxy version (4 or 5)
//
// SOCKS5 Custom authentication
custom_auth_method: 0x80,
custom_auth_request_handler: btpCfSocks5ProxyUtils.getCustomAuthRequestHandler(
opts.conn_svc_token, ccLocation),
custom_auth_response_size: btpCfSocks5ProxyUtils.SOCKS5_CUSTOM_RESP_SIZE,
custom_auth_response_handler: btpCfSocks5ProxyUtils.customAuthResponseHandler
},

command: 'connect', // SOCKS command (createConnection factory function only supports the connect command)

destination: {
host: opts.remote_host,
port: opts.remote_port
}
};

return SocksClient.createConnection(options);
},

/**
* @callback CustomAuthRequestHandler
* @param {string} connSvcToken - JWT token obtained via client_credentials grant for bound connectivity service
* @param {string} cloudConnectorLocation - cloud connector location or ""
* @returns {Promise<Buffer>}
*/
/** @type {CustomAuthRequestHandler} */
customAuthRequestHandler: async function (connSvcToken, cloudConnectorLocation) {

// This will be called when it's time to send the custom auth handshake. You must return a Buffer containing the data to send as your authentication.
const _1_authMethodVersion = Buffer.from([btpCfSocks5ProxyUtils.SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION]); // Authentication method version
const _3_jwtBuf = Buffer.from(connSvcToken, 'binary'); // X bytes: The actual value of the JWT in its encoded form
const _2_jwtBufLength = Buffer.allocUnsafe(4); // 4 bytes: Length of the JWT
_2_jwtBufLength.writeInt32BE(_3_jwtBuf.length);
const _5_ccNameB64 = Buffer.from(Buffer.from(cloudConnectorLocation).toString('base64'), 'binary');
// Y - The value of the Cloud Connector location ID in base64-encoded form
const _4_ccNameLength = Buffer.allocUnsafe(1); // 1 byte: Length of the Cloud Connector location ID (0 if no Cloud Connector location ID is used)
_4_ccNameLength.writeUInt8(_5_ccNameB64.length);

let retBuf = Buffer.alloc(
_1_authMethodVersion.length +
_2_jwtBufLength.length +
_3_jwtBuf.length +
_4_ccNameLength.length +
_5_ccNameB64.length
);

/**
* @type {number}
*/
let offset = 0;
_1_authMethodVersion.copy(retBuf, offset); offset += _1_authMethodVersion.length;
_2_jwtBufLength.copy(retBuf, offset); offset += _2_jwtBufLength.length;
_3_jwtBuf.copy(retBuf, offset); offset += _3_jwtBuf.length;
_4_ccNameLength.copy(retBuf, offset); offset += _4_ccNameLength.length;
if (_5_ccNameB64.length > 0) {
_5_ccNameB64.copy(retBuf, offset); offset += _5_ccNameB64.length;
}

assert.equal(offset, retBuf.length);

return retBuf;
},

/**
* @param {Buffer} data SOCKS proxy authentication response
* @returns Promise<boolean>
*/
customAuthResponseHandler: async function (data) {

assert.equal(data.length, btpCfSocks5ProxyUtils.SOCKS5_CUSTOM_RESP_SIZE);

const authenticationMethodVersion = data[0];
const authenticationStatus = data[1];
// console.log(data);

if (btpCfSocks5ProxyUtils.SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION !== authenticationMethodVersion) {
throw new Error(`Unsupported authentication method version - expected ${btpCfSocks5ProxyUtils.SOCKS5_JWT_AUTHENTICATION_METHOD_VERSION}, but received ${authenticationMethodVersion}`);
}
if (btpCfSocks5ProxyUtils.SOCKS5_AUTHENTICATION_SUCCESS_BYTE !== authenticationStatus) {
throw new Error(`Authentication failed (${authenticationStatus})!`);
}
return btpCfSocks5ProxyUtils.SOCKS5_AUTHENTICATION_SUCCESS_BYTE === authenticationStatus;
},

/**
* @param {string} connSvcToken - JWT token obtained via client_credentials grant for bound connectivity service
* @param {string} cloudConnectorLocation - cloud connector location or ""
* @returns {() => Promise<Buffer>}
*/
getCustomAuthRequestHandler: function (connSvcToken, cloudConnectorLocation) {
return btpCfSocks5ProxyUtils.customAuthRequestHandler.bind(null, connSvcToken, cloudConnectorLocation);
}
};

module.exports = btpCfSocks5ProxyUtils;

This example shows how the 'btp-cf-socks5-proxy-utils' module can be used to connect to the SOCKS5 proxy provided by a bound connectivity service:
const btpCfSocks5ProxyUtils = require('btp-cf-socks5-proxy-utils');
const sdkCore = require('@sap-cloud-sdk/core');
const xsenv = require('@sap/xsenv');

// Connectivity service
const connServiceCredentials = xsenv.serviceCredentials({ tag: 'connectivity' });
const connSvcToken = await sdkCore.serviceToken('connectivity', {
isolationStrategy: sdkCore.IsolationStrategy.No_Isolation, // there's just one bound connectivity service, no tenants
useCache: true
});

// SOCKS proxy
const info = await btpCfSocks5ProxyUtils.createConnection({
cc_location: options.ldapsVirtualLocation,
conn_svc_token: connSvcToken,
remote_host: options.ldapsVirtualHost,
remote_port: options.ldapsVirtualPort,
onpremise_proxy_host: connServiceCredentials.onpremise_proxy_host,
onpremise_socks5_proxy_port: connServiceCredentials.onpremise_socks5_proxy_port
});

// Example LDAP client that uses the socket - info.socket - from above
const newLdapClient = new LdapClient({
idleTimeout: options.idleTimeoutMillisec || 0,
tlsOptions: Object.assign({}, tlsOptions, { socket: info.socket }),
url: [ldapUrl]
});

Summary


In this blog I presented a to-the-point Node.js example for using the SOCK5 TCP proxy with SAP BTP cloud applications.

(A public, open source module - 'btp-cf-socks5-proxy-utils' - of the code presented here is soon to be published.)

Author and motivation


Laszlo Kajan is a full stack SAP developer present on the field since 2015, diversifying into the area of SAP Business Technology Platform (BTP) development.

The motivation behind this blog post is to complement the Java 'Using the TCP Protocol for Cloud Applications' example available on help.sap.com with a Node.js example. This is done in the hope that it will same fellow developers some time.

Further reading


9 Comments
Labels in this area