Added initial WebID code. WebIDs are verified, but verification is not cashed so it is slow. Authentification is not used yet either. webid
authorHenry Story <henry.story@bblfish.net>
Wed, 12 Oct 2011 00:21:48 +0200
branchwebid
changeset 49 5d7d8b0b208f
parent 46 c75484bb9208
child 52 ba2ae3860a8e
Added initial WebID code. WebIDs are verified, but verification is not cashed so it is slow. Authentification is not used yet either.
.hgignore
README.markdown
project/build.scala
src/main/scala/AuthFilter.scala
src/main/scala/Main.scala
src/main/scala/Resource.scala
src/main/scala/WebCache.scala
src/main/scala/util.scala
--- a/.hgignore	Thu Oct 06 20:27:12 2011 -0400
+++ b/.hgignore	Wed Oct 12 00:21:48 2011 +0200
@@ -15,4 +15,6 @@
 src/test/scala.egp
 sbt-launch*.jar
 .scala_dependencies
-*.orig
\ No newline at end of file
+*.orig
+.idea
+.idea_modules
--- a/README.markdown	Thu Oct 06 20:27:12 2011 -0400
+++ b/README.markdown	Wed Oct 12 00:21:48 2011 +0200
@@ -73,3 +73,14 @@
  *   --strict  Documents must be created using PUT else they return 404
     
     
+HTTPS with WebID 
+----------------
+
+### to run on https with WebID
+
+    > java -Djetty.ssl.keyStoreType=JKS -Djetty.ssl.keyStore=KEYSTORE.jks -Djetty.ssl.keyStorePassword=secret -jar target/read-write-web.jar -https 8443
+
+### with debug enabled  add the following parameters after 'java'
+
+     -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
+
--- a/project/build.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/project/build.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -5,13 +5,13 @@
 // they are pulled only if used
 object Dependencies {
   val specs = "org.scala-tools.testing" %% "specs" % "1.6.9" % "test"
-  val dispatch = "net.databinder" %% "dispatch-http" % "0.8.5" % "test"
-  val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % "0.4.1"
-  val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % "0.4.1"
+  val dispatch_http = "net.databinder" %% "dispatch-http" % "0.8.5" 
+  val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % "0.5.0"
+  val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % "0.5.0"
   // val unfiltered_spec = "net.databinder" %% "unfiltered-spec" % "0.4.1" % "test"
   val ivyUnfilteredSpec =
     <dependencies>
-      <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev="0.4.1">
+      <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev="0.5.0">
         <exclude org="net.databinder" module="dispatch-mime_2.9.0-1"/>
       </dependency>
     </dependencies>
@@ -21,8 +21,7 @@
   val arq = "com.hp.hpl.jena" % "arq" % "2.8.8"
   val grizzled = "org.clapper" %% "grizzled-scala" % "1.0.8" % "test"
   val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.2"
-
-
+  val jsslutils = "org.jsslutils" % "jsslutils" % "1.0.7"
 
 }
 
@@ -78,7 +77,7 @@
       libraryDependencies += specs,
 //      libraryDependencies += unfiltered_spec,
       ivyXML := ivyUnfilteredSpec,
-      libraryDependencies += dispatch,
+      libraryDependencies += dispatch_http,
       libraryDependencies += unfiltered_filter,
       libraryDependencies += unfiltered_jetty,
 //      libraryDependencies += slf4jSimple,
@@ -87,6 +86,8 @@
       libraryDependencies += antiXML,
       libraryDependencies += grizzled,
       libraryDependencies += scalaz,
