Skip to content

integration test for jenkins-75676 #1077

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
36 changes: 36 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,42 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>nginx</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<!--<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<version>2.26.3</version>
<scope>test</scope>
</dependency>-->
<!--<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>wiremock</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<repositories>
Expand Down
191 changes: 191 additions & 0 deletions src/test/java/integration/JENKINS_75676_Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package integration;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.utility.MountableFile;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;

import static org.assertj.core.api.Assertions.assertThat;

public class JENKINS_75676_Test {
static MockWebServer bitbucketMock;
static NginxContainer<?> nginx;
static File tmpConf;
@Rule
public JenkinsRule j = new JenkinsRule();

@BeforeClass
public static void setup() throws IOException, InterruptedException {
runScript("src/test/resources/JENKINS-75676/generate-mtls-certs.sh");
bitbucketMock = new MockWebServer();
bitbucketMock.enqueue(new MockResponse()
.setBody("Bitbucket Mock Server Up")
.setResponseCode(200));
bitbucketMock.start();

// 2. Dynamically write nginx.conf, using the mockwebserver port and host.docker.internal
int bbPort = bitbucketMock.getPort();
String nginxConf = renderNginxConfTemplate(bbPort);

tmpConf = File.createTempFile("nginx-ssl", ".conf");
Files.write(tmpConf.toPath(), nginxConf.getBytes(StandardCharsets.UTF_8));

// 3. Start Nginx container, using the temp conf
nginx = new NginxContainer<>("nginx:1.28")
.withExposedPorts(8443)
.withCopyFileToContainer(MountableFile.forHostPath(tmpConf.getAbsolutePath()), "/etc/nginx/nginx.conf")
.withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/server.crt"), "/etc/nginx/certs/server.crt")
.withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/server.key"), "/etc/nginx/certs/server.key")
.withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/rootCA.crt"), "/etc/nginx/certs/rootCA.crt");
nginx.start();
}
@AfterClass
public static void tearDown() throws IOException {
// 5. Cleanup
if (nginx != null)
nginx.stop();
if(bitbucketMock != null)
bitbucketMock.shutdown();
if (tmpConf != null && tmpConf.exists())
tmpConf.delete();
}
@Ignore
@Test
public void canAccessViaNginxProxy() throws Exception {
String proxyUrl = "http://" + nginx.getHost() + ":" + nginx.getMappedPort(8443) + "/";
HttpURLConnection conn = (HttpURLConnection) new URL(proxyUrl).openConnection();
int code = conn.getResponseCode();
String body = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A").next();
assertThat(code).isEqualTo(200);
assertThat(body).contains("Bitbucket Mock Server Up");


}

@Ignore
@Test
public void proxyReturnsMasterBranch() throws Exception {
// Enqueue response for the branches endpoint
bitbucketMock.enqueue(new MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody("{ \"values\": [ { \"displayId\": \"master\", \"id\": \"refs/heads/master\", \"isDefault\": true } ], \"isLastPage\": true }"));

// Create the proxy URL
String branchesUrl = "http://" + nginx.getHost() + ":" + nginx.getMappedPort(8080) +
"/rest/api/1.0/projects/myorg/repos/testrepo/branches";

HttpURLConnection conn = (HttpURLConnection) new URL(branchesUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode();
assertThat(responseCode).isEqualTo(200);

BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
reader.close();

String bodyString = body.toString().replaceAll("\\s+", "");
assertThat(bodyString).contains("\"displayId\":\"master\"");
assertThat(bodyString).contains("\"isDefault\":true");
}


@Test
public void canAccessViaNginxProxyWithClientCert() throws Exception {
SSLContext sslContext = setupSSLContext();
String proxyUrl = "https://" + nginx.getHost() + ":" + nginx.getMappedPort(8443) + "/";
HttpsURLConnection conn = (HttpsURLConnection) new URL(proxyUrl).openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
int code = conn.getResponseCode();
String body = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A").next();
assertThat(code).isEqualTo(200);
assertThat(body).contains("Bitbucket Mock Server Up");
}

public static SSLContext setupSSLContext() throws Exception {
// Load PKCS12 keystore (from cert generation script)
char[] password = "changeit".toCharArray();
KeyStore clientStore = KeyStore.getInstance("PKCS12");
try (InputStream in = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/certs/client_keystore.p12")) {
clientStore.load(in, password);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientStore, password);

// Trust store with CA root
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(null, null);
try (InputStream in = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/certs/rootCA.crt")) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
trustStore.setCertificateEntry("ca", cf.generateCertificate(in));
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return sslContext;
}

private static String renderNginxConfTemplate(int bitbucketPort) throws IOException {
try (InputStream is = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/nginx.conf.tmpl")) {
if (is == null)
throw new IllegalStateException("Missing nginx.conf.tmpl file in test resources");
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
return content.replace("${BITBUCKET_PORT}", String.valueOf(bitbucketPort));
}
}

private static void runScript(String scriptPath) throws IOException, InterruptedException {
File script = new File(scriptPath);
if (!script.exists()) {
throw new IllegalArgumentException("Cert generation script does not exist: " + scriptPath);
}
// Make script executable if not already
script.setExecutable(true);

// Run the script
ProcessBuilder pb = new ProcessBuilder("bash", script.getAbsolutePath());
pb.redirectErrorStream(true); // merge stderr with stdout
Process process = pb.start();
try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = r.readLine()) != null) {
System.out.println("[certgen] " + line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IllegalStateException("Cert generation script failed with exit code " + exitCode);
}
}

}
82 changes: 82 additions & 0 deletions src/test/resources/JENKINS-75676/generate-mtls-certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash
set -euo pipefail

BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CERT_DIR="$BASE_DIR/certs"
rm -rf "$CERT_DIR"
mkdir -p "$CERT_DIR"
cd "$CERT_DIR"

echo "1. Generate Root CA"
openssl genrsa -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1825 -out rootCA.crt \
-subj "/C=ES/L=Sevilla/O=TestOrg/CN=TestRootCA"

echo "2. Create Server Certificate (localhost, 127.0.0.1 SAN)"
cat > server_cert.cnf <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext

[dn]
C = ES
ST = Sevilla
L = Sevilla
O = TestOrg
OU = TestOrgEng
CN = localhost

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
EOF

openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -config server_cert.cnf
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out server.crt -days 365 -sha256 -extensions req_ext -extfile server_cert.cnf

echo "3. Create Client Certificate (CN=client)"
cat > client_cert.cnf <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[dn]
C = ES
ST = Sevilla
L = Sevilla
O = TestOrg
OU = TestOrgEng
CN = client
EOF

openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -config client_cert.cnf
openssl x509 -req -in client.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out client.crt -days 365 -sha256

echo "4. Create PKCS12 for Java/Jenkins"
openssl pkcs12 -export -out client_keystore.p12 -inkey client.key -in client.crt -certfile rootCA.crt \
-password pass:changeit -name client

echo "5. Copy certs for nginx docker use"
mkdir -p nginx
cp server.crt server.key rootCA.crt nginx/

echo "✅ All certificates generated in $CERT_DIR"
ls -l "$CERT_DIR"
ls -l "$CERT_DIR/nginx"

echo "🔥 CLIENT TEST (curl, must succeed):"
echo " curl -vik --cert client.crt --key client.key --cacert rootCA.crt https://localhost:8443/"
echo "🔥 NEGATIVE TEST (no client cert, must fail):"
echo " curl -vik --cacert rootCA.crt https://localhost:8443/"
21 changes: 21 additions & 0 deletions src/test/resources/JENKINS-75676/nginx.conf.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
events {}

http {
server {
listen 8443 ssl;
server_name localhost;

ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_client_certificate /etc/nginx/certs/rootCA.crt;
ssl_verify_client on;

location / {
proxy_pass http://host.docker.internal:${BITBUCKET_PORT};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}