Reached a stable state & stumbled on a unfiltered issue where I need to point to this code on the mailing list.
--- 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._