11package com .marklogic .appdeployer .command .security ;
22
3- import java .io .File ;
4- import java .util .List ;
5-
63import com .marklogic .appdeployer .ConfigDir ;
74import com .marklogic .appdeployer .command .AbstractCommand ;
85import com .marklogic .appdeployer .command .CommandContext ;
96import com .marklogic .appdeployer .command .SortOrderConstants ;
7+ import com .marklogic .mgmt .ManageClient ;
108import com .marklogic .mgmt .resource .security .CertificateTemplateManager ;
9+ import org .springframework .util .FileCopyUtils ;
10+
11+ import java .io .File ;
12+ import java .io .IOException ;
13+ import java .util .List ;
1114
1215/**
1316 * Inserts host certificates for each certificate template returned by the Manage API. Host certificates are inserted
1417 * via the endpoint at http://docs.marklogic.com/REST/POST/manage/v2/certificate-templates/[id-or-name]#insertHC .
15- *
18+ * <p>
1619 * To allow for host certificates to be associated with a certificate template, this command expects to find host
1720 * certificates in a directory named "(configuration directory)/security/certificate-templates/host-certificates/(name of template)/".
18- *
21+ * <p>
1922 * The public certificate file must be a PEM-formatted file with a file extension of ".crt". And the private key file must
2023 * be a PEM-formatted file with a file extension of ".key".
2124 */
@@ -41,17 +44,17 @@ public void execute(CommandContext context) {
4144 }
4245 }
4346
44- /**
45- * Looks for host certificates for the given template name.
46- *
47- * @param context
48- * @param templateName Name of the template the host certificates should be inserted into
49- */
50- protected void insertHostCertificatesForTemplate (CommandContext context , String templateName ) {
47+ /**
48+ * Looks for host certificates for the given template name.
49+ *
50+ * @param context
51+ * @param templateName Name of the template the host certificates should be inserted into
52+ */
53+ protected void insertHostCertificatesForTemplate (CommandContext context , String templateName ) {
5154 for (ConfigDir configDir : context .getAppConfig ().getConfigDirs ()) {
5255 File hostCertDir = new File (configDir .getCertificateTemplatesDir () + File .separator + "host-certificates" + File .separator + templateName );
5356 logger .info (format ("Looking for host certificate files ending in '%s' for template '%s' in: %s" , publicCertificateFileExtension , templateName , hostCertDir .getAbsolutePath ()));
54- if (hostCertDir .exists ()){
57+ if (hostCertDir .exists ()) {
5558 for (File f : hostCertDir .listFiles ()) {
5659 if (f .getName ().endsWith (publicCertificateFileExtension )) {
5760 File privateKeyFile = determinePrivateKeyFile (f );
@@ -66,33 +69,113 @@ protected void insertHostCertificatesForTemplate(CommandContext context, String
6669 }
6770 }
6871 }
69- }
72+ }
7073
71- protected File determinePrivateKeyFile (File publicCertificateFile ) {
72- String path = publicCertificateFile .getAbsolutePath ();
73- return new File (path .substring (0 , path .length () - publicCertificateFileExtension .length ()) + privateKeyFileExtension );
74- }
74+ protected File determinePrivateKeyFile (File publicCertificateFile ) {
75+ String path = publicCertificateFile .getAbsolutePath ();
76+ return new File (path .substring (0 , path .length () - publicCertificateFileExtension .length ()) + privateKeyFileExtension );
77+ }
7578
7679 /**
7780 * @param context
78- * @param templateName The name of the certificate template that the host certificate will be inserted into
81+ * @param templateName The name of the certificate template that the host certificate will be inserted into
82+ * Assumes filename is hostname + .crt: ex: host1.marklogic.com.crt
7983 * @param publicCertFile
8084 * @param privateKeyFile
8185 */
8286 protected void insertHostCertificate (CommandContext context , String templateName , File publicCertFile , File privateKeyFile ) {
83- CertificateTemplateManager mgr = new CertificateTemplateManager (context .getManageClient ());
84- if (!mgr .certificateExists (templateName )) {
87+ if (!certificateExists (templateName , publicCertFile , context .getManageClient ())) {
8588 logger .info (format ("Inserting host certificate for certificate template '%s'" , templateName ));
8689 String pubCertString = copyFileToString (publicCertFile );
8790 String privateKeyString = copyFileToString (privateKeyFile );
88- mgr .insertHostCertificate (templateName , pubCertString , privateKeyString );
91+ new CertificateTemplateManager ( context . getManageClient ()) .insertHostCertificate (templateName , pubCertString , privateKeyString );
8992 logger .info (format ("Inserted host certificate for certificate template '%s'" , templateName ));
9093 } else {
9194 logger .info (format ("Host certificate already exists for certificate template '%s', so not inserting host certificate found at: %s" ,
9295 templateName , publicCertFile .getAbsolutePath ()));
9396 }
9497 }
9598
99+ /**
100+ * @param templateName
101+ * @param publicCertFile
102+ * @param manageClient
103+ * @return
104+ */
105+ protected boolean certificateExists (String templateName , File publicCertFile , ManageClient manageClient ) {
106+ CertificateTemplateManager mgr = new CertificateTemplateManager (manageClient );
107+
108+ String hostName = null ;
109+ try {
110+ hostName = getCertificateHostName (publicCertFile , manageClient );
111+ } catch (Exception ex ) {
112+ logger .warn ("Unable to determine host name for public certificate file: " + publicCertFile + "; cause: " + ex .getMessage () +
113+ ". Due to this, the check to determine if the certificate exists already will not include a host name but will only be " +
114+ "based on the name of the template." );
115+ }
116+
117+ if (hostName != null ) {
118+ logger .info (format ("Checking for existing certificate with name '%s' and host name '%s'" , templateName , hostName ));
119+ return mgr .certificateExists (templateName , hostName );
120+ }
121+
122+ // This is very unexpected, as it would mean that the /v1/eval query was not able to extract a host name
123+ logger .info (format ("Could not determine host name, so checking for existing certificate with name '%s'" , templateName ));
124+ return mgr .certificateExists (templateName );
125+ }
126+
127+ /**
128+ * Uses the /v1/eval endpoint on the Manage server to extract the host name from the given public certificate file.
129+ *
130+ * @param publicCertFile
131+ * @param manageClient
132+ * @return
133+ */
134+ protected String getCertificateHostName (File publicCertFile , ManageClient manageClient ) {
135+ final String query = makeQueryForHostName (publicCertFile );
136+ String response = manageClient .postForm ("/v1/eval" , "xquery" , query ).getBody ();
137+ return extractHostNameFromEvalResponse (response );
138+ }
139+
140+ /**
141+ * Builds an XQuery query that can extract the host name from the given public certificate file.
142+ *
143+ * @param publicCertFile
144+ * @return
145+ */
146+ protected String makeQueryForHostName (File publicCertFile ) {
147+ String certContents ;
148+ try {
149+ certContents = new String (FileCopyUtils .copyToByteArray (publicCertFile ));
150+ } catch (IOException e ) {
151+ throw new RuntimeException ("Unable to read certificate from file: " + publicCertFile + "; cause: " + e .getMessage ());
152+ }
153+
154+ return format ("xdmp:x509-certificate-extract(\" %s\" )/*:subject/*:commonName/fn:string()" , certContents );
155+ }
156+
157+ /**
158+ * The /v1/eval endpoint returns a multipart/mixed response that Spring 5.x does not yet seem to handle, even though
159+ * it appears that 5.2.x should. So there's some really hacky code here to extract the value of the host name
160+ * from the /v1/eval response.
161+ *
162+ * @param response
163+ * @return
164+ */
165+ protected String extractHostNameFromEvalResponse (String response ) {
166+ final String token = "X-Primitive: string" ;
167+ int pos = response .indexOf (token );
168+ if (pos < 0 ) {
169+ throw new IllegalArgumentException ("Unable to extract host name from eval response: " + response + "; did not find: " + token );
170+ }
171+ response = response .substring (pos + token .length ()).trim ();
172+ pos = response .indexOf ("--" );
173+ if (pos < 0 ) {
174+ throw new IllegalArgumentException ("Unable to extract host name from eval response: " + response + "; did not find '--' after " + token );
175+ }
176+ return response .substring (0 , pos ).trim ();
177+ }
178+
96179 public void setPublicCertificateFileExtension (String publicCertificateFileExtension ) {
97180 this .publicCertificateFileExtension = publicCertificateFileExtension ;
98181 }
0 commit comments