Skip to content

[Do not merge] - Python - DEVTOOLING-997 #1019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 84 additions & 35 deletions resources/scripts/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ import pkg from 'http-proxy';
import * as tls from "tls";
const { createProxyServer } = pkg;

// Logger function to standardize logging format
const log = (activity: string, details?: any) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${activity}`, details ? details : '');
};

export default class GatewayServer {
public gateway: pkg.httpProxy;
public server: https.Server;

private environment: string;
constructor() {
log('Initializing GatewayServer');
this.gateway = createProxyServer();
const environment = this.fetchEnvironment();
this.environment = this.fetchEnvironment("login");
const domain = 'localhost';
log('Server configuration', { environment: this.environment, domain });

// SSL/TLS options for the proxy server
const serverOptions: https.ServerOptions = {
Expand All @@ -24,79 +32,120 @@ export default class GatewayServer {
requestCert: true,
rejectUnauthorized: true, // Verify client certificates
};
log('SSL/TLS certificates loaded successfully');

// HTTPS server to listen for incoming requests
this.server = https.createServer(serverOptions, (req, res) => {
log('Incoming request received', {
method: req.method,
url: req.url,
headers: req.headers
});

let reqURL: string | undefined;
if (req.url?.includes('/login')) {
reqURL = req.url.replace(/^\/login/, '')
this.environment = this.fetchEnvironment("login");
} else if (req.url?.includes('/api')) {
reqURL = req.url.replace(/^\/api/, '')
this.environment = this.fetchEnvironment("api");
} else {
reqURL = req.url || ''
}
// Parse incoming request URL
const targetHost = url.parse(req.url || '');
const targetHost = url.parse(reqURL || '');
log('Parsed target host', targetHost);

const options: https.RequestOptions = {
hostname: environment,
hostname: this.environment,
port: 443, // HTTPS port
path: targetHost.path,
method: req.method,
headers: {
...req.headers,
host: environment,
host: this.environment,
},
rejectUnauthorized: false,
};
log('Proxy request options prepared', options);

const proxyReq = https.request(options, (proxyRes) => {
log('Proxy response received', {
headers: proxyRes.headers,
statusCode: proxyRes.statusCode
});
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
proxyRes.pipe(res);
log('Response piped back to client');
});

proxyReq.on('error', (err) => {
console.error('Error during proxy request:', err.message);
log('Proxy request error', {
error: err.message,
stack: err.stack
});
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end('Bad Gateway');
});

req.pipe(proxyReq);


log('Request piped to proxy');
});

// Handle CONNECT method for tunneling
this.server.on('connect', this.handleConnectRequest.bind(this));
log('CONNECT handler registered');
}

private fetchEnvironment():string{
return "api."+process.env.PURECLOUD_ENV
private fetchEnvironment(path: string):string{
const envUrl = path+"."+process.env.PURECLOUD_ENV;
log('Environment URL resolved', envUrl);
return envUrl
}

private handleConnectRequest(req: http.IncomingMessage, clientSocket: net.Socket, head: Buffer) {
const targetUrl = url.parse(`//${req.url}`, false, true);
const environment = this.fetchEnvironment();
const serverSocket = tls.connect(
{
host: environment,
port: 443,
rejectUnauthorized: false
}, () => {
console.log("connection extablishes")
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
}
log('CONNECT request received', {
url: req.url,
method: req.method,
headers: req.headers
});

);
const targetUrl = url.parse(`//${req.url}`, false, true);
const environment = this.fetchEnvironment("api");
log('CONNECT request details', { targetUrl, environment });

const serverSocket = tls.connect(
{
host: environment,
port: 443,
rejectUnauthorized: false
}, () => {
log('TLS connection established', { host: environment });
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
log('Bidirectional pipe established');
}
);

serverSocket.on('error', (err) => {
console.error('error with server socket:', err.message)
log('Server socket error', {
message: err.message,
environment: environment,
stack: err.stack
});
clientSocket.end();
} );
});
}
}

const gatewayServer = new GatewayServer();
console.log('HTTPS Gateway server trying to start on port 4027');
log('Starting HTTPS Gateway server on port 4027');
gatewayServer.server.listen(4027, () => {
console.log('HTTPS Gateway server listening on port 4027');
});
log('HTTPS Gateway server successfully started on port 4027');
});
69 changes: 62 additions & 7 deletions resources/scripts/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,112 @@

import http from 'http';
import net from 'net';
import url from 'url';
import pkg from 'http-proxy';
import Logger from '../../modules/log/logger';
const { createProxyServer } = pkg;

const logger = new Logger();

export default class ProxyServer {

public proxy: pkg.httpProxy;
public server: http.Server;

constructor() {
logger.info('Initializing proxy server...');
this.proxy = createProxyServer();

// Log proxy errors
this.proxy.on('error', (err, req, res) => {
logger.error(`Proxy error: ${err.message}`);
logger.debug(`Proxy error details: ${err.stack}`);
logger.debug(`Failed request URL: ${req.url}`);
logger.debug(`Failed request headers: ${JSON.stringify(req.headers, null, 2)}`);
if (res instanceof http.ServerResponse) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Proxy error occurred');
}
});

this.server = http.createServer((req, res) => {
const { hostname, port } = url.parse(req.url);
logger.debug(`Incoming request for ${req.url}`);
logger.debug(`Request method: ${req.method}`);
logger.debug(`Request headers: ${JSON.stringify(req.headers, null, 2)}`);

if (hostname && port) {
this.proxy.web(req, res, { target: `http://${hostname}:${port}` });
const target = `http://${hostname}:${port}`;
logger.info(`Proxying request to ${target}`);
logger.debug(`Target details - Hostname: ${hostname}, Port: ${port}`);
this.proxy.web(req, res, { target });
} else {
logger.warn(`Invalid request received: ${req.url}`);
logger.debug(`Parse result - Hostname: ${hostname}, Port: ${port}`);
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid request');
}
});

this.server.on('connect', this.handleConnectRequest.bind(this));
logger.info('Proxy server initialized successfully');
}