+      libraryDependencies += jsslutils,
+
       jarName in assembly := "read-write-web.jar"
     )
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/AuthFilter.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -0,0 +1,379 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms are permitted
+ * provided that the above copyright notice and this paragraph are
+ * duplicated in all such forms and that any documentation,
+ * advertising materials, and other materials related to such
+ * distribution and use acknowledge that the software was developed
+ * by Henry Story.  The name of bblfish.net may not be used to endorse
+ * or promote products derived
+ * from this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+package org.w3.readwriteweb.webid
+
+
+
+import java.security.cert.X509Certificate
+import javax.servlet._
+import org.slf4j.LoggerFactory
+import org.w3.readwriteweb._
+
+import java.util.{LinkedList, Date}
+import java.security.interfaces.RSAPublicKey
+import java.security.{Principal, PublicKey}
+import java.net.URL
+import java.math.BigInteger
+import com.hp.hpl.jena.rdf.model.RDFNode
+import collection.JavaConversions._
+import javax.security.auth.{Subject, Refreshable}
+import com.hp.hpl.jena.query._
+
+/**
+ * @author Henry Story from http://bblfish.net/
+ * @created: 09/10/2011
+ */
+
+case class WebIdPrincipal(webid: String) extends Principal {
+  def getName = webid
+  override def equals(that: Any) = that match {
+    case other: WebIdPrincipal => other.webid == webid
+    case _ => false
+  }
+}
+
+case class Anonymous() extends Principal {
+  def getName = "anonymous"
+  override def equals(that: Any) =  that match {
+      case other: WebIdPrincipal => other eq this 
+      case _ => false
+    } //anonymous principals are equal only when they are identical. is this wise?
+      //well we don't know when two anonymous people are the same or different.
+}
+
+class AuthFilter extends Filter {
+  def init(filterConfig: FilterConfig) {}
+
+  def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
+    val certChain = request.getAttribute("javax.servlet.request.X509Certificate") match {
+      case certs: Array[X509Certificate] => certs.toList
+      case _ => Nil
+    }
+    val subject = new Subject()
+    if (certChain.size == 0) {
+      System.err.println("No certificate found!")
+      subject.getPrincipals.add(Anonymous())
+    } else {
+      val x509c = new X509Claim(certChain(0))
+      subject.getPublicCredentials.add(x509c)
+
+
+      val verified = for (
+        claim <- x509c.webidclaims;
+        if (claim.verified)
+      ) yield claim.principal
+
+      subject.getPrincipals.addAll(verified)
+      System.err.println("Found "+verified.size+" principals: "+verified)
+    }
+
+    chain.doFilter(request, response)
+  }
+
+  def destroy() {}
+}
+
+object X509Claim {
+  final val logger = LoggerFactory.getLogger(classOf[X509Claim])
+
+  /**
+   * Extracts the URIs in the subject alternative name extension of an X.509
+   * certificate
+   *
+   * @param cert X.509 certificate from which to extract the URIs.
+   * @return Iterator of URIs as strings found in the subjectAltName extension.
+   */
+	def getClaimedWebIds(cert: X509Certificate): Iterator[String] =
+    if (cert == null)  Iterator.empty;
+    else cert.getSubjectAlternativeNames() match {
+      case coll if (coll != null) => {
+        for (sanPair <- coll
+             if (sanPair.get(0) == 6)
+        ) yield sanPair(1).asInstanceOf[String]
+      }.iterator
+      case _ => Iterator.empty
+    }
+
+
+
+}
+
+
+/**
+ * An X509 Claim maintains information about the proofs associated with claims
+ * found in an X509 Certificate. It is the type of object that can be passed
+ * into the public credentials part of a Subject node
+ *
+ * todo: think of what this would look like for a chain of certificates
+ *
+ * @author bblfish
+ * @created: 30/03/2011
+ */
+class X509Claim(val cert: X509Certificate) extends Refreshable {
+
+  import X509Claim._
+  val claimReceivedDate = new Date();
+  lazy val tooLate = claimReceivedDate.after(cert.getNotAfter())
+  lazy val tooEarly = claimReceivedDate.before(cert.getNotBefore())
+
+  /* a list of unverified principals */
+  lazy val webidclaims = getClaimedWebIds(cert).map {
+    webid =>new WebIDClaim(webid, cert.getPublicKey)
+  }.toSet
+
+
+  //note could also implement Destroyable
+  //
+  //http://download.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html#Credentials
+  //
+  //if updating validity periods can also take into account the WebID reference, then it is possible
+  //that a refresh could have as consequence to do a fetch on the WebID profile
+  //note: one could also take the validity period to be dependent on the validity of the profile representation
+  //in which case updating the validity period would make more sense.
+
+  override
+  def refresh() {
+  }
+
+  /* The certificate is currently within the valid time zone */
+  override
+  def isCurrent(): Boolean = !(tooLate||tooEarly)
+
+  lazy val error = {}
+
+  def canEqual(other: Any) = other.isInstanceOf[X509Claim]
+
+  override
+  def equals(other: Any): Boolean =
+    other match {
+      case that: X509Claim => (that eq this) || (that.canEqual(this) && cert == that.cert)
+      case _ => false
+    }
+
+  override
+  lazy val hashCode: Int = 41 * (41 +
+    (if (cert != null) cert.hashCode else 0))
+
+
+}
+
+object WebIDClaim {
+     final val cert: String = "http://www.w3.org/ns/auth/cert#"
+     final val xsd: String = "http://www.w3.org/2001/XMLSchema#"
+
+    val selectQuery = QueryFactory.create("""
+ 		  PREFIX cert: <http://www.w3.org/ns/auth/cert#>
+ 		  PREFIX rsa: <http://www.w3.org/ns/auth/rsa#>
+ 		  SELECT ?key ?m ?e ?mod ?exp
+ 		  WHERE {
+ 		   ?key cert:identity ?webid ;
+ 		      rsa:modulus ?m ;
+ 		      rsa:public_exponent ?e .
+ 
+ 		    OPTIONAL { ?m cert:hex ?mod . }
+ 		    OPTIONAL { ?e cert:decimal ?exp . }
+ 		  }""")
+
+  /**
+    * Transform an RDF representation of a number into a BigInteger
+    * <p/>
+    * Passes a statement as two bindings and the relation between them. The
+    * subject is the number. If num is already a literal number, that is
+    * returned, otherwise if enough information from the relation to optstr
+    * exists, that is used.
+    *
+    * @param num the number node
+    * @param optRel name of the relation to the literal
+    * @param optstr the literal representation if it exists
+    * @return the big integer that num represents, or null if undetermined
+    */
+   def toInteger(num: RDFNode, optRel: String, optstr: RDFNode): Option[BigInteger] =
+       if (null == num) None
+       else if (num.isLiteral) {
+         val lit = num.asLiteral()
+         toInteger_helper(lit.getLexicalForm,lit.getDatatypeURI)
+       } else if (null != optstr && optstr.isLiteral) {
+           toInteger_helper(optstr.asLiteral().getLexicalForm,optRel)
+       } else None
+
+
+    private def intValueOfHexString(s: String): BigInteger = {
+      val strval = cleanHex(s);
+      new BigInteger(strval, 16);
+    }
+
+
+    /**
+     * This takes any string and returns in order only those characters that are
+     * part of a hex string
+     *
+     * @param strval
+     *            any string
+     * @return a pure hex string
+     */
+
+    private def cleanHex(strval: String) = {
+      def legal(c: Char) = {
+        //in order of appearance probability
+        ((c >= '0') && (c <= '9')) ||
+          ((c >= 'A') && (c <= 'F')) ||
+          ((c >= 'a') && (c <= 'f'))
+      }
+      (for (c <- strval; if legal(c)) yield c)
+    }
+
+
+   /**
+    * This transforms a literal into a number if possible ie, it returns the
+    * BigInteger of "ddd"^^type
+    *
+    * @param num the string representation of the number
+    * @param tpe the type of the string representation
+    * @return the number
+    */
+   protected def toInteger_helper(num: String, tpe: String): Option[BigInteger] =
+     try {
+       if (tpe.equals(cert + "decimal") || tpe.equals(cert + "int")
+         || tpe.equals(xsd + "integer") || tpe.equals(xsd + "int")
+         || tpe.equals(xsd + "nonNegativeInteger")) {
+         // cert:decimal is deprecated
+         Some(new BigInteger(num.trim(), 10));
+       } else if (tpe.equals(cert + "hex")) {
+         Some(intValueOfHexString(num));
+       } else {
+         // it could be some other encoding - one should really write a
+         // special literal transformation class
+         None;
+       }
+     } catch {
+       case e: NumberFormatException => None
+     }
+
+
+}
+
+/**
+ * An X509 Claim maintains information about the proofs associated with claims
+ * found in an X509 Certificate. It is the type of object that can be passed
+ * into the public credentials part of a Subject node
+ *
+ * todo: think of what this would look like for a chain of certificates
+ *
+ * @author bblfish
+ * @created 30/03/2011
+ */
+class WebIDClaim(val webId: String, val key: PublicKey) {
+
+	val errors = new LinkedList[java.lang.Throwable]()
+
+	lazy val principal = new WebIdPrincipal(webId)
+	lazy val tests: List[Verification] = verify()  //I need to try to keep more verification state
+
+
+	/**
+	 * verify this claim
+	 * @param authSrvc: the authentication service contains information about where to get graphs
+	 */
+	//todo: make this asynchronous
+	lazy val verified: Boolean =  tests.exists(v => v.isInstanceOf[Verified])
+
+  private def verify(): List[Verification] = {
+    import util.wrapValidation
+    import collection.JavaConversions._
+    import WebIDClaim._
+    if (!webId.startsWith("http:") && !webId.startsWith("https:")) {
+      //todo: ftp, and ftps should also be doable, though content negotiations is then lacking
+      unsupportedProtocol::Nil
+    } else if (!key.isInstanceOf[RSAPublicKey]) {
+      certificateKeyTypeNotSupported::Nil
+    } else {
+      val cache = Lookup.get(classOf[WebCache]).head
+      val res = for {
+        model <- cache.resource(new URL(webId)).get() failMap {
+          t => new ProfileError("error fetching profile", t)
+        }
+      } yield {
+        val initialBinding = new QuerySolutionMap();
+        initialBinding.add("webid",model.createResource(webId))
+        val qe: QueryExecution = QueryExecutionFactory.create(WebIDClaim.selectQuery, model,initialBinding)
+        try {
+          qe.execSelect().map( qs => {
+            val modulus = toInteger(qs.get("m"), cert + "hex", qs.get("mod"))
+            val exponent = toInteger(qs.get("e"), cert + "decimal", qs.get("exp"))
+
+            (modulus, exponent) match {
+              case (Some(mod), Some(exp)) => {
+                val rsakey = key.asInstanceOf[RSAPublicKey]
+                if (rsakey.getPublicExponent == exp && rsakey.getModulus == mod) verifiedWebID
+                else keyDoesNotMatch
+              }
+              case _ => new KeyProblem("profile contains key that cannot be analysed:" +
+                qs.varNames().map(nm => nm + "=" + qs.get(nm).toString) + "; ")
+            }
+          }).toList
+          //it would be nice if we could keep a lot more state of what was verified and how
+          //will do that when implementing tests, so that these tests can then be used directly as much as possible
+        } finally {
+          qe.close()
+        }
+      }
+      res.either match {
+        case Right(tests) => tests
+        case Left(profileErr) => profileErr::Nil
+      }
+    }
+
+
+  }
+  
+
+
+	def canEqual(other: Any) = other.isInstanceOf[WebIDClaim]
+
+	override
+	def equals(other: Any): Boolean =
+		other match {
+			case that: WebIDClaim => (that eq this) || (that.canEqual(this) && webId == that.webId && key == that.key)
+			case _ => false
+		}
+
+	override
+	lazy val hashCode: Int = 41 * (
+		41 * (
+			41 + (if (webId != null) webId.hashCode else 0)
+			) + (if (key != null) key.hashCode else 0)
+		)
+}
+
+
+class Verification(msg: String)
+class Verified(msg: String) extends Verification(msg)
+class Unverified(msg: String) extends Verification(msg)
+
+class TestFailure(msg: String) extends Verification(msg)
+class ProfileError(msg: String,  t: Throwable ) extends TestFailure(msg)
+class KeyProblem(msg: String) extends TestFailure(msg)
+
+object unsupportedProtocol extends TestFailure("WebID protocol not supported")
+object noMatchingKey extends TestFailure("No keys in profile matches key in cert")
+object keyDoesNotMatch extends TestFailure("Key does not match")
+
+object verifiedWebID extends Verified("WebId verified")
+object notstarted extends Unverified("No verification attempt started")
+object failed extends Unverified("Tests failed")
+object certificateKeyTypeNotSupported extends TestFailure("The certificate key type is not supported. We only support RSA")
\ No newline at end of file
--- a/src/main/scala/Main.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/Main.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -16,6 +16,7 @@
 import com.hp.hpl.jena.query._
 import com.hp.hpl.jena.update._
 import com.hp.hpl.jena.shared.JenaException
