View Javadoc

1   /*
2    * Copyright (C) 2007 ETH Zurich
3    *
4    * This file is part of Fosstrak (www.fosstrak.org).
5    *
6    * Fosstrak is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License version 2.1, as published by the Free Software Foundation.
9    *
10   * Fosstrak is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with Fosstrak; if not, write to the Free
17   * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18   * Boston, MA  02110-1301  USA
19   */
20  
21  package org.fosstrak.epcis.captureclient;
22  
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.StringWriter;
30  import java.net.Authenticator;
31  import java.net.HttpURLConnection;
32  import java.net.MalformedURLException;
33  import java.net.PasswordAuthentication;
34  import java.net.ProtocolException;
35  import java.net.URL;
36  import java.security.KeyStore;
37  import java.security.SecureRandom;
38  import java.security.cert.CertificateException;
39  import java.security.cert.X509Certificate;
40  import java.util.Properties;
41  
42  import javax.net.ssl.HostnameVerifier;
43  import javax.net.ssl.HttpsURLConnection;
44  import javax.net.ssl.KeyManagerFactory;
45  import javax.net.ssl.SSLContext;
46  import javax.net.ssl.SSLSession;
47  import javax.net.ssl.TrustManager;
48  import javax.net.ssl.X509TrustManager;
49  import javax.xml.bind.JAXBContext;
50  import javax.xml.bind.JAXBElement;
51  import javax.xml.bind.JAXBException;
52  import javax.xml.bind.Marshaller;
53  
54  import org.fosstrak.epcis.model.Document;
55  import org.fosstrak.epcis.model.EPCISDocumentType;
56  import org.fosstrak.epcis.model.EPCISMasterDataDocumentType;
57  import org.fosstrak.epcis.model.ObjectFactory;
58  import org.fosstrak.epcis.utils.AuthenticationType;
59  
60  /**
61   * This client provides access to an EPCIS Capture Interface. EPCIS events will
62   * be sent to the capture interface using HTTP POST requests. This client
63   * supports the following authentication options: HTTP BASIC AUTH and HTTPS with
64   * client certificate.
65   * 
66   * @author Marco Steybe
67   */
68  public class CaptureClient implements X509TrustManager, HostnameVerifier {
69  
70      private static final String PROPERTY_FILE = "/captureclient.properties";
71      private static final String PROPERTY_CAPTURE_URL = "default.url";
72      private static final String DEFAULT_CAPTURE_URL = "http://demo.fosstrak.org/epcis/capture";
73  
74      /**
75       * The URL String of the EPCIS Capture Interface.
76       */
77      private String captureUrl;
78  
79      private Object[] authOptions;
80  
81      /**
82       * Constructs a new CaptureClient using a default URL and no authentication.
83       */
84      public CaptureClient() {
85          this(null, null);
86      }
87  
88      /**
89       * Constructs a new CaptureClient using the given URL and no authentication.
90       * 
91       * @param url
92       *            The URL to the EPCIS Capture Interface.
93       */
94      public CaptureClient(String url) {
95          this(url, null);
96      }
97  
98      /**
99       * Constructs a new CaptureClient using the given URL and authentication
100      * options. The following authentication options are supported:
101      * <p>
102      * <table border="1">
103      * <tr>
104      * <td><b><code>authOptions[0]</code></b></td>
105      * <td><b><code>authOptions[1]</code></b></td>
106      * <td><b><code>authOptions[2]</code></b></td>
107      * </tr>
108      * <tr>
109      * <td><code>AuthenticationType.BASIC</code></td>
110      * <td>username</td>
111      * <td>password</td>
112      * </tr>
113      * <tr>
114      * <td><code>AuthenticationType.HTTPS_WITH_CLIENT_CERT</code></td>
115      * <td>keystore file</td>
116      * <td>password</td>
117      * </tr>
118      * </table>
119      * 
120      * @param url
121      *            The URL to the EPCIS Capture Interface.
122      * @param authOptions
123      *            The authentication options as described above.
124      */
125     public CaptureClient(final String url, Object[] authOptions) {
126         // set the URL
127         if (url != null) {
128             captureUrl = url;
129         } else {
130             Properties props = loadProperties();
131             if (props != null) {
132                 captureUrl = props.getProperty(PROPERTY_CAPTURE_URL);
133             }
134             if (captureUrl == null) {
135                 captureUrl = DEFAULT_CAPTURE_URL;
136             }
137         }
138         this.authOptions = authOptions;
139     }
140 
141     /**
142      * @return The capture client properties.
143      */
144     private Properties loadProperties() {
145         Properties props = new Properties();
146         InputStream is = getClass().getResourceAsStream(PROPERTY_FILE);
147         if (is != null) {
148             try {
149                 props.load(is);
150                 is.close();
151             } catch (IOException e) {
152                 System.out.println("Unable to load properties from " + PROPERTY_FILE + ". Using defaults.");
153             }
154         } else {
155             System.out.println("Unable to load properties from file " + PROPERTY_FILE + ". Using defaults.");
156         }
157         return props;
158     }
159 
160     /**
161      * Sends the XML available from the given InputStream to the EPCIS capture
162      * interface. Please see the <a
163      * href="http://www.fosstrak.org/epcis/docs/user-guide.html">Fosstrak
164      * User-Guide</a> for more information and code samples.
165      * 
166      * @param xmlStream
167      *            An input stream providing an EPCISDocument with a list of
168      *            events.
169      * @return The HTTP response code from the repository.
170      * @throws CaptureClientException
171      *             If an error sending the document occurred.
172      */
173     public int capture(final InputStream xmlStream) throws CaptureClientException {
174         try {
175             return doPost(xmlStream, "text/xml");
176         } catch (IOException e) {
177             throw new CaptureClientException("error communicating with EPCIS cpature interface: " + e.getMessage(), e);
178         }
179     }
180 
181     /**
182      * Sends the given XML String to the EPCIS capture interface. Please see the
183      * <a href="http://www.fosstrak.org/epcis/docs/user-guide.html">Fosstrak
184      * User-Guide</a> for more information and code samples.
185      * 
186      * @param eventXml
187      *            The XML String with the EPCISDocument and a list of events.
188      * @return The HTTP response code from the repository.
189      * @throws CaptureClientException
190      *             If an error sending the document occurred.
191      */
192     public int capture(final String eventXml) throws CaptureClientException {
193         try {
194             return doPost(eventXml, "text/xml");
195         } catch (IOException e) {
196             throw new CaptureClientException("error communicating with EPCIS cpature interface: " + e.getMessage(), e);
197         }
198     }
199 
200     /**
201      * Sends the given EPCIS Document to the EPCIS capture interface. Please see
202      * the <a href="http://www.fosstrak.org/epcis/docs/user-guide.html">Fosstrak
203      * User-Guide</a> for more information and code samples.
204      * 
205      * @param epcisDoc
206      *            The EPCIS Document with a list of events.
207      * @return The HTTP response code from the repository.
208      * @throws IOException
209      *             If an error sending the document occurred.
210      * @throws JAXBException
211      *             If an error serializing the given document into XML occurred.
212      */
213     public int capture(final Document epcisDoc) throws CaptureClientException {
214         StringWriter writer = new StringWriter();
215         ObjectFactory objectFactory = new ObjectFactory();
216         try {
217             JAXBContext context = JAXBContext.newInstance("org.fosstrak.epcis.model");
218             JAXBElement<? extends Document> item;
219             if (epcisDoc instanceof EPCISDocumentType) {
220                 item = objectFactory.createEPCISDocument((EPCISDocumentType) epcisDoc);
221             } else {
222                 item = objectFactory.createEPCISMasterDataDocument((EPCISMasterDataDocumentType) epcisDoc);
223             }
224             Marshaller marshaller = context.createMarshaller();
225             marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
226             marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
227             marshaller.marshal(item, writer);
228         } catch (JAXBException e) {
229             throw new CaptureClientException("error serializing EPCIS Document: " + e.getMessage(), e);
230         }
231         return capture(writer.toString());
232     }
233 
234     /**
235      * Invokes the non-standardized <code>dbReset</code> operation in the
236      * Fosstrak EPCIS capture interface. It deletes all event data in the EPCIS
237      * database. This operation is only allowed if the corresponding property is
238      * set in the repository's configuration.
239      * 
240      * @return The response from the capture module.
241      * @throws CaptureClientException
242      *             If a communication error occurred.
243      */
244     public int dbReset() throws CaptureClientException {
245         String formParam = "dbReset=true";
246         try {
247             return doPost(formParam, "application/x-www-form-urlencoded");
248         } catch (IOException e) {
249             throw new CaptureClientException("error communicating with EPCIS cpature interface: " + e.getMessage(), e);
250         }
251     }
252 
253     private boolean isEmpty(String s) {
254         return s == null || "".equals(s);
255     }
256 
257     /**
258      * Opens a connection to the EPCIS capture interface.
259      * 
260      * @param contentType
261      *            The HTTP content-type, e.g., <code>text/xml</code>
262      * @return The HTTP connection object.
263      */
264     private HttpURLConnection getConnection(final String contentType) throws CaptureClientException, IOException {
265         URL serviceUrl;
266         try {
267             serviceUrl = new URL(captureUrl);
268         } catch (MalformedURLException e) {
269             throw new CaptureClientException(captureUrl + " is not an URL", e);
270         }
271         HttpURLConnection connection;
272         SSLContext sslContext = null;
273 
274         if (authOptions != null) {
275 
276             if (AuthenticationType.BASIC.equals(authOptions[0])) {
277 
278                 // logger.debug("Authenticating via Basic as: " +
279                 // authenticationOptions[1]);
280 
281                 final String username = (String) authOptions[1];
282                 final String password = (String) authOptions[2];
283 
284                 if (isEmpty(username) || isEmpty(password)) {
285                     throw new CaptureClientException("Authentication method " + authOptions[0]
286                             + " requires a valid user name and password");
287                 }
288 
289                 Authenticator.setDefault(new Authenticator() {
290                     @Override
291                     protected PasswordAuthentication getPasswordAuthentication() {
292                         return new PasswordAuthentication(username, password.toCharArray());
293                     }
294                 });
295 
296             } else if (AuthenticationType.HTTPS_WITH_CLIENT_CERT.equals(authOptions[0])) {
297 
298                 // logger.debug("Authenticating with certificate in file: " +
299                 // authenticationOptions[1]);
300 
301                 if (!"HTTPS".equalsIgnoreCase(serviceUrl.getProtocol())) {
302                     throw new CaptureClientException("Authentication method " + authOptions[0]
303                             + " requires the use of HTTPS");
304                 }
305 
306                 String keyStoreFile = (String) authOptions[1];
307                 String password = (String) authOptions[2];
308 
309                 if (isEmpty(keyStoreFile) || isEmpty(password)) {
310                     throw new CaptureClientException("Authentication method " + authOptions[0]
311                             + " requires a valid keystore (PKCS12 or JKS) and password");
312                 }
313 
314                 try {
315                     KeyStore keyStore = KeyStore.getInstance(keyStoreFile.endsWith(".p12") ? "PKCS12" : "JKS");
316                     keyStore.load(new FileInputStream(new File(keyStoreFile)), password.toCharArray());
317 
318                     Authenticator.setDefault(null);
319                     sslContext = getSSLContext(keyStore, password.toCharArray());
320                 } catch (Throwable t) {
321                     throw new CaptureClientException("unable to load keystore or set up SSL context", t);
322                 }
323             } else {
324                 Authenticator.setDefault(null);
325             }
326         } else {
327             Authenticator.setDefault(null);
328         }
329 
330         connection = (HttpURLConnection) serviceUrl.openConnection();
331         if (sslContext != null && connection instanceof HttpsURLConnection) {
332             HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
333             httpsConnection.setHostnameVerifier(this);
334             httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
335         }
336         connection.setRequestProperty("content-type", contentType);
337         try {
338             connection.setRequestMethod("POST");
339         } catch (ProtocolException e) {
340             throw new CaptureClientException("unable to set HTTP request method POST", e);
341         }
342         connection.setDoInput(true);
343         connection.setDoOutput(true);
344         return connection;
345     }
346 
347     /**
348      * Send data to the repository's capture operation using HTTP POST. The data
349      * will be sent using the given content-type.
350      * 
351      * @param data
352      *            The data to send.
353      * @return The HTTP response message
354      * @throws IOException
355      *             If an error on the HTTP layer occurred.
356      */
357     private int doPost(final String data, final String contentType) throws CaptureClientException, IOException {
358         HttpURLConnection connection = getConnection(contentType);
359         // write the data
360         OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream());
361         wr.write(data);
362         wr.flush();
363         wr.close();
364 
365         return connection.getResponseCode();
366     }
367 
368     /**
369      * Send data to the repository's capture operation using HTTP POST. The data
370      * will be sent using the given content-type.
371      * 
372      * @param data
373      *            The data to send.
374      * @return The HTTP response message from the repository.
375      * @throws IOException
376      *             If an error on the HTTP layer occurred.
377      * @throws CaptureClientException
378      */
379     private int doPost(final InputStream data, final String contentType) throws IOException, CaptureClientException {
380         HttpURLConnection connection = getConnection(contentType);
381         // read from input and write to output
382         OutputStream os = connection.getOutputStream();
383         int b;
384         while ((b = data.read()) != -1) {
385             os.write(b);
386         }
387         os.flush();
388         os.close();
389 
390         return connection.getResponseCode();
391     }
392 
393     /**
394      * @return The URL String at which the Capture Operations Module listens.
395      */
396     public String getCaptureUrl() {
397         return captureUrl;
398     }
399 
400     public Object[] getAuthOptions() {
401         return authOptions;
402     }
403 
404     // X509TrustManager methods: Note that this client will trust any server
405     // you point it at. This is probably OK for the usage for which this program
406     // is intended, but is hardly a robust implementation.
407 
408     public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
409     }
410 
411     public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
412     }
413 
414     public X509Certificate[] getAcceptedIssuers() {
415         return null;
416     }
417 
418     // HostnameVerifier methods: Note that this client will believe the
419     // authenticity of any DNS name it is given. Again, probably OK for the
420     // nature of this client, but generally not a good idea.
421 
422     public boolean verify(String arg0, SSLSession arg1) {
423         return true;
424     }
425 
426     private SSLContext getSSLContext(KeyStore keyStore, char[] password) throws Exception {
427         KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
428         keyManagerFactory.init(keyStore, password);
429         SSLContext context = SSLContext.getInstance("TLS");
430         context.init(keyManagerFactory.getKeyManagers(), new TrustManager[] { this }, new SecureRandom());
431         return context;
432     }
433 }