Thursday, January 7, 2016

HTTPS Connections with Groovy using Apache HTTPClient

Sample definition of Keystores, their usage and setting up of an HTTPS client connection.

Implemented as a Groovy script, meaning there is no top-level class per se. Could probably be more concise.

//imports removed for brevity


   // Housekeeping - see what the User wants
   //   Groovy note: not declaring a var puts it into "the binding", which makes it globally accessible
   //      * this is only valid for Groovy scripts
   //      * declaring a type (or def) of a var automatically scopes it locally
   config = new Config()
   parseInput(args)

   baseUrl = "https://" + config.host + ":" + config.port + "/myapp/"

   debug "\tBase URL: " + baseUrl

   println "Loading keys..."

   // Create client keystore - this needs to contain:
   //   Client's certificate (i.e., Subject CN == clientname)
   //   Client's private key
   // Needs to be a JKS keystore
   clientKeys  = createKeystore(config.clientKeyFile, config.clientKeyPass)
                                                                                          
   // Truststore for server verification - this needs to contain:
   //   Server certificate: Subject CN == hostname
   //   Server private key
   // Needs to be a JKS keystore
   serverTrust  = createKeystore(config.serverKeyFile, config.serverKeyPass)

   debug "\tclient key: " + clientKeys
   debug "\tserver key: " + serverTrust

   try {
      // ** Create Connection **
      socketFactory = new SSLSocketFactory(
                   SSLSocketFactory.TLS,
                   clientKeys,
                   config.clientKeyPass,
                   serverTrust,
                   null,
                   new TrustSelfSignedStrategy(),                 // not a good choice for production
                   SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); // not a good choice for production

      // Connect socket to enable client PKI authentication
      HttpParams params = new BasicHttpParams();
      params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000);

      SSLSocket socket = (SSLSocket) socketFactory.createSocket(params);
      socket.setEnabledCipherSuites("SSL_RSA_WITH_RC4_128_MD5");

      InetSocketAddress address = new InetSocketAddress(config.host, Integer.parseInt(config.port));
      socketFactory.connectSocket(socket, address, null, params);

      sch = new Scheme("https", new Integer(config.port), socketFactory);

      httpclient = new DefaultHttpClient();
      httpclient.getConnectionManager().getSchemeRegistry().register(sch);

      // Create a local instance of cookie store
      cookieStore = new BasicCookieStore();

      // Create local HTTP context
      localContext = new BasicHttpContext();
      // Bind custom cookie store to the local context
      localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);


      // ****** Example usage *******

      // This is the map-based impl - produces same output as the object-based
      // .... put your own data in here
      def data = [
         "_method" : "POST" ,
         "displayName" : row[DISPLAY_NAME],
         "imageUrlSmall" : row[IMAGE_URL_SMALL] ,
         "imageUrlLarge" : row[IMAGE_URL_LARGE],
         "width" : row[WIDTH],
         "height" : row[HEIGHT]
      ]

      String dataStr = JSONSerializer.toJSON(data)
      try {
         int retries = 5
         while (!created && retries-- > 0) {
         rsp = post (baseUrl + "widget", ["data":dataStr])

         matcher = rsp =~ /\"success\":([\w]+)/

         if (matcher[0]) {
            val =  matcher[0][1]
            debug "\tCREATE RESPONSE: ${val}"

            created = Boolean.parseBoolean(val)
         }

         // This is just for logging
         if (!created) {
            println "ERROR: Creation failed with response: ${rsp}"

            // Detect message failure and retry
            //   Detect failure by GUID invalid format
            matcher = rsp =~ /Property [name].*does not match the required pattern/
            if (matcher[0]) {
               debug "\tERROR RECEIVED:: name is not correct format: ${guid}"
            }
         }
         } // end while
      }
      catch (Exception e) {
          debug "Error creating listing: " + e
          e.printStackTrace()
          guid = null
      }
}

  /**
  *  Return a Keystore from a given filename and password
  */
  def KeyStore createKeystore (filename, password) {
      debug "Default KeyAlgo: " + KeyManagerFactory.getDefaultAlgorithm()
      debug "Default KeyStore: " + KeyStore.getDefaultType()  // jks

      // Pull off file extension
      keyType = filename[-3..-1]
      debug "Requested KeyType: " + keyType

      // Client keystore, for client authentication
      KeyStore keys
      switch (keyType) {
         case "p12":
            keys  = KeyStore.getInstance("pkcs12");
            break
         default:
            keys  = KeyStore.getInstance(KeyStore.getDefaultType());
      }

      FileInputStream instream = new FileInputStream(new File(filename));
      try {
         keys.load(instream, password.toCharArray());
      } finally {
         try { instream.close(); } catch (Exception ignore) {}
      }
      return keys
   }

  def post(url, paramMap) {
      HttpPost httpPost = new HttpPost(url)
      String result

      // Add specific params
      List  nvps = new ArrayList ();
      // These params always present
      nvps.add(new BasicNameValuePair("sample1", "3.6.0-GA"))
      nvps.add(new BasicNameValuePair("dojo.preventCache", "1302893700594"))

      paramMap.each() { key, value ->
         debug "${key} ==> ${value}"
         // Expect the caller to JSON-ize each param, as required
         if (value instanceof List) {
             // Add value multiple times with same key
            value.each {
               nvps.add(new BasicNameValuePair(key, it.toString()))
            }
         }
         else {
            nvps.add(new BasicNameValuePair(key, value))
         }
      }

      httpPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8))

      // Debug
      debug(dump(httpPost))

      debug "Posting request: " + httpPost
      HttpResponse response = httpclient.execute(httpPost, localContext);
      //String response = httpclient.execute(httpPost, new BasicResponseHandler());

      debug "#### RESPONSE TYPE: " + response.class.name

      HttpEntity entity = response.getEntity();

      debug "#### RESPONSE BODY RECEIVED: " + entity.toString()

      // ** Dump results **
      debug "\t" + response.getStatusLine()
      StringBuilder sb = new StringBuilder(255)
      if (entity != null) {

         InputStream ris = entity.getContent()
         OutputStream os = new ByteArrayOutputStream(BUFFER_SIZE)
         byte[] buffer = new byte[BUFFER_SIZE];
         try {
            while ((l = ris.read(buffer)) != -1) {
                os.write(buffer, 0, l)
                sb.append(os.toString())
                os.reset()
            }
         } finally {
             ris.close()
         }

         debug "\tEntity contentLength: " + sb.length()

         result = sb?.toString()

         debug "\t-------------------------------------"
         List cookies = cookieStore.getCookies();
         for (int i = 0; i < cookies.size(); i++) {
            println("\tCookie: " + cookies.get(i));
         }                                                                                    
         debug "\t-------------------------------------"

      }
      else {
          println "Error connecting...check connection details"
          //!! EXIT HERE
          return
      }

      debug "-------------------------------------"
      if (result && result.size() > 256) {
         debug "RESPONSE: " + result.substring(0, 256)
      }
      else {
         debug "RESPONSE: " + result
      }
      debug "-------------------------------------"

      return result
  }

   def debug(stmt) {
       if (config.verbose) {
           println stmt
       }
   }

   def dump(HttpPost post) {
       StringBuilder sb = new StringBuilder(128)

       sb.append("\n\t------------------------------")
       sb.append("\n\tRequestURL: ").append(post.getURI())
       sb.append("\n\tType: ").append(post.getMethod())
       sb.append("\n\tParams: ").append(post.getParams())
       org.apache.http.params.BasicHttpParams
       sb.append("\n\t------------------------------")

       return sb.toString()
   }
         
  class Config {
      String host = "localhost"
      String port = "8443"
      boolean verbose = false
      String inputFile
      String clientKeyFile
      String clientKeyPass
      String serverKeyFile
      String serverKeyPass
  }

   def parseInput(args) {
       if (!args || args.length < 1) {
           usage()
       }
       for(int i=0; i < args.length; i++) {
           it = args[i]
           println "Processing arg: " + it
           switch (it) {
           case "-v":
               debug "Verbose output enabled "
               config.verbose = true
               break
           case "-h":
               it = args[++i]
               debug "Setting Host/Port from: " + it
               // Parse out host and port
               def serverAddr = it.split(":")
               try {
                  if (serverAddr.length > 0 && serverAddr[0] != null) {
                     config.host = serverAddr[0]
                  }
                  if (serverAddr.length > 1 && serverAddr[1] != null) {
                     config.port = serverAddr[1]
                  }
               } catch(Exception e) {
                   usage()
               }
               break
           case "-clientKeys":
               it = args[++i]
               debug "Setting client keyfile to: " + it
               config.clientKeyFile = it
               break
           case "-clientKeyPass":
               it = args[++i]
               debug "Setting client keyfile pwd to: " + it
               config.clientKeyPass = it
               break
           case "-serverKeys":
               it = args[++i]
               debug "Setting server keyfile to: " + it
               config.serverKeyFile = it
               break
           case "-serverKeyPass":
               it = args[++i]
               debug "Setting server keyfile pwd to: " + it
               config.serverKeyPass = it
               break
           default:
               debug "Setting inputFile to: " + it
               config.inputFile = it;
           }
       }
   }