+import org.w3.readwriteweb.{Resource}
 import Query.{QueryTypeSelect => SELECT, QueryTypeAsk => ASK,
               QueryTypeConstruct => CONSTRUCT, QueryTypeDescribe => DESCRIBE}
 
@@ -23,6 +24,15 @@
 import Scalaz._
 
 import org.w3.readwriteweb.util._
+import java.security.KeyStore
+import org.jsslutils.keystores.KeyStoreLoader
+import org.jsslutils.sslcontext.{X509TrustManagerWrapper, X509SSLContextFactory}
+import javax.net.ssl.{X509TrustManager, SSLContext}
+import org.jsslutils.sslcontext.trustmanagers.TrustAllClientsWrappingTrustManager
+import java.security.cert.X509Certificate
+import scala.sys.SystemProperties
+import collection.{mutable,immutable}
+import webid.AuthFilter
 
 class ReadWriteWeb(rm:ResourceManager) {
   
@@ -137,51 +147,132 @@
 
 }
 
+object Lookup {
+  // a place to register services that can be looked up from anywhere.
+  // this is very naive registration, compared to tools like Clerezza that use Apaches Felix's OSGI implementation
+
+  private val db = new mutable.HashMap[Class[_],AnyRef]
+
+  def get[T<:AnyRef](clzz :Class[T]) :Option[T] = db.get(clzz).map(e=>e.asInstanceOf[T])
+
+  //http://stackoverflow.com/questions/3587286/how-does-scalas-2-8-manifest-work
+  def put[T<:AnyRef : Manifest](obj: T): T = {
+    def zref = manifest[T].erasure
+    val ref: AnyRef = obj
+    db.put(zref,ref).asInstanceOf[T]
+  }
+
+}
 
 object ReadWriteWebMain {
 
   val logger:Logger = LoggerFactory.getLogger(this.getClass)
 
+
   // regular Java main
   def main(args: Array[String]) {
-    
+   
     val argsList = args.toList
+    var httpPort: Int = 8080
+    var httpsPort: Option[Int] = None
+    var baseDir = new File(".")
+    var baseUrl: String = "/"
+    var relax = false
     
-    val (port, baseDirectory, baseURL) = argsList match {
-      case port :: directory :: base :: _ => (port.toInt, new File(directory), base)
-      case _ => {
-        println(
-"""example usage:
-    java -jar read-write-web.jar 8080 ~/WWW/2011/09 /2011/09 [-strict|-relax]
+ 
+     def msg(err: String, exitCode: Int=1) = {
+        println("ERROR:")
+        println(err)
+        println("""
+
+example usage:
+java -jar read-write-web.jar [-http 8080] [-https 8443] -dir ~/WWW/2011/09 -base /2011/09 [-strict|-relax]
+
+Required:
+  -dir $localpath :  the directory where the files are located
+  -base $urlpath : the base url-path for those files
 
 Options:
+ -http $port  : set the http port to the port number, by default this will be port 8080
+ -https $port : start the https server on the given port number
  -relax all resources potentially exist, meaning you get an empty RDF graph instead of a 404 (still experimental)
  -strict a GET on a resource will fail with a 404 (default mode if you omit it)
-""")
-        System.exit(1)
+
+Properties:  (can be passed with -Dprop=value)
+
+ * Keystore properties.
+  jetty.ssl.keyStoreType : the type of the keystore, JKS by default usually
+  jetty.ssl.keyStore=path : specify path to key store (for https server certificate)
+  jetty.ssl.keyStorePassword=password : specify password for keystore store
+
+ * Trust store
+   Trust stores are not needed because we use the WebID protocol, and client certs are nearly never signed by CAs
+ """)
+        System.exit(exitCode)
         null
+    }
+    
+
+    def parse(args: List[String]): Unit = {
+      val res = args match {
+        case "-https"::num::rest =>  { 
+          httpsPort = Some(Integer.parseInt(num))
+          rest
+        }
+        case "-http"::num::rest => {
+          httpPort = num.toInt
+          rest
+        }
+        case "-dir"::dir::rest => {
+          baseDir = new File(dir)
+          rest
+        }
+        case "-base"::path::rest=> {
+          baseUrl=path
+          rest
+        }
+        case "-strict"::rest=> {
+          relax = false
+          rest
+        }
+        case "-relax"::rest=> {
+          relax = true
+          rest
+        }
+        case something::other => msg("could not parse command: `"+something+"`",1)
+        case Nil => return
       }
+      parse(res)
     }
+    
+    parse(argsList)
 
     val mode =
-      if (argsList contains "-relax") {
+      if (relax) {
         logger.info("info: using experimental relax mode")
         AllResourcesAlreadyExist
       } else {
         ResourcesDontExistByDefault
       }
     
-    if (! baseDirectory.exists) {
-      println("%s does not exist" format (baseDirectory.getAbsolutePath))
-      System.exit(2)
+    if (! baseDir.exists) {
+      msg("%s does not exist" format (baseDir.getAbsolutePath),2)
     }
 
-    val filesystem = new Filesystem(baseDirectory, baseURL, lang="TURTLE")(mode)
+    val filesystem = new Filesystem(baseDir, baseUrl, lang="TURTLE")(mode)
     
     val app = new ReadWriteWeb(filesystem)
 
+    val service = httpsPort match {
+      case Some(port) => HttpsTrustAll(port,"0.0.0.0")
+      case None => Http(httpPort)
+    }
+
+    val webCache = new WebCache()
+    Lookup.put(webCache)
+
     // configures and launches a Jetty server
-    unfiltered.jetty.Http(port).filter {
+    service.filter {
       // a jee Servlet filter that logs HTTP requests
       new Filter {
         def destroy():Unit = ()
@@ -195,7 +286,8 @@
         def init(filterConfig:FilterConfig):Unit = ()
       }
     // Unfiltered filters
-    }.context("/public"){ ctx:ContextBuilder =>
+    }.filter(new AuthFilter)
+     .context("/public"){ ctx:ContextBuilder =>
       ctx.resources(MyResourceManager.fromClasspath("public/").toURI.toURL)
     }.filter(app.read).run()
     
@@ -203,3 +295,38 @@
 
 }
 
+case class HttpsTrustAll(override val port: Int, override val host: String) extends Https(port, host) with TrustAll
+
+trait TrustAll { self: Ssl =>
+   import scala.sys.SystemProperties._
+
+   lazy val sslContextFactory = new X509SSLContextFactory(
+               serverCertKeyStore,
+               tryProperty("jetty.ssl.keyStorePassword"),
+               serverCertKeyStore); //this one is not needed since our wrapper ignores all trust managers
+
+   lazy val trustWrapper = new X509TrustManagerWrapper {
+     def wrapTrustManager(trustManager: X509TrustManager) = new TrustAllClientsWrappingTrustManager(trustManager)
+   }
+
+   lazy val serverCertKeyStore = {
+      val keyStoreLoader = new KeyStoreLoader
+   		keyStoreLoader.setKeyStoreType(System.getProperty("jetty.ssl.keyStoreType","JKS"))
+   		keyStoreLoader.setKeyStorePath(trustStorePath)
+   		keyStoreLoader.setKeyStorePassword(System.getProperty("jetty.ssl.keyStorePassword","password"))
+      keyStoreLoader.loadKeyStore();
+   }
+
+   sslContextFactory.setTrustManagerWrapper(trustWrapper);
+
+
+ 	 lazy val trustStorePath =  new SystemProperties().get("jetty.ssl.keyStore") match {
+       case Some(path) => path
+       case None => new File(new File(tryProperty("user.home")), ".keystore").getAbsolutePath
+   }
+
+   sslConn.setSslContext(sslContextFactory.buildSSLContext())
+   sslConn.setWantClientAuth(true)
+
+}
+
--- a/src/main/scala/Resource.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/Resource.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -8,8 +8,6 @@
 import com.hp.hpl.jena.rdf.model._
 import com.hp.hpl.jena.shared.JenaException
 
-import org.w3.readwriteweb.util._
-
 import scalaz._
 import Scalaz._
 
@@ -20,6 +18,7 @@
   def sanityCheck():Boolean
   def resource(url:URL):Resource
 }
+
 trait Resource {
   def get():Validation[Throwable, Model]
   def save(model:Model):Validation[Throwable, Unit]
@@ -76,3 +75,4 @@
   }
   
 }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/WebCache.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -0,0 +1,75 @@
+ /*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms are permitted
+ * provided that the above copyright notice and this paragraph are
+ * duplicated in all such forms and that any documentation,
+ * advertising materials, and other materials related to such
+ * distribution and use acknowledge that the software was developed
+ * by Henry Story.  The name of bblfish.net may not be used to endorse 
+ * or promote products derived
+ * from this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ */ 
+
+package org.w3.readwriteweb
+
+import com.hp.hpl.jena.rdf.model.Model
+import java.net.URL
+import org.apache.http.MethodNotSupportedException
+import org.w3.readwriteweb.RDFEncoding._
+import org.w3.readwriteweb.util._
+import org.w3.readwriteweb.{RDFEncoding, RDFXML}
+import scalaz._
+import Scalaz._
+
+/**
+ * @author Henry Story
+ * @created: 12/10/2011
+ *
+ * The WebCache currently does not cache
+ */
+class WebCache extends ResourceManager {
+  import dispatch._
+
+  val http = new Http
+  
+  def basePath = null //should be cache dir?
+
+  def sanityCheck() = true  //cache dire exists? But is this needed for functioning?
+
+  def resource(u : URL) = new org.w3.readwriteweb.Resource {
+
+    def get() = {
+      // note we prefer rdf/xml and turtle over html, as html does not always contain rdfa, and we prefer those over n3,
+      // as we don't have a full n3 parser. Better would be to have a list of available parsers for whatever rdf framework is
+      // installed (some claim to do n3 when they only really do turtle)
+      // we can't currently accept */* as we don't have GRDDL implemented
+      val request = url(u.toString) <:< Map("Accept"->
+        "application/rdf+xml,text/turtle,application/xhtml+xml;q=0.8,text/html;q=0.7,text/n3;q=0.6")
+
+      //we need to tell the model about the content type
+      val handler: Handler[Validation[Throwable, Model]] = request.>+>[Validation[Throwable, Model]](res =>  {
+        res >:> { headers =>
+          val encoding = headers("Content-Type").headOption match {
+            case Some(mime) => RDFEncoding(mime)
+            case None => RDFXML  // it would be better to try to do a bit of guessing in this case by looking at content
+          }
+          val loc = headers("Content-Location").headOption match {
+            case Some(loc) => new URL(u,loc)
+            case None => new URL(u.getProtocol,u.getAuthority,u.getPort,u.getPath)
+          }
+          res>>{ in=>modelFromInputStream(in,loc.toString,encoding) }
+
+        }
+      })
+      http(handler)
+
+    }
+
+    def save(model: Model) = { throw new MethodNotSupportedException("not implemented"); null }
+  }
+}
--- a/src/main/scala/util.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/util.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -23,7 +23,6 @@
 import unfiltered.request._
 import unfiltered.response._
 import unfiltered.jetty._
-
 sealed trait RWWMode
 case object AllResourcesAlreadyExist extends RWWMode
 case object ResourcesDontExistByDefault extends RWWMode
@@ -40,12 +39,21 @@
 
 object RDFEncoding {
   
-  def apply(contentType:String):RDFEncoding =
-    contentType match {
+  def apply(contentType:String):RDFEncoding = {
+    val i = contentType.indexOf(';')
+    (if (i<0) contentType
+    else contentType.substring(0,i).trim()).toLowerCase match {
       case "text/turtle" => TURTLE
       case "application/rdf+xml" => RDFXML
-      case _ => RDFXML
+      case _ => RDFXML       
     }
+  }
+
+  def jena(encoding: RDFEncoding) = encoding match {
+    case RDFXML => "RDF/XML-ABBREV"
+    case TURTLE => "TURTLE"
+    case _      => "RDF/XML-ABBREV" //don't like this default
+  }
     
   def apply(req:HttpRequest[_]):RDFEncoding = {
     val contentType = Accept(req).headOption
@@ -60,7 +68,7 @@
 }
 
 package object util {
-  
+
   val defaultLang = "RDF/XML-ABBREV"
 
   class MSAuthorVia(value:String) extends ResponseHeader("MS-Author-Via", List(value))
@@ -88,6 +96,9 @@
       }
   }
 
+  def modelFromInputStream(is:InputStream, base: String,  lang: RDFEncoding): Validation[Throwable, Model]=
+    modelFromInputStream(is, base, RDFEncoding.jena(lang))
+  
   def modelFromInputStream(
       is:InputStream,
       base:String,