1 /*
   2  * Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.security.timestamp;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.DataOutputStream;
  30 import java.io.EOFException;
  31 import java.io.IOException;
  32 import java.net.URI;
  33 import java.net.URL;
  34 import java.net.HttpURLConnection;
  35 import java.util.*;
  36 
  37 import sun.security.util.Debug;
  38 
  39 /**
  40  * A timestamper that communicates with a Timestamping Authority (TSA)
  41  * over HTTP.
  42  * It supports the Time-Stamp Protocol defined in:
  43  * <a href="http://www.ietf.org/rfc/rfc3161.txt">RFC 3161</a>.
  44  *
  45  * @since 1.5
  46  * @author Vincent Ryan
  47  */
  48 
  49 public class HttpTimestamper implements Timestamper {
  50 
  51     private static final int CONNECT_TIMEOUT = 15000; // 15 seconds
  52 
  53     // The MIME type for a timestamp query
  54     private static final String TS_QUERY_MIME_TYPE =
  55         "application/timestamp-query";
  56 
  57     // The MIME type for a timestamp reply
  58     private static final String TS_REPLY_MIME_TYPE =
  59         "application/timestamp-reply";
  60 
  61     private static final Debug debug = Debug.getInstance("ts");
  62 
  63     /*
  64      * HTTP URI identifying the location of the TSA
  65      */
  66     private URI tsaURI = null;
  67 
  68     /**
  69      * Creates a timestamper that connects to the specified TSA.
  70      *
  71      * @param tsaURI The location of the TSA. It must be an HTTP or HTTPS URI.
  72      * @throws IllegalArgumentException if tsaURI is not an HTTP or HTTPS URI
  73      */
  74     public HttpTimestamper(URI tsaURI) {
  75         if (!tsaURI.getScheme().equalsIgnoreCase("http") &&
  76                 !tsaURI.getScheme().equalsIgnoreCase("https")) {
  77             throw new IllegalArgumentException(
  78                     "TSA must be an HTTP or HTTPS URI");
  79         }
  80         this.tsaURI = tsaURI;
  81     }
  82 
  83     /**
  84      * Connects to the TSA and requests a timestamp.
  85      *
  86      * @param tsQuery The timestamp query.
  87      * @return The result of the timestamp query.
  88      * @throws IOException The exception is thrown if a problem occurs while
  89      *         communicating with the TSA.
  90      */
  91     public TSResponse generateTimestamp(TSRequest tsQuery) throws IOException {
  92 
  93         HttpURLConnection connection =
  94             (HttpURLConnection) tsaURI.toURL().openConnection();
  95         connection.setDoOutput(true);
  96         connection.setUseCaches(false); // ignore cache
  97         connection.setRequestProperty("Content-Type", TS_QUERY_MIME_TYPE);
  98         connection.setRequestMethod("POST");
  99         // Avoids the "hang" when a proxy is required but none has been set.
 100         connection.setConnectTimeout(CONNECT_TIMEOUT);
 101 
 102         if (debug != null) {
 103             Set<Map.Entry<String, List<String>>> headers =
 104                 connection.getRequestProperties().entrySet();
 105             debug.println(connection.getRequestMethod() + " " + tsaURI +
 106                 " HTTP/1.1");
 107             for (Map.Entry<String, List<String>> e : headers) {
 108                 debug.println("  " + e);
 109             }
 110             debug.println();
 111         }
 112         connection.connect(); // No HTTP authentication is performed
 113 
 114         // Send the request
 115         DataOutputStream output = null;
 116         try {
 117             output = new DataOutputStream(connection.getOutputStream());
 118             byte[] request = tsQuery.encode();
 119             output.write(request, 0, request.length);
 120             output.flush();
 121             if (debug != null) {
 122                 debug.println("sent timestamp query (length=" +
 123                         request.length + ")");
 124             }
 125         } finally {
 126             if (output != null) {
 127                 output.close();
 128             }
 129         }
 130 
 131         // Receive the reply
 132         BufferedInputStream input = null;
 133         byte[] replyBuffer = null;
 134         try {
 135             input = new BufferedInputStream(connection.getInputStream());
 136             if (debug != null) {
 137                 String header = connection.getHeaderField(0);
 138                 debug.println(header);
 139                 int i = 1;
 140                 while ((header = connection.getHeaderField(i)) != null) {
 141                     String key = connection.getHeaderFieldKey(i);
 142                     debug.println("  " + ((key==null) ? "" : key + ": ") +
 143                         header);
 144                     i++;
 145                 }
 146                 debug.println();
 147             }
 148             verifyMimeType(connection.getContentType());
 149 
 150             int clen = connection.getContentLength();
 151             replyBuffer = input.readAllBytes();
 152             if (clen != -1 && replyBuffer.length != clen)
 153                 throw new EOFException("Expected:" + clen +
 154                                        ", read:" + replyBuffer.length);
 155 
 156             if (debug != null) {
 157                 debug.println("received timestamp response (length=" +
 158                         replyBuffer.length + ")");
 159             }
 160         } finally {
 161             if (input != null) {
 162                 input.close();
 163             }
 164         }
 165         return new TSResponse(replyBuffer);
 166     }
 167 
 168     /*
 169      * Checks that the MIME content type is a timestamp reply.
 170      *
 171      * @param contentType The MIME content type to be checked.
 172      * @throws IOException The exception is thrown if a mismatch occurs.
 173      */
 174     private static void verifyMimeType(String contentType) throws IOException {
 175         if (! TS_REPLY_MIME_TYPE.equalsIgnoreCase(contentType)) {
 176             throw new IOException("MIME Content-Type is not " +
 177                 TS_REPLY_MIME_TYPE);
 178         }
 179     }
 180 }