org.pipservices3.rpc.services.HttpEndpoint Maven / Gradle / Ivy
Show all versions of pip-services3-rpc Show documentation
package org.pipservices3.rpc.services;
import com.sun.net.httpserver.HttpServer;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.ext.Provider;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.pipservices3.commons.config.ConfigParams;
import org.pipservices3.commons.config.IConfigurable;
import org.pipservices3.commons.convert.JsonConverter;
import org.pipservices3.commons.errors.ApplicationException;
import org.pipservices3.commons.errors.ConfigException;
import org.pipservices3.commons.errors.ConnectionException;
import org.pipservices3.commons.refer.DependencyResolver;
import org.pipservices3.commons.refer.IReferenceable;
import org.pipservices3.commons.refer.IReferences;
import org.pipservices3.commons.refer.ReferenceException;
import org.pipservices3.commons.run.IOpenable;
import org.pipservices3.commons.validate.Schema;
import org.pipservices3.commons.validate.ValidationException;
import org.pipservices3.components.connect.ConnectionParams;
import org.pipservices3.components.count.CompositeCounters;
import org.pipservices3.components.count.CounterTiming;
import org.pipservices3.components.log.CompositeLogger;
import org.pipservices3.rpc.connect.HttpConnectionResolver;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Used for creating HTTP endpoints. An endpoint is a URL, at which a given service can be accessed by a client.
*
* ### Configuration parameters ###
*
* Parameters to pass to the {@link HttpEndpoint#configure} method for component configuration:
*
* - cors_headers - a comma-separated list of allowed CORS headers
*
- cors_origins - a comma-separated list of allowed CORS origins
*
- connection(s): the connection resolver's connections;
*
* - "connection.discovery_key" - the key to use for connection resolving in a discovery service;
*
- "connection.protocol" - the connection's protocol;
*
- "connection.host" - the target host;
*
- "connection.port" - the target port;
*
- "connection.uri" - the target URI.
*
* - credential - the HTTPS credentials:
*
* - "credential.ssl_key_file" - the SSL private key in PEM
*
- "credential.ssl_crt_file" - the SSL certificate in PEM
*
- "credential.ssl_ca_file" - the certificate authorities (root cerfiticates) in PEM
*
*
*
* ### References ###
*
* A logger, counters, and a connection resolver can be referenced by passing the
* following references to the object's setReferences()
method:
*
* - logger:
"*:logger:*:*:1.0"
;
* - counters:
"*:counters:*:*:1.0"
;
* - discovery:
"*:discovery:*:*:1.0"
(for the connection resolver).
*
*
* ### Examples ###
*
* {@code
* public MyMethod(String correlationId, ConfigParams _config, IReferences _references) {
* HttpEndpoint endpoint = new HttpEndpoint();
* if (this._config)
* endpoint.configure(this._config);
* if (this._references)
* endpoint.setReferences(this._references);
* ...
*
* this._endpoint.open(correlationId);
* ...
* }
* }
*
*/
public class HttpEndpoint implements IOpenable, IConfigurable, IReferenceable {
private static final ConfigParams _defaultConfig = ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "0.0.0.0",
"connection.port", 3000,
"credential.ssl_key_file", null,
"credential.ssl_crt_file", null,
"credential.ssl_ca_file", null,
"options.maintenance_enabled", false,
"options.request_max_size", 1024 * 1024,
"options.connect_timeout", 60000,
"options.connect_timeout", 60000,
"options.debug", true);
protected HttpConnectionResolver _connectionResolver = new HttpConnectionResolver();
protected CompositeLogger _logger = new CompositeLogger();
protected CompositeCounters _counters = new CompositeCounters();
protected DependencyResolver _dependencyResolver = new DependencyResolver(_defaultConfig);
private String _url;
private HttpServer _server;
private ResourceConfig _resources;
private final List _registrations = new ArrayList<>();
private boolean _protocolUpgradeEnabled = false;
private boolean _maintenanceEnabled = false;
private long _fileMaxSize = 200 * 1024 * 1024;
private List _allowedHeaders = List.of("correlation_id");
private List _allowedOrigins = new ArrayList<>();
/**
* Configures this HttpEndpoint using the given configuration parameters.
*
* __Configuration parameters:__
* - __connection(s)__ - the connection resolver's connections;
*
* - "connection.discovery_key" - the key to use for connection resolving in a discovery service;
*
* - "connection.protocol" - the connection's protocol;
*
* - "connection.host" - the target host;
*
* - "connection.port" - the target port;
*
* - "connection.uri" - the target URI.
*
* - "credential.ssl_key_file" - SSL private key in PEM
*
* - "credential.ssl_crt_file" - SSL certificate in PEM
*
* - "credential.ssl_ca_file" - Certificate authority (root certificate) in PEM
*
* @param config configuration parameters, containing a "connection(s)" section.
* @see ConfigParams (in the PipServices "Commons" package)
*/
@Override
public void configure(ConfigParams config) throws ConfigException {
config = config.setDefaults(_defaultConfig);
_connectionResolver.configure(config);
this._maintenanceEnabled = config.getAsBooleanWithDefault("options.maintenance_enabled", this._maintenanceEnabled);
this._fileMaxSize = config.getAsLongWithDefault("options.file_max_size", this._fileMaxSize);
this._protocolUpgradeEnabled = config.getAsBooleanWithDefault("options.protocol_upgrade_enabled", this._protocolUpgradeEnabled);
var headers = config.getAsStringWithDefault("cors_headers", "").split(",");
for (var header : headers) {
header = header.trim();
if (!header.equals("")) {
final String finalHeader = header;
this._allowedHeaders = this._allowedHeaders.stream().filter(h -> !h.equals(finalHeader)).collect(Collectors.toList());
this._allowedHeaders.add(finalHeader);
}
}
var origins = config.getAsStringWithDefault("cors_origins", "").split(",");
for (var origin : origins) {
origin = origin.trim();
if (!origin.equals("")) {
final String finalOrigin = origin;
this._allowedOrigins = this._allowedOrigins.stream().filter(h -> !h.equals(finalOrigin)).collect(Collectors.toList());
this._allowedOrigins.add(finalOrigin);
}
}
_dependencyResolver.configure(config);
}
/**
* Sets references to this endpoint's logger, counters, and connection resolver.
*
* __References:__ - logger: "\*:logger:\*:\*:1.0"
- counters:
* "\*:counters:\*:\*:1.0"
- discovery:
* "\*:discovery:\*:\*:1.0"
(for the connection resolver)
*
* @param references an IReferences object, containing references to a logger,
* counters, and a connection resolver.
* @throws ReferenceException when no found references.
* @see IReferences
*/
public void setReferences(IReferences references) throws ReferenceException {
_logger.setReferences(references);
_counters.setReferences(references);
_dependencyResolver.setReferences(references);
_connectionResolver.setReferences(references);
}
/**
* Adds instrumentation to log calls and measure call time. It returns a Timing
* object that is used to end the time measurement.
*
* @param correlationId (optional) transaction id to trace execution through
* call chain.
* @param name a method name.
* @return Timing object to end the time measurement.
*/
protected CounterTiming Instrument(String correlationId, String name) {
_logger.trace(correlationId, "Executing {0} method", name);
return _counters.beginTiming(name + ".exec_time");
}
/**
* Gets an HTTP server instance.
*
* @return an HTTP server instance of null
if endpoint is closed.
*/
public HttpServer getServer() {
return this._server;
}
/**
* Checks if the component is opened.
*
* @return whether or not this endpoint is open with an actively listening REST
* server.
*/
@Override
public boolean isOpen() {
return _server != null;
}
/**
* Opens a connection using the parameters resolved by the referenced connection
* resolver and creates a REST server (service) using the set options and
* parameters.
*
* @param correlationId (optional) transaction id to trace execution through
* call chain.
* @throws ApplicationException when error occured.
*/
@Override
public void open(String correlationId) throws ApplicationException {
if (isOpen())
return;
SSLContext sslContext = null;
ConnectionParams connection = _connectionResolver.resolve(correlationId);
String protocol = connection.getProtocolWithDefault("http");
String host = connection.getHost();
int port = connection.getPort();
URI uri = UriBuilder.fromUri(protocol + "://" + host).port(port).path("/").build();
_url = uri.toString();
try {
_resources = new ResourceConfig();
if (Objects.equals(connection.getAsStringWithDefault("protocol", "http"), "https")) {
// TODO check using of certificates
var sslKeyFile = connection.getAsNullableString("ssl_key_file");
var privateKey = readCertFile(sslKeyFile);
var sslCrtFile = connection.getAsNullableString("ssl_crt_file");
var ca = new ArrayList<>();
var sslCaFile = connection.getAsNullableString("ssl_ca_file");
if (sslCaFile != null) {
var caText = readCertFile(sslCaFile);
while (caText != null && caText.trim().length() > 0) {
var crtIndex = caText.lastIndexOf("-----BEGIN CERTIFICATE-----");
if (crtIndex > -1) {
ca.add(caText.substring(crtIndex));
caText = caText.substring(0, crtIndex);
}
}
}
sslContext = SSLContext.getInstance("SSL");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
FileInputStream fis = new FileInputStream(sslCrtFile);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(fis);
keyStore.setCertificateEntry("ssl", cert);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, privateKey.toCharArray());
try {
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
} catch (KeyManagementException e) {
throw new IllegalStateException(e);
}
}
performRegistrations();
if (sslContext != null)
_server = JdkHttpServerFactory.createHttpServer(uri, _resources, sslContext);
else
_server = JdkHttpServerFactory.createHttpServer(uri, _resources);
// _server.start();
_logger.info(correlationId, "Opened REST service at %s", _url);
} catch (Exception ex) {
_server = null;
throw new ConnectionException(correlationId, "CANNOT_CONNECT", "Opening HTTP endpoint failed").wrap(ex)
.withDetails("url", _url);
}
}
private String readCertFile(String path) throws IOException {
if (path == null) return null;
try (FileInputStream fis = new FileInputStream(path)) {
return new String(fis.readAllBytes(), Charset.defaultCharset());
}
}
/**
* Closes this endpoint and the REST server (service) that was opened earlier.
*
* @param correlationId (optional) transaction id to trace execution through
* call chain.
*/
@Override
public void close(String correlationId) {
if (_server != null) {
// Eat exceptions
try {
_server.stop(0);
_logger.info(correlationId, "Closed HTTP endpoint at %s", _url);
} catch (Exception ex) {
_logger.warn(correlationId, "Failed while closing HTTP endpoint: %s", ex);
}
_server = null;
_resources = null;
_url = null;
}
}
private void performRegistrations() {
for (IRegisterable registration : _registrations)
registration.register();
}
/**
* Registers a registerable object for dynamic endpoint discovery.
*
* @param registration the registration to add.
* @see IRegisterable
*/
public void register(IRegisterable registration) {
_registrations.add(registration);
}
/**
* Unregisters a registerable object, so that it is no longer used in dynamic
* endpoint discovery.
*
* @param registration the registration to remove.
* @see IRegisterable
*/
public void unregister(IRegisterable registration) {
_registrations.remove(registration);
}
/**
* Returns correlationId from request
*
* @param req - http request
* @return Returns correlationId from request
*/
public String getCorrelationId(ContainerRequestContext req) {
var correlationId = getQueryParameter(req, "correlation_id");
if (correlationId == null)
correlationId = req.getHeaderString("correlation_id");
return correlationId;
}
protected String getQueryParameter(ContainerRequestContext request, String name) {
String value = null;
name = URLEncoder.encode(name, StandardCharsets.UTF_8);
if (request.getUriInfo().getQueryParameters().containsKey(name)) {
value = request.getUriInfo().getQueryParameters().getFirst(name);
value = value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null;
}
return value;
}
/**
* Registers an action in this objects REST server (service) by the given method
* and route.
*
* @param method the HTTP method of the route.
* @param route the route to register in this object's REST server (service).
* @param action the action to perform at the given route.
*/
public void registerRoute(String method, String route, Inflector action) {
// Routes cannot start with '/'
if (route.charAt(0) == '/')
route = route.substring(1);
Resource.Builder builder = Resource.builder().addChildResource(route);
method = method.toUpperCase();
builder.addMethod(method).handledBy(action);
Resource resource = builder.build();
if (_resources != null)
_resources.registerResources(resource);
}
/**
* Registers an action in this objects REST server (service) by the given method
* and route.
*
* @param method the HTTP method of the route.
* @param route the route to register in this object's REST server (service).
* @param schema the schema to use for parameter validation.
* @param action the action to perform at the given route.
*/
public void registerRoute(String method, String route, Schema schema, Inflector action) {
// Hack!!! Wrapping action to preserve prototyping context
Inflector actionCurl = new Inflector() {
@Override
public Response apply(ContainerRequestContext req) {
// TODO: maybe replace this on Jersey filters
if (schema != null) {
var params = getAllParams(req);
var correlationId = getCorrelationId(req);
try {
schema.validateAndThrowException(correlationId, params, false);
} catch (ValidationException err) {
return HttpResponseSender.sendError(err);
}
}
// Todo: perform verification?
return action.apply(req);
}
};
this.registerRoute(method, route, actionCurl);
}
public void registerRouteWithAuth(String method, String route, Schema schema, Function authorize, Function action) {
// TODO
}
@Provider
private static class InterceptorRegister implements ContainerRequestFilter {
private Function _interceptor = null;
private String _route = "";
public InterceptorRegister(Function interceptor, String route) {
_interceptor = interceptor;
_route = route;
}
@Override
public void filter(ContainerRequestContext ctx) throws IOException {
Pattern stringPattern = Pattern.compile(_route);
var matcher = stringPattern.matcher(fixRoute(ctx.getUriInfo().getPath(true)));
var resMatch = new ArrayList();
while (matcher.find())
resMatch.add(matcher.group());
if (!resMatch.isEmpty() && _route != null && !_route.equals(""))
_interceptor.apply(ctx);
}
}
private static String fixRoute(String route) {
if (route != null && route.length() > 0 && !route.startsWith("/")) {
route = "/" + route;
}
return route;
}
public void registerInterceptor(String route, Function action) {
route = fixRoute(route);
_resources.register(new InterceptorRegister(action, route));
}
private Map, ?> getAllParams(ContainerRequestContext req) {
var pathParams = req.getUriInfo().getPathParameters();
var queryParams = req.getUriInfo().getQueryParameters();
byte[] bodyBytes;
Map body;
try {
bodyBytes = req.getEntityStream().readAllBytes();
var json = new String(bodyBytes, StandardCharsets.UTF_8);
body = JsonConverter.toMap(json);
// return req to the initial state
req.setEntityStream(new ByteArrayInputStream(bodyBytes));
} catch (IOException e) {
throw new RuntimeException(e);
}
var params = new HashMap<>();
params.put("body", body);
pathParams.forEach((k, v) -> {
if (k != null && !k.isEmpty() && v != null && !v.stream().findFirst().orElse("").isEmpty())
params.put(k, v.stream().findFirst().get());
});
queryParams.forEach((k, v) -> {
if (k != null && !k.isEmpty() && v != null && !v.stream().findFirst().orElse("").isEmpty())
params.put(k, v.stream().findFirst().get());
});
return params;
}
}