private handleConnectRequest(req: http.IncomingMessage, clientSocket: net.Socket, head: Buffer) {
const { port, hostname } = url.parse(`//${req.url}`, false, true);
logger.debug(`CONNECT request received for ${req.url}`);
logger.debug(`CONNECT request headers: ${JSON.stringify(req.headers, null, 2)}`);

if (hostname && port) {
logger.info(`Establishing tunnel to ${hostname}:${port}`);
logger.debug(`Attempting connection with parameters - Hostname: ${hostname}, Port: ${port}`);
const serverSocket = net.connect(parseInt(port, 10), hostname, () => {
logger.debug(`Tunnel established to ${hostname}:${port}`);
logger.debug(`Local address: ${serverSocket.localAddress}:${serverSocket.localPort}`);
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n');
serverSocket.write(head);

// Setup bidirectional tunnel
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
logger.debug('Bidirectional tunnel established successfully');

// Log socket events
serverSocket.on('error', (err) => {
logger.error(`Server socket error: ${err.message}`);
logger.debug(`Server socket error details: ${err.stack}`);
});

clientSocket.on('error', (err) => {
logger.error(`Client socket error: ${err.message}`);
logger.debug(`Client socket error details: ${err.stack}`);
});
});

serverSocket.on('error', (err) => {
logger.error(`Failed to establish tunnel to ${hostname}:${port}: ${err.message}`);
clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'Failed to establish connection');
clientSocket.end();
});
} else {
logger.warn(`Invalid CONNECT request received: ${req.url}`);
clientSocket.write('HTTP/1.1 400 Bad Request\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'Invalid request');
clientSocket.end();
}
}

}

const proxyServer = new ProxyServer();
console.log('HTTP proxy server trying to start on port 4001');
logger.info('HTTP proxy server trying to start on port 4001');
proxyServer.server.listen(4001, () => {
console.log('HTTP proxy server listening on port 4001');
logger.info('HTTP proxy server listening on port 4001');
});


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod

class AbstractHttpClient(ABC):
def __init__(self):
self.timeout = 16000
self.https_agent = None #it is a http proxy agent will be used later

def set_timeout(self, timeout):
if not isinstance(timeout, (int, float)):
raise ValueError("The 'timeout' property must be a number")
self.timeout = timeout

@abstractmethod
def request(self, http_request_options):
raise NotImplementedError("method must be implemented")
79 changes: 79 additions & 0 deletions resources/sdk/purecloudpython/extensions/default_http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from .abstract_http_client import AbstractHttpClient
from .http_request_options import HttpRequestOptions
from .rest import RESTClientObject
from .configuration import Configuration

class DefaultHttpClient(AbstractHttpClient):
def __init__(self, timeout=None, https_agent=None):
super().__init__()
if timeout is not None:
self.set_timeout(timeout)

Configuration().create_mtls_or_ssl_context()

#This object "self.rest_client" handles all REST API communication (requests/responses).
#Implementation: rest.py (templates/rest.mustache).
self.rest_client = RESTClientObject()

def request(self, http_request_options):
if not isinstance(http_request_options, HttpRequestOptions):
raise ValueError("httpRequestOptions must be an instance of HttpRequestOptions")
config = self.to_rest_client_config(http_request_options)

if config['method'] == "GET":
return self.rest_client.GET(config['url'],
query_params=config['query_params'],
headers=config['headers'])
elif config['method'] == "HEAD":
return self.rest_client.HEAD(config['url'],
query_params=config['query_params'],
headers=config['headers'])
elif config['method'] == "OPTIONS":
return self.rest_client.OPTIONS(config['url'],
query_params=config['query_params'],
headers=config['headers'],
post_params=config['post_params'],
body=config['body'])
elif config['method'] == "POST":
return self.rest_client.POST(config['url'],
query_params=config['query_params'],
headers=config['headers'],
post_params=config['post_params'],
body=config['body'])
elif config['method'] == "PUT":
return self.rest_client.PUT(config['url'],
query_params=config['query_params'],
headers=config['headers'],
post_params=config['post_params'],
body=config['body'])
elif config['method'] == "PATCH":
return self.rest_client.PATCH(config['url'],
query_params=config['query_params'],
headers=config['headers'],
post_params=config['post_params'],
body=config['body'])
elif config['method'] == "DELETE":
return self.rest_client.DELETE(config['url'],
query_params=config['query_params'],
headers=config['headers'],
body=config['body'])
else:
raise ValueError(
"http method must be `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`, `HEAD` or `TRACE`."
)


def to_rest_client_config(self, http_request_options):
if not http_request_options.url or not http_request_options.method:
raise ValueError("Mandatory fields 'url' and 'method' must be set before making a request")

config = {
"url": http_request_options.url,
"method": http_request_options.method,
"headers": http_request_options.headers or {},
"query_params": http_request_options.query_params or {},
"post_params": http_request_options.post_params or {},
"body": http_request_options.body or None,
"timeout": self.timeout if self.timeout else None,
}
return config
Loading