Reached a stable state & stumbled on a unfiltered issue where I need to point to this code on the mailing list. webid
authorHenry Story <henry.story@bblfish.net>
Thu, 13 Oct 2011 21:50:21 +0200
branchwebid
changeset 59 34ec240b4fd2
parent 57 f647f0335a35 (current diff)
parent 58 6f860f662ed4 (diff)
child 60 fcfad2d7d118
Reached a stable state & stumbled on a unfiltered issue where I need to point to this code on the mailing list.
README.markdown
project/build.scala
src/main/scala/AuthFilter.scala
src/main/scala/Main.scala
src/main/scala/ReadWriteWebMain.scala
src/main/scala/auth/AuthFilter.scala
src/main/scala/auth/Authn.scala
src/main/scala/auth/Principals.scala
src/main/scala/auth/WebIdClaim.scala
src/main/scala/auth/WebIdRealm.scala
src/main/scala/auth/X509Claim.scala
src/main/scala/plan.scala
--- a/README.markdown	Wed Oct 12 15:26:31 2011 +0200
+++ b/README.markdown	Thu Oct 13 21:50:21 2011 +0200
@@ -77,10 +77,11 @@
 ----------------
 
 ### to run on https with WebID
+    1. make a directory called tmp 
+    2. lauch
+    > java -Djetty.ssl.keyStoreType=JKS -Djetty.ssl.keyStore=/Users/hjs/tmp/cert/KEYSTORE.jks -Djetty.ssl.keyStorePassword=secret -jar target/read-write-web.jar --https 8443 tmp /2011/09
 
-    > 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'
+### to enable debug add the following parameters after 'java'
 
      -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
 
--- a/project/build.scala	Wed Oct 12 15:26:31 2011 +0200
+++ b/project/build.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -15,7 +15,7 @@
         <exclude org="net.databinder" module="dispatch-mime_2.9.0-1"/>
       </dependency>
     </dependencies>
-  val slf4jSimple = "org.slf4j" % "slf4j-simple" % "1.5.8"
+  val slf4jSimple = "org.slf4j" % "slf4j-simple" % "1.6"
   val antiXML = "com.codecommit" %% "anti-xml" % "0.4-SNAPSHOT" % "test"
   val jena = "com.hp.hpl.jena" % "jena" % "2.6.4"
   val arq = "com.hp.hpl.jena" % "arq" % "2.8.8"
@@ -23,6 +23,7 @@
   val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.2"
   val jsslutils = "org.jsslutils" % "jsslutils" % "1.0.7"
   val argot =  "org.clapper" %% "argot" % "0.3.5"
+  val guava =  "com.google.guava" % "guava" % "10.0.1"
 }
 
 // some usefull repositories
@@ -88,6 +89,7 @@
       libraryDependencies += scalaz,
       libraryDependencies += jsslutils,
       libraryDependencies += argot,
+      libraryDependencies += guava,
 
       jarName in assembly := "read-write-web.jar"
     )
