Skip to content
Open
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ test-basic-proxy/lib/positive/*.js
!test-basic-proxy/lib/positive/*Test.js
test-basic-proxy/lib/negative/*.js
!test-basic-proxy/lib/negative/*Test.js

# SSL certificates generated at deploy time by mlDeploy/extractSslCertificate.
# Never commit private keys or regenerated certs — they change on every deployment.
test-app/src/main/ml-config/self-signed-ca.pem
test-app/src/main/ml-config/self-signed-ca-key.pem
test-app/src/main/ml-config/ssl-server-cert.pem
test-app/src/main/ml-config/ssl-server-key.pem
test-app/src/main/ml-config/ssl-server.csr
test-app/src/main/ml-config/ssl-server-ext.cnf
test-app/src/main/ml-config/*.srl
17 changes: 13 additions & 4 deletions etc/test-config-qa-ssl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
const fs = require('fs');
const path = require('path');

// self-signed-ca.pem is written by the Gradle extractSslCertificate task after mlDeploy.
// It is not committed to the repository. SSL test connections fail gracefully
// (cert verification error) if mlDeploy has not been run yet.
const CA_PATH = path.join(__dirname, '../test-app/src/main/ml-config/self-signed-ca.pem');
const SSL_CA = fs.existsSync(CA_PATH) ? fs.readFileSync(CA_PATH) : undefined;

var testHost = 'localhost';

var restPort = '8016';
Expand All @@ -23,7 +32,7 @@ var restEvaluatorPassword = 'x';

var testServerName = 'node-client-api-ssl-server';

// For SSL without client cert, use rejectUnauthorized: false
// Do NOT use rejectUnauthorized: false in production.
module.exports = {
testServerName: testServerName,
testHost: testHost,
Expand Down Expand Up @@ -69,7 +78,7 @@ module.exports = {
user: restAdminUser,
password: restAdminPassword,
authType: 'BASIC',
rejectUnauthorized: false,
ssl: true
ssl: true,
ca: SSL_CA
}
};
17 changes: 13 additions & 4 deletions etc/test-config-qa.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
const fs = require('fs');
const path = require('path');

// self-signed-ca.pem is written by the Gradle extractSslCertificate task after mlDeploy.
// It is not committed to the repository. SSL test connections fail gracefully
// (cert verification error) if mlDeploy has not been run yet.
const CA_PATH = path.join(__dirname, '../test-app/src/main/ml-config/self-signed-ca.pem');
const SSL_CA = fs.existsSync(CA_PATH) ? fs.readFileSync(CA_PATH) : undefined;

var testHost = 'localhost';

var restPort = '8024';
Expand Down Expand Up @@ -31,7 +40,7 @@ var configAdminPassword = 'admin';
var testServerName = 'node-client-api-rest-server';
var dmsdktestServerName = 'dmsdk-api-rest-server';

// For SSL without client cert, use rejectUnauthorized: false
// Do NOT use rejectUnauthorized: false in production.
module.exports = {
testServerName: testServerName,
testHost: testHost,
Expand Down Expand Up @@ -114,7 +123,7 @@ module.exports = {
user: restAdminUser,
password: restAdminPassword,
authType: 'BASIC',
rejectUnauthorized: false,
ssl: true
ssl: true,
ca: SSL_CA
}
};
21 changes: 14 additions & 7 deletions etc/test-config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
const fs = require('fs');
const path = require('path');

// self-signed-ca.pem is written by the Gradle extractSslCertificate task after mlDeploy.
// It is not committed to the repository. SSL test connections fail gracefully
// (cert verification error) if mlDeploy has not been run yet.
const CA_PATH = path.join(__dirname, '../test-app/src/main/ml-config/self-signed-ca.pem');
const SSL_CA = fs.existsSync(CA_PATH) ? fs.readFileSync(CA_PATH) : undefined;

let testHost = 'localhost';

let restPort = '8015';
Expand Down Expand Up @@ -33,7 +42,7 @@ let testPassword = 'x';
let tdeUser = 'tde-user';
let tdePassword = 'x';

// For SSL without client cert, use rejectUnauthorized: false
// Do NOT use rejectUnauthorized: false in production.
module.exports = {
testServerName: testServerName,
testHost: testHost,
Expand Down Expand Up @@ -93,8 +102,8 @@ module.exports = {
user: restAdminUser,
password: restAdminPassword,
authType: 'BASIC',
rejectUnauthorized: false,
ssl: true,
ca: SSL_CA,
enableGzippedResponses: true
},
testConnection: {
Expand All @@ -103,7 +112,6 @@ module.exports = {
user: testUser,
password: testPassword,
authType: restAuthType,
rejectUnauthorized: false,
enableGzippedResponses: true
},
tdeConnection: {
Expand All @@ -112,7 +120,6 @@ module.exports = {
user: tdeUser,
password: tdePassword,
authType: restAuthType,
rejectUnauthorized: false,
enableGzippedResponses: true
},
restWriterConnectionWithBasePath: {
Expand Down Expand Up @@ -144,7 +151,7 @@ module.exports = {
user: restWriterUser,
password: restWriterPassword,
authType: restAuthType,
ssl: true,
rejectUnauthorized: false
ssl: true,
ca: SSL_CA
}
};
60 changes: 60 additions & 0 deletions test-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,66 @@ ext {
mlAppDeployer.commands.add(command)
}

/*
* After mlDeploy, MarkLogic holds the generated self-signed certificate. This task
* fetches its PEM via the Management API and writes it to the well-known path
* src/main/ml-config/self-signed-ca.pem so that Node.js test clients can trust it
* via the `ca` option — do NOT use rejectUnauthorized: false in production.
*
* Because the cert is regenerated on every mlDeploy this file is not committed to the
* repository; it is listed in .gitignore and produced at deploy time.
*/
tasks.register("extractSslCertificate") {
description = "Extracts the generated SSL CA certificate from MarkLogic and writes it for Node.js test clients."
doLast {
def manageHost = project.findProperty("mlManageHost") ?: project.findProperty("mlHost") ?: "localhost"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic can be simplified by using the ManageClient like the following:

getManageClient().getJson(
            "/manage/v2/certificate-templates/node-client-ssl-template?format=json")

That has the added benefit of handling the different authentication types for us.

Also, we probably want a task to generate the SSL Certificate and insert it, instead of extracting it from MarkLogic something like below:

/*
 * Generates a CA + server certificate pair using openssl and inserts the server
 * cert into the MarkLogic node-client-ssl-template.  The self-signed CA cert is
 * written to src/main/ml-config/self-signed-ca.pem so Node.js test clients can
 * trust the MarkLogic SSL server via the `ca` option.
 *
 * Why openssl instead of MarkLogic's generateTemporaryCertificate:
 *   MarkLogic's internal CA (used to sign the temporary cert) is not exposed by
 *   the Management API, so there is no way to retrieve it for use in the Node.js
 *   `ca` option.  Generating our own CA + server cert with openssl solves this
 *   while keeping rejectUnauthorized: false out of production code.
 *   Do NOT use rejectUnauthorized: false in production.
 *
 * The generated files are listed in .gitignore and recreated on every run.
 */
tasks.register("generateAndInstallSslCertificate", com.marklogic.gradle.task.MarkLogicTask) {
  description = "Generates CA + server certs with openssl and inserts the server cert into MarkLogic."
  doLast {
    def certDir = file("src/main/ml-config")
    certDir.mkdirs()

    def caKeyFile  = new File(certDir, "self-signed-ca.key")
    def caCertFile = new File(certDir, "self-signed-ca.pem")
    def srvKeyFile = new File(certDir, "ssl-server.key")
    def srvCsrFile = new File(certDir, "ssl-server.csr")
    def srvCertFile = new File(certDir, "ssl-server.pem")
    def extFile    = new File(certDir, "ssl-server-ext.cnf")

    // Step 1 — self-signed CA cert (subject == issuer so Node.js can use it as a trust anchor)
    exec {
      commandLine "openssl", "req", "-x509", "-newkey", "rsa:2048",
        "-keyout", caKeyFile.absolutePath, "-out", caCertFile.absolutePath,
        "-days", "365", "-nodes",
        "-subj", "/C=US/ST=VA/L=McLean/O=MarkLogic/OU=Sales/CN=localhost"
      errorOutput = System.err
    }

    // Step 2 — server CSR
    exec {
      commandLine "openssl", "req", "-newkey", "rsa:2048",
        "-keyout", srvKeyFile.absolutePath, "-out", srvCsrFile.absolutePath,
        "-nodes", "-subj", "/CN=localhost"
      errorOutput = System.err
    }

    // Step 3 — server cert signed by our CA (SAN required by modern TLS clients)
    extFile.text = "subjectAltName=IP:127.0.0.1,DNS:localhost\nextendedKeyUsage=serverAuth"
    exec {
      commandLine "openssl", "x509", "-req",
        "-in", srvCsrFile.absolutePath,
        "-CA", caCertFile.absolutePath, "-CAkey", caKeyFile.absolutePath,
        "-CAcreateserial", "-out", srvCertFile.absolutePath,
        "-days", "365", "-extfile", extFile.absolutePath
      errorOutput = System.err
    }

    // Step 4 — insert server cert + key into the MarkLogic certificate template.
    // ManageClient handles Digest auth automatically.
    def payload = new groovy.json.JsonBuilder([
      operation    : "insert-host-certificates",
      certificates : [[certificate: [cert: srvCertFile.text, pkey: srvKeyFile.text]]]
    ]).toString()
    getManageClient().postJson(
      "/manage/v2/certificate-templates/node-client-ssl-template", payload)

    println "SSL CA cert  → ${caCertFile.absolutePath}"
    println "SSL server cert inserted into MarkLogic template: node-client-ssl-template"
  }
}
mlDeploy.finalizedBy generateAndInstallSslCertificate

def managePort = project.findProperty("mlManagePort") ?: "8002"
def username = project.findProperty("mlManageUsername") ?: project.findProperty("mlUsername")
def password = project.findProperty("mlManagePassword") ?: project.findProperty("mlPassword")
if (!username || !password) {
throw new GradleException("Missing MarkLogic credentials: set mlUsername/mlPassword or mlManageUsername/mlManagePassword")
}
def credentials = Base64.getEncoder().encodeToString(
"${username}:${password}".getBytes("UTF-8"))

// Step 1 — find the certificate ID(s) attached to the template
def templateUrl = new URL(
"http://${manageHost}:${managePort}/manage/v2/certificate-templates/node-client-ssl-template?format=json")
def templateConn = templateUrl.openConnection() as HttpURLConnection
templateConn.setConnectTimeout(10_000)
templateConn.setReadTimeout(10_000)
templateConn.setRequestProperty("Authorization", "Basic ${credentials}")
templateConn.setRequestProperty("Accept", "application/json")
if (templateConn.responseCode >= 400) {
throw new GradleException("Failed to fetch certificate template 'node-client-ssl-template': HTTP ${templateConn.responseCode} ${templateConn.responseMessage}")
}
def templateJson = templateConn.inputStream.withCloseable { new groovy.json.JsonSlurper().parse(it) }

def certEntry = templateJson["certificate-template-default"]["certificate-list"]["certificate"]
def certId = (certEntry instanceof List ? certEntry[0] : certEntry)["certificate-id"]

// Step 2 — fetch the PEM for that certificate
def certUrl = new URL(
"http://${manageHost}:${managePort}/manage/v2/certificates/${certId}?format=json")
def certConn = certUrl.openConnection() as HttpURLConnection
certConn.setConnectTimeout(10_000)
certConn.setReadTimeout(10_000)
certConn.setRequestProperty("Authorization", "Basic ${credentials}")
certConn.setRequestProperty("Accept", "application/json")
if (certConn.responseCode >= 400) {
throw new GradleException("Failed to fetch certificate '${certId}': HTTP ${certConn.responseCode} ${certConn.responseMessage}")
}
def certJson = certConn.inputStream.withCloseable { new groovy.json.JsonSlurper().parse(it) }
def pem = certJson["certificate-default"]["pem"]

// Step 3 — write to well-known path (overwritten on every mlDeploy)
def outFile = file("src/main/ml-config/self-signed-ca.pem")
outFile.text = pem
println "SSL certificate extracted from node-client-ssl-template → ${outFile.absolutePath}"
}
}
mlDeploy.finalizedBy extractSslCertificate

tasks.register("curlPeople", Exec) {
commandLine = [
'curl',
Expand Down
Loading
Loading