@sap-ux/backend-proxy-middleware-cf 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config.js +4 -0
- package/dist/config/env.d.ts +20 -5
- package/dist/config/env.js +55 -23
- package/dist/middleware.js +7 -1
- package/dist/proxy/routes.js +36 -0
- package/dist/tunnel/destination-check.d.ts +11 -0
- package/dist/tunnel/destination-check.js +110 -0
- package/dist/tunnel/tunnel.d.ts +26 -0
- package/dist/tunnel/tunnel.js +168 -0
- package/dist/types.d.ts +24 -0
- package/package.json +2 -2
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_REWRITE_CONTENT_TYPES = void 0;
|
|
4
4
|
exports.mergeEffectiveOptions = mergeEffectiveOptions;
|
|
5
|
+
const adp_tooling_1 = require("@sap-ux/adp-tooling");
|
|
5
6
|
exports.DEFAULT_REWRITE_CONTENT_TYPES = [
|
|
6
7
|
'text/html',
|
|
7
8
|
'application/json',
|
|
@@ -28,6 +29,9 @@ function mergeEffectiveOptions(configuration) {
|
|
|
28
29
|
appendAuthRoute: false,
|
|
29
30
|
disableWelcomeFile: false,
|
|
30
31
|
disableUi5ServerRoutes: false,
|
|
32
|
+
disableSshTunnel: false,
|
|
33
|
+
tunnelAppName: adp_tooling_1.DEFAULT_TUNNEL_APP_NAME,
|
|
34
|
+
skipSshEnable: false,
|
|
31
35
|
extensions: [],
|
|
32
36
|
...configuration
|
|
33
37
|
};
|
package/dist/config/env.d.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import type { ToolsLogger } from '@sap-ux/logger';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AppRouterEnvOptions } from '@sap-ux/adp-tooling';
|
|
3
|
+
import type { ConnectivityProxyInfo, EffectiveOptions } from '../types';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Extract connectivity proxy host and port from VCAP_SERVICES.
|
|
6
|
+
*
|
|
7
|
+
* @param vcapServices - Parsed VCAP_SERVICES object.
|
|
8
|
+
* @returns Proxy info or undefined if no connectivity service is present.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getConnectivityProxyInfo(vcapServices: Record<string, unknown> | undefined): ConnectivityProxyInfo | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Apply options to process.env with JSON-stringified destinations and VCAP_SERVICES.
|
|
13
|
+
* Overrides the connectivity proxy host to localhost so traffic flows through the local SSH tunnel.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Env options to apply.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyToProcessEnv(options: AppRouterEnvOptions): void;
|
|
18
|
+
/**
|
|
19
|
+
* Load env options from file or CF and merge destinations from effectiveOptions.
|
|
5
20
|
*
|
|
6
21
|
* When effectiveOptions.envOptionsPath is set, loads that JSON file. When null, loads mta.yaml one level
|
|
7
|
-
* above rootPath and fetches VCAP_SERVICES from CF. effectiveOptions.destinations is
|
|
22
|
+
* above rootPath and fetches VCAP_SERVICES from CF. effectiveOptions.destinations is appended so
|
|
8
23
|
* middleware config takes precedence over file/env.
|
|
9
24
|
*
|
|
10
25
|
* @param rootPath - Project root path.
|
|
11
26
|
* @param effectiveOptions - Merged config; envOptionsPath and destinations are used.
|
|
12
27
|
* @param logger - Logger for CF path.
|
|
13
|
-
* @returns
|
|
28
|
+
* @returns Loaded and merged env options.
|
|
14
29
|
*/
|
|
15
|
-
export declare function
|
|
30
|
+
export declare function loadEnvOptions(rootPath: string, effectiveOptions: EffectiveOptions, logger: ToolsLogger): Promise<AppRouterEnvOptions>;
|
|
16
31
|
/**
|
|
17
32
|
* Ensure the ui5-server destination exists and has the correct URL.
|
|
18
33
|
* If ui5-server doesn't exist in configuration, it will be auto-created.
|
package/dist/config/env.js
CHANGED
|
@@ -3,12 +3,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getConnectivityProxyInfo = getConnectivityProxyInfo;
|
|
7
|
+
exports.applyToProcessEnv = applyToProcessEnv;
|
|
8
|
+
exports.loadEnvOptions = loadEnvOptions;
|
|
7
9
|
exports.updateUi5ServerDestinationPort = updateUi5ServerDestinationPort;
|
|
8
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
12
|
const adp_tooling_1 = require("@sap-ux/adp-tooling");
|
|
11
13
|
const constants_1 = require("./constants");
|
|
14
|
+
/**
|
|
15
|
+
* Extract connectivity proxy host and port from VCAP_SERVICES.
|
|
16
|
+
*
|
|
17
|
+
* @param vcapServices - Parsed VCAP_SERVICES object.
|
|
18
|
+
* @returns Proxy info or undefined if no connectivity service is present.
|
|
19
|
+
*/
|
|
20
|
+
function getConnectivityProxyInfo(vcapServices) {
|
|
21
|
+
if (!vcapServices) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const connectivity = vcapServices['connectivity'];
|
|
25
|
+
if (!Array.isArray(connectivity)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of connectivity) {
|
|
29
|
+
const host = entry.credentials?.onpremise_proxy_host;
|
|
30
|
+
const port = entry.credentials?.onpremise_proxy_port;
|
|
31
|
+
if (host && port) {
|
|
32
|
+
return { host: String(host), port: Number(port) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
12
37
|
/**
|
|
13
38
|
* Load and parse env options JSON file.
|
|
14
39
|
*
|
|
@@ -32,10 +57,21 @@ function loadEnvOptionsFromFile(rootPath, envOptionsPath) {
|
|
|
32
57
|
}
|
|
33
58
|
/**
|
|
34
59
|
* Apply options to process.env with JSON-stringified destinations and VCAP_SERVICES.
|
|
60
|
+
* Overrides the connectivity proxy host to localhost so traffic flows through the local SSH tunnel.
|
|
35
61
|
*
|
|
36
62
|
* @param options - Env options to apply.
|
|
37
63
|
*/
|
|
38
64
|
function applyToProcessEnv(options) {
|
|
65
|
+
if (options.VCAP_SERVICES) {
|
|
66
|
+
const connectivity = options.VCAP_SERVICES['connectivity'];
|
|
67
|
+
if (Array.isArray(connectivity)) {
|
|
68
|
+
for (const entry of connectivity) {
|
|
69
|
+
if (entry.credentials?.onpremise_proxy_host) {
|
|
70
|
+
entry.credentials.onpremise_proxy_host = 'localhost';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
39
75
|
const envOptions = {
|
|
40
76
|
...options,
|
|
41
77
|
...(options.destinations ? { destinations: JSON.stringify(options.destinations) } : {}),
|
|
@@ -44,44 +80,40 @@ function applyToProcessEnv(options) {
|
|
|
44
80
|
Object.assign(process.env, envOptions);
|
|
45
81
|
}
|
|
46
82
|
/**
|
|
47
|
-
* Load env options from file or CF
|
|
83
|
+
* Load env options from file or CF and merge destinations from effectiveOptions.
|
|
48
84
|
*
|
|
49
85
|
* When effectiveOptions.envOptionsPath is set, loads that JSON file. When null, loads mta.yaml one level
|
|
50
|
-
* above rootPath and fetches VCAP_SERVICES from CF. effectiveOptions.destinations is
|
|
86
|
+
* above rootPath and fetches VCAP_SERVICES from CF. effectiveOptions.destinations is appended so
|
|
51
87
|
* middleware config takes precedence over file/env.
|
|
52
88
|
*
|
|
53
89
|
* @param rootPath - Project root path.
|
|
54
90
|
* @param effectiveOptions - Merged config; envOptionsPath and destinations are used.
|
|
55
91
|
* @param logger - Logger for CF path.
|
|
56
|
-
* @returns
|
|
92
|
+
* @returns Loaded and merged env options.
|
|
57
93
|
*/
|
|
58
|
-
async function
|
|
94
|
+
async function loadEnvOptions(rootPath, effectiveOptions, logger) {
|
|
59
95
|
const { envOptionsPath, destinations: middlewareDestinations } = effectiveOptions;
|
|
60
|
-
let options;
|
|
61
96
|
if (envOptionsPath) {
|
|
62
97
|
const envOptions = loadEnvOptionsFromFile(rootPath, envOptionsPath);
|
|
63
98
|
const destinations = envOptions.destinations
|
|
64
99
|
? [...envOptions.destinations, ...middlewareDestinations]
|
|
65
100
|
: middlewareDestinations;
|
|
66
|
-
|
|
101
|
+
return { ...envOptions, destinations };
|
|
67
102
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
throw new Error(`mta.yaml not found at "${mtaPath}". Cannot load CF env options.`);
|
|
76
|
-
}
|
|
77
|
-
const mtaYaml = (0, adp_tooling_1.getYamlContent)(mtaPath);
|
|
78
|
-
const VCAP_SERVICES = await (0, adp_tooling_1.buildVcapServicesFromResources)(mtaYaml.resources, spaceGuid, logger);
|
|
79
|
-
options = {
|
|
80
|
-
VCAP_SERVICES,
|
|
81
|
-
destinations: middlewareDestinations
|
|
82
|
-
};
|
|
103
|
+
const mtaPath = node_path_1.default.resolve(rootPath, '..', 'mta.yaml');
|
|
104
|
+
const spaceGuid = await (0, adp_tooling_1.getSpaceGuidFromUi5Yaml)(rootPath, logger);
|
|
105
|
+
if (!spaceGuid) {
|
|
106
|
+
throw new Error('No space GUID (from config or ui5.yaml). Cannot load CF env options.');
|
|
107
|
+
}
|
|
108
|
+
if (!node_fs_1.default.existsSync(mtaPath)) {
|
|
109
|
+
throw new Error(`mta.yaml not found at "${mtaPath}". Cannot load CF env options.`);
|
|
83
110
|
}
|
|
84
|
-
|
|
111
|
+
const mtaYaml = (0, adp_tooling_1.getYamlContent)(mtaPath);
|
|
112
|
+
const VCAP_SERVICES = await (0, adp_tooling_1.buildVcapServicesFromResources)(mtaYaml.resources, spaceGuid, logger);
|
|
113
|
+
return {
|
|
114
|
+
VCAP_SERVICES,
|
|
115
|
+
destinations: middlewareDestinations
|
|
116
|
+
};
|
|
85
117
|
}
|
|
86
118
|
/**
|
|
87
119
|
* Ensure the ui5-server destination exists and has the correct URL.
|
package/dist/middleware.js
CHANGED
|
@@ -16,6 +16,7 @@ const xssecurity_1 = require("./platform/xssecurity");
|
|
|
16
16
|
const bas_1 = require("./platform/bas");
|
|
17
17
|
const routes_1 = require("./proxy/routes");
|
|
18
18
|
const env_1 = require("./config/env");
|
|
19
|
+
const tunnel_1 = require("./tunnel/tunnel");
|
|
19
20
|
dotenv_1.default.config();
|
|
20
21
|
/**
|
|
21
22
|
* UI5 server middleware: runs `@sap/approuter` and proxies matching requests to it.
|
|
@@ -45,8 +46,13 @@ async function backendProxyMiddlewareCf({ options, middlewareUtil }) {
|
|
|
45
46
|
if (!node_fs_1.default.existsSync(xsappJsonPath)) {
|
|
46
47
|
throw new Error(`xs-app.json not found at "${xsappJsonPath}"`);
|
|
47
48
|
}
|
|
48
|
-
await (0, env_1.
|
|
49
|
+
const envOptions = await (0, env_1.loadEnvOptions)(rootPath, effectiveOptions, logger);
|
|
50
|
+
const connectivityInfo = (0, env_1.getConnectivityProxyInfo)(envOptions.VCAP_SERVICES);
|
|
51
|
+
(0, env_1.applyToProcessEnv)(envOptions);
|
|
49
52
|
await (0, xssecurity_1.updateXsuaaService)(rootPath, logger);
|
|
53
|
+
if (!effectiveOptions.disableSshTunnel && connectivityInfo) {
|
|
54
|
+
await (0, tunnel_1.setupSshTunnel)(rootPath, connectivityInfo, effectiveOptions, logger);
|
|
55
|
+
}
|
|
50
56
|
const sourcePath = project.getSourcePath();
|
|
51
57
|
const xsappConfig = (0, routes_1.loadAndPrepareXsappConfig)({
|
|
52
58
|
rootPath,
|
package/dist/proxy/routes.js
CHANGED
|
@@ -18,6 +18,41 @@ const UI5_SERVER_AUTH_ROUTE = {
|
|
|
18
18
|
destination: constants_1.UI5_SERVER_DESTINATION,
|
|
19
19
|
authenticationType: 'xsuaa'
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Inject localDir routes for ADP live-reload so adaptation project changes
|
|
23
|
+
* and i18n files are served directly from webapp/ without a build.
|
|
24
|
+
*
|
|
25
|
+
* Routes are prepended so they take priority over the catch-all approuter routes.
|
|
26
|
+
*
|
|
27
|
+
* @param routes - Mutable routes array to prepend ADP routes into.
|
|
28
|
+
* @param rootPath - Project root used to locate webapp/manifest.appdescr_variant.
|
|
29
|
+
* @param logger - Logger for info/warn messages.
|
|
30
|
+
*/
|
|
31
|
+
function injectAdpLiveReloadRoutes(routes, rootPath, logger) {
|
|
32
|
+
const manifestPath = node_path_1.default.join(rootPath, 'webapp', 'manifest.appdescr_variant');
|
|
33
|
+
if (!node_fs_1.default.existsSync(manifestPath)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
38
|
+
const variantId = manifest.id.replaceAll('.', '_');
|
|
39
|
+
routes.unshift({
|
|
40
|
+
source: `^/changes/${variantId}/(.*)$`,
|
|
41
|
+
target: 'changes/$1',
|
|
42
|
+
localDir: 'webapp',
|
|
43
|
+
authenticationType: 'none'
|
|
44
|
+
}, {
|
|
45
|
+
source: `^/${variantId}/i18n/(.*)$`,
|
|
46
|
+
target: 'i18n/$1',
|
|
47
|
+
localDir: 'webapp',
|
|
48
|
+
authenticationType: 'none'
|
|
49
|
+
});
|
|
50
|
+
logger.info(`ADP live-reload: injected localDir routes for /changes/${variantId}/* and /${variantId}/i18n/*`);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
logger.warn(`Failed to read manifest.appdescr_variant: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
21
56
|
/**
|
|
22
57
|
* Load xs-app.json and prepare it for the approuter (filter routes, set auth, optionally append auth route).
|
|
23
58
|
* Mutates and returns the config; does not build RouteEntry[].
|
|
@@ -41,6 +76,7 @@ function loadAndPrepareXsappConfig(options) {
|
|
|
41
76
|
}
|
|
42
77
|
return true;
|
|
43
78
|
});
|
|
79
|
+
injectAdpLiveReloadRoutes(xsappConfig.routes, rootPath, logger);
|
|
44
80
|
if (effectiveOptions.disableWelcomeFile) {
|
|
45
81
|
delete xsappConfig.welcomeFile;
|
|
46
82
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolsLogger } from '@sap-ux/logger';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether the adaptation project's webapp/xs-app.json exists and contains at least
|
|
4
|
+
* one route whose destination is configured as OnPremise in the BTP Destination Service.
|
|
5
|
+
*
|
|
6
|
+
* @param rootPath - Project root path.
|
|
7
|
+
* @param logger - Logger instance.
|
|
8
|
+
* @returns True if an OnPremise destination is found; false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hasOnPremiseDestination(rootPath: string, logger: ToolsLogger): Promise<boolean>;
|
|
11
|
+
//# sourceMappingURL=destination-check.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
exports.hasOnPremiseDestination = hasOnPremiseDestination;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const btp_utils_1 = require("@sap-ux/btp-utils");
|
|
10
|
+
const adp_tooling_1 = require("@sap-ux/adp-tooling");
|
|
11
|
+
/**
|
|
12
|
+
* Extract unique destination names from the routes in webapp/xs-app.json.
|
|
13
|
+
*
|
|
14
|
+
* @param rootPath - Project root path.
|
|
15
|
+
* @returns Array of unique destination names, or empty array if no webapp/xs-app.json or no destinations.
|
|
16
|
+
*/
|
|
17
|
+
function getWebappXsappDestinationNames(rootPath) {
|
|
18
|
+
const xsappPath = node_path_1.default.join(rootPath, 'webapp', 'xs-app.json');
|
|
19
|
+
if (!node_fs_1.default.existsSync(xsappPath)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const xsappConfig = JSON.parse(node_fs_1.default.readFileSync(xsappPath, 'utf8'));
|
|
24
|
+
const names = new Set();
|
|
25
|
+
for (const route of xsappConfig.routes ?? []) {
|
|
26
|
+
if (route.destination) {
|
|
27
|
+
names.add(route.destination);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [...names];
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract BTP destination service auth from process.env.VCAP_SERVICES.
|
|
38
|
+
*
|
|
39
|
+
* @returns Auth info or undefined if no destination service is bound.
|
|
40
|
+
*/
|
|
41
|
+
function getBtpDestinationServiceAuth() {
|
|
42
|
+
const raw = process.env.VCAP_SERVICES;
|
|
43
|
+
if (!raw) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let vcapServices;
|
|
47
|
+
try {
|
|
48
|
+
vcapServices = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const entries = vcapServices['destination'];
|
|
54
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const credentials = entries[0].credentials;
|
|
58
|
+
if (!credentials?.clientid || !credentials?.clientsecret || !credentials?.url || !credentials?.uri) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
uaa: { clientid: credentials.clientid, clientsecret: credentials.clientsecret, url: credentials.url },
|
|
63
|
+
uri: String(credentials.uri)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check whether the adaptation project's webapp/xs-app.json exists and contains at least
|
|
68
|
+
* one route whose destination is configured as OnPremise in the BTP Destination Service.
|
|
69
|
+
*
|
|
70
|
+
* @param rootPath - Project root path.
|
|
71
|
+
* @param logger - Logger instance.
|
|
72
|
+
* @returns True if an OnPremise destination is found; false otherwise.
|
|
73
|
+
*/
|
|
74
|
+
async function hasOnPremiseDestination(rootPath, logger) {
|
|
75
|
+
const destinationNames = getWebappXsappDestinationNames(rootPath);
|
|
76
|
+
if (destinationNames.length === 0) {
|
|
77
|
+
logger.debug('No webapp/xs-app.json or no destinations in routes, skipping OnPremise check.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const auth = getBtpDestinationServiceAuth();
|
|
81
|
+
if (!auth) {
|
|
82
|
+
logger.debug('No destination service credentials in VCAP_SERVICES, cannot check destination types.');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
let token;
|
|
86
|
+
try {
|
|
87
|
+
token = await (0, adp_tooling_1.getToken)(auth.uaa, logger);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
91
|
+
logger.warn(`Failed to obtain OAuth token for destination service: ${message}`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
for (const name of destinationNames) {
|
|
95
|
+
try {
|
|
96
|
+
const config = await (0, adp_tooling_1.getBtpDestinationConfig)(auth.uri, token, name, logger);
|
|
97
|
+
if (config?.ProxyType === btp_utils_1.DestinationProxyType.ON_PREMISE) {
|
|
98
|
+
logger.info(`Destination "${name}" is OnPremise, SSH tunnel is needed.`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
104
|
+
logger.debug(`Could not check destination "${name}": ${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
logger.debug('No OnPremise destinations found in webapp/xs-app.json routes.');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=destination-check.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { ToolsLogger } from '@sap-ux/logger';
|
|
3
|
+
import type { ConnectivityProxyInfo, SshTunnelOptions, EffectiveOptions } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Start an SSH tunnel to the connectivity proxy if needed.
|
|
6
|
+
* Skips if running in BAS, if the port is already in use, or if no connectivity service is present.
|
|
7
|
+
* Errors are logged as warnings; the middleware continues without the tunnel.
|
|
8
|
+
*
|
|
9
|
+
* @param connectivityInfo - Original connectivity proxy host and port from VCAP_SERVICES.
|
|
10
|
+
* @param tunnelAppName - CF app name to SSH into.
|
|
11
|
+
* @param logger - Logger instance.
|
|
12
|
+
* @param options - Optional tunnel configuration.
|
|
13
|
+
* @returns The SSH tunnel child process, or undefined if not started.
|
|
14
|
+
*/
|
|
15
|
+
export declare function startSshTunnelIfNeeded(connectivityInfo: ConnectivityProxyInfo, tunnelAppName: string, logger: ToolsLogger, options?: SshTunnelOptions): Promise<ChildProcess | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Check for OnPremise destinations and set up an SSH tunnel if needed.
|
|
18
|
+
* Handles the full lifecycle: destination check, tunnel app deployment, SSH enable, and tunnel spawn.
|
|
19
|
+
*
|
|
20
|
+
* @param rootPath - Project root path.
|
|
21
|
+
* @param connectivityInfo - Connectivity proxy host and port from VCAP_SERVICES.
|
|
22
|
+
* @param effectiveOptions - Merged middleware options.
|
|
23
|
+
* @param logger - Logger instance.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setupSshTunnel(rootPath: string, connectivityInfo: ConnectivityProxyInfo, effectiveOptions: EffectiveOptions, logger: ToolsLogger): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
exports.startSshTunnelIfNeeded = startSshTunnelIfNeeded;
|
|
7
|
+
exports.setupSshTunnel = setupSshTunnel;
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const btp_utils_1 = require("@sap-ux/btp-utils");
|
|
11
|
+
const adp_tooling_1 = require("@sap-ux/adp-tooling");
|
|
12
|
+
const destination_check_1 = require("./destination-check");
|
|
13
|
+
/**
|
|
14
|
+
* Check if a port is already in use.
|
|
15
|
+
*
|
|
16
|
+
* @param port - Port number to check.
|
|
17
|
+
* @returns True if the port is in use.
|
|
18
|
+
*/
|
|
19
|
+
function isPortInUse(port) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const server = node_net_1.default.createServer();
|
|
22
|
+
server.once('error', () => resolve(true));
|
|
23
|
+
server.once('listening', () => {
|
|
24
|
+
server.close(() => resolve(false));
|
|
25
|
+
});
|
|
26
|
+
server.listen(port, '127.0.0.1');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wait for a port to become reachable via TCP connect.
|
|
31
|
+
*
|
|
32
|
+
* @param port - Port to connect to.
|
|
33
|
+
* @param timeoutMs - Maximum wait time in ms.
|
|
34
|
+
* @returns True if the port became reachable within the timeout.
|
|
35
|
+
*/
|
|
36
|
+
function waitForPort(port, timeoutMs) {
|
|
37
|
+
const start = Date.now();
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
function attempt() {
|
|
40
|
+
if (Date.now() - start > timeoutMs) {
|
|
41
|
+
resolve(false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const socket = node_net_1.default.connect(port, '127.0.0.1');
|
|
45
|
+
socket.once('connect', () => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
resolve(true);
|
|
48
|
+
});
|
|
49
|
+
socket.once('error', () => {
|
|
50
|
+
socket.destroy();
|
|
51
|
+
setTimeout(attempt, 500);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
attempt();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Spawn the long-running cf ssh tunnel process.
|
|
59
|
+
*
|
|
60
|
+
* @param appName - CF app name.
|
|
61
|
+
* @param localPort - Local port to bind.
|
|
62
|
+
* @param remoteHost - Remote connectivity proxy host.
|
|
63
|
+
* @param remotePort - Remote connectivity proxy port.
|
|
64
|
+
* @param logger - Logger instance.
|
|
65
|
+
* @returns The spawned child process.
|
|
66
|
+
*/
|
|
67
|
+
function spawnSshTunnel(appName, localPort, remoteHost, remotePort, logger) {
|
|
68
|
+
const tunnelArg = `${localPort}:${remoteHost}:${remotePort}`;
|
|
69
|
+
logger.info(`Starting SSH tunnel: cf ssh ${appName} -N -T -L ${tunnelArg}`);
|
|
70
|
+
const child = (0, node_child_process_1.spawn)('cf', ['ssh', appName, '-N', '-T', '-L', tunnelArg], {
|
|
71
|
+
stdio: 'pipe',
|
|
72
|
+
shell: process.platform === 'win32'
|
|
73
|
+
});
|
|
74
|
+
child.stderr?.on('data', (data) => {
|
|
75
|
+
logger.warn(`SSH tunnel stderr: ${data.toString().trim()}`);
|
|
76
|
+
});
|
|
77
|
+
child.on('error', (err) => {
|
|
78
|
+
logger.warn(`SSH tunnel process error: ${err.message}`);
|
|
79
|
+
});
|
|
80
|
+
child.on('exit', (code) => {
|
|
81
|
+
if (code !== null && code !== 0) {
|
|
82
|
+
logger.warn(`SSH tunnel exited with code ${code}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return child;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Register cleanup handlers to kill the SSH tunnel on process exit.
|
|
89
|
+
*
|
|
90
|
+
* @param tunnelProcess - The SSH tunnel child process.
|
|
91
|
+
* @param logger - Logger instance.
|
|
92
|
+
*/
|
|
93
|
+
function registerCleanup(tunnelProcess, logger) {
|
|
94
|
+
const cleanup = () => {
|
|
95
|
+
if (!tunnelProcess.killed) {
|
|
96
|
+
logger.debug('Killing SSH tunnel process.');
|
|
97
|
+
tunnelProcess.kill('SIGTERM');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
process.on('exit', cleanup);
|
|
101
|
+
process.once('SIGTERM', cleanup);
|
|
102
|
+
process.once('SIGINT', cleanup);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Start an SSH tunnel to the connectivity proxy if needed.
|
|
106
|
+
* Skips if running in BAS, if the port is already in use, or if no connectivity service is present.
|
|
107
|
+
* Errors are logged as warnings; the middleware continues without the tunnel.
|
|
108
|
+
*
|
|
109
|
+
* @param connectivityInfo - Original connectivity proxy host and port from VCAP_SERVICES.
|
|
110
|
+
* @param tunnelAppName - CF app name to SSH into.
|
|
111
|
+
* @param logger - Logger instance.
|
|
112
|
+
* @param options - Optional tunnel configuration.
|
|
113
|
+
* @returns The SSH tunnel child process, or undefined if not started.
|
|
114
|
+
*/
|
|
115
|
+
async function startSshTunnelIfNeeded(connectivityInfo, tunnelAppName, logger, options) {
|
|
116
|
+
try {
|
|
117
|
+
if ((0, btp_utils_1.isAppStudio)()) {
|
|
118
|
+
logger.debug('Running in BAS, SSH tunnel not needed.');
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const localPort = options?.localPort ?? connectivityInfo.port;
|
|
122
|
+
if (await isPortInUse(localPort)) {
|
|
123
|
+
logger.info(`Port ${localPort} already in use, assuming SSH tunnel is already running.`);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
if (!options?.skipSshEnable) {
|
|
127
|
+
await (0, adp_tooling_1.enableSshAndRestart)(tunnelAppName, logger);
|
|
128
|
+
}
|
|
129
|
+
const child = spawnSshTunnel(tunnelAppName, localPort, connectivityInfo.host, connectivityInfo.port, logger);
|
|
130
|
+
registerCleanup(child, logger);
|
|
131
|
+
const ready = await waitForPort(localPort, 10_000);
|
|
132
|
+
if (ready) {
|
|
133
|
+
logger.info(`SSH tunnel ready on localhost:${localPort}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
logger.warn(`SSH tunnel did not become ready within 10s on localhost:${localPort}`);
|
|
137
|
+
}
|
|
138
|
+
return child;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
142
|
+
logger.warn(`SSH tunnel setup failed: ${message}. On-premise connectivity may not work.`);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check for OnPremise destinations and set up an SSH tunnel if needed.
|
|
148
|
+
* Handles the full lifecycle: destination check, tunnel app deployment, SSH enable, and tunnel spawn.
|
|
149
|
+
*
|
|
150
|
+
* @param rootPath - Project root path.
|
|
151
|
+
* @param connectivityInfo - Connectivity proxy host and port from VCAP_SERVICES.
|
|
152
|
+
* @param effectiveOptions - Merged middleware options.
|
|
153
|
+
* @param logger - Logger instance.
|
|
154
|
+
*/
|
|
155
|
+
async function setupSshTunnel(rootPath, connectivityInfo, effectiveOptions, logger) {
|
|
156
|
+
const needsSshTunnel = await (0, destination_check_1.hasOnPremiseDestination)(rootPath, logger);
|
|
157
|
+
if (!needsSshTunnel) {
|
|
158
|
+
logger.info('No OnPremise destination found in webapp/xs-app.json, skipping SSH tunnel setup.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const tunnelAppName = effectiveOptions.tunnelAppName ?? adp_tooling_1.DEFAULT_TUNNEL_APP_NAME;
|
|
162
|
+
await (0, adp_tooling_1.ensureTunnelAppExists)(tunnelAppName, logger);
|
|
163
|
+
await startSshTunnelIfNeeded(connectivityInfo, tunnelAppName, logger, {
|
|
164
|
+
localPort: effectiveOptions.tunnelLocalPort,
|
|
165
|
+
skipSshEnable: effectiveOptions.skipSshEnable
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=tunnel.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -52,6 +52,14 @@ export interface BackendProxyMiddlewareCfConfig {
|
|
|
52
52
|
disableWelcomeFile?: boolean;
|
|
53
53
|
/** Disable automatic injection of ui5-server routes (resources, test-resources, catch-all) */
|
|
54
54
|
disableUi5ServerRoutes?: boolean;
|
|
55
|
+
/** Disable SSH tunnel to connectivity proxy for OnPremise destinations */
|
|
56
|
+
disableSshTunnel?: boolean;
|
|
57
|
+
/** CF app name used as SSH tunnel target */
|
|
58
|
+
tunnelAppName?: string;
|
|
59
|
+
/** Local port for the SSH tunnel (defaults to connectivity proxy port) */
|
|
60
|
+
tunnelLocalPort?: number;
|
|
61
|
+
/** Skip cf enable-ssh and cf restart (assume SSH is already enabled on the tunnel app) */
|
|
62
|
+
skipSshEnable?: boolean;
|
|
55
63
|
}
|
|
56
64
|
/** Effective options with defaults applied. */
|
|
57
65
|
export interface EffectiveOptions extends BackendProxyMiddlewareCfConfig {
|
|
@@ -129,6 +137,22 @@ export interface CreateProxyOptions {
|
|
|
129
137
|
/** External URL for BAS (from exposePort). Overrides x-forwarded-host/proto in proxy requests. */
|
|
130
138
|
basExternalUrl?: URL;
|
|
131
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Connectivity proxy coordinates from VCAP_SERVICES.
|
|
142
|
+
*/
|
|
143
|
+
export interface ConnectivityProxyInfo {
|
|
144
|
+
host: string;
|
|
145
|
+
port: number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Options for the SSH tunnel setup.
|
|
149
|
+
*/
|
|
150
|
+
export interface SshTunnelOptions {
|
|
151
|
+
/** Local port to bind the tunnel to (defaults to remotePort). */
|
|
152
|
+
localPort?: number;
|
|
153
|
+
/** Skip cf enable-ssh and cf restart (assume SSH is already enabled). */
|
|
154
|
+
skipSshEnable?: boolean;
|
|
155
|
+
}
|
|
132
156
|
/**
|
|
133
157
|
* Approuter extension handler: Express-like (req, res, next) with optional 4th params from config
|
|
134
158
|
*/
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"bugs": {
|
|
10
10
|
"url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Abackend-proxy-middleware-cf"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.3",
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"author": "@SAP/ux-tools-team",
|
|
15
15
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"http-proxy-middleware": "3.0.5",
|
|
28
28
|
"mime-types": "^2.1.35",
|
|
29
29
|
"portfinder": "^1.0.32",
|
|
30
|
-
"@sap-ux/adp-tooling": "0.18.
|
|
30
|
+
"@sap-ux/adp-tooling": "0.18.113",
|
|
31
31
|
"@sap-ux/btp-utils": "1.1.12",
|
|
32
32
|
"@sap-ux/logger": "0.8.5"
|
|
33
33
|
},
|