--- a/src/main/scala/AuthFilter.scala	Wed Oct 12 15:26:31 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,378 +0,0 @@
-/*
- * 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(implicit webCache: WebCache) 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)(implicit webCache: WebCache) 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)(implicit cache: WebCache) {
-
-	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 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	Wed Oct 12 15:26:31 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-package org.w3.readwriteweb
-
-import javax.servlet._
-import javax.servlet.http._
-import unfiltered.request._
-import unfiltered.response._
-import unfiltered.jetty._
-
-import java.io._
-import scala.io.Source
-import java.net.URL
-
-import org.slf4j.{Logger, LoggerFactory}
-
-import com.hp.hpl.jena.rdf.model._
-import com.hp.hpl.jena.query._
-import com.hp.hpl.jena.update._
-import org.w3.readwriteweb.Resource
-import Query.{QueryTypeSelect => SELECT, QueryTypeAsk => ASK,
-              QueryTypeConstruct => CONSTRUCT, QueryTypeDescribe => DESCRIBE}
-
-import scalaz._
-import Scalaz._
-
-import org.w3.readwriteweb.util._
-import collection.mutable
-import webid.AuthFilter
-class ReadWriteWeb(rm: ResourceManager) {
-  
-  val logger:Logger = LoggerFactory.getLogger(this.getClass)
-
-  def isHTML(accepts:List[String]):Boolean = {
-    val accept = accepts.headOption
-    accept == Some("text/html") || accept == Some("application/xhtml+xml")
-  }
-  
-  /** I believe some documentation is needed here, as many different tricks
-   *  are used to make this code easy to read and still type-safe
-   *  
-   *  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 ... => ...
-   *  
-   *  this code makes use of the Validation monad. For example of how to use it, see
-   *  http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html
-   *  
-   *  the Resource abstraction returns Validation[Throwable, ?something]
-   *  we use the for monadic constructs.
-   *  Everything construct are mapped to Validation[ResponseFunction, ResponseFuntion],
-   *  the left value always denoting the failure. Hence, the rest of the for-construct
-   *  is not evaluated, but let the reader of the code understand clearly what's happening.
-   *  
-   *  This mapping is made possible with the failMap method. I couldn't find an equivalent
-   *  in the ScalaZ API so I made my own through an implicit.
-   *  
-   *  At last, Validation[ResponseFunction, ResponseFuntion] is exposed as a ResponseFunction
-   *  through another implicit conversion. It saves us the call to the Validation.lift() method
-   */
-  val read = unfiltered.filter.Planify {
-    case req @ Path(path) if path startsWith rm.basePath => {
-      val baseURI = req.underlying.getRequestURL.toString
-      val r:Resource = rm.resource(new URL(baseURI))
-      req match {
-        case GET(_) & Accept(accepts) if isHTML(accepts) => {
-          val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
-          val body = source.getLines.mkString("\n")
-          Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
-        }
-        case GET(_) | HEAD(_) =>
-          for {
-            model <- r.get() failMap { x => NotFound }
-            encoding = RDFEncoding(req)
-          } yield {
-            req match {
-              case GET(_) => Ok ~> ViaSPARQL ~> ContentType(encoding.toContentType) ~> ResponseModel(model, baseURI, encoding)
-              case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(encoding.toContentType)
-            }
-          }
-        case PUT(_) =>
-          for {
-            bodyModel <- modelFromInputStream(Body.stream(req), baseURI) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
-            _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
-          } yield Created
-        case POST(_) => {
-          Post.parse(Body.stream(req), baseURI) match {
-            case PostUnknown => {
-              logger.info("Couldn't parse the request")
-              BadRequest ~> ResponseString("You MUST provide valid content for either: SPARQL UPDATE, SPARQL Query, RDF/XML, TURTLE")
-            }
-            case PostUpdate(update) => {
-              logger.info("SPARQL UPDATE:\n" + update.toString())
-              for {
-                model <- r.get() failMap { t => NotFound }
-                // TODO: we should handle an error here
-                _ = UpdateAction.execute(update, model)
-                _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
-              } yield Ok
-            }
-            case PostRDF(diffModel) => {
-              logger.info("RDF content:\n" + diffModel.toString())
-              for {
-                model <- r.get() failMap { t => NotFound }
-                // TODO: we should handle an error here
-                _ = model.add(diffModel)
-                _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
-              } yield Ok
-            }
-            case PostQuery(query) => {
-              logger.info("SPARQL Query:\n" + query.toString())
-              lazy val encoding = RDFEncoding(req)
-              for {
-                model <- r.get() failMap { t => NotFound }
-              } yield {
-                val qe:QueryExecution = QueryExecutionFactory.create(query, model)
-                query.getQueryType match {
-                  case SELECT =>
-                    Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execSelect())
-                  case ASK =>
-                    Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execAsk())
-                  case CONSTRUCT => {
-                    val result:Model = qe.execConstruct()
-                    Ok ~> ContentType(encoding.toContentType) ~> ResponseModel(model, baseURI, encoding)
-                  }
-                  case DESCRIBE => {
-                    val result:Model = qe.execDescribe()
-                    Ok ~> ContentType(encoding.toContentType) ~> ResponseModel(model, baseURI, encoding)
-                  }
-                }
-              }
-            }
-          }
-        }
-        case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
-      }
-    }
-
-  }
-
-}
-
-
-object ReadWriteWebMain {
-  import org.clapper.argot._
-  import ArgotConverters._
-
-  val logger:Logger = LoggerFactory.getLogger(this.getClass)
-
-  val postUsageMsg= Some("""
-  |PROPERTIES
-  |
-  | * Keystore properties that need to be set if https is started
-  |  -Djetty.ssl.keyStoreType=type : the type of the keystore, JKS by default usually
-  |  -Djetty.ssl.keyStore=path : specify path to key store (for https server certificate)
-  |  -Djetty.ssl.keyStorePassword=password : specify password for keystore store (optional)
-  |
-  |NOTES
-  |
-  |  - Trust stores are not needed because we use the WebID protocol, and client certs are nearly never signed by CAs
-  |  - one of --http or --https must be selected
-     """.stripMargin);
-
-  val parser = new ArgotParser("read-write-web",postUsage=postUsageMsg)
-
-  val mode = parser.option[RWWMode](List("mode"), "m", "wiki mode: wiki or strict") {
-      (sValue, opt) =>
-        sValue match {
-          case "wiki" => AllResourcesAlreadyExist
-          case "strict" => ResourcesDontExistByDefault
-          case _ => throw new ArgotConversionException("Option %s: must be either wiki or strict" format (opt.name, sValue))
-        }
-      }
-
-    val rdfLanguage = parser.option[String](List("language"), "l", "RDF language: n3, turtle, or rdfxml") {
-      (sValue, opt) =>
-        sValue match {
-          case "n3" => "N3"
-          case "turtle" => "N3"
-          case "rdfxml" => "RDF/XML-ABBREV"
-          case _ => throw new ArgotConversionException("Option %s: must be either n3, turtle or rdfxml" format (opt.name, sValue))
-      }
-  }
-
-    val httpPort = parser.option[Int]("http", "Port","start the http server on port")
-    val httpsPort = parser.option[Int]("https","port","start the https server on port")
-
-    val rootDirectory = parser.parameter[File]("rootDirectory", "root directory", false) {
-      (sValue, opt) => {
-        val file = new File(sValue)
-        if (! file.exists)
-          throw new ArgotConversionException("Option %s: %s must be a valid path" format (opt.name, sValue))
-        else
-          file
-    }
-    }
-
-   implicit val webCache = new WebCache()
-
-
-   val baseURL = parser.parameter[String]("baseURL", "base URL", false)
-
-
-  // regular Java main
-  def main(args: Array[String]) {
-
-    try {
-       parser.parse(args)
-     } catch {
-       case e: ArgotUsageException => println(e.message); System.exit(1)
-    }
-
-    val filesystem =
-        new Filesystem(
-          rootDirectory.value.get,
-          baseURL.value.get,
-          lang=rdfLanguage.value getOrElse "N3")(mode.value getOrElse ResourcesDontExistByDefault)
-    val app = new ReadWriteWeb(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) => HttpsTrustAll(port,"0.0.0.0")
-      case None => Http(httpPort.value.get)
-    }
-
-    // configures and launches a Jetty server
-    service.filter {
-      // a jee Servlet filter that logs HTTP requests
-      new Filter {
-        def destroy():Unit = ()
-        def doFilter(request:ServletRequest, response:ServletResponse, chain:FilterChain):Unit = {
-          val r:HttpServletRequest = request.asInstanceOf[HttpServletRequest]
-          val method = r.getMethod
-          val uri = r.getRequestURI 
-          logger.info("%s %s" format (method, uri))
-          chain.doFilter(request, response)
-        }
-        def init(filterConfig:FilterConfig):Unit = ()
-      }
-    // Unfiltered filters
-    }.filter(new AuthFilter)
-     .context("/public"){ ctx:ContextBuilder =>
-      ctx.resources(MyResourceManager.fromClasspath("public/").toURI.toURL)
-    }.filter(app.read).run()
-    
-  }
-
-}
-
--- a/src/main/scala/ReadWriteWebMain.scala	Wed Oct 12 15:26:31 2011 +0200
+++ b/src/main/scala/ReadWriteWebMain.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -94,7 +94,7 @@
 
     // configures and launches a Jetty server
     service.filter(new FilterLogger(logger)).
