Skip to content

Commit

Permalink
Added context route extractors
Browse files Browse the repository at this point in the history
  • Loading branch information
veebs committed Apr 22, 2012
1 parent 35c597c commit 669edd5
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 166 deletions.
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# SOCKO

A [Scala](http://www.scala-lang.org/) web server powered by
An embedded [Scala](http://www.scala-lang.org/) web server powered by
[Netty](http://netty.io/) networking and [Akka](http://akka.io/) processing.

## Background
We are currently working on a HTML5 style app called MashupBots.

We wanted an open source, lightweight and embeddable Scala web server that can serve static files and support a RESTful API to our business logic implemented in Akka.
We wanted an open source, lightweight and embeddable Scala web server that can serve static files and support RESTful APIs to our business logic implemented in Akka.

We do not need a web application framework: server side templating, caching, session management, etc.

Expand Down Expand Up @@ -35,20 +35,18 @@ Socko is:

* Embedded
* Socko runs within your Scala application.
* Routing DSL like [Unfilted](http://unfiltered.databinder.net/Unfiltered.html) and
[Play2 Mini](https://github.com/typesafehub/play2-mini). Route by HTTP method, host, path and querystring.
See [example](https://github.com/mashupbots/socko/blob/master/socko-examples/src/main/scala/org/mashupbots/socko/examples/routes/RouteApp.scala).
* Configurable from inside your code and/or via settings in Akka's configuration file.
* Plenty of [examples](https://github.com/mashupbots/socko/tree/master/socko-examples/src/main/scala/org/mashupbots/socko/examples)

* Supportive of HTTP and HTML5 Standards
* HTTP/S and WebSockets
* HTTP compression
* HTTP headers
* Decoding HTTP POST, file uploads and query strings

* Trying to be as Simple and as Easy as possible
* Routing DSL like [Unfilted HTTP ToolKit](http://unfiltered.databinder.net/Unfiltered.html) and
[Play Mini](https://github.com/typesafehub/play2-mini). Route by HTTP method, host, path and querystring.
See [example](https://github.com/mashupbots/socko/blob/master/socko-examples/src/main/scala/org/mashupbots/socko/examples/routes/RouteApp.scala).
* Configurable from inside your code and/or via settings in Akka's configuration file.
* Plenty of [examples](https://github.com/mashupbots/socko/tree/master/socko-examples/src/main/scala/org/mashupbots/socko/examples)


* Open Source
* [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ import org.jboss.netty.util.CharsetUtil
import org.mashupbots.socko.context.HttpRequestProcessingContext
import org.mashupbots.socko.processors.StaticFileProcessor
import org.mashupbots.socko.processors.StaticFileRequest
import org.mashupbots.socko.routes.GET
import org.mashupbots.socko.routes.Path
import org.mashupbots.socko.routes.Routes
import org.mashupbots.socko.routes._
import org.mashupbots.socko.utils.Logger
import org.mashupbots.socko.webserver.WebServer
import org.mashupbots.socko.webserver.WebServerConfig
Expand Down Expand Up @@ -82,27 +80,29 @@ object BenchmarkApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(Path("/test.html")) => {
val request = new StaticFileRequest(
ctx.asInstanceOf[HttpRequestProcessingContext],
contentDir,
new File(contentDir, "test.html"),
tempDir)
staticFileProcessorRouter ! request
}
case ctx @ GET(Path("/data.dat")) => {
val request = new StaticFileRequest(
ctx.asInstanceOf[HttpRequestProcessingContext],
contentDir,
new File(contentDir, "data.dat"),
tempDir)
staticFileProcessorRouter ! request
}
case ctx @ GET(Path("/dynamic")) => {
actorSystem.actorOf(Props[DynamicBenchmarkProcessor]) ! ctx
}
case ctx @ GET(Path("/favicon.ico")) => {
ctx.asInstanceOf[HttpRequestProcessingContext].writeErrorResponse(HttpResponseStatus.NOT_FOUND)
case HttpRequest(httpRequest) => httpRequest match {
case GET(Path("/test.html")) => {
val staticFileRequest = new StaticFileRequest(
httpRequest,
contentDir,
new File(contentDir, "test.html"),
tempDir)
staticFileProcessorRouter ! staticFileRequest
}
case GET(Path("/data.dat")) => {
val staticFileRequest = new StaticFileRequest(
httpRequest,
contentDir,
new File(contentDir, "data.dat"),
tempDir)
staticFileProcessorRouter ! staticFileRequest
}
case GET(Path("/dynamic")) => {
actorSystem.actorOf(Props[DynamicBenchmarkProcessor]) ! httpRequest
}
case GET(Path("/favicon.ico")) => {
httpRequest.writeErrorResponse(HttpResponseStatus.NOT_FOUND)
}
}
})

Expand Down Expand Up @@ -182,12 +182,12 @@ object BenchmarkApp extends Logger {
// data.dat - 1MB file
buf.setLength(0)
for (i <- 0 until (1024 * 1024)) {
buf.append('a')
buf.append('a')
}

val bigFile = new File(dir, "data.dat")
val out2 = new FileOutputStream(bigFile)
out2.write(buf.toString.getBytes(CharsetUtil.UTF_8))
out2.close()
out2.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ object AkkaConfigApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(_) => {
actorSystem.actorOf(Props[HelloProcessor]) ! ctx
case GET(context) => {
actorSystem.actorOf(Props[HelloProcessor]) ! context
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ object CodedConfigApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(_) => {
actorSystem.actorOf(Props[HelloProcessor]) ! ctx
case GET(context) => {
actorSystem.actorOf(Props[HelloProcessor]) ! context
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ import org.jboss.netty.util.CharsetUtil
import org.mashupbots.socko.context.HttpRequestProcessingContext
import org.mashupbots.socko.processors.StaticFileProcessor
import org.mashupbots.socko.processors.StaticFileRequest
import org.mashupbots.socko.routes.GET
import org.mashupbots.socko.routes.POST
import org.mashupbots.socko.routes.Path
import org.mashupbots.socko.routes.PathSegments
import org.mashupbots.socko.routes.Routes
import org.mashupbots.socko.routes._
import org.mashupbots.socko.utils.Logger
import org.mashupbots.socko.webserver.WebServer
import org.mashupbots.socko.webserver.WebServerConfig
Expand Down Expand Up @@ -90,26 +86,25 @@ object FileUploadApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(Path("/")) => {
// Redirect to index.html
// This is a quick non-blocking operation so executing it in the netty thread pool is OK.
val httpContext = ctx.asInstanceOf[HttpRequestProcessingContext]
val endPoint = httpContext.endPoint
httpContext.redirect("http://localhost:8888/index.html")
}
case ctx @ GET(Path(PathSegments(fileName :: Nil))) => {
// download requested file
val request = new StaticFileRequest(
ctx.asInstanceOf[HttpRequestProcessingContext],
contentDir,
new File(contentDir, fileName),
tempDir)
staticFileProcessorRouter ! request
}
case ctx @ POST(Path("/upload")) => {
// save file to the content directory so it can be downloaded
val request = FileUploadRequest(ctx.asInstanceOf[HttpRequestProcessingContext], contentDir)
fileUploadProcessorRouter ! request
case HttpRequest(httpRequest) => httpRequest match {
case GET(Path("/")) => {
// Redirect to index.html
// This is a quick non-blocking operation so executing it in the netty thread pool is OK.
httpRequest.redirect("http://localhost:8888/index.html")
}
case GET(Path(PathSegments(fileName :: Nil))) => {
// Download requested file
val staticFileRequest = new StaticFileRequest(
httpRequest,
contentDir,
new File(contentDir, fileName),
tempDir)
staticFileProcessorRouter ! staticFileRequest
}
case POST(Path("/upload")) => {
// Save file to the content directory so it can be downloaded
fileUploadProcessorRouter ! FileUploadRequest(httpRequest, contentDir)
}
}
})

Expand Down Expand Up @@ -202,11 +197,11 @@ object FileUploadApp extends Logger {
val out = new FileOutputStream(indexFile)
out.write(buf.toString.getBytes(CharsetUtil.UTF_8))
out.close()

buf.setLength(0)
buf.append("body { font-family: Arial,Helv,Courier,Serif}\n")
buf.append("div.field {margin-top:20px;}\n")

val cssFile = new File(dir, "mystyle.css")
val out2 = new FileOutputStream(cssFile)
out2.write(buf.toString.getBytes(CharsetUtil.UTF_8))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ object HelloApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(_) => {
actorSystem.actorOf(Props[HelloProcessor]) ! ctx
case GET(request) => {
actorSystem.actorOf(Props[HelloProcessor]) ! request
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,34 @@ object RouteApp extends Logger {
// `TimeProcessor` will `stop()` itself after processing each request.
//
val routes = Routes({

case HttpRequest(httpRequest) => httpRequest match {
// *** HOW TO EXTRACT QUERYSTRING VARIABLES AND USE CONCATENATION ***
// If the timezone is specified on the query string, (like "/time?tz=sydney"), pass the
// timezone to the TimeProcessor
case (GET(Path("/time")) & TimezoneQueryString(timezone)) => {
actorSystem.actorOf(Props[TimeProcessor]) ! TimeRequest(httpRequest, Some(timezone))
}

// *** HOW TO EXTRACT QUERYSTRING VARIABLES AND USE CONCATENATION ***
// If the timezone is specified on the query string, (like "/time?tz=sydney"), pass the
// timezone to the TimeProcessor
case ctx @ GET(Path("/time")) & TimezoneQueryString(timezone) => {
val request = TimeRequest(ctx.asInstanceOf[HttpRequestProcessingContext], Some(timezone))
actorSystem.actorOf(Props[TimeProcessor]) ! request
}

// *** HOW TO MATCH AND EXTRACT A PATH SEGMENT ***
// If the timezone is specified on the path (like "/time/sydney"), pass the
// timezone to the TimeProcessor
case ctx @ GET(Path(PathSegments("time" :: timezone :: Nil))) => {
val request = TimeRequest(ctx.asInstanceOf[HttpRequestProcessingContext], Some(timezone))
actorSystem.actorOf(Props[TimeProcessor]) ! request
}
// *** HOW TO MATCH AND EXTRACT A PATH SEGMENT ***
// If the timezone is specified on the path (like "/time/sydney"), pass the
// timezone to the TimeProcessor
case GET(Path(PathSegments("time" :: timezone :: Nil))) => {
actorSystem.actorOf(Props[TimeProcessor]) ! TimeRequest(httpRequest, Some(timezone))
}

// *** HOW TO MATCH AN EXACT PATH ***
// No timezone specified, make TimeProcessor return the time in the default timezone
case ctx @ GET(Path("/time")) => {
val request = TimeRequest(ctx.asInstanceOf[HttpRequestProcessingContext], None)
actorSystem.actorOf(Props[TimeProcessor]) ! request
}
// *** HOW TO MATCH AN EXACT PATH ***
// No timezone specified, make TimeProcessor return the time in the default timezone
case GET(Path("/time")) => {
actorSystem.actorOf(Props[TimeProcessor]) ! TimeRequest(httpRequest, None)
}

// If favicon.ico, just return a 404 because we don't have that file
case ctx @ Path("/favicon.ico") => {
val httpContext = ctx.asInstanceOf[HttpRequestProcessingContext]
httpContext.writeErrorResponse(HttpResponseStatus.NOT_FOUND, false, "")
// If favicon.ico, just return a 404 because we don't have that file
case Path("/favicon.ico") => {
httpRequest.writeErrorResponse(HttpResponseStatus.NOT_FOUND, false, "")
}
}

})

object TimezoneQueryString extends QueryStringMatcher("tz")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import org.apache.http.HttpHeaders
import org.jboss.netty.channel.ChannelLocal
import org.mashupbots.socko.context.HttpChunkProcessingContext
import org.mashupbots.socko.context.HttpRequestProcessingContext
import org.mashupbots.socko.context.WsProcessingContext
import org.mashupbots.socko.postdecoder.InterfaceHttpData.HttpDataType
import org.mashupbots.socko.postdecoder.Attribute
import org.mashupbots.socko.postdecoder.DefaultHttpDataFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ object SecureApp extends Logger {
// STEP #2 - Define Routes
//
val routes = Routes({
case ctx @ GET(_) => {
actorSystem.actorOf(Props[SecureHelloProcessor]) ! ctx
case GET(request) => {
actorSystem.actorOf(Props[SecureHelloProcessor]) ! request
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ object SnoopApp extends Logger {
// `SnoopProcessor` will `stop()` itself after processing each request.
//
val routes = Routes({
case ctx @ _ => {
actorSystem.actorOf(Props[SnoopProcessor]) ! ctx
case context => {
actorSystem.actorOf(Props[SnoopProcessor]) ! context
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ package org.mashupbots.socko.examples.websocket

import org.jboss.netty.handler.codec.http.HttpResponseStatus
import org.mashupbots.socko.context.HttpRequestProcessingContext
import org.mashupbots.socko.context.WsFrameProcessingContext
import org.mashupbots.socko.context.WsHandshakeProcessingContext
import org.mashupbots.socko.context.WsProcessingContext
import org.mashupbots.socko.routes.GET
import org.mashupbots.socko.routes.Path
import org.mashupbots.socko.routes.Routes
import org.mashupbots.socko.routes._
import org.mashupbots.socko.utils.Logger
import org.mashupbots.socko.webserver.WebServer
import org.mashupbots.socko.webserver.WebServerConfig
Expand Down Expand Up @@ -51,27 +49,31 @@ object WebSocketApp extends Logger {
// `WebSocketProcessor` will `stop()` itself after processing the request.
//
val routes = Routes({
case ctx @ GET(Path("/html")) => {
// Return HTML page to establish web socket
actorSystem.actorOf(Props[WebSocketProcessor]) ! ctx

case HttpRequest(httpRequest) => httpRequest match {
case GET(Path("/html")) => {
// Return HTML page to establish web socket
actorSystem.actorOf(Props[WebSocketProcessor]) ! httpRequest
}
case Path("/favicon.ico") => {
// If favicon.ico, just return a 404 because we don't have that file
httpRequest.writeErrorResponse(HttpResponseStatus.NOT_FOUND, false, "")
}
}
case ctx @ Path("/websocket/") => ctx match {
case ctx: WsHandshakeProcessingContext => {

case WebSocketHandshake(wsHandshake) => wsHandshake match {
case Path("/websocket/") => {
// For WebSocket processing, we first have to authorize the handshake by setting the "isAllowed" property.
// This is a security measure to make sure that web sockets can only be established at your specified end points.
val hctx = ctx.asInstanceOf[WsHandshakeProcessingContext]
hctx.isAllowed = true
}
case ctx: WsProcessingContext => {
// Once handshaking has taken place, the client can then send text to us for processing
actorSystem.actorOf(Props[WebSocketProcessor]) ! ctx
wsHandshake.isAllowed = true
}
}
case ctx @ Path("/favicon.ico") => {
// If favicon.ico, just return a 404 because we don't have that file
val httpContext = ctx.asInstanceOf[HttpRequestProcessingContext]
httpContext.writeErrorResponse(HttpResponseStatus.NOT_FOUND, false, "")

case WebSocketFrame(wsFrame) => {
// Once handshaking has taken place, we can now process frames sent from the client
actorSystem.actorOf(Props[WebSocketProcessor]) ! wsFrame
}

})

//
Expand Down
Loading

0 comments on commit 669edd5

Please sign in to comment.