Creating a much more user friendly IDP. Required a few things: that we be able to get a certificate without forcing a fetch of it - on entry to the site for example. webid
authorHenry Story <henry.story@bblfish.net>
Sun, 18 Dec 2011 14:57:55 +0100
branchwebid
changeset 150 ae0e626ba6c4
parent 149 893ac9c9c019
child 151 452667628c73
Creating a much more user friendly IDP. Required a few things: that we be able to get a certificate without forcing a fetch of it - on entry to the site for example.
src/main/resources/template/NoWebId.xhtml
src/main/scala/auth/WebIDSrvc.scala
src/main/scala/auth/WebIdClaim.scala
src/main/scala/auth/X509Cert.scala
src/main/scala/auth/X509Claim.scala
src/main/scala/auth/X509view.scala
src/main/scala/auth/earl.scala
src/main/scala/netty/ReadWriteWebNetty.scala
src/main/scala/sommer/GraphReader.scala
src/main/scala/sommer/ResourceReader.scala
--- a/src/main/resources/template/NoWebId.xhtml	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/resources/template/NoWebId.xhtml	Sun Dec 18 14:57:55 2011 +0100
@@ -3,22 +3,6 @@
   ~ under the MIT licence defined at
   ~    http://www.opensource.org/licenses/mit-license.html
   ~
-  ~ Permission is hereby granted, free of charge, to any person obtaining a copy of
-  ~ this software and associated documentation files (the "Software"), to deal in the
-  ~ Software without restriction, including without limitation the rights to use, copy,
-  ~ modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
-  ~ and to permit persons to whom the Software is furnished to do so, subject to the
-  ~ following conditions:
-  ~
-  ~ The above copyright notice and this permission notice shall be included in all
-  ~ copies or substantial portions of the Software.
-  ~
-  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-  ~ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-  ~ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-  ~ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-  ~ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-  ~ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
--- a/src/main/scala/auth/WebIDSrvc.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/WebIDSrvc.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -25,20 +25,29 @@
 
 import java.io.File
 import unfiltered.Cycle
-import xml.{Elem, XML}
-import unfiltered.response.{Html, Ok}
-import org.fusesource.scalate.scuery.Transformer
-import unfiltered.request.{Params, Path}
-import unfiltered.request.&
 import java.net.{URLEncoder, URL}
 import java.util.Calendar
 import org.apache.commons.codec.binary.Base64
-import org.w3.readwriteweb.auth.{WebID, X509CertSigner, X509Claim}
 import java.text.SimpleDateFormat
 import org.w3.readwriteweb.util.trySome
 import unfiltered.request.Params.ParamMapper
-import com.hp.hpl.jena.sparql.vocabulary.FOAF
-import com.hp.hpl.jena.rdf.model.{RDFNode, ModelFactory}
+import com.hp.hpl.jena.rdf.model.ModelFactory
+import sommer.{CertAgent, Extractors}
+import org.w3.readwriteweb.util._
+import org.w3.readwriteweb.auth._
+import unfiltered.request._
+import org.fusesource.scalate.scuery.{Transform, Transformer}
+import org.w3.readwriteweb.netty.ReadWriteWebNetty.StaticFiles
+import java.lang.String
+import xml._
+import unfiltered.response._
+
+object WebIDSrvc {
+  val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+
+  val noImage = "idp/profile_anonymous.png" //"http://eagereyes.org/media/2010/empty-frame.jpg"
+
+}
 
 /**
  * @author Henry Story
@@ -48,74 +57,147 @@
   implicit def manif: Manifest[Req]
   val signer: X509CertSigner
 
-
-  private val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+  import WebIDSrvc._
 
-    /**
-     * @param webid  a list of webIds identifying the user (only the first few will be  used)
-     * @param replyTo the service that the response is sent to
-     * @return the URL of the response with the webid, timestamp appended and signed
-     */
-  private def createSignedResponse(webid: Seq[WebID], replyTo: URL): URL = {
-      var uri = replyTo.toExternalForm+"?"+ webid.slice(0,3).foldRight("") {
-        (wid,str) => "webid="+  URLEncoder.encode(wid.url.toExternalForm, "UTF-8")+"&"
-      }
-      uri = uri + "ts=" + URLEncoder.encode(dateFormat.format(Calendar.getInstance.getTime), "UTF-8")
-      val signedUri =	uri +"&sig=" + URLEncoder.encode(new String(Base64.encodeBase64URLSafeString(signer.sign(uri))), "UTF-8")
-      return new URL(signedUri)
+
+
+  def sign(urlStr: String): URL = {
+    val timeStampedUrlStr = urlStr + "ts=" + URLEncoder.encode(dateFormat.format(Calendar.getInstance.getTime), "UTF-8")
+    val signedUri = timeStampedUrlStr +
+      "&sig=" + URLEncoder.encode(new String(Base64.encodeBase64URLSafeString(signer.sign(timeStampedUrlStr))), "UTF-8")
+    return new URL(signedUri)
   }
 
-  val fileDir: File = new File(this.getClass.getResource("/template/").toURI)
 
-  lazy val webidSrvcPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.login.xhtml"))
-  lazy val aboutPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.about.xhtml"))
+  val fileDir: File = new File(this.getClass.getResource("/template/webidp/").toURI)
+
+  /**
+   * using three different templates uses up more memory for the moment, and could be more maintenance work
+   * if all three templates require similar changes, but it makes it easier to visualise the result without
+   * needing a web server. 
+   */
+  lazy val errorPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.badcert.html"))
+  lazy val authenticatedPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.auth.html"))
+  lazy val aboutPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.about.html"))
+  lazy val profilePg: Elem = XML.loadFile(new File(fileDir, "WebIdService.entry.html"))
 
   def intent : Cycle.Intent[Req,Res] = {
-    case req @ Path(path) if path.startsWith("/srv/idp")  => req match {
-      case Params(RelyingParty(rp)) & X509Claim(claim) => Ok ~> Html( new ServiceTrans(rp,claim).apply(webidSrvcPg) )
-      case _ => Ok ~> Html(aboutTransform(aboutPg))
+    case req @ Path(Seg("srv" :: "idp":: file :: Nil)) => srvStaticFiles(file)
+    case req @ Path("/srv/idp")  => req match {
+      case Params(RelyingParty(rp)) => req match {
+          // we authenticate the user only if he has agreed to be authenticated on the page, which we know if the
+          // request is a POST
+          case POST(_) & X509Claim(claim: X509Claim) => { //repetition because of intellij scala 0.5.273 bug
+            val pg = if ( claim.verified.size > 0 ) authenticatedPg else errorPg 
+            Ok ~> Html5(new ServiceTrans(rp,claim).apply(pg))
+          }
+          // nevertheless the user may have authenticated allready
+          case GET(_) & XClaim(claim: XClaim) => {
+            val pg = claim match {
+              case NoClaim => profilePg
+              case claim: X509Claim => if ( claim.verified.size > 0 ) authenticatedPg else errorPg
+            }
+            Ok ~> Html5(new ServiceTrans(rp,claim).apply(pg))
+          }
+      }
+      case _ => Ok ~> Html5(aboutTransform(aboutPg))
     }
 
   }
-  
-  object aboutTransform extends Transformer
 
 
-  class ServiceTrans(relyingParty: URL, claim: X509Claim) extends Transformer {
-    val union = claim.verified.flatMap(_.getDefiningModel.toOption).fold(ModelFactory.createDefaultModel()){
-      (m1,m2)=>m1.add(m2)
+
+  object TransUtils {
+    //taken from http://stackoverflow.com/questions/2569580/how-to-change-attribute-on-scala-xml-element
+    implicit def addGoodCopyToAttribute(attr: Attribute) = new {
+      def goodcopy(key: String = attr.key, value: Any = attr.value): Attribute =
+        Attribute(attr.pre, key, Text(value.toString), attr.next)
     }
-    $(".user_name").contents =
-      if (claim.verified.size==0)
-          claim.cert.getSubjectDN.getName //we have something, we can't rely on it, but we can use it
-      else {
-        val names = union.listObjectsOfProperty(union.createResource(claim.verified.head.url.toExternalForm),FOAF.name)
-        if (!names.hasNext) "nonname"
-        else {
-          val node: RDFNode = names.next()
-          if (node.isLiteral) node.asLiteral().getLexicalForm
-          else "anonymous"
-        }
-      }
 
-    $(".mugshot").attribute("src").value =
-      if (claim.verified.size==0)
-         "http://www.yourbdnews.com/wp-content/uploads/2011/08/anonymous.jpg"
-      else {
-        val names = union.listObjectsOfProperty(union.createResource(claim.verified.head.url.toExternalForm),FOAF.depiction)
-        if (!names.hasNext) "http://www.yourbdnews.com/wp-content/uploads/2011/08/anonymous.jpg"
-        else {
-         names.next.toString
-        }
+    implicit def iterableToMetaData(items: Iterable[MetaData]): MetaData = {
+      items match {
+        case Nil => Null
+        case head :: tail => head.copy(next=iterableToMetaData(tail))
       }
-
-    $(".rp_name").contents = relyingParty.getHost
-    $(".rp_url").attribute("href").value = createSignedResponse(claim.verified,relyingParty).toExternalForm
+    }
   }
 
+  object srvStaticFiles extends StaticFiles {
+    override def toLocal(file: String) = "/template/webidp/idp/"+file
+  }
+  
+  object aboutTransform extends Transformer //todo: need to change public keys in template
+
+  class ServiceTrans(relyingParty: URL, claim: XClaim) extends Transformer {
+    $(".webidform") { node =>
+      val elem = node.asInstanceOf[scala.xml.Elem]
+      import TransUtils._
+      val newelem = elem.copy(attributes=for(attr <- elem.attributes) yield attr match {
+         case [email protected]("action", _, _) => attr.goodcopy(value="/srv/idp?rs="+relyingParty.toExternalForm)
+         case other => other
+      })
+      new Transform(newelem) {
+        $(".authenticated") {  node =>
+          claim match {
+            case NoClaim => <span/>
+            case _ => new Transform(node) {
+              val union = claim.verified.flatMap(_.getDefiningModel.toOption).fold(ModelFactory.createDefaultModel()) {
+                (m1, m2) => m1.add(m2)
+              }
+              //this works because we have verified before
+              val person = if (union.size() > 0) Extractors.namedPerson(union, claim.verified.head.url)
+              else new CertAgent(claim.cert.getSubjectDN.getName)
+              $(".user_name").contents = person.name
+              $(".mugshot").attribute("src").value = person.depictions.collectFirst {
+                case o => o.toString
+              }.getOrElse(noImage)
+            }.toNodes()
+          }
+        }
+        $(".error").contents = {
+          if (claim==NoClaim) "We received no Certificate"
+          else if (claim.claims.size==0) "Certificate contained no WebID"
+          else if (claim.verified.size==0) "Could not verify any of the WebIDs"
+          else "Some error occured"
+        }
+        $(".response") { nodeRes =>
+          val elemRes = nodeRes.asInstanceOf[scala.xml.Elem]
+          val newElem = elemRes.copy(attributes=for(attr <- elemRes.attributes) yield attr match {
+            case [email protected]("href", _, _) => attr.goodcopy(value= {
+              val answer = if (claim == NoClaim) "error=nocert"
+              else if (claim.claims.size == 0) "error=noWebID"
+              else if (claim.verified.size == 0) "error=noVerifiedWebID" +
+                claim.claims.map(claim => claim.verify.failMap(e => e.getMessage)).mkString("&cause=")
+              else claim.verified.slice(0, 3).foldRight("") {
+                (wid, str) => "webid=" + URLEncoder.encode(wid.url.toExternalForm, "UTF-8") + "&"
+              }
+              sign(relyingParty.toExternalForm + "?" + answer).toExternalForm
+            })
+            case other => other
+          })
+          new Transform(newElem) {
+            $(".sitename").contents = relyingParty.getHost
+          }.toNodes()
+        }
+
+      }.toNodes()
+    }
+  }
 }
 
-/**
+case class Html5(nodes: scala.xml.NodeSeq) extends ComposeResponse(HtmlContent ~> {
+  val w = new java.io.StringWriter()
+  val html = nodes.head match {
+    case <html>{_*}</html> => nodes.head
+    case _ => <html>{nodes}</html>
+  }
+  xml.XML.write( w, html, "UTF-8", xmlDecl = false, doctype =
+    xml.dtd.DocType( "html", xml.dtd.SystemID( "about:legacy-compat" ), Nil ))
+  ResponseString(w.toString)
+})
+
+
+  /**
  * similar to Extract superclass, but is useful when one has a number of attributes that
  * have the same meaning. This can arise if one has legacy URLS or if one has code that
  * has human readable and shorter ones
@@ -128,5 +210,4 @@
 
 object urlMap extends ParamMapper(_.flatMap(u=>trySome{new URL(u)}).headOption)
 object RelyingParty extends ExtractN(List("rs","authreqissuer"), urlMap )
-
-
+object Login extends Params.Extract("login",_=>Some(true))
--- a/src/main/scala/auth/WebIdClaim.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/WebIdClaim.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -35,8 +35,7 @@
 
 /**
  * @author Henry Story
- * @created: 13/10/2011
- */
+ **/
 
 /**
  * One can only construct a WebID via the WebIDClaim apply
--- a/src/main/scala/auth/X509Cert.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509Cert.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -192,11 +192,11 @@
 
 object Certs {
 
-  def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[IndexedSeq[Certificate]] = {
+  def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T], fetch: Boolean=true): Option[IndexedSeq[Certificate]] = {
     if (m <:< manifest[HttpServletRequest])
       unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
     else if (m <:< manifest[ReceivedMessage])
-      unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]])
+      unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]],fetch)
     else
       None //todo: should  throw an exception here?
   }
@@ -210,24 +210,27 @@
       case _ => None
     }
   
-  private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T]): Option[IndexedSeq[Certificate]] = {
+  private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T], fetch: Boolean): Option[IndexedSeq[Certificate]] = {
 
     import org.jboss.netty.handler.ssl.SslHandler
     
     val sslh = r.underlying.context.getPipeline.get(classOf[SslHandler])
     
     trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq) orElse {
-      sslh.setEnableRenegotiation(true)
-      r match {
-        case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
-        case _ => sslh.getEngine.setWantClientAuth(true)  
+      if (!fetch) None
+      else {
+        sslh.setEnableRenegotiation(true)
+        r match {
+          case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
+          case _ => sslh.getEngine.setWantClientAuth(true)
+        }
+        val future = sslh.handshake()
+        future.await(30000) //that's certainly way too long.
+        if (future.isDone && future.isSuccess)
+          trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
+        else
+          None
       }
-      val future = sslh.handshake()
-      future.await(30000) //that's certainly way too long.
-      if (future.isDone && future.isSuccess)
-        trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
-      else
-        None
     }
 
   }
--- a/src/main/scala/auth/X509Claim.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509Claim.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -27,20 +27,15 @@
 
 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._
 import unfiltered.request.HttpRequest
 import java.security.interfaces.RSAPublicKey
 import collection.immutable.List
-import collection.mutable.HashMap
-import scalaz.{Scalaz, Success, Validation}
-import Scalaz._
-import java.security.PublicKey
 import com.google.common.cache.{CacheLoader, CacheBuilder, Cache}
 import java.util.concurrent.TimeUnit
 import org.w3.readwriteweb.util.trySome
+import java.util.Date
 
 /**
  * @author hjs
@@ -49,7 +44,7 @@
 
 object X509Claim {
   final val logger = LoggerFactory.getLogger(classOf[X509Claim])
-
+  implicit val fetch = true //fetch the certificate if we don't have it
 
 // this is cool because it is not in danger of running out of memory but it makes it impossible to create the claim
 // with an implicit  WebCache...
@@ -66,7 +61,6 @@
   }
 
 
-
   /**
    * Extracts the URIs in the subject alternative name extension of an X.509
    * certificate
@@ -80,13 +74,42 @@
       case coll if (coll != null) => {
         for {
           sanPair <- coll if (sanPair.get(0) == 6)
-        } yield sanPair(1).asInstanceOf[String]
+        } yield sanPair(1).asInstanceOf[String].trim
       }
       case _ => Nil
     }
 
 }
 
+object ExistingClaim {
+  implicit val fetch = false //don't fetch the certificate if we don't have it -- ie, don't force the fetching of it
+
+  def unapply[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[X509Claim] = r match {
+    case Certs(c1: X509Certificate, _*) => trySome(X509Claim.idCache.get(c1))
+    case _ => None
+  }
+  
+}
+
+object XClaim {
+  implicit val fetch = false
+  def unapply[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[XClaim] = r match {
+    case Certs(c1: X509Certificate, _*) => trySome(X509Claim.idCache.get(c1))
+    case _ => Some(NoClaim)
+  }
+}
+
+
+
+/**
+ * This looks like something that could be abstracted into type perhaps XClaim[X509Certificate,WebIDClaim]
+ */
+sealed trait XClaim {
+   def cert: X509Certificate
+   def valid: Boolean
+   def claims: List[WebIDClaim]
+   def verified: List[WebID]
+}
 
 /**
  * An X509 Claim maintains information about the proofs associated with claims
@@ -99,7 +122,7 @@
  * @created: 30/03/2011
  */
 // can't be a case class as it then creates object which clashes with defined one
-class X509Claim(val cert: X509Certificate) extends Refreshable {
+class X509Claim(val cert: X509Certificate) extends Refreshable with XClaim {
 
   import X509Claim._
   val claimReceivedDate = new Date()
@@ -107,9 +130,11 @@
   lazy val tooEarly = claimReceivedDate.before(cert.getNotBefore())
 
 
-  lazy val webidclaims: List[WebIDClaim] = getClaimedWebIds(cert) map { webid => new WebIDClaim(webid, cert.getPublicKey.asInstanceOf[RSAPublicKey]) }
+  lazy val claims: List[WebIDClaim] = getClaimedWebIds(cert) map { webid => 
+    new WebIDClaim(webid, cert.getPublicKey.asInstanceOf[RSAPublicKey]) 
+  }
 
-  lazy val verified: List[WebID] = webidclaims.flatMap(_.verify.toOption)
+  lazy val verified: List[WebID] = claims.flatMap(_.verify.toOption)
 
   //note could also implement Destroyable
   //
@@ -125,7 +150,7 @@
   /* The certificate is currently within the valid time zone */
   override def isCurrent(): Boolean = ! (tooLate || tooEarly)
 
-  def canEqual(other: Any) = other.isInstanceOf[X509Claim]
+  protected 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)
@@ -135,5 +160,18 @@
   override lazy val hashCode: Int =
     41 * (41 + (if (cert != null) cert.hashCode else 0))
 
+  def valid = isCurrent()
 }
 
+/**
+ *  A bit like a None for X509Claims
+ */
+case object NoClaim extends XClaim {
+  override def cert = throw new NoSuchElementException("None.get")
+
+  def valid = false
+
+  def claims = List.empty
+
+  def verified = List.empty
+}
\ No newline at end of file
--- a/src/main/scala/auth/X509view.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509view.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -89,7 +89,7 @@
   }
   $(".no_webid") { node => if (x509.verified.size==0) node else <span/> }
   $(".webid_test") { node =>
-    val ff = for (idclaim <- x509.webidclaims) yield {
+    val ff = for (idclaim <- x509.claims) yield {
       val idAsrt = new Assertion(webidClaimTst, idclaim)
       new Transform(node) {
         $(".webid").contents = idclaim.san
--- a/src/main/scala/auth/earl.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/earl.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -123,8 +123,8 @@
 }
 
 object certProvidedSan extends TestObj[X509Claim]("certificateProvidedSAN") {
-  def apply(x509: X509Claim) = new Result(" There are "+x509.webidclaims.size+" SANs in the certificate",
-    x509.webidclaims.size >0)
+  def apply(x509: X509Claim) = new Result(" There are "+x509.claims.size+" SANs in the certificate",
+    x509.claims.size >0)
 
 }
 
--- a/src/main/scala/netty/ReadWriteWebNetty.scala	Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -29,10 +29,12 @@
 import org.w3.readwriteweb.auth.{X509view, RDFAuthZ}
 import org.w3.readwriteweb._
 import org.jboss.netty.handler.codec.http.HttpResponse
-import unfiltered.netty.{ServerErrorResponse, ReceivedMessage, cycle}
 import unfiltered.request.Path
-import unfiltered.netty.async
-import unfiltered.response.{JsContent, NotFound, ResponseString, Ok}
+import java.io.{InputStream, OutputStream}
+import unfiltered.response._
+import unfiltered.netty._
+import collection.immutable.List
+import java.lang.String
 
 /**
  * ReadWrite Web for Netty server, allowing TLS renegotiation
@@ -78,19 +80,49 @@
      
    }
 
-  object publicStatic extends  cycle.Plan  with cycle.ThreadPool with ServerErrorResponse {
+
+  trait StaticFiles extends PartialFunction[String, ResponseFunction[Any]] {
+    /* override this if the local path is somehow different from the url path */
+    def toLocal(webpath: String): String = webpath
+    val extension = "([^\\.]*?)$".r
+    val extList: List[String] = List("css", "png")
+
+    private def toString(in: InputStream): String = {
+      val source = scala.io.Source.fromInputStream(in)
+      val lines = source.mkString
+      source.close()
+      lines
+    }
+
+    def isDefinedAt(path: String): Boolean = try {
+      val in = classOf[StaticFiles].getResourceAsStream(toLocal(path))
+      (in != null) & (extension.findFirstIn(path).exists(extList contains _))
+    } catch {
+      case _ => false
+    }
+
+    def apply(path: String): ResponseFunction[Any] = {
+      try {
+        val in = classOf[StaticFiles].getResourceAsStream(toLocal(path))
+        extension.findFirstIn(path).getOrElse("css") match {
+          case "css" => Ok ~> ResponseString(toString(in)) ~> CssContent
+          case "js" => Ok ~> ResponseString(toString(in)) ~> JsContent
+          case "png" => Ok ~> ResponseBin(in) ~> ContentType("image/png")
+        }
+      } catch {
+        case _ => NotFound
+      }
+
+    }
+
+
+  }
+
+  object publicStatic  extends  cycle.Plan  with cycle.ThreadPool with ServerErrorResponse with StaticFiles {
+    val initialPath= "/public"
+
     def intent = {
-      case Path(path) if path.startsWith("/public") => {
-        try {
-          val in = publicStatic.getClass.getResourceAsStream(path)
-          val source = scala.io.Source.fromInputStream(in)
-          val lines = source.mkString
-          source.close()
-          Ok ~> ResponseString(lines) ~> JsContent //currently that's all I am interested in, but of course this is not right.
-        } catch {
-          case _ => NotFound
-        }
-      }
+      case Path(path) if path.startsWith(initialPath) => apply(path)
     }
   }
 
@@ -105,3 +137,14 @@
 
 }
 
+
+case class ResponseBin(bis: InputStream) extends ResponseStreamer {
+  override def stream(out: OutputStream) {
+    var c=0
+    val buf = new Array[Byte](1024)
+    do {
+      c = bis.read(buf)
+      if (c > 0) out.write(buf,0,c)
+    } while (c > -1)
+  }
+}
\ No newline at end of file
--- a/src/main/scala/sommer/GraphReader.scala	Sun Dec 11 20:48:10 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-/*
- * Copyright (c) 2011 Henry Story (bblfish.net)
- * under the MIT licence defined at
- *    http://www.opensource.org/licenses/mit-license.html
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in the
- * Software without restriction, including without limitation the rights to use, copy,
- * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so, subject to the
- * following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
- * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-package sommer
-
-import com.hp.hpl.jena.vocabulary.RDF
-import com.hp.hpl.jena.sparql.vocabulary.FOAF
-import java.lang.String
-import org.w3.readwriteweb.{Resource, WebCache}
-import scalaz.Validation
-import java.net.{URI, URL}
-import collection._
-import com.hp.hpl.jena.rdf.model.{RDFNode, Literal, ResourceFactory, ModelFactory, Resource => JResource, Model}
-import collection.JavaConverters._
-
-
-/**
- * Some initial ideas on mappers between rdf and scala classes
- * exploring ideas from
- * http://dl.dropbox.com/u/7810909/docs/reader-monad/chunk-html/index.html
- *
- * @author Henry Story
- */
-
-case class GraphReader[A](extract: Resource => Validation[scala.Throwable,A]) {
-  def map[B](g: A => B) = GraphReader(r => extract(r).map(g(_)))
-  def flatMap[B](g: A => GraphReader[B]): GraphReader[B] = GraphReader(r => extract(r).map(g(_)).flatMap(_.extract(r)))
-}
-
-case class Person(name: String)
-
-case class IdPerson(webid: JResource) {
-
-  // this map is no longer necessary if we use the graph mapped resource
-  val relations : mutable.MultiMap[JResource,RDFNode] = new mutable.HashMap[JResource, mutable.Set[RDFNode]] with  mutable.MultiMap[JResource,RDFNode]
-  cacheProp
-
-  import Extractors.toProperty
-  // a couple of useful methods for foaf relations. One could add a few others for other vocabs
-  def foafRel(p: String) = relations.get(toProperty(FOAF.NS+p))
-
-  //very very simple implementation, not taking account of first/last name, languages etc...
-  def name = {
-    foafRel("name").mkString(" ")
-  }
-
-  /**
-   * Notice how here the graph has been mapped into the object, via the webid JResource that still
-   * has a pointer to the graph.
-   */
-  def cacheProp {
-    for (s <- webid.listProperties().asScala
-         if (!s.getObject.isAnon)
-    ) {
-      relations.addBinding(s.getPredicate, s.getObject)
-    }
-  }
-
-}
-
-
-object Extractors {
-  type Val[A] = Validation[scala.Throwable,A]
- 
-  def findPeople(m: Resource): Validation[scala.Throwable,Set[Person]] = {
-     for (gr<-m.get) yield {
-       for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
-        val subj = st.getSubject;
-        st2 <- gr.listStatements(subj, FOAF.name,null).asScala
-        ) yield {
-         new Person(st2.getObject.asLiteral().toString)
-        }
-     }.toSet
-  }
-
-  def findDefinedPeople(m: Resource): Validation[scala.Throwable,Set[IdPerson]] = {
-    for (gr<-m.get) yield {
-      for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
-           val subj = st.getSubject;
-           if (subj.isURIResource && subj.toString.startsWith(m.name.toString));
-           st2 <- gr.listStatements(subj, FOAF.name,null).asScala
-      ) yield {
-        new IdPerson(subj)
-      }
-    }.toSet
-  }
-  
-  def findIdPeople(m: Resource): Val[Set[IdPerson]] = {
-    for (gr<-m.get) yield {
-      for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
-           val subj = st.getSubject;
-           if (subj.isURIResource)
-      ) yield {
-        val p = new IdPerson(subj)
-
-        p
-      }
-    }.toSet
-    
-  } 
-  
-  implicit def toResource(str: String) = ResourceFactory.createResource(str)
-  implicit def toProperty(str: String) = ResourceFactory.createProperty(str)
-
-}
-
-object Test {
-  implicit def urlToResource(u: URL) = WebCache.resource(u)
-  import System._
-
-  val peopleRd = new GraphReader[Set[Person]](Extractors.findPeople)
-  val definedPeopleRd = new GraphReader[Set[IdPerson]](Extractors.findDefinedPeople)
-  val idPeopleRd = new GraphReader[Set[IdPerson]](Extractors.findIdPeople)
-  val definedPeopleFriends = definedPeopleRd.flatMap(people =>GraphReader[Set[IdPerson]]{
-    resource: Resource =>
-       resource.get.map(gr=>
-         for ( p <- people;
-               st <- gr.listStatements(p.webid, FOAF.knows, null).asScala ;
-              val friend = st.getObject;
-              if (friend.isURIResource)
-         ) yield IdPerson(friend.asInstanceOf[JResource])
-       )
-  } )
-  
-  def main(args: Array[String]) {
-
-    val url: String = "http://bblfish.net/people/henry/card"
-
-    // extract the people who are defined in the graph (rarely more than one)
-    for (people <- definedPeopleRd.extract(new URL(url));
-         p <- people) {
-      System.out.println("found "+p.name)
-    }
-    out.println
-
-    out.println("friends of people defined using flatmaped reader")
-    //use the flatMap to go from defined people to their friends
-    //get these friends names
-    for (people <- definedPeopleFriends.extract(new URL(url));
-         p <- people) {
-      System.out.println("found "+p.name)
-    }
-    out.println
-
-
-
-    // extract all the people with ids.
-    // and show all their properties
-    // this produces a lot of data so its commented out
-//    out.println("=== ID PEOPLE ===")
-//    out.println
-//    for (people <- idPeopleRd.extract(new URL(url));
-//         p <- people) {
-//      out.println
-//      out.println("----------")
-//      out.println("id "+p.webid)
-//      out.println("with properties:")
-//      val str = for ((prop,setovals) <- p.relations.iterator) yield {
-//        setovals.map(n=>prop.getLocalName+" is "+n.toString).mkString("\n")
-//      }
-//      out.print(str.mkString("\r\n"))
-//    }
-
-  }
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/sommer/ResourceReader.scala	Sun Dec 18 14:57:55 2011 +0100
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined at
+ *    http://www.opensource.org/licenses/mit-license.html
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in the
+ * Software without restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package sommer
+
+import com.hp.hpl.jena.vocabulary.RDF
+import com.hp.hpl.jena.sparql.vocabulary.FOAF
+import java.lang.String
+import org.w3.readwriteweb.{Resource, WebCache}
+import scalaz.Validation
+import java.net.URL
+import collection._
+import collection.JavaConverters._
+import com.hp.hpl.jena.rdf.model.{Model, ResourceFactory, Resource => JResource}
+import java.security.cert.X509Certificate
+
+
+/**
+ * Some initial ideas on mappers between rdf and scala classes
+ * exploring ideas from
+ * http://dl.dropbox.com/u/7810909/docs/reader-monad/chunk-html/index.html
+ *
+ * @author Henry Story
+ */
+
+
+/**
+ * Resource readers are Monads
+ * These function directly on types given by resources specified by URIs
+ * But the map from resource to validation, makes things a bit heavy.
+ */
+case class ResourceReader[A](extract: Resource => Validation[scala.Throwable,A]) {
+  def map[B](g: A => B) = ResourceReader(r => extract(r).map(g(_)))
+  def flatMap[B](g: A => ResourceReader[B]): ResourceReader[B] = ResourceReader(r => extract(r).map(g(_)).flatMap(_.extract(r)))
+}
+
+trait Agent {
+  def name: String
+  def foaf(attr: String): Iterator[AnyRef] = Iterator.empty
+  def depictions: Iterator[JResource] = Iterator.empty
+}
+
+object CertAgent {
+  val CNregex = "cn=(.*?),".r
+}
+
+/**
+ *Information about an agent gleaned from the certificate
+ *(One could generalise this by having a function from certificates to graphs)
+ **/
+class CertAgent(dn : String) extends Agent {
+  val name =  CertAgent.CNregex.findFirstMatchIn(dn).map(_.group(1)).getOrElse("(unnamed)")
+
+  override def foaf(attr: String) = if (attr == "name") Iterator(name) else Iterator.empty
+
+}
+
+case class Person(name: String)
+
+
+
+case class IdPerson(id: JResource) extends Agent {
+
+
+  import Extractors.toProperty
+  // a couple of useful methods for foaf relations. One could add a few others for other vocabs
+  override def foaf(attr: String) = id.listProperties(toProperty(FOAF.NS+attr)).asScala.map(_.getObject)
+
+  //very very simple implementation, not taking account of first/last name, languages etc...
+  override def name = {
+    foaf("name").mkString(" ")
+  }
+  
+  override def depictions = foaf("depiction").collect{case n if n.isURIResource => n.asResource()}
+
+}
+
+object ANONYMOUS extends Agent {
+  val pix = ResourceFactory.createResource("http://massivnews.com/wp-content/uploads/2011/07/Anonymous-000006.jpg")
+  override val name = "_ANONYMOUS_"
+  override def foaf(attr: String) = if (attr == "name") Iterator(name) else Iterator.empty
+  override val depictions = List(pix).iterator
+}
+
+
+object Extractors {
+  type Val[A] = Validation[scala.Throwable,A]
+ 
+  def findPeople(m: Resource): Validation[scala.Throwable,Set[Person]] = {
+     for (gr<-m.get) yield {
+       for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
+        val subj = st.getSubject;
+        st2 <- gr.listStatements(subj, FOAF.name,null).asScala
+        ) yield {
+         new Person(st2.getObject.asLiteral().toString)
+        }
+     }.toSet
+  }
+
+  def definedPeople(gr: Model, doc: URL): Iterator[IdPerson] = {
+    for (st <- gr.listStatements(null, RDF.`type`, FOAF.Person).asScala;
+         val subj = st.getSubject;
+         //todo: come up with a better definition of "is defined in"
+         if (subj.isURIResource && subj.toString.split("#")(0) == doc.toString.split("#")(0));
+         st2 <- gr.listStatements(subj, FOAF.name, null).asScala
+    ) yield {
+      new IdPerson(subj)
+    }
+  }
+
+  /**
+   * Argh. Jena does not make a good difference between read only models, and RW ones
+   * So one should verify the person exists before doing this if one does not want to create a RW model
+   */
+  def namedPerson(gr: Model, webid: URL): IdPerson = {
+      IdPerson(gr.createResource(webid.toString))
+  }
+
+  def findDefinedPeople(m: Resource): Validation[scala.Throwable,Set[IdPerson]] = {
+    for (gr<-m.get) yield {
+      definedPeople(gr, m.name)
+    }.toSet
+  }
+  
+  def findIdPeople(m: Resource): Val[Set[IdPerson]] = {
+    for (gr<-m.get) yield {
+      for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
+           val subj = st.getSubject;
+           if (subj.isURIResource)
+      ) yield {
+        val p = new IdPerson(subj)
+
+        p
+      }
+    }.toSet
+    
+  } 
+  
+  implicit def toResource(str: String) = ResourceFactory.createResource(str)
+  implicit def toProperty(str: String) = ResourceFactory.createProperty(str)
+
+}
+
+object Test {
+  implicit def urlToResource(u: URL) = WebCache.resource(u)
+  import System._
+
+  val peopleRd = new ResourceReader[Set[Person]](Extractors.findPeople)
+  val definedPeopleRd = new ResourceReader[Set[IdPerson]](Extractors.findDefinedPeople)
+  val idPeopleRd = new ResourceReader[Set[IdPerson]](Extractors.findIdPeople)
+  val definedPeopleFriends = definedPeopleRd.flatMap(people =>ResourceReader[Set[IdPerson]]{
+    resource: Resource =>
+       resource.get.map(gr=>
+         for ( p <- people;
+               st <- gr.listStatements(p.id, FOAF.knows, null).asScala ;
+              val friend = st.getObject;
+              if (friend.isURIResource)
+         ) yield IdPerson(friend.asInstanceOf[JResource])
+       )
+  } )
+  
+  def main(args: Array[String]) {
+
+    val url: String = "http://bblfish.net/people/henry/card"
+
+    // extract the people who are defined in the graph (rarely more than one)
+    for (people <- definedPeopleRd.extract(new URL(url));
+         p <- people) {
+      System.out.println("found "+p.name)
+    }
+    out.println
+
+    out.println("friends of people defined using flatmaped reader")
+    //use the flatMap to go from defined people to their friends
+    //get these friends names
+    for (people <- definedPeopleFriends.extract(new URL(url));
+         p <- people) {
+      System.out.println("found "+p.name)
+    }
+    out.println
+
+
+
+    // extract all the people with ids.
+    // and show all their properties
+    // this produces a lot of data so its commented out
+//    out.println("=== ID PEOPLE ===")
+//    out.println
+//    for (people <- idPeopleRd.extract(new URL(url));
+//         p <- people) {
+//      out.println
+//      out.println("----------")
+//      out.println("id "+p.webid)
+//      out.println("with properties:")
+//      val str = for ((prop,setovals) <- p.relations.iterator) yield {
+//        setovals.map(n=>prop.getLocalName+" is "+n.toString).mkString("\n")
+//      }
+//      out.print(str.mkString("\r\n"))
+//    }
+
+  }
+}
\ No newline at end of file