-      filter(new webid.AuthFilter).
+      filter(new auth.Authn).
       context("/public"){ ctx:ContextBuilder =>
       ctx.resources(ClasspathUtils.fromClasspath("public/").toURI.toURL)
     }.filter(app.plan).run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/Authn.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -0,0 +1,70 @@
+/*
+ * 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.auth
+
+import java.security.cert.X509Certificate
+import javax.servlet._
+import org.w3.readwriteweb._
+
+import collection.JavaConversions._
+import javax.security.auth.Subject
+import java.security.PrivilegedExceptionAction
+import java.util.concurrent.TimeUnit
+import com.google.common.cache.{CacheBuilder, Cache, CacheLoader}
+
+class Authn(implicit webCache: WebCache) extends Filter {
+  def init(filterConfig: FilterConfig) {}
+
+  val idCache: Cache[X509Certificate, X509Claim] =
+    CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).
+      build(new CacheLoader[X509Certificate, X509Claim]() {
+        def load(x509: X509Certificate) = new X509Claim(x509)
+    })
+
+
+  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 = idCache.get(certChain.get(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)
+    }
+    try {
+      Subject.doAs(subject,new PrivilegedExceptionAction[Unit]() { def run(): Unit = chain.doFilter(request, response) } )
+    } catch {
+      case e: Exception => System.err.println("cought "+e)
+    }
+//    chain.doFilter(request, response)
+  }
+
+  def destroy() {}
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/Principals.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -0,0 +1,47 @@
+/*
+ * 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.auth
+
+import java.security.Principal
+
+/**
+ * @author hjs
+ * @created: 13/10/2011
+ */
+
+/**
+ * @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.
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/WebIdClaim.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -0,0 +1,239 @@
+/*
+ * 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.auth
+
+import com.hp.hpl.jena.rdf.model.RDFNode
+import java.math.BigInteger
+import java.security.PublicKey
+import org.w3.readwriteweb.WebCache
+import java.util.LinkedList
+import java.security.interfaces.RSAPublicKey
+import java.net.URL
+import com.hp.hpl.jena.query.{QueryExecutionFactory, QueryExecution, QuerySolutionMap, QueryFactory}
+
+/**
+ * @author hjs
+ * @created: 13/10/2011
+ */
+
+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)(implicit cache: WebCache)  {
+
+	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 org.w3.readwriteweb.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 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/X509Claim.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -0,0 +1,115 @@
+/*
+ * 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.auth
+
+
+
+import org.slf4j.LoggerFactory
+import java.security.cert.X509Certificate
+import org.w3.readwriteweb.WebCache
+import javax.security.auth.Refreshable
+import java.util.Date
+import collection.JavaConversions._
+
+
+/**
+ * @author hjs
+ * @created: 13/10/2011
+ */
+
+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)(implicit webCache: WebCache) 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))
+
+}
+
--- a/src/main/scala/plan.scala	Wed Oct 12 15:26:31 2011 +0200
+++ b/src/main/scala/plan.scala	Thu Oct 13 21:50:21 2011 +0200
@@ -1,20 +1,3 @@
-/*
- * 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 org.w3.readwriteweb.util._