Skip to content

Commit

Permalink
Merge pull request #2242 from ballerina-platform/bug-fixes-2201.9.x
Browse files Browse the repository at this point in the history
[2201.9.x] Add support for configuring server name to be used in the SSL SNI extension
  • Loading branch information
TharmiganK authored Dec 6, 2024
2 parents cfb4c7e + de5a8ba commit ca430bb
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 29 deletions.
16 changes: 0 additions & 16 deletions ballerina-tests/http-security-tests/tests/ssl_disable_ssl_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,3 @@ public function testSslDisabledClient1() returns error? {
test:assertFail(msg = "Found unexpected output: " + resp.message());
}
}

http:ClientConfiguration disableSslClientConf2 = {
secureSocket: {
}
};

@test:Config {}
public function testSslDisabledClient2() {
http:Client|error httpClient = new ("https://localhost:9238", disableSslClientConf2);
string expectedErrMsg = "Need to configure cert with client SSL certificates file";
if (httpClient is error) {
test:assertEquals(httpClient.message(), expectedErrMsg);
} else {
test:assertFail(msg = "Expected mutual SSL error not found");
}
}
130 changes: 130 additions & 0 deletions ballerina-tests/http-security-tests/tests/ssl_sni_host_name_test.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2024 WSO2 Inc. (http://www.wso2.org).
//
// WSO2 Inc. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/http;
import ballerina/test;
import ballerina/http_test_common as common;

listener http:Listener http2SniListener = new (http2SniListenerPort, http2SslServiceConf);

http:ListenerConfiguration http1SniServiceConf = {
httpVersion: http:HTTP_1_1,
secureSocket: {
key: {
path: common:KEYSTORE_PATH,
password: "ballerina"
}
}
};

listener http:Listener http1SniListener = new (http1SniListenerPort, http1SniServiceConf);

service /http2SniService on http2SniListener {
resource function get .() returns string {
return "Sni works with HTTP_2!";
}
}

service /http1SniService on http1SniListener {
resource function get .() returns string {
return "Sni works with HTTP_1.1!";
}
}

http:ClientConfiguration http2SniClientConf = {
secureSocket: {
cert: {
path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "localhost"
}
};

http:ClientConfiguration http1SniClientConf = {
httpVersion: http:HTTP_1_1,
secureSocket: {
cert: {
path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "localhost"
}
};

http:ClientConfiguration http1SniClientConf2 = {
httpVersion: http:HTTP_1_1,
secureSocket: {
cert: {
path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "xxxx"
}
};

http:ClientConfiguration http2SniClientConf3 = {
secureSocket: {
serverName: "localhost"
}
};

@test:Config {}
public function testHttp2WithSni() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9207", http2SniClientConf);
string resp = check clientEP->get("/http2SniService/");
common:assertTextPayload(resp, "Sni works with HTTP_2!");
}

@test:Config {}
public function testHttp1WithSni() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http1SniClientConf);
string resp = check clientEP->get("/http1SniService/");
common:assertTextPayload(resp, "Sni works with HTTP_1.1!");
}

@test:Config {}
public function testSniFailure() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http1SniClientConf2);
string|error resp = clientEP->get("/http1SniService/");
if resp is error {
test:assertEquals(resp.message(), "SSL connection failed:No subject alternative names present /127.0.0.1:9208");
} else {
test:assertFail("Test `testSniFailure` is expecting an error. But received a success response");
}
}

@test:Config {}
public function testSniWhenUsingDefaultCerts() returns error? {
http:Client httpClient = check new("https://www.google.com", http2SniClientConf3);
string|error resp = httpClient->get("/");
// This response is success because even though we send a wrong server name, google.com sends the default cert which
// is valid and trusted by the client.
if resp is error {
test:assertFail("Found unexpected output: " + resp.message());
}
}

@test:Config {}
public function testSniFailureWhenUsingDefaultCerts() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http2SniClientConf3);
string|error resp = clientEP->get("/http1SniService/");
if resp is error {
common:assertTrueTextPayload(resp.message(), "SSL connection failed:javax.net.ssl.SSLHandshakeException:");
} else {
test:assertFail("Test `testSniFailureWhenUsingDefaultCerts` is expecting an error. But received a success response");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ const int stsPort = 9445;

const int http2GeneralPort = 9100;
const int http2SslGeneralPort = 9107;

const int http2SniListenerPort = 9207;
const int http1SniListenerPort = 9208;
2 changes: 2 additions & 0 deletions ballerina/http_client_config.bal
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public type RetryConfig record {|
# + shareSession - Enable/disable new SSL session creation
# + handshakeTimeout - SSL handshake time out
# + sessionTimeout - SSL session time out
# + serverName - Server name indication(SNI) to be used. If this is not present, hostname from the target URL will be used
public type ClientSecureSocket record {|
boolean enable = true;
crypto:TrustStore|string cert?;
Expand All @@ -114,6 +115,7 @@ public type ClientSecureSocket record {|
boolean shareSession = true;
decimal handshakeTimeout?;
decimal sessionTimeout?;
string serverName?;
|};

# Provides configurations for controlling the endpoint's behaviour in response to HTTP redirect related responses.
Expand Down
12 changes: 10 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
# Change Log
This file contains all the notable changes done to the Ballerina HTTP package through the releases.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- [Add support for configuring server name to be used in the SSL SNI extension](https://github.com/ballerina-platform/ballerina-library/issues/7435)

### Fixed
- [Fix the issue of not being able to configure only server name in the secureSocket config](https://github.com/ballerina-platform/ballerina-library/issues/7443)

## [2.11.6] - 2024-11-26

### Fixed
Expand Down Expand Up @@ -381,7 +389,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- [Fix missing error of invalid inbound request parameter for forward() method](https://github.com/ballerina-platform/ballerina-standard-library/issues/311)
- [Fix HTTP Circuit Breaker failure when status codes are not provided in the configuration](https://github.com/ballerina-platform/ballerina-standard-library/issues/339)
- [Fix HTTP FailOver client failure when status codes are overridden by an empty array](https://github.com/ballerina-platform/ballerina-standard-library/issues/1598)
- [Fix already built incompatible payload thrown error](https://github.com/ballerina-platform/ballerina-standard-library/issues/1600)
- [Fix already built incompatible payload thrown error](https://github.com/ballerina-platform/ballerina-standard-library/issues/1600)
- [Optional Types Not Supported in HTTP Client Request Operation Target Type](https://github.com/ballerina-platform/ballerina-standard-library/issues/1433)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ public final class HttpConstants {
public static final BString SECURESOCKET_CONFIG_VERIFY_CLIENT = StringUtils.fromString("verifyClient");
public static final BString SECURESOCKET_CONFIG_CERT_VALIDATION_TYPE_OCSP_STAPLING =
StringUtils.fromString("OCSP_STAPLING");
public static final BString SECURESOCKET_CONFIG_SNI_HOST_NAME = StringUtils.fromString("serverName");

//Socket Config
public static final BString SOCKET_CONFIG = StringUtils.fromString("socketConfig");
Expand Down
13 changes: 6 additions & 7 deletions native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -1407,13 +1407,7 @@ public static void populateSSLConfiguration(SslConfiguration senderConfiguration
}
Object cert = secureSocket.get(HttpConstants.SECURESOCKET_CONFIG_CERT);
if (cert == null) {
BMap<BString, Object> key = getBMapValueIfPresent(secureSocket, HttpConstants.SECURESOCKET_CONFIG_KEY);
if (key != null) {
senderConfiguration.useJavaDefaults();
} else {
throw createHttpError("Need to configure cert with client SSL certificates file",
HttpErrorType.SSL_ERROR);
}
senderConfiguration.useJavaDefaults();
} else {
evaluateCertField(cert, senderConfiguration);
}
Expand Down Expand Up @@ -1898,6 +1892,11 @@ private static void evaluateCommonFields(BMap<BString, Object> secureSocket, Ssl
Parameter enableSessionCreationParam = new Parameter(HttpConstants.SECURESOCKET_CONFIG_SHARE_SESSION.getValue(),
enableSessionCreation);
paramList.add(enableSessionCreationParam);
if (secureSocket.containsKey(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME)) {
Parameter sniHostNameParam = new Parameter(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME.getValue(),
String.valueOf(secureSocket.getStringValue(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME)));
paramList.add(sniHostNameParam);
}
}

private static BMap<BString, Object> getBMapValueIfPresent(BMap<BString, Object> map, BString key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class Constants {
public static final String CLIENT_SUPPORT_CIPHERS = "ciphers";
public static final String CLIENT_SUPPORT_SSL_PROTOCOLS = "sslEnabledProtocols";
public static final String CLIENT_ENABLE_SESSION_CREATION = "shareSession";
public static final String SNI_SERVER_NAME = "serverName";
public static final String MUTUAL_SSL_PASSED = "passed";
public static final String MUTUAL_SSL_FAILED = "failed";
public static final String MUTUAL_SSL_DISABLED = "disabled";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORTED_SNIMATCHERS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORT_CIPHERS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORT_SSL_PROTOCOLS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SNI_SERVER_NAME;
import static io.ballerina.stdlib.http.transport.contract.Constants.TLS_PROTOCOL;
/**
* SSL configuration for HTTP connection.
Expand Down Expand Up @@ -244,6 +245,7 @@ private SSLConfig getSSLConfigForListener() {
}

private SSLConfig getSSLConfigForSender() {
setSslParameters();
if (sslConfig.isDisableSsl() || sslConfig.useJavaDefaults()) {
return sslConfig;
}
Expand All @@ -264,7 +266,10 @@ private SSLConfig getSSLConfigForSender() {
sslConfig.setSSLProtocol(sslProtocol);
String tlsStoreType = sslConfig.getTLSStoreType() != null ? sslConfig.getTLSStoreType() : JKS;
sslConfig.setTLSStoreType(tlsStoreType);
return sslConfig;
}

private void setSslParameters() {
if (parameters != null) {
for (Parameter parameter : parameters) {
switch (parameter.getName()) {
Expand All @@ -277,12 +282,14 @@ private SSLConfig getSSLConfigForSender() {
case CLIENT_ENABLE_SESSION_CREATION:
sslConfig.setEnableSessionCreation(Boolean.parseBoolean(parameter.getValue()));
break;
case SNI_SERVER_NAME:
sslConfig.setSniHostName(parameter.getValue());
break;
default:
//do nothing
break;
}
}
}
return sslConfig;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ private static SSLEngine getSslEngineForCerts(SocketChannel socketChannel, Strin
SslHandler sslHandler = sslContext.newHandler(socketChannel.alloc(), host, port);
SSLEngine sslEngine = sslHandler.engine();
sslHandlerFactory.addCommonConfigs(sslEngine);
sslHandlerFactory.setSNIServerNames(sslEngine, host);
configureSniServerName(sslConfig, host, sslHandlerFactory, sslEngine);
if (sslConfig.isHostNameVerificationEnabled()) {
setHostNameVerfication(sslEngine);
}
Expand Down Expand Up @@ -507,14 +507,20 @@ private static SSLEngine instantiateAndConfigSSL(SSLConfig sslConfig, String hos
if (sslConfig != null) {
sslEngine = sslHandlerFactory.buildClientSSLEngine(host, port);
sslEngine.setUseClientMode(true);
sslHandlerFactory.setSNIServerNames(sslEngine, host);
configureSniServerName(sslConfig, host, sslHandlerFactory, sslEngine);
if (hostNameVerificationEnabled) {
sslHandlerFactory.setHostNameVerfication(sslEngine);
}
}
return sslEngine;
}

private static void configureSniServerName(SSLConfig sslConfig, String host, SSLHandlerFactory sslHandlerFactory,
SSLEngine sslEngine) {
sslHandlerFactory.setSNIServerNames(sslEngine,
sslConfig.getSniHostName() != null ? sslConfig.getSniHostName() : host);
}

/**
* Get integer type property value from a property map.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class SSLConfig {
private long handshakeTimeOut;
private boolean disableSsl = false;
private boolean useJavaDefaults = false;
private String sniHostName;

public SSLConfig() {}

Expand Down Expand Up @@ -179,6 +180,14 @@ public void setEnableSessionCreation(boolean enableSessionCreation) {
this.enableSessionCreation = enableSessionCreation;
}

public void setSniHostName(String sniHostName) {
this.sniHostName = sniHostName;
}

public String getSniHostName() {
return sniHostName;
}

public String[] getEnableProtocols() {
return enableProtocols == null ? null : enableProtocols.clone();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ private void configureSslForHttp2(SocketChannel ch, ChannelPipeline clientPipeli
SslContext sslCtx = sslHandlerFactory.createHttp2TLSContextForClient(false);
SslHandler sslHandler = sslCtx.newHandler(ch.alloc(), httpRoute.getHost(), httpRoute.getPort());
SSLEngine sslEngine = sslHandler.engine();
sslHandlerFactory.setSNIServerNames(sslEngine, httpRoute.getHost());
sslHandlerFactory.setSNIServerNames(sslEngine,
sslConfig.getSniHostName() != null ? sslConfig.getSniHostName() : httpRoute.getHost());
if (sslConfig.isHostNameVerificationEnabled()) {
setHostNameVerfication(sslEngine);
}
Expand Down

0 comments on commit ca430bb

Please sign in to comment.