ReadWriteWeb now also runs on netty. Old tests work. webid
authorHenry Story <henry.story@bblfish.net>
Sun, 23 Oct 2011 16:45:49 +0200
branchwebid
changeset 97 b4d164ab53ae
parent 96 7954862e9c68
child 98 adda01e32faf
ReadWriteWeb now also runs on netty. Old tests work.
project/build.scala
src/main/scala/Authoritative.scala
src/main/scala/Filesystem.scala
src/main/scala/ReadWriteWebMain.scala
src/main/scala/auth/Authz.scala
src/main/scala/auth/X509view.scala
src/main/scala/netty/ReadWriteWebNetty.scala
src/main/scala/plan.scala
src/test/scala/util/specs.scala
--- a/project/build.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/project/build.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -9,6 +9,7 @@
   val unfiltered_version = "0.5.1"
   val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % unfiltered_version 
   val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % unfiltered_version 
+  val unfiltered_netty = "net.databinder" %% "unfiltered-netty" % unfiltered_version 
   // val unfiltered_spec = "net.databinder" %% "unfiltered-spec" % "0.4.1" % "test"
   val ivyUnfilteredSpec =
     <dependencies>
@@ -82,6 +83,7 @@
       libraryDependencies += dispatch_http,
       libraryDependencies += unfiltered_filter,
       libraryDependencies += unfiltered_jetty,
+      libraryDependencies += unfiltered_netty,
 //      libraryDependencies += slf4jSimple,
       libraryDependencies += jena,
       libraryDependencies += arq,
--- a/src/main/scala/Authoritative.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/Authoritative.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -24,14 +24,26 @@
 package org.w3.readwriteweb
 
 import unfiltered.request._
-import java.net.URL
+import java.security.cert.Certificate
+import javax.servlet.http.HttpServletRequest
+import unfiltered.netty.ReceivedMessage
+import org.eclipse.jetty.util.URIUtil
+import java.net.{MalformedURLException, URL}
 
 object Authoritative {
   
   val r = """^(.*)\.(\w{0,4})$""".r
-  
-  def unapply(req: HttpRequest[javax.servlet.http.HttpServletRequest]): Option[(URL, Representation)] = {
-    val uri = req.underlying.getRequestURL.toString
+
+  // all of this would be unnecessary if req.uri would really return the full URI
+  // we should try to push for that to be done at unfiltered layer
+  def reqURL[T](m: Manifest[T], r: HttpRequest[T]): String = {
+    if (m <:< manifest[HttpServletRequest]) reqUrlServlet(r.asInstanceOf[HttpRequest[HttpServletRequest]])
+    else if (m <:< manifest[ReceivedMessage]) reqUrlNetty(r.asInstanceOf[HttpRequest[ReceivedMessage]])
+    else "" //todo: should perhaps throw an exception here.
+  }
+
+  def unapply[T](req: HttpRequest[T]) (implicit m: Manifest[T]) : Option[(URL, Representation)] =  {
+    val uri = reqURL(m, req)
     val suffixOpt = uri match {
       case r(_, suffix) => Some(suffix)
       case _ if uri endsWith "/" => Some("/")
@@ -40,4 +52,30 @@
     Some((new URL(uri), Representation(suffixOpt, Accept(req))))
   }
 
+
+  private def reqUrlServlet[T <: HttpServletRequest](req: HttpRequest[T]): String = {
+    req.underlying.getRequestURL.toString
+  }
+
+  private def reqUrlNetty[T <: ReceivedMessage](req: HttpRequest[T]): String = {
+      try {
+        val u = new URL(req.uri)
+        new URL(u.getProtocol,u.getHost,u.getPort,u.getPath).toExternalForm
+      } catch {
+        case e:  MalformedURLException => {
+          val url: StringBuffer = new StringBuffer (48)
+          val scheme = if (req.isSecure) "https" else "http"
+          val hostport = {//we assume there was some checking done earlier, and that we can rely on this
+          val host = req.headers ("Host")
+          if (host.hasNext) host.next () else "localhost"
+          }
+          url.append (scheme)
+          url.append ("://")
+          url.append (hostport)
+          url.append(req.uri)
+          url.toString
+        }
+      }
+  }
+
 }
--- a/src/main/scala/Filesystem.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/Filesystem.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -20,7 +20,7 @@
   
   def sanityCheck(): Boolean =
     baseDirectory.exists && baseDirectory.isDirectory
-  
+
   def resource(url: URL): Resource = new Resource {
     val relativePath: String = url.getPath.replaceAll("^"+basePath.toString+"/?", "")
     val fileOnDisk = new File(baseDirectory, relativePath)
--- a/src/main/scala/ReadWriteWebMain.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/ReadWriteWebMain.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -92,7 +92,11 @@
         baseURL.value.get,
         lang=rdfLanguage.value getOrElse RDFXML)(mode.value getOrElse ResourcesDontExistByDefault)
     
-    val app = new ReadWriteWeb(filesystem, new RDFAuthZ(webCache,filesystem))
+    val rww = new ReadWriteWeb[HttpServletRequest,HttpServletResponse] {
+      val rm = filesystem
+      def manif = manifest[HttpServletRequest]
+      override implicit val authz = new RDFAuthZ[HttpServletRequest,HttpServletResponse](webCache,filesystem)
+    }
 
     //this is incomplete: we should be able to start both ports.... not sure how to do this yet.
     val service = httpsPort.value match {
@@ -105,12 +109,14 @@
       context("/public"){ ctx:ContextBuilder =>
         ctx.resources(ClasspathUtils.fromClasspath("public/").toURI.toURL)
     }.
-      filter(app.plan).
+      filter(Planify(rww.intent)).
       filter(Planify(x509v.intent)).
       filter(new EchoPlan().plan).run()
     
   }
 
+
+
   object x509v extends X509view[HttpServletRequest,HttpServletResponse] {
     def wc = webCache
     def manif = manifest[HttpServletRequest]
--- a/src/main/scala/auth/Authz.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/auth/Authz.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -72,18 +72,18 @@
   }
 }
 
-object NullAuthZ extends AuthZ {
-  def subject(req: NullAuthZ.Req) = null
+class NullAuthZ[Request,Response] extends AuthZ[Request,Response] {
+  override def subject(req: Req): Option[Subject] = None
 
-  def guard(m: Method, path: String) = null
+  override def guard(m: Method, path: String): Guard = null
 
   override def protect(in: Req=>Res) = in
 }
 
 
-abstract class AuthZ {
-  type Req = HttpRequest[HttpServletRequest]
-  type Res = ResponseFunction[HttpServletResponse]
+abstract class AuthZ[Request,Response] {
+  type Req = HttpRequest[Request]
+  type Res = ResponseFunction[Response]
 
   def protect(in: Req=>Res): Req=>Res =  {
       case req @ HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
@@ -108,7 +108,8 @@
 }
 
 
-class RDFAuthZ(val webCache: WebCache, rm: ResourceManager) extends AuthZ {
+class RDFAuthZ[Request,Response](val webCache: WebCache, rm: ResourceManager)
+  (implicit val m: Manifest[Request]) extends AuthZ[Request,Response] {
   import AuthZ.x509toSubject
   implicit val cache : WebCache = webCache
 
--- a/src/main/scala/auth/X509view.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/auth/X509view.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -30,17 +30,20 @@
 
 /**
  * This plan just described the X509 WebID authentication information.
+ * It works independently of the underlying Cycle.Intent implementations of Request and Response,
+ * so it can work with servlet filters just as well as with netty.
+ *
  * This is a simple version. A future version will show EARL output, and so be useful for debugging the endpoint.
  *
  * @author hjs
  * @created: 13/10/2011
  */
 
-trait X509view[A,B]  {
+trait X509view[Request,Response]  {
    implicit def wc: WebCache
-   implicit def manif: Manifest[A]
+   implicit def manif: Manifest[Request]
 
-    def intent: Cycle.Intent[A, B] =  {
+    def intent: Cycle.Intent[Request, Response] =  {
       case req @ Path(path) if path startsWith "/test/auth/x509" =>
         Ok ~> ContentType("text/html") ~> Html(
           <html><head><title>Authentication Page</title></head>
--- a/src/main/scala/netty/ReadWriteWebNetty.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -27,8 +27,8 @@
 import scala.Console._
 import org.w3.readwriteweb.auth.{X509view, RDFAuthZ}
 import org.w3.readwriteweb._
-import unfiltered.netty.{ReceivedMessage, ServerErrorResponse, cycle}
 import org.jboss.netty.handler.codec.http.HttpResponse
+import unfiltered.netty.{ServerErrorResponse, ReceivedMessage, cycle}
 
 /**
  * ReadWrite Web for Netty server, allowing content renegotiation
@@ -54,8 +54,13 @@
          baseURL.value.get,
          lang=rdfLanguage.value getOrElse RDFXML)(mode.value getOrElse ResourcesDontExistByDefault)
      
-     val app = new ReadWriteWeb(filesystem, new RDFAuthZ(webCache,filesystem))
- 
+//     val app = new ReadWriteWeb(filesystem, new RDFAuthZ(webCache,filesystem))
+     val rww = new cycle.Plan  with cycle.ThreadPool with ServerErrorResponse with ReadWriteWeb[ReceivedMessage,HttpResponse]{
+          val rm = filesystem
+          def manif = manifest[ReceivedMessage]
+          override val authz = new RDFAuthZ[ReceivedMessage,HttpResponse](webCache,filesystem)
+     }
+
      //this is incomplete: we should be able to start both ports.... not sure how to do this yet.
      val service = httpsPort.value match {
        case Some(port) => new KeyAuth_Https(port)
@@ -63,7 +68,8 @@
      }
 
      // configures and launches a Netty server
-     service.plan( x509v ).run()
+     service.plan( x509v ).
+             plan( rww ).run()
      
    }
 
--- a/src/main/scala/plan.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/main/scala/plan.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -21,6 +21,7 @@
 import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 import unfiltered.request._
 import unfiltered.response._
+import unfiltered.Cycle
 
 //object ReadWriteWeb {
 //
@@ -30,34 +31,29 @@
 //
 //}
 
-class ReadWriteWeb(rm: ResourceManager, implicit val authz: AuthZ = NullAuthZ)  {
+/**
+ * The ReadWriteWeb intent.
+ * It is independent of jetty or netty
+ */
+trait ReadWriteWeb[Req,Res] {
+  val rm: ResourceManager
+  implicit def manif: Manifest[Req]
+  implicit val authz: AuthZ[Req,Res] = new NullAuthZ[Req,Res]
   // a few type short cuts to make it easier to reason with the code here
   // one may want to generalise this code so that it does not depend so strongly on servlets.
-  type Req = HttpRequest[HttpServletRequest]
-  type Res = ResponseFunction[HttpServletResponse]
+//  type Request = HttpRequest[Req]
+//  type Response = ResponseFunction[Res]
 
   val logger: Logger = LoggerFactory.getLogger(this.getClass)
 
-
- /**
-  *  The Servlet Filter for the ReadWriteWeb
-  *
-  *  Planify.apply takes an Intent, which is defined in Cycle by
-  *  type Intent [-A, -B] = PartialFunction[HttpRequest[A], ResponseFunction[B]]
-  *  the corresponding syntax is: case ... => ...
-  */
-  //bblfish: I don't understand why this has to be lazy, but if it is not, it the plan ends up throwing a null
-  //pointer exception.
-  lazy val plan = unfiltered.filter.Planify(intent)
-
   /**
    * The partial function that if triggered sends to the readwrite web code.
    * It wraps the ReadWriteWeb function with the AuthZ passed in the argument
    * ( Note that we don't want to protect this intent, since that would be to apply the security to all other applications,
    * many of which may want different authorization implementations )
    */
-  val intent : Plan.Intent = {
-      case req @ Path(path) if path startsWith rm.basePath => authz.protect(readWriteWebIntent)(req)
+  def intent : Cycle.Intent[Req,Res] = {
+      case req @ Path(path) if path startsWith rm.basePath => authz.protect(rwwIntent)(req)
   }
 
   /**
@@ -79,11 +75,11 @@
    *  At last, Validation[ResponseFunction, ResponseFuntion] is exposed as a ResponseFunction
    *  through another implicit conversion. It saves us the call to the Validation.fold() method
    */
-  val readWriteWebIntent =  (req: Req) => {
+  def rwwIntent  =  (req: HttpRequest[Req]) => {
 
           val Authoritative(uri, representation) = req
           val r: Resource = rm.resource(uri)
-          val res: Res = req match {
+          val res: ResponseFunction[Res] = req match {
             case GET(_) if representation == HTMLRepr => {
               val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
               val body = source.getLines.mkString("\n")
--- a/src/test/scala/util/specs.scala	Sat Oct 22 20:36:17 2011 +0200
+++ b/src/test/scala/util/specs.scala	Sun Oct 23 16:45:49 2011 +0200
@@ -2,6 +2,7 @@
 
 import org.w3.readwriteweb._
 
+import auth.RDFAuthZ
 import org.specs._
 import java.net.URL
 import unfiltered.response._
@@ -18,12 +19,19 @@
 
 import org.w3.readwriteweb.util._
 import org.w3.readwriteweb.utiltest._
+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+import unfiltered.filter.Planify
 
 trait ResourceManaged extends Specification with unfiltered.spec.jetty.Served {
   
   def resourceManager: ResourceManager
-  
-  def setup = { _.filter(new ReadWriteWeb(resourceManager).plan) }
+
+  val rww = new ReadWriteWeb[HttpServletRequest,HttpServletResponse] {
+     val rm = resourceManager
+     def manif = manifest[HttpServletRequest]
+   }
+
+  def setup = { _.filter(Planify(rww.intent)) }
  
 }