Skip to content

Commit

Permalink
To release 3.2.2 (#166)
Browse files Browse the repository at this point in the history
* Retrieve patron identifier (barcode) as part of checkin response

* Add test

* Explain GET /admin/health at port 8081

* Show port number 8081 in the log

* SIP2-202: Vert.x 4.5.7 fixing netty form POST OOM CVE-2024-29025

https://folio-org.atlassian.net/browse/SIP2-202

Upgrade Vertx from 4.5.4 to 4.5.7. This indirectly upgrades Netty from 4.1.107.Final to 4.1.108.Final fixing netty-codec-http form POST OOM:

GHSA-5jpm-x58v-624v
https://www.cve.org/CVERecord?id=CVE-2024-29025

* [SIP2-200-1] Enable TLS on WebClient (#164)

* [SIP2-200] Enable TLS on WebClient

* [SIP2-203] - Update NEWS.md and revert the code.

* [maven-release-plugin] prepare release v3.2.2

* [maven-release-plugin] prepare for next development iteration

---------

Co-authored-by: Kurt Nordstrom <[email protected]>
Co-authored-by: Julian Ladisch <[email protected]>
Co-authored-by: BKadirkhodjaev <[email protected]>
  • Loading branch information
4 people authored May 28, 2024
1 parent 628a5b2 commit 9e7dd2a
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 9 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 3.2.2 2024-05-28
* [SIP2-200](https://issues.folio.org/browse/SIP2-200): Enhance WebClient TLS Configuration for Secure Connections to OKAPI
* [SIP2-201](https://issues.folio.org/browse/SIP2-201): Enhance SIP2 Endpoint Security with TLS and FIPS-140-2 Compliant Cryptography
* [SIP2-202](https://issues.folio.org/browse/SIP2-202): Vert.x 4.5.7 fixing netty form POST OOM CVE-2024-29025

## 3.2.1 2024-04-19
* [SIP2-195](https://issues.folio.org/browse/SIP2-195): FolioResourceProvider is submitting expired access tokens to FOLIO

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ command line arguments to point it to the right path (e.g. `-conf /path/to/confi

|Config option|Type|Description|
|-------------|--|-----------|
|`port`|int|The port the module will use to bind, typically 1024 < port < 65,535.|
|`port`|int|The port the module will use to bind, typically 1024 <= port <= 65,535; must not be 8081 that is used for health check.|
|`okapiUrl`|string|The URL of the Okapi server used by FOLIO.|
|`tenantConfigRetrieverOptions`|JSON object|Location for tenant configuration.|
|`scanPeriod`|int|Frequency in msec that sip2 will check for and reload tenant configuration changes.|
Expand Down Expand Up @@ -304,6 +304,10 @@ All permission associated with edge-sip2

For local development, there is no requirement to encrypt communications from a SIP2 client to edge-sip2. Unencrypted TCP sockets are the default when launching edge-sip2 as described in the [Configuration](#configuration) section. Encrypted communication from a SIP2 client is only required when explicitly configured via the [above options](#security) and is up to the developer to provide that secure connection for edge-sip2.

## Health check

A GET /admin/health request sent to port 8081 gets a response with 200 HTTP status code.

## Metrics

This module makes use of [Micrometer](https://micrometer.io) to collect SIP2, Vert.x and JVM metrics. The metrics need to be collected by a monitoring system backed. This is where Micrometer provides flexibility, by allowing the module to code to the Micrometer interface, which is vendor neutral. Once determined, the vendor specific backend binding is provided runtime and can be easily replaced.
Expand Down
11 changes: 8 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.folio</groupId>
<artifactId>edge-sip2</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>3.2.3-SNAPSHOT</version>
<name>Standard Interchange Protocol v2 (SIP2)</name>
<url>https://github.com/folio-org/edge-sip2</url>
<description>Support for SIP2 in FOLIO. This allow self service circulation and patron services stations to perform supported operations in FOLIO.</description>
Expand Down Expand Up @@ -42,7 +42,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<exec.mainClass>org.folio.edge.sip2.MainVerticle</exec.mainClass>
<vertx.version>4.5.4</vertx.version>
<vertx.version>4.5.7</vertx.version>
<log4j2.version>2.23.0</log4j2.version>
<micrometer.version>1.9.4</micrometer.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
Expand Down Expand Up @@ -188,7 +188,12 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15to18</artifactId>
<version>1.74</version>
<version>1.78</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/org/folio/edge/sip2/MainVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.folio.edge.sip2.parser.Parser;
import org.folio.edge.sip2.session.SessionData;
import org.folio.edge.sip2.utils.TenantUtils;
import org.folio.edge.sip2.utils.WebClientUtils;

public class MainVerticle extends AbstractVerticle {

Expand Down Expand Up @@ -320,7 +321,7 @@ private void resendPreviousMessage(SessionData sessionData,
private void setupHanlders() {
if (handlers == null) {
String okapiUrl = config().getString("okapiUrl");
final WebClient webClient = WebClient.create(vertx);
final WebClient webClient = WebClientUtils.create(vertx, config());
final Injector injector = Guice.createInjector(
new FolioResourceProviderModule(okapiUrl, webClient),
new ApplicationModule());
Expand Down Expand Up @@ -358,10 +359,10 @@ private void callAdminHealthCheckService() {
}
});

var msg = "Listening for /admin/health requests at port " + HEALTH_CHECK_PORT;
httpServer.listen(HEALTH_CHECK_PORT)
.onSuccess(x -> log.info("Health endpoint is listening now"))
.onFailure(e -> log.error("The call to admin health check service "
+ "failed due to : {}", e.getMessage(), e));
.onSuccess(x -> log.info("{} now", msg))
.onFailure(e -> log.error("{} failed: {}", msg, e.getMessage(), e));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.folio.edge.sip2.utils;

public class WebClientConfigException extends RuntimeException {

public WebClientConfigException(String message) {
super(message);
}
}
55 changes: 55 additions & 0 deletions src/main/java/org/folio/edge/sip2/utils/WebClientUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.folio.edge.sip2.utils;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import java.util.Objects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class WebClientUtils {

public static final String SYS_PORT = "port";
public static final String SYS_NET_SERVER_OPTIONS = "netServerOptions";
public static final String SYS_PEM_KEY_CERT_OPTIONS = "pemKeyCertOptions";
public static final String SYS_CERT_PATHS = "certPaths";
private static final Logger log = LogManager.getLogger();

private WebClientUtils() {
}

/**
* Create WebClient with TLS.
* @param vertx instance
* @param config json config
* @return WebClient
*/
public static WebClient create(Vertx vertx, JsonObject config) {
JsonObject netServerOptions = config.getJsonObject(SYS_NET_SERVER_OPTIONS);
if (Objects.nonNull(netServerOptions)
&& netServerOptions.containsKey(SYS_PEM_KEY_CERT_OPTIONS)) {
log.info("Creating WebClient with TLS on...");

JsonArray certPaths = netServerOptions.getJsonObject(SYS_PEM_KEY_CERT_OPTIONS)
.getJsonArray(SYS_CERT_PATHS);
if (Objects.isNull(certPaths) || certPaths.isEmpty()) {
throw new WebClientConfigException("No TLS certPaths were found in config");
}

final PemTrustOptions pemTrustOptions = new PemTrustOptions();
certPaths.forEach(entry -> pemTrustOptions.addCertPath((String) entry));

final WebClientOptions webClientOptions = new WebClientOptions()
.setSsl(true)
.setTrustOptions(pemTrustOptions);
return WebClient.create(vertx, webClientOptions);
} else {
log.info("Creating WebClient with TLS off...");
return WebClient.create(vertx);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.junit5.VertxTestContext;
import java.text.SimpleDateFormat;
Expand Down
166 changes: 166 additions & 0 deletions src/test/java/org/folio/edge/sip2/utils/WebClientUtilsTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.folio.edge.sip2.utils;

import static org.folio.edge.sip2.utils.WebClientUtils.SYS_CERT_PATHS;
import static org.folio.edge.sip2.utils.WebClientUtils.SYS_NET_SERVER_OPTIONS;
import static org.folio.edge.sip2.utils.WebClientUtils.SYS_PEM_KEY_CERT_OPTIONS;
import static org.folio.edge.sip2.utils.WebClientUtils.SYS_PORT;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Vertx;
import io.vertx.core.file.FileSystem;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.SelfSignedCertificate;
import io.vertx.ext.web.client.WebClient;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import java.io.IOException;
import java.net.ServerSocket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(VertxExtension.class)
public class WebClientUtilsTests {

private static final String RESPONSE_MESSAGE = "<OK>";
private static final Logger log = LogManager.getLogger();
private Integer serverPort;
private SelfSignedCertificate selfSignedCertificate;

@BeforeEach
void setup() {
this.serverPort = getAvailablePort();
this.selfSignedCertificate = SelfSignedCertificate.create();
}

@AfterEach
void tearDown() {
this.serverPort = null;
this.selfSignedCertificate = null;
}

@Test
void testCreateWebClientTlsOff(Vertx vertx) {
JsonObject config = new JsonObject();
Assertions.assertDoesNotThrow(() -> WebClientUtils.create(vertx, config));
}

@Test
void testCreateWebClientTlsOn(Vertx vertx) {
JsonObject config = new JsonObject().put(SYS_NET_SERVER_OPTIONS, new JsonObject()
.put(SYS_PEM_KEY_CERT_OPTIONS, selfSignedCertificate.keyCertOptions().toJson()));
Assertions.assertDoesNotThrow(() -> WebClientUtils.create(vertx, config));
}

@Test
void testCreateWebClientTlsOnMultipleCerts(Vertx vertx) {
JsonArray certPathsArray = new JsonArray()
.add(SelfSignedCertificate.create().certificatePath())
.add(SelfSignedCertificate.create().certificatePath())
.add(SelfSignedCertificate.create().certificatePath());
JsonObject certPaths = new JsonObject().put(SYS_CERT_PATHS, certPathsArray);

JsonObject config = new JsonObject().put(SYS_NET_SERVER_OPTIONS, new JsonObject()
.put(SYS_PEM_KEY_CERT_OPTIONS, certPaths));
Assertions.assertDoesNotThrow(() -> WebClientUtils.create(vertx, config));
}

@Test
void testCreateWebClientWithMissingCertPaths(Vertx vertx) {
JsonObject config = new JsonObject().put(SYS_NET_SERVER_OPTIONS, new JsonObject()
.put(SYS_PEM_KEY_CERT_OPTIONS, new JsonObject()));
Assertions.assertThrows(WebClientConfigException.class,
() -> WebClientUtils.create(vertx, config));
}

@Test
void testCreateWebClientWithEmptyCertPaths(Vertx vertx) {
JsonObject config = new JsonObject().put(SYS_NET_SERVER_OPTIONS, new JsonObject()
.put(SYS_PEM_KEY_CERT_OPTIONS, new JsonObject().put(SYS_CERT_PATHS, new JsonArray())));
Assertions.assertThrows(WebClientConfigException.class,
() -> WebClientUtils.create(vertx, config));
}

@Test
void testWebClientServerCommunication(Vertx vertx, VertxTestContext testContext) {
JsonObject sipConfig = getCommonSipConfig(vertx);

sipConfig.put(SYS_PORT, serverPort);
sipConfig.put(SYS_NET_SERVER_OPTIONS, new JsonObject()
.put(SYS_PEM_KEY_CERT_OPTIONS, selfSignedCertificate.keyCertOptions().toJson()));

createServerTlsOn(vertx, testContext);

final WebClient webClient = WebClientUtils.create(vertx, sipConfig);
webClient.get(serverPort, "localhost", "/")
.send()
.onComplete(testContext.succeeding(response -> {
String message = response.body().toString();
log.info("WebClient sent message to port {}, message: {}", serverPort, message);
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode());
Assertions.assertEquals(RESPONSE_MESSAGE, message);
testContext.completeNow();
}));
}

@Test
void testFailingWebClientServerCommunication(Vertx vertx, VertxTestContext testContext) {
JsonObject sipConfig = getCommonSipConfig(vertx);

sipConfig.put(SYS_PORT, serverPort);
sipConfig.put(SYS_NET_SERVER_OPTIONS, new JsonObject());

createServerTlsOn(vertx, testContext);

final WebClient webClient = WebClientUtils.create(vertx, sipConfig);
webClient.get(serverPort, "localhost", "/")
.send()
.onComplete(testContext.failing(err -> {
log.info("Connection error: ", err);
testContext.completeNow();
}));
}

private void createServerTlsOn(Vertx vertx, VertxTestContext testContext) {
final HttpServerOptions httpServerOptions = new HttpServerOptions()
.setPort(serverPort)
.setSsl(true)
.setKeyCertOptions(selfSignedCertificate.keyCertOptions());

final HttpServer httpServer = vertx.createHttpServer(httpServerOptions);
httpServer
.requestHandler(req -> req.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
.end(RESPONSE_MESSAGE))
.listen(serverPort, http -> {
if (http.succeeded()) {
log.info("Server started on port: {}", serverPort);
} else {
testContext.failNow(http.cause());
}
});
}

private static JsonObject getCommonSipConfig(Vertx vertx) {
final FileSystem fileSystem = vertx.fileSystem();
return fileSystem.readFileBlocking("test-sip2.conf").toJsonObject();
}

private static int getAvailablePort() {
do {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
} catch (IOException e) {
// ignore
}
} while (true);
}
}

0 comments on commit 9e7dd2a

Please sign in to comment.