diff --git a/src/org/jlab/elog/Attachment.java b/src/org/jlab/elog/Attachment.java index 1f3703e..19dc65e 100644 --- a/src/org/jlab/elog/Attachment.java +++ b/src/org/jlab/elog/Attachment.java @@ -1,150 +1,150 @@ -package org.jlab.elog; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import org.jlab.elog.exception.LogIOException; -import org.jlab.elog.exception.LogRuntimeException; -import org.jlab.elog.util.XMLUtil; -import org.w3c.dom.Element; - -/** - * A file attachment. - * - * @author ryans - */ -public class Attachment { - - private final Element attachmentElement; - - Attachment(Element attachmentElement) { - this.attachmentElement = attachmentElement; - } - - /** - * Return the attachment caption. - * - * @return The caption - */ - public String getCaption() { - Element capElement = XMLUtil.getChildElementByName(attachmentElement, - "caption"); - - if (capElement == null) { - throw new LogRuntimeException("Unexpected XML DOM structure; " - + "Attachment caption element missing."); - } - - return capElement.getTextContent(); - } - - /** - * Return the attachment file name. - * - * @return The file name - */ - public String getFileName() { - Element nameElement = XMLUtil.getChildElementByName(attachmentElement, - "filename"); - - if (nameElement == null) { - throw new LogRuntimeException("Unexpected XML DOM structure; " - + "Attachment filename element missing."); - } - - return nameElement.getTextContent(); - } - - /** - * Return the attachment mime type. - * - * @return The mime type - */ - public String getMimeType() { - Element mimeElement = XMLUtil.getChildElementByName(attachmentElement, - "type"); - - if (mimeElement == null) { - throw new LogRuntimeException("Unexpected XML DOM structure; " - + "Attachment type element missing."); - } - - return mimeElement.getTextContent(); - } - - /** - * Return the attachment URL or null if unavailable. The URL is unavailable - * when the attachment has not been submitted to the server yet. - * - * @return The URL or null - */ - public String getURL() { - String url = null; - - Element dataElement = XMLUtil.getChildElementByName(attachmentElement, - "data"); - - if (dataElement == null) { - throw new LogRuntimeException("Unexpected XML DOM structure; " - + "Attachment data element missing."); - } - - if (dataElement.getAttribute("encoding").equals("url")) { - url = dataElement.getTextContent(); - } - - return url; - } - - /** - * Return true if the attachment is accessed via URL or false if local only - * (has not been submitted yet). - * - * Note: you could just call getURL() and check if the result is null. - * - * @return true if the attachment is accessed via URL - */ - public boolean isURL() { - return (getURL() != null); - } - - /** - * Return the attachment data via an InputStream. The user should close - * the InputStream when done. - * - * @return The attachment data - * @throws LogIOException If unable to obtain the InputStream - */ - public InputStream getData() throws LogIOException { - InputStream is = null; - - Element dataElement = XMLUtil.getChildElementByName(attachmentElement, - "data"); - - if (dataElement == null) { - throw new LogRuntimeException("Unexpected XML DOM structure; " - + "Attachment data element missing."); - } - - if (dataElement.getAttribute("encoding").equals("url")) { - String urlStr = dataElement.getTextContent(); - - try { - URL url = new URL(urlStr); - is = url.openStream(); - } catch (MalformedURLException e) { - throw new LogIOException("Unable to open input stream.", e); - } catch(IOException e) { - throw new LogIOException("Unable to open input stream.", e); - } - } else { - String dataStr = dataElement.getTextContent(); - byte[] data = XMLUtil.decodeBase64(dataStr); - is = new ByteArrayInputStream(data); - } - - return is; - } -} +package org.jlab.elog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import org.jlab.elog.exception.LogIOException; +import org.jlab.elog.exception.LogRuntimeException; +import org.jlab.elog.util.XMLUtil; +import org.w3c.dom.Element; + +/** + * A file attachment. + * + * @author ryans + */ +public class Attachment { + + private final Element attachmentElement; + + Attachment(Element attachmentElement) { + this.attachmentElement = attachmentElement; + } + + /** + * Return the attachment caption. + * + * @return The caption + */ + public String getCaption() { + Element capElement = XMLUtil.getChildElementByName(attachmentElement, + "caption"); + + if (capElement == null) { + throw new LogRuntimeException("Unexpected XML DOM structure; " + + "Attachment caption element missing."); + } + + return capElement.getTextContent(); + } + + /** + * Return the attachment file name. + * + * @return The file name + */ + public String getFileName() { + Element nameElement = XMLUtil.getChildElementByName(attachmentElement, + "filename"); + + if (nameElement == null) { + throw new LogRuntimeException("Unexpected XML DOM structure; " + + "Attachment filename element missing."); + } + + return nameElement.getTextContent(); + } + + /** + * Return the attachment mime type. + * + * @return The mime type + */ + public String getMimeType() { + Element mimeElement = XMLUtil.getChildElementByName(attachmentElement, + "type"); + + if (mimeElement == null) { + throw new LogRuntimeException("Unexpected XML DOM structure; " + + "Attachment type element missing."); + } + + return mimeElement.getTextContent(); + } + + /** + * Return the attachment URL or null if unavailable. The URL is unavailable + * when the attachment has not been submitted to the server yet. + * + * @return The URL or null + */ + public String getURL() { + String url = null; + + Element dataElement = XMLUtil.getChildElementByName(attachmentElement, + "data"); + + if (dataElement == null) { + throw new LogRuntimeException("Unexpected XML DOM structure; " + + "Attachment data element missing."); + } + + if (dataElement.getAttribute("encoding").equals("url")) { + url = dataElement.getTextContent(); + } + + return url; + } + + /** + * Return true if the attachment is accessed via URL or false if local only + * (has not been submitted yet). + * + * Note: you could just call getURL() and check if the result is null. + * + * @return true if the attachment is accessed via URL + */ + public boolean isURL() { + return (getURL() != null); + } + + /** + * Return the attachment data via an InputStream. The user should close + * the InputStream when done. + * + * @return The attachment data + * @throws LogIOException If unable to obtain the InputStream + */ + public InputStream getData() throws LogIOException { + InputStream is = null; + + Element dataElement = XMLUtil.getChildElementByName(attachmentElement, + "data"); + + if (dataElement == null) { + throw new LogRuntimeException("Unexpected XML DOM structure; " + + "Attachment data element missing."); + } + + if (dataElement.getAttribute("encoding").equals("url")) { + String urlStr = dataElement.getTextContent(); + + try { + java.net.URL url = new URL(urlStr); + is = url.openStream(); + } catch (MalformedURLException e) { + throw new LogIOException("Unable to open input stream.", e); + } catch(IOException e) { + throw new LogIOException("Unable to open input stream.", e); + } + } else { + String dataStr = dataElement.getTextContent(); + byte[] data = XMLUtil.decodeBase64(dataStr); + is = new ByteArrayInputStream(data); + } + + return is; + } +} diff --git a/src/org/jlab/elog/LogEntry.java b/src/org/jlab/elog/LogEntry.java index 11cda6a..a662db2 100644 --- a/src/org/jlab/elog/LogEntry.java +++ b/src/org/jlab/elog/LogEntry.java @@ -4,6 +4,7 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Properties; import javax.xml.xpath.XPathConstants; @@ -11,6 +12,7 @@ import javax.xml.xpath.XPathExpressionException; import org.jlab.elog.exception.AttachmentSizeException; import org.jlab.elog.exception.InvalidXMLException; +import org.jlab.elog.exception.LogException; import org.jlab.elog.exception.LogIOException; import org.jlab.elog.exception.LogRuntimeException; import org.jlab.elog.exception.MalformedXMLException; @@ -1062,4 +1064,78 @@ public void setBody(String content, Body.ContentType type) { public void setBody(Body body) throws LogRuntimeException { super.setBody(body); } + + public static void main(String[] args) throws Exception { + ArrayList possibleArgs = new ArrayList(); + java.lang.reflect.Method[] methods = LogEntry.class.getDeclaredMethods(); + for (java.lang.reflect.Method method : methods) { + if (method.getName().startsWith("set")) { + java.lang.reflect.Parameter[] methodParams = method.getParameters(); + if (methodParams != null && methodParams.length == 1 && methodParams[0].getType().equals(String.class)) { + char c[] = method.getName().substring(3).toCharArray(); + c[0] = Character.toLowerCase(c[0]); + String arg = new String(c); + if (!possibleArgs.contains(arg)) { + possibleArgs.add(arg); + } + } + } + } + Properties mainArgs = new Properties(); + if (args != null) { + for (String arg : args) { + if ("-help".equals(arg)){ + System.out.println(String.format("Usage: %s {args}", LogEntry.class.getName())); + System.out.println("Possible args:"); + System.out.println(" -help: Print this message"); + System.out.println("The rest of these args must be supplied in the form: -argName=argValue"); + for (String possibleArg : possibleArgs) { + System.out.println(String.format(" -%s (string): The log entry's %s", possibleArg, possibleArg)); + } + System.out.println(String.format(" -bodyType (string): The content type of the log entry's body. Can be one of the following: %s", Arrays.asList(Body.ContentType.values()))); + System.out.println(String.format(" -pemFilePath (string): The location of the client certificate/private key combination (PEM file) used for submitting the log entry to the server. If not supplied, the system will attempt to use this default: %s", getDefaultCertificatePath())); + System.out.println(String.format(" -allowQueue (boolean): If submission to the server fails, allow the log entry to be queued to: %s", getQueuePath())); + System.exit(0); + } + int firstEquals = arg.indexOf("="); + if (firstEquals > -1) { + String argName = arg.substring(0, firstEquals); + String argValue = arg.substring(firstEquals + 1); + if (argName != null && argValue != null) { + mainArgs.put(argName, argValue); + } + } + } + } + String title = mainArgs.getProperty("-title"); + String logbooks = mainArgs.getProperty("-logbooks"); + if (title == null || logbooks == null) { + throw new LogException("Title and logbooks are required"); + } + LogEntry entry = new LogEntry(title, logbooks.toUpperCase()); + String entryMakers = mainArgs.getProperty("-entryMakers"); + if (entryMakers != null) { + entry.addEntryMakers(entryMakers); + } + String body = mainArgs.getProperty("-body"); + if (body != null) { + String bodyType = mainArgs.getProperty("-bodyType"); + if (bodyType != null) { + entry.setBody(body, Body.ContentType.valueOf(bodyType)); + } + else { + entry.setBody(body); + } + } + String pemFilePath = mainArgs.getProperty("-pemFilePath"); + Boolean allowQueue = Boolean.parseBoolean(mainArgs.getProperty("-allowQueue")); + Long entryId = null; + if (pemFilePath != null) { + entryId = allowQueue ? entry.submit(pemFilePath) : entry.submitNow(pemFilePath); + } + else { + entryId = allowQueue ? entry.submit() : entry.submitNow(); + } + System.out.println(entryId); + } } diff --git a/src/org/jlab/elog/LogItem.java b/src/org/jlab/elog/LogItem.java index 746d552..e030526 100644 --- a/src/org/jlab/elog/LogItem.java +++ b/src/org/jlab/elog/LogItem.java @@ -15,6 +15,7 @@ import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -22,6 +23,7 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Properties; + import javax.net.ssl.HttpsURLConnection; import javax.xml.XMLConstants; import javax.xml.datatype.DatatypeConfigurationException; @@ -54,6 +56,7 @@ import org.w3c.dom.CDATASection; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; @@ -156,7 +159,7 @@ public LogItem(String rootTagName) throws LogRuntimeException { Element authorElement = doc.createElement("Author"); root.appendChild(authorElement); XMLUtil.appendElementWithText(doc, authorElement, "username", - System.getProperty("user.name")); + System.getProperty("user.name")); } /** @@ -734,6 +737,31 @@ public String getXML() throws LogRuntimeException { return xml; } + + /** + * Return the XML of the LogItem, substituting the CN of the certificate subject for the author. + * + * @return The XML + * @throws LogRuntimeException If unable to get the XML + */ + public String getXML(String pemFilePath) throws LogRuntimeException { + // try to set the author to the CN on the certificate + try { + X509Certificate cert = SecurityUtil.fetchCertificateFromPEM(IOUtil.fileToBytes(new File(pemFilePath))); + String commonName = SecurityUtil.getCommonNameFromCertificate(cert); + if (commonName != null) { + Node authorNode = (Node) authorTextExpression.evaluate(doc, XPathConstants.NODE); + if (authorNode != null) { + authorNode.setNodeValue(commonName); + } + } + } + catch (Exception ex) { + // do nothing + ex.printStackTrace(); + } + return getXML(); + } /** * Return the URL to the schema needed for validation of this log book item. @@ -767,7 +795,7 @@ void validate() throws SchemaUnavailableException, InvalidXMLException, } } - URL schemaURL = new URL(getSchemaURL()); + java.net.URL schemaURL = new URL(getSchemaURL()); SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI); schema = factory.newSchema(schemaURL); @@ -858,8 +886,8 @@ long parseServerResponse(InputStream is) throws LogIOException, long performHttpPutToServer(String pemFilePath) throws LogIOException, LogCertificateException, LogRuntimeException { long id; - - String xml = getXML(); + + String xml = getXML(pemFilePath); HttpsURLConnection con; @@ -867,7 +895,8 @@ long performHttpPutToServer(String pemFilePath) throws LogIOException, boolean ignoreServerCert = "true".equals(props.getProperty("IGNORE_SERVER_CERT_ERRORS")); try { - URL url = new URL(buildHttpPutUrl()); + String putUrl = buildHttpPutUrl(); + java.net.URL url = new URL(putUrl); con = (HttpsURLConnection) url.openConnection(); con.setSSLSocketFactory(SecurityUtil.getClientCertSocketFactoryPEM( pemFilePath, !ignoreServerCert)); @@ -990,7 +1019,7 @@ String buildHttpPutUrl() throws LogRuntimeException { * * @return The default client certificate path */ - String getDefaultCertificatePath() { + static String getDefaultCertificatePath() { return new File(System.getProperty("user.home"), PEM_FILE_NAME).getAbsolutePath(); } @@ -1027,8 +1056,13 @@ public long submit(String pemFilePath) throws InvalidXMLException, try { id = performHttpPutToServer(pemFilePath); - } catch (LogException e) { - submitException = e; + } catch (Exception e) { + if (e instanceof LogException) { + submitException = (LogException) e; + } + else { + submitException = new LogException(e.getMessage(), e); + } queue(); } @@ -1115,7 +1149,7 @@ String generateXMLFilename() { * * @return The queue path */ - String getQueuePath() { + static String getQueuePath() { Properties props = Library.getConfiguration(); String queuePath = props.getProperty("QUEUE_PATH"); @@ -1174,7 +1208,7 @@ void queue(String filepath) throws InvalidXMLException, LogIOException { OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8")) { writer.write(xml); } catch (IOException e) { - throw new LogIOException("Unable to write XML file.", e); + throw new LogIOException("Unable to write XML file to queue.", e); } } } diff --git a/src/org/jlab/elog/util/SecurityUtil.java b/src/org/jlab/elog/util/SecurityUtil.java index 5e4b71d..12aed53 100644 --- a/src/org/jlab/elog/util/SecurityUtil.java +++ b/src/org/jlab/elog/util/SecurityUtil.java @@ -20,6 +20,10 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.logging.Level; import java.util.logging.Logger; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; @@ -29,7 +33,11 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; import javax.xml.bind.DatatypeConverter; +import javax.xml.xpath.XPathConstants; + +import org.w3c.dom.NodeList; /** * Security Utilities. @@ -143,13 +151,8 @@ public static SSLSocketFactory getClientCertSocketFactoryPEM(String pemPath, SSLContext context = SSLContext.getInstance("TLS"); byte[] certAndKey = IOUtil.fileToBytes(new File(pemPath)); - byte[] certBytes = parseDERFromPEM(certAndKey, - "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"); - byte[] keyBytes = parseDERFromPEM(certAndKey, - "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"); - - X509Certificate cert = generateX509CertificateFromDER(certBytes); - RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes); + X509Certificate cert = fetchCertificateFromPEM(certAndKey); + RSAPrivateKey key = fetchPrivateKeyFromPEM(certAndKey); KeyStore keystore = KeyStore.getInstance("JKS"); keystore.load(null); @@ -273,6 +276,39 @@ public static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, return DatatypeConverter.parseBase64Binary(tokens[0]); } + /** + * Get an X509Certificate from PEM bytes + * + * @param pem + * @return The X509Certificate + * @throws CertificateException + */ + public static X509Certificate fetchCertificateFromPEM(byte[] pem) throws CertificateException { + String data = new String(pem); + String[] tokens = data.split("-----BEGIN CERTIFICATE-----"); + tokens = tokens[1].split( "-----END CERTIFICATE-----"); + byte[] certBytes = DatatypeConverter.parseBase64Binary(tokens[0]); + X509Certificate cert = generateX509CertificateFromDER(certBytes); + return cert; + } + + /** + * Get an RSAPublicKey from PEM bytes + * + * @param pem + * @return The RSAPrivateKey + * @throws InvalidKeySpecException + * @throws NoSuchAlgorithmException + */ + public static RSAPrivateKey fetchPrivateKeyFromPEM(byte[] pem) throws InvalidKeySpecException, NoSuchAlgorithmException { + String data = new String(pem); + String[] tokens = data.split("-----BEGIN PRIVATE KEY-----"); + tokens = tokens[1].split("-----END PRIVATE KEY-----"); + byte[] keyBytes = DatatypeConverter.parseBase64Binary(tokens[0]); + RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes); + return key; + } + /** * Generates an RSAPrivateKey from an array of DER encoded bytes. * @@ -305,4 +341,23 @@ public static X509Certificate generateX509CertificateFromDER( return (X509Certificate) factory.generateCertificate( new ByteArrayInputStream(certBytes)); } + + /** + * Get the CN from the subject DN on an X509Certificate + * + * @param cert + * @return The CN string + * @throws InvalidNameException + */ + public static String getCommonNameFromCertificate(X509Certificate cert) throws InvalidNameException { + String commonName = null; + LdapName ln = new LdapName(cert.getSubjectX500Principal().getName(X500Principal.RFC2253)); + for (Rdn rdn : ln.getRdns()) { + if (rdn.getType().equalsIgnoreCase("CN")) { + commonName = String.valueOf(rdn.getValue()); + break; + } + } + return commonName; + } } diff --git a/test/org/jlab/elog/LogEntryTest.java b/test/org/jlab/elog/LogEntryTest.java index 1dde386..038b21d 100644 --- a/test/org/jlab/elog/LogEntryTest.java +++ b/test/org/jlab/elog/LogEntryTest.java @@ -5,12 +5,16 @@ import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.security.cert.CertificateException; import java.text.SimpleDateFormat; import java.util.GregorianCalendar; import java.util.Properties; + +import javax.naming.InvalidNameException; import javax.net.ssl.HttpsURLConnection; import org.jlab.elog.exception.AttachmentSizeException; import org.jlab.elog.exception.LogException; @@ -59,6 +63,10 @@ public void setUp() throws LogException { config.setProperty("LOG_ENTRY_SCHEMA_URL", "https://" + logbookHostname + "/schema/Logentry.xsd"); config.setProperty("COMMENT_SCHEMA_URL", "https://" + logbookHostname + "/schema/Comment.xsd"); config.setProperty("IGNORE_SERVER_CERT_ERRORS", "true"); + config.setProperty("DEFAULT_UNIX_QUEUE_PATH", System.getProperty("java.io.tmpdir")); + config.setProperty("DEFAULT_WINDOWS_QUEUE_PATH", System.getProperty("java.io.tmpdir")); + + config.list(System.out); } @After @@ -260,6 +268,16 @@ public void testGetXML() throws LogException { assertEquals(expected, actual); } + @Test + public void testGetAuthorFromXMLWithCertificate() throws LogException, CertificateException, IOException, InvalidNameException { + File pemFile = new File(System.getProperty("user.home"), ".elogcert"); + String xml = entry.getXML(pemFile.getAbsolutePath()); + //System.out.println(xml); + String expected = SecurityUtil.getCommonNameFromCertificate(SecurityUtil.fetchCertificateFromPEM(IOUtil.fileToBytes(pemFile))); + String actual = xml.split("")[1].split("")[0]; + assertEquals(expected, actual); + } + @Test public void testValidate() throws LogException { entry.validate(); @@ -452,7 +470,10 @@ public void testProblemReport() throws Exception { ProblemReport report = new ProblemReport(ProblemReportType.OPS, true, 62, 9, 16413); entry.setProblemReport(report); /*System.out.println(entry.getXML());*/ - entry.submit(); + long entryId = entry.submit(); + if (entryId == 0) { + throw new Exception("It was queued!", entry.whyQueued()); + } } /*@Test*/