Thursday, May 14, 2015

Today I learned... basic authenticated HTTP connections in Scala

I needed to connect to Jenkins to install (on a remote server over SSH using JSch) the latest stable trunk build of a particular project if it was newer than the version that was already installed.

I spent a lot of time fighting with various Java/Scala APIs to do HTTP connections with authentication, and many of them didn't work as I needed and/or were too complicated to use.

I also spent a lot of time trying out existing Java APIs and StackOverflow posts for how to interact with Jenkins. I discovered through trial and error that the RisingOak Jenkins API simply does not work to get a nested job.

I finally got something to work. I'm using it in this manner:


  • Read contents of [jenkinsServerUrl]/job/JobName[/job/SubJobName]*/lastStableBuild/buildNumber to determine the last stable build number
  • Compare to build number of known installation. If greater, ensure it's a stable trunk build (if it's the same as lastStableBuild then skip checking if stable).
    • To check if a trunk build, parsing build parameter "branch" from the page [buildNumber]/injectedEnvVars under my Jenkins job sub URL
    • To check if a stable build, parse the HTML of the [buildNumber] page under the Jenkins job sub URL, and check the status icon (this is brittle, but was the only/easiest way I could find).
  • If not stable trunk build, decrement number until it's equal to the installed build, then stop.
  • If later build found, download the archive, SFTP it up, and install it using SSH commands.


Here's the helper object that I use to make the authenticated HTTP requests:

package Helpers

import java.io.InputStream
import java.net.URL
import Helpers.InputStreamHelper.InputStreamExtensions
import org.apache.commons.codec.binary.Base64
import scala.io.Source

/** * HTTP Helper methods */object HttpHelper {
  def download(url: String, savePath: String, user: String = null, passwordOrToken: String = null, requestProperties: Map[String, String]= Map("User-Agent"->"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)")) : Unit = {
    val inputStream = getInputStreamFromUrl(url, user, passwordOrToken, requestProperties)
    try {
      inputStream.downloadToFile(savePath)
    } finally {
      inputStream.close()
    }
  }

  def getPageContentFromUrl(url: String, user: String = null, passwordOrToken: String = null, requestProperties: Map[String, String]= Map("User-Agent"->"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)")) : String = {
    val inputStream = getInputStreamFromUrl(url, user, passwordOrToken, requestProperties)
    try {
      Source.fromInputStream(inputStream).getLines().mkString("\n")
    } finally {
      inputStream.close()
    }
  }

  def getInputStreamFromUrl(url: String, user: String = null, passwordOrToken: String = null, requestProperties: Map[String, String]= Map("User-Agent"->"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)")) : InputStream = {
    val connection = new URL(url).openConnection
    requestProperties.foreach({
      case (name, value) => connection.setRequestProperty(name, value)
    })

    if (user != null && passwordOrToken != null) {
      connection.setRequestProperty("Authorization", getHeader(user, passwordOrToken))
    }

    connection.getInputStream
  }

  def encodeCredentials(username: String, password: String): String = {
    new String(Base64.encodeBase64String((username + ":" + password).getBytes))
  }

  def getHeader(username: String, password: String): String = {
    "Basic " + encodeCredentials(username, password)
  }
}


package Helpers

import java.io.{FileOutputStream, InputStream}

/** * Created by Samer Adra on 5/13/2015. */object InputStreamHelper {
  implicit class InputStreamExtensions(val inputStream: InputStream) {
    def downloadToFile(path: String): Unit = {
      val buffer = new Array[Byte](8 * 1024)

      val outStream = new FileOutputStream(path)
      try {
        var bytesRead = 0        while ({bytesRead = inputStream.read(buffer); bytesRead != -1}) {
          outStream.write(buffer, 0, bytesRead)
        }
      } finally {
        outStream.close()
      }
    }
  }
}

No comments:

Post a Comment