Skip to content

Instantly share code, notes, and snippets.

@bmc
Last active April 13, 2016 21:21
Show Gist options
  • Select an option

  • Save bmc/6b338ae9122c1534f92a1c0a8e547e5c to your computer and use it in GitHub Desktop.

Select an option

Save bmc/6b338ae9122c1534f92a1c0a8e547e5c to your computer and use it in GitHub Desktop.

Revisions

  1. bmc revised this gist Apr 10, 2016. 1 changed file with 16 additions and 6 deletions.
    22 changes: 16 additions & 6 deletions BrainDeadHTTP.scala
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    package grizzled.http.braindead
    package grizzled.testutil

    import java.io.{PrintWriter, OutputStreamWriter}
    import java.net.InetAddress
    @@ -8,6 +8,7 @@ import java.util.Date
    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.io.Source
    import scala.util.Try

    /** Brain dead HTTP server, solely for testing. I could use an external
    * package, but doing it this way reduces external dependencies, since I'm
    @@ -77,6 +78,13 @@ object BrainDeadHTTP {
    */
    class Server(bindPort: Int, handlers: Seq[Handler]) {

    /** Create a server with only a single handler.
    *
    * @param bindPort the port on which to listen
    * @param handler the handler
    */
    def this(bindPort: Int, handler: Handler) = this(bindPort, Vector(handler))

    import java.net._

    private val loopback = InetAddress.getLoopbackAddress
    @@ -89,9 +97,7 @@ object BrainDeadHTTP {
    override def run(): Unit = {
    try {
    while (! socket.isClosed) {
    val connection = socket.accept()
    println("--- accepted connection")
    handle(connection)
    acceptAndHandle(socket)
    }
    }
    catch {
    @@ -108,8 +114,13 @@ object BrainDeadHTTP {
    socket.close()
    }

    private def acceptAndHandle(socket: ServerSocket): Unit = {
    val connection = socket.accept()
    handle(connection).andThen { case t: Try[Unit] => connection.close }
    }

    private val GetRequest = """^GET\s+(\S+)\s+HTTP/1.\d\s*$""".r
    private def handle(connection: Socket): Unit = {
    private def handle(connection: Socket): Future[Unit] = {
    Future {
    val out = new PrintWriter(
    new OutputStreamWriter(connection.getOutputStream)
    @@ -160,7 +171,6 @@ object BrainDeadHTTP {

    result.content.foreach(w.print)
    w.flush()
    w.close()
    }
    }
    }
  2. bmc revised this gist Apr 10, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions BrainDeadHTTP.scala
    Original file line number Diff line number Diff line change
    @@ -55,9 +55,9 @@ object BrainDeadHTTP {
    /** HTTP result, from a handler
    *
    * @param code the code
    * @param content the text content
    * @param content the text content, if any
    */
    case class Response(code: ResultCode.Value, content: Option[String])
    case class Response(code: ResultCode.Value, content: Option[String] = None)

    /** Minimalist. Only the ones we're using.
    */
  3. bmc revised this gist Apr 10, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion BrainDeadHTTP.scala
    Original file line number Diff line number Diff line change
    @@ -42,7 +42,7 @@ object BrainDeadHTTP {
    /** Defines a handler for a request.
    *
    * @param path the path, minus any leading "/"
    * @param handle the handler. Returns a Result object.
    * @param handle the handler. Takes a Request and returns a Response.
    */
    case class Handler(path: String, handle: Request => Response)

  4. bmc revised this gist Apr 10, 2016. 1 changed file with 17 additions and 1 deletion.
    18 changes: 17 additions & 1 deletion BrainDeadHTTP.scala
    Original file line number Diff line number Diff line change
    @@ -21,7 +21,23 @@ import scala.io.Source
    * - EXTREMELY minimalist.
    */
    object BrainDeadHTTP {
    import scala.concurrent.ExecutionContext.Implicits.global

    /** Given an instantiated, but not running, Brain Dead HTTP server, execute
    * the specified code (which presumably queries the server), and shut the
    * server down.
    *
    * @param server The not-running server.
    * @param code The code to run before shutting the server down.
    */
    def withHTTPServer(server: Server)(code: => Unit): Unit = {
    try {
    server.start()
    code
    }
    finally {
    server.stop()
    }
    }

    /** Defines a handler for a request.
    *
  5. bmc created this gist Apr 10, 2016.
    150 changes: 150 additions & 0 deletions BrainDeadHTTP.scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,150 @@
    package grizzled.http.braindead

    import java.io.{PrintWriter, OutputStreamWriter}
    import java.net.InetAddress
    import java.text.SimpleDateFormat
    import java.util.Date

    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.io.Source

    /** Brain dead HTTP server, solely for testing. I could use an external
    * package, but doing it this way reduces external dependencies, since I'm
    * only using a lightweight HTTP server in ScalaTest tests.
    *
    * Restrictions:
    * - Only handles text/plain, because that's all the tests need.
    * - Doesn't scale (and doesn't really need to).
    * - Only sends 200 (OK) and 404 (Not Found).
    * - Doesn't bother with client-friendly headers like "Last-Modified".
    * - EXTREMELY minimalist.
    */
    object BrainDeadHTTP {
    import scala.concurrent.ExecutionContext.Implicits.global

    /** Defines a handler for a request.
    *
    * @param path the path, minus any leading "/"
    * @param handle the handler. Returns a Result object.
    */
    case class Handler(path: String, handle: Request => Response)

    /** The incoming request. Minimalist.
    *
    * @param ipAddress the client's IP address
    */
    case class Request(ipAddress: InetAddress)

    /** HTTP result, from a handler
    *
    * @param code the code
    * @param content the text content
    */
    case class Response(code: ResultCode.Value, content: Option[String])

    /** Minimalist. Only the ones we're using.
    */
    object ResultCode extends Enumeration {
    type ResultCode = Value

    val OK = Value("200 OK")
    val NotFound = Value("404 Not Found")
    }

    import ResultCode._

    /** The actual server, which operates purely at the socket level.
    *
    * @param bindPort Bind to the specified TCP port
    * @param handlers Sequence of handlers to process requests
    */
    class Server(bindPort: Int, handlers: Seq[Handler]) {

    import java.net._

    private val loopback = InetAddress.getLoopbackAddress
    private val socket = new ServerSocket(bindPort, 5, loopback)

    /** Start the server.
    */
    def start(): Unit = {
    new Thread() {
    override def run(): Unit = {
    try {
    while (! socket.isClosed) {
    val connection = socket.accept()
    println("--- accepted connection")
    handle(connection)
    }
    }
    catch {
    case _: SocketException =>
    }
    }
    }
    .start()
    }

    /** Stop the server.
    */
    def stop(): Unit = {
    socket.close()
    }

    private val GetRequest = """^GET\s+(\S+)\s+HTTP/1.\d\s*$""".r
    private def handle(connection: Socket): Unit = {
    Future {
    val out = new PrintWriter(
    new OutputStreamWriter(connection.getOutputStream)
    )

    Source.fromInputStream(connection.getInputStream)
    .getLines
    .take(1)
    .toList match {
    case GetRequest(path) :: Nil =>
    processGet(path, connection.getInetAddress, out)

    case _ => issue404(out)
    }
    }
    }

    private def issue404(w: PrintWriter): Unit = {
    sendResponse(w, new Response(NotFound, None))
    }

    private def processGet(path: String,
    clientIP: InetAddress,
    w: PrintWriter): Unit = {
    val handler = handlers.find { h => s"/${h.path}" == path }

    if (handler.isDefined) {
    sendResponse(w, handler.get.handle(Request(clientIP)))
    }
    else {
    issue404(w)
    }
    }

    private val df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss zzz")
    df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))

    private def sendResponse(w: PrintWriter, result: Response): Unit = {
    val contentLength = result.content.map(_.length).getOrElse(0)
    w.println(
    s"""|HTTP/1.1 ${result.code}
    |Server: BrainDeadHTTP/0.0.1
    |Date: ${df.format(new Date)}
    |Content-Type: text/plain
    |Content-Length: $contentLength
    |""".stripMargin
    )

    result.content.foreach(w.print)
    w.flush()
    w.close()
    }
    }
    }