--- a/.hgignore Sat Nov 12 08:29:00 2011 -0500
+++ b/.hgignore Sat Nov 12 08:33:10 2011 -0500
@@ -17,4 +17,6 @@
.scala_dependencies
*.orig
.cache
-.settings
\ No newline at end of file
+.settings
+.idea
+.idea_modules
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/INSTALL.txt Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,84 @@
+
+0. download code using mercurial, and switch to the webid branch
+ $ hg clone https://dvcs.w3.org/hg/read-write-web
+ $ cd read-write-web
+ $ hg checkout webid
+
+1. download java 6 or above for your Operating System - don't download just the JRE but also the java tools
+
+2. download scala http://www.scala-lang.org/
+
+3. download the scala build tool ( sbt ) from https://github.com/harrah/xsbt
+ I have an xsbt shell script that contains
+ java $SBT_PROPS -Xmx512m -Dfile.encoding=UTF-8 -jar `dirname $0`/jars/sbt-launch-0.11.0.jar "$@"
+
+4. set up some environmental variables for the https server to run on
+
+ export SBT_PROPS='-Djetty.ssl.keyStoreType=JKS -Djetty.ssl.keyStore=keys/KEYSTORE.jks -Djetty.ssl.keyStorePassword=secret'
+
+ Notice if you want to avoid browser warnings and you want to put this up on a public site then you need to get a CA signed certificate for your domain. There are providers that give those for free. There are protocols that will appear to make this no longer necessary
+
+5. run sbt, then start the server from the command line
+
+ $ sbt
+ > run --https 8443 test_www /2011/09
+
+ This will compile the server, start the https port on 8442 and use files in the test_www directory so that you can access them at https://localhost:8443/2011/09/
+
+6. connect to different resources
+
+6.1 GETing a public resource
+
+ $ curl -k https://localhost:8443/public/
+
+ If you access this via your browser and you have more than one webid certificate, your browser will ask you for even when you access this. We are working on a solution to stop this from happening.
+
+6.2 GETing a protected resource
+
+The following is a protected resource so if you access it without authentification credentials you will get
+
+ $ curl -i -k https://localhost:8443/2011/09/foaf.n3
+HTTP/1.1 401 Unauthorized
+Content-Length: 0
+Server: Jetty(7.2.2.v20101205)
+
+If you have a WebID enabled certificate ( http://webid.info/ ) with the public and private keys in a pem file, then you can access the resource with,
+
+ $ curl -E Certificates.pem -i -k https://localhost:8443/2011/09/foaf.n3
+
+since only Henry Story can view it as seen from the file
+
+ $ cat test_www/foaf.n3.protect.n3
+
+@prefix acl: <http://www.w3.org/ns/auth/acl#> .
+@prefix : <#> .
+
+:a1 a acl:Authorization;
+ acl:accessTo <foaf.n3>;
+ acl:mode acl:Read;
+ acl:agent <http://bblfish.net/people/henry/card#me> .
+
+If you want to give yourself access then replace Henry's WebID, "http://bblfish.net/people/henry/card#me" with your own, or make another example for that.
+
+6.3 Uploading a resource
+
+Say you wanted to upload an RDF file to the server
+
+$ curl http://bblfish.net/people/henry/card.rdf | curl -i -k -H "Content-Type: application/rdf+xml" -X PUT https://localhost:8443/2011/09/test2.rdf -T -
+HTTP/1.1 100 Continue
+
+ % Total % Received % Xferd Average Speed Time Time Time Current
+ Dload Upload Total Spent Left Speed
+100 24386 100 24386 0 0 2223 0 0:00:10 0:00:10 --:--:-- 33405
+HTTP/1.1 201 Created
+Content-Length: 0
+Server: Jetty(7.2.2.v20101205)
+
+the file will then be available as /2011/09/test2.rdf on the localhost server
+
+6.4 querying a resource with SPARQL
+
+You can write a SPARQL query and then query the given model by POSTing the query to the resource
+
+curl -k -i -H "Content-Type: application/sparql-query" -X POST https://localhost:8443/2011/09/test.rdf -T queryfriends.sparql
+
--- a/README.markdown Sat Nov 12 08:29:00 2011 -0500
+++ b/README.markdown Sat Nov 12 08:33:10 2011 -0500
@@ -73,3 +73,15 @@
* --strict Documents must be created using PUT else they return 404
+HTTPS with WebID
+----------------
+
+### 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
+
+### to enable debug add the following parameters after 'java'
+
+ -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/rwsbt.sh Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+KS=src/test/resources/KEYSTORE.jks
+while [ $# -gt 0 ]
+do
+ case $1 in
+ -d) PROPS="$PROPS -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
+ ;;
+ -n) PROPS="$PROPS -Dnetty.ssl.keyStoreType=JKS -Dnetty.ssl.keyStore=$KS -Dnetty.ssl.keyStorePassword=secret"
+ ;;
+ -j) PROPS="$PROPS -Djetty.ssl.keyStoreType=JKS -Djetty.ssl.keyStore=$KS -Djetty.ssl.keyStorePassword=secret"
+ ;;
+ -sslUnsafe) PROPS="$PROPS -Dsun.security.ssl.allowUnsafeRenegotiation=true"
+ ;; # see: http://download.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#workarounds
+ -sslLegacy) PROPS="$PROPS -Dsun.security.ssl.allowLegacyHelloMessages=true"
+ ;;
+ -sslDebug) PROPS="$PROPS -Djavax.net.debug=all"
+ ;; # see http://download.oracle.com/javase/1,5,0/docs/guide/security/jsse/ReadDebug.html
+ *) echo the arguments to use are -d
+ ;;
+ esac
+ shift 1
+ done
+
+
+export SBT_PROPS=$PROPS
+xsbt
--- a/project/build.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/project/build.scala Sat Nov 12 08:33:10 2011 -0500
@@ -5,28 +5,36 @@
// they are pulled only if used
object Dependencies {
val specs = "org.scala-tools.testing" %% "specs" % "1.6.9" % "test"
- val dispatch = "net.databinder" %% "dispatch-http" % "0.8.5" % "test"
- val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % "0.4.1"
- val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % "0.4.1"
+ val dispatch_http = "net.databinder" %% "dispatch-http" % "0.8.5"
+ val unfiltered_version = "0.5.1"
+ val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % unfiltered_version
+ val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % unfiltered_version
+ val unfiltered_netty = "net.databinder" %% "unfiltered-netty" % unfiltered_version
// val unfiltered_spec = "net.databinder" %% "unfiltered-spec" % "0.4.1" % "test"
val ivyUnfilteredSpec =
<dependencies>
- <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev="0.4.1">
+ <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev={unfiltered_version}>
<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"
val grizzled = "org.clapper" %% "grizzled-scala" % "1.0.8" % "test"
val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.2"
+
val argot = "org.clapper" %% "argot" % "0.3.5"
+ val guava = "com.google.guava" % "guava" % "10.0.1"
+// val restlet = "org.restlet.dev" % "org.restlet" % "2.1-SNAPSHOT"
+// val restlet_ssl = "org.restlet.dev" % "org.restlet.ext.ssl" % "2.1-SNAPSHOT"
+ val jsslutils = "org.jsslutils" % "jsslutils" % "1.0.5"
}
// some usefull repositories
object Resolvers {
val novus = "repo.novus snaps" at "http://repo.novus.com/snapshots/"
+ val mavenLocal = "Local Maven Repository" at "file://" + (Path.userHome / ".m2" / "repository").absolutePath
}
// general build settings
@@ -71,24 +79,31 @@
val projectSettings =
Seq(
+ resolvers += mavenLocal,
resolvers += ScalaToolsReleases,
resolvers += ScalaToolsSnapshots,
libraryDependencies += specs,
// libraryDependencies += unfiltered_spec,
ivyXML := ivyUnfilteredSpec,
- libraryDependencies += dispatch,
+ libraryDependencies += dispatch_http,
libraryDependencies += unfiltered_filter,
libraryDependencies += unfiltered_jetty,
+ libraryDependencies += unfiltered_netty,
// libraryDependencies += slf4jSimple,
libraryDependencies += jena,
libraryDependencies += arq,
libraryDependencies += antiXML,
libraryDependencies += grizzled,
libraryDependencies += scalaz,
- jarName in assembly := "read-write-web.jar",
- libraryDependencies += argot
+ libraryDependencies += jsslutils,
+ libraryDependencies += argot,
+ libraryDependencies += guava,
+
+ jarName in assembly := "read-write-web.jar"
)
+
+
lazy val project = Project(
id = "read-write-web",
base = file("."),
--- a/src/main/scala/Authoritative.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/Authoritative.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,14 +1,49 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
import unfiltered.request._
-import java.net.URL
+import java.security.cert.Certificate
+import javax.servlet.http.HttpServletRequest
+import unfiltered.netty.ReceivedMessage
+import org.eclipse.jetty.util.URIUtil
+import java.net.{MalformedURLException, URL}
object Authoritative {
val r = """^(.*)\.(\w{0,4})$""".r
-
- def unapply(req: HttpRequest[javax.servlet.http.HttpServletRequest]): Option[(URL, Representation)] = {
- val uri = req.underlying.getRequestURL.toString
+
+ // all of this would be unnecessary if req.uri would really return the full URI
+ // we should try to push for that to be done at unfiltered layer
+ def reqURL[T](m: Manifest[T], r: HttpRequest[T]): String = {
+ if (m <:< manifest[HttpServletRequest]) reqUrlServlet(r.asInstanceOf[HttpRequest[HttpServletRequest]])
+ else if (m <:< manifest[ReceivedMessage]) reqUrlNetty(r.asInstanceOf[HttpRequest[ReceivedMessage]])
+ else "" //todo: should perhaps throw an exception here.
+ }
+
+ def unapply[T](req: HttpRequest[T]) (implicit m: Manifest[T]) : Option[(URL, Representation)] = {
+ val uri = reqURL(m, req)
val suffixOpt = uri match {
case r(_, suffix) => Some(suffix)
case _ if uri endsWith "/" => Some("/")
@@ -17,4 +52,30 @@
Some((new URL(uri), Representation(suffixOpt, Accept(req))))
}
+
+ private def reqUrlServlet[T <: HttpServletRequest](req: HttpRequest[T]): String = {
+ req.underlying.getRequestURL.toString
+ }
+
+ private def reqUrlNetty[T <: ReceivedMessage](req: HttpRequest[T]): String = {
+ try {
+ val u = new URL(req.uri)
+ new URL(u.getProtocol,u.getHost,u.getPort,u.getPath).toExternalForm
+ } catch {
+ case e: MalformedURLException => {
+ val url: StringBuffer = new StringBuffer (48)
+ val scheme = if (req.isSecure) "https" else "http"
+ val hostport = {//we assume there was some checking done earlier, and that we can rely on this
+ val host = req.headers ("Host")
+ if (host.hasNext) host.next () else "localhost"
+ }
+ url.append (scheme)
+ url.append ("://")
+ url.append (hostport)
+ url.append(req.uri)
+ url.toString
+ }
+ }
+ }
+
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/EchoPlan.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
+
+import unfiltered.request.Path
+import unfiltered.response.{ResponseString, PlainTextContent, ContentType, Ok}
+import io.BufferedSource
+
+
+/**
+ * @author hjs
+ * @created: 19/10/2011
+ */
+
+class EchoPlan {
+ import collection.JavaConversions._
+
+ lazy val plan = unfiltered.filter.Planify({
+ case req@Path(path) if path startsWith "/test/http/echo" => {
+ Ok ~> PlainTextContent ~> {
+ val headers = req.underlying.getHeaderNames()
+ val result = for (name <- headers ;
+ val nameStr = name.asInstanceOf[String]
+ ) yield {
+ nameStr + ": " + req.underlying.getHeader(nameStr)+"\r\n"
+ }
+ ResponseString(result.mkString+ "\r\n" + new BufferedSource(req.inputStream).mkString)
+ }
+ }
+
+ })
+}
\ No newline at end of file
--- a/src/main/scala/Filesystem.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/Filesystem.scala Sat Nov 12 08:33:10 2011 -0500
@@ -5,10 +5,10 @@
import java.io._
import java.net.URL
import org.slf4j.{Logger, LoggerFactory}
-import com.hp.hpl.jena.rdf.model._
+import com.hp.hpl.jena.rdf.model.{Resource=>JResource,_}
import com.hp.hpl.jena.shared.JenaException
-import scalaz.{sys => _, _}
+import scalaz.{Resource => SzResource, sys => _, _}
import Scalaz._
class Filesystem(
@@ -20,7 +20,7 @@
def sanityCheck(): Boolean =
baseDirectory.exists && baseDirectory.isDirectory
-
+
def resource(url: URL): Resource = new Resource {
val relativePath: String = url.getPath.replaceAll("^"+basePath.toString+"/?", "")
val fileOnDisk = new File(baseDirectory, relativePath)
@@ -46,10 +46,17 @@
def get(): Validation[Throwable, Model] = {
val model = ModelFactory.createDefaultModel()
+ val guessLang = fileOnDisk.getName match {
+ case Authoritative.r(_,suffix) => Representation.fromSuffix(suffix) match {
+ case RDFRepr(rdfLang) => rdfLang
+ case _ => lang
+ }
+ case _ => lang
+ }
if (fileOnDisk.exists()) {
val fis = new FileInputStream(fileOnDisk)
try {
- val reader = model.getReader(lang.jenaLang)
+ val reader = model.getReader(guessLang.jenaLang)
reader.read(model, fis, url.toString)
} catch {
case je: JenaException => throw je
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/HttpsTrustAll.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
+
+import java.io.File
+import javax.net.ssl.X509TrustManager
+import org.jsslutils.keystores.KeyStoreLoader
+import org.jsslutils.sslcontext.trustmanagers.TrustAllClientsWrappingTrustManager
+import org.jsslutils.sslcontext.{X509TrustManagerWrapper, X509SSLContextFactory}
+import sys.SystemProperties
+import unfiltered.jetty.{Ssl, Https}
+
+
+/**
+ * @author Henry Story
+ * @created: 12/10/2011
+ */
+
+case class HttpsTrustAll(override val port: Int, override val host: String) extends Https(port, host) with TrustAll
+
+
+/**
+ * Trust all ssl connections. Authentication will be done at a different layer
+ * This code is very much tied to jetty
+ * It requires the following System properties to be set
+ *
+ * - jetty.ssl.keyStoreType
+ * - jetty.ssl.keyStore
+ * - jetty.ssl.keyStorePassword
+ *
+ * Client Auth is set to Want.
+ *
+ * Authentication could be done here, allowing the code to reject broken certificates, but then
+ * the user experience would be very bad, since TLS does not give many options for explaining what the problem
+ * is.
+ */
+trait TrustAll { self: Ssl =>
+ import scala.sys.SystemProperties._
+
+ lazy val sslContextFactory = new X509SSLContextFactory(
+ serverCertKeyStore,
+ tryProperty("jetty.ssl.keyStorePassword"),
+ serverCertKeyStore); //this one is not needed since our wrapper ignores all trust managers
+
+ lazy val trustWrapper = new X509TrustManagerWrapper {
+ def wrapTrustManager(trustManager: X509TrustManager) = new TrustAllClientsWrappingTrustManager(trustManager)
+ }
+
+ lazy val serverCertKeyStore = {
+ val keyStoreLoader = new KeyStoreLoader
+ keyStoreLoader.setKeyStoreType(System.getProperty("jetty.ssl.keyStoreType","JKS"))
+ keyStoreLoader.setKeyStorePath(trustStorePath)
+ keyStoreLoader.setKeyStorePassword(System.getProperty("jetty.ssl.keyStorePassword","password"))
+ keyStoreLoader.loadKeyStore();
+ }
+
+ sslContextFactory.setTrustManagerWrapper(trustWrapper);
+
+
+ lazy val trustStorePath = new SystemProperties().get("jetty.ssl.keyStore") match {
+ case Some(path) => path
+ case None => new File(new File(tryProperty("user.home")), ".keystore").getAbsolutePath
+ }
+
+ sslConn.setSslContext(sslContextFactory.buildSSLContext())
+ sslConn.setWantClientAuth(true)
+
+}
+
--- a/src/main/scala/Lang.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/Lang.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,3 +1,26 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
import unfiltered.request._
@@ -15,7 +38,7 @@
case TURTLE => "TURTLE"
case N3 => "N3"
}
-
+
}
object Lang {
@@ -32,7 +55,7 @@
case "text/turtle" => Some(TURTLE)
case "application/rdf+xml" => Some(RDFXML)
case _ => None
- }
+ }
def apply(cts: Iterable[String]): Option[Lang] =
cts map Lang.apply collectFirst { case Some(lang) => lang }
--- a/src/main/scala/ReadWriteWebMain.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/ReadWriteWebMain.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,9 +1,8 @@
package org.w3.readwriteweb
+import auth.{RDFAuthZ, X509view}
import org.w3.readwriteweb.util._
-import javax.servlet._
-import javax.servlet.http._
import unfiltered.jetty._
import java.io.File
import Console.err
@@ -11,14 +10,28 @@
import org.clapper.argot._
import ArgotConverters._
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
-object ReadWriteWebMain {
-
+trait ReadWriteWebArgs {
val logger: Logger = LoggerFactory.getLogger(this.getClass)
- val parser = new ArgotParser("read-write-web")
+ 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 mode = parser.option[RWWMode](List("mode"), "m", "wiki mode") {
+ 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
@@ -37,7 +50,8 @@
}
}
- val port = parser.parameter[Int]("port", "Port to use", false)
+ 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) => {
@@ -49,8 +63,17 @@
}
}
+ val webCache = new WebCache()
+
val baseURL = parser.parameter[String]("baseURL", "base URL", false)
+}
+
+
+object ReadWriteWebMain extends ReadWriteWebArgs {
+
+ import unfiltered.filter.Planify
+
// regular Java main
def main(args: Array[String]) {
@@ -66,17 +89,36 @@
baseURL.value.get,
lang=rdfLanguage.value getOrElse RDFXML)(mode.value getOrElse ResourcesDontExistByDefault)
- val app = new ReadWriteWeb(filesystem)
+ val rww = new ReadWriteWeb[HttpServletRequest,HttpServletResponse] {
+ val rm = filesystem
+ def manif = manifest[HttpServletRequest]
+ override implicit val authz = new RDFAuthZ[HttpServletRequest,HttpServletResponse](webCache,filesystem)
+ }
+
+ //this is incomplete: we should be able to start both ports.... not sure how to do this yet.
+ val service = httpsPort.value match {
+ case Some(port) => HttpsTrustAll(port,"0.0.0.0")
+ case None => Http(httpPort.value.get)
+ }
// configures and launches a Jetty server
- unfiltered.jetty.Http(port.value.get)
- .filter(new FilterLogger(logger))
- .context("/public") {
- ctx: ContextBuilder =>
+ service.filter(new FilterLogger(logger)).
+ context("/public"){ ctx:ContextBuilder =>
ctx.resources(ClasspathUtils.fromClasspath("public/").toURI.toURL)
- }.filter(app.plan).run()
+ }.
+ filter(Planify(rww.intent)).
+ filter(Planify(x509v.intent)).
+ filter(new EchoPlan().plan).run()
}
+
+
+ object x509v extends X509view[HttpServletRequest,HttpServletResponse] {
+ def wc = webCache
+ def manif = manifest[HttpServletRequest]
+ }
+
}
+
--- a/src/main/scala/Representation.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/Representation.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,3 +1,4 @@
+
package org.w3.readwriteweb
import unfiltered.request._
@@ -18,6 +19,7 @@
}
}
+
val htmlContentTypes = Set("text/html", "application/xhtml+xml")
def acceptsHTML(ct: Iterable[String]) =
--- a/src/main/scala/RequestLang.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/RequestLang.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,3 +1,26 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
import unfiltered.request._
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/WebCache.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,82 @@
+ /*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb
+
+import com.hp.hpl.jena.rdf.model.Model
+import java.net.URL
+import org.apache.http.MethodNotSupportedException
+import org.w3.readwriteweb.util._
+import scalaz._
+import Scalaz._
+
+/**
+ * @author Henry Story
+ * @created: 12/10/2011
+ *
+ * The WebCache currently does not cache
+ */
+class WebCache extends ResourceManager {
+ import dispatch._
+
+ val http = new Http
+
+ def basePath = null //should be cache dir?
+
+ def sanityCheck() = true //cache dire exists? But is this needed for functioning?
+
+ def resource(u : URL) = new org.w3.readwriteweb.Resource {
+ def get() = {
+ // note we prefer rdf/xml and turtle over html, as html does not always contain rdfa, and we prefer those over n3,
+ // as we don't have a full n3 parser. Better would be to have a list of available parsers for whatever rdf framework is
+ // installed (some claim to do n3 when they only really do turtle)
+ // we can't currently accept */* as we don't have GRDDL implemented
+ val request = url(u.toString) <:< Map("Accept"->
+ "application/rdf+xml,text/turtle,application/xhtml+xml;q=0.8,text/html;q=0.7,text/n3;q=0.6")
+
+ //we need to tell the model about the content type
+ val handler: Handler[Validation[Throwable, Model]] = request.>+>[Validation[Throwable, Model]](res => {
+ res >:> { headers =>
+ val encoding = headers("Content-Type").headOption match {
+ case Some(mime) => Lang(mime) getOrElse Lang.default
+ case None => RDFXML // it would be better to try to do a bit of guessing in this case by looking at content
+ }
+ val loc = headers("Content-Location").headOption match {
+ case Some(loc) => new URL(u,loc)
+ case None => new URL(u.getProtocol,u.getAuthority,u.getPort,u.getPath)
+ }
+ res>>{ in=> modelFromInputStream(in,loc,encoding) }
+
+ }
+ })
+ http(handler)
+
+ }
+
+ // when fetching information from the web creating directories does not make sense
+ //perhaps the resource manager should be split into read/write sections?
+ def save(model: Model) = throw new MethodNotSupportedException("not implemented")
+
+ def createDirectory(model: Model) = throw new MethodNotSupportedException("not implemented")
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/AuthenticationFilter.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 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}
+
+/**
+ * This filter places the all the principals into a Subject,
+ * which can then be accessed later on in by the code.
+ *
+ * note: It would be better if this were only called at the point when authentication
+ * is needed. That is in fact possible with TLS renegotiation, but requires a server that allows
+ * access to the TLS layer. This is an intermediary solution.
+ */
+class AuthenticationFilter(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/Authz.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.auth
+
+import unfiltered.filter.Plan
+import unfiltered.request._
+import collection.JavaConversions._
+import javax.security.auth.Subject
+import java.net.URL
+import com.hp.hpl.jena.query.{QueryExecutionFactory, QueryExecution, QuerySolutionMap, QueryFactory}
+import sun.management.resources.agent
+import unfiltered.response.{ResponseFunction, Unauthorized}
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+import com.hp.hpl.jena.rdf.model.{RDFNode, ResourceFactory}
+import org.w3.readwriteweb.{Authoritative, Resource, ResourceManager, WebCache}
+
+/**
+ * @author hjs
+ * @created: 14/10/2011
+ */
+
+object HttpMethod {
+ def unapply(req: HttpRequest[_]): Option[Method] =
+ Some(
+ req.method match {
+ case "GET" => GET
+ case "PUT" => PUT
+ case "HEAD" => HEAD
+ case "POST" => POST
+ case "CONNECT" => CONNECT
+ case "OPTIONS" => OPTIONS
+ case "TRACE" => TRACE
+ case m => new Method(m)
+ })
+
+
+}
+
+object AuthZ {
+
+
+ implicit def x509toSubject(x509c: X509Claim)(implicit cache: WebCache): Subject = {
+ val subject = new Subject()
+ subject.getPublicCredentials.add(x509c)
+ val verified = for (
+ claim <- x509c.webidclaims;
+ if (claim.verified)
+ ) yield claim.principal
+ subject.getPrincipals.addAll(verified)
+ subject
+ }
+}
+
+class NullAuthZ[Request,Response] extends AuthZ[Request,Response] {
+ override def subject(req: Req): Option[Subject] = None
+
+ override def guard(m: Method, path: URL): Guard = null
+
+ override def protect(in: Req=>Res)(implicit m: Manifest[Request]) = in
+}
+
+
+abstract class AuthZ[Request,Response] {
+ type Req = HttpRequest[Request]
+ type Res = ResponseFunction[Response]
+
+
+ def protect(in: Req=>Res)(implicit m: Manifest[Request]): Req=>Res = {
+ case req @ HttpMethod(method) & Authoritative(url,_) if guard(method, url).allow(() => subject(req)) => in(req)
+ case _ => Unauthorized
+ }
+
+
+ protected def subject(req: Req): Option[Subject]
+
+ /** create the guard defined in subclass */
+ protected def guard(m: Method, path: URL): Guard
+
+ abstract class Guard(m: Method, url: URL) {
+
+ /**
+ * verify if the given request is authorized
+ * @param subj function returning the subject to be authorized if the resource needs authorization
+ */
+ def allow(subj: () => Option[Subject]): Boolean
+ }
+
+}
+
+
+class RDFAuthZ[Request,Response](val webCache: WebCache, rm: ResourceManager)
+ (implicit val m: Manifest[Request]) extends AuthZ[Request,Response] {
+ import AuthZ.x509toSubject
+ implicit val cache : WebCache = webCache
+
+ def subject(req: Req) = req match {
+ case X509Claim(claim) => Option(claim)
+ case _ => None
+ }
+
+ object RDFGuard {
+ val acl = "http://www.w3.org/ns/auth/acl#"
+ val foafAgent = ResourceFactory.createResource("http://xmlns.com/foaf/0.1/Agent")
+ val Read = acl+"Read"
+ val Write = acl+"Write"
+ val Control = acl+"Control"
+
+ val selectQuery = QueryFactory.create("""
+ PREFIX acl: <http://www.w3.org/ns/auth/acl#>
+ SELECT ?mode ?group ?agent
+ WHERE {
+ ?auth acl:accessTo ?res ;
+ acl:mode ?mode .
+ OPTIONAL { ?auth acl:agentClass ?group . }
+ OPTIONAL { ?auth acl:agent ?agent . }
+ }""")
+ }
+
+ def guard(method: Method, url: URL) = new Guard(method, url) {
+ import RDFGuard._
+ import org.w3.readwriteweb.util.wrapValidation
+ import org.w3.readwriteweb.util.ValidationW
+
+
+
+ def allow(subj: () => Option[Subject]) = {
+ val r: Resource = rm.resource(new URL(url,".meta.n3"))
+ val res: ValidationW[Boolean,Boolean] = for {
+ model <- r.get() failMap { x => true }
+ } yield {
+ val initialBinding = new QuerySolutionMap();
+ initialBinding.add("res", model.createResource(url.toString)) //todo: this will work only if the files are described with relative URLs
+ val qe = QueryExecutionFactory.create(selectQuery, model, initialBinding)
+ val agentsAllowed = try {
+ val exec = qe.execSelect()
+ val res = for (qs <- exec) yield {
+ val methods = qs.get("mode").toString match {
+ case Read => List(GET)
+ case Write => List(PUT, POST)
+ case Control => List(POST)
+ case _ => List(GET, PUT, POST, DELETE) //nothing everything is allowed
+ }
+ if (methods.contains(method)) Some((qs.get("agent"), qs.get("group")))
+ else None
+ }
+ res.flatten.toList
+ } finally {
+ qe.close()
+ }
+ if (agentsAllowed.size > 0) {
+ if (agentsAllowed.exists( pair => pair._2 == foafAgent )) true
+ else subj() match {
+ case Some(s) => {
+ agentsAllowed.exists{
+ p => s.getPrincipals(classOf[WebIdPrincipal]).
+ exists(id=> {
+ val ps = if (p._1 != null) p._1.toString else null;
+ ps == id.webid
+ })
+ }
+ }
+ case None => false
+ }
+ //currently we just check for agent match. Group match would require us to have a store
+ //of trusted information of which groups someone was member of -- one would probably need a reasoning engine there.
+ } else false //
+ }
+ res.validation.fold()
+ } // end allow()
+
+
+ }
+
+}
+
+
+class ResourceGuard(path: String, reqMethod: Method) {
+
+
+ def allow(subjFunc: () => Option[Subject]) = {
+ subjFunc().isEmpty
+ }
+}
+
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/Principals.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 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 Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 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 ?m ?e ?mod ?exp
+ WHERE {
+ {
+ ?key cert:identity ?webid .
+ } UNION {
+ ?webid cert:key ?key .
+ }
+ ?key rsa:modulus ?m ;
+ rsa:public_exponent ?e .
+
+ OPTIONAL { ?m cert:hex ?mod . }
+ OPTIONAL { ?e cert:decimal ?exp . }
+ }""") //Including OPTIONAL notation, for backward compatibility - should remove that after a while
+
+ /**
+ * 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) {
+
+ var errors = new LinkedList[java.lang.Throwable]()
+
+ lazy val principal = new WebIdPrincipal(webId)
+
+ var tests: List[Verification] = List()
+
+ private var valid = false
+
+ def verified(implicit cache: WebCache): Boolean = {
+ if (!valid) tests = verify(cache)
+ tests.exists(v => v.isInstanceOf[Verified])
+ }
+
+ private def verify(implicit cache: WebCache): List[Verification] = {
+ import org.w3.readwriteweb.util.wrapValidation
+
+ import collection.JavaConversions._
+ import WebIDClaim._
+ try {
+ return 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
+ }
+ }
+ } finally {
+ valid = true
+ }
+
+
+ }
+
+
+
+ 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/X509Cert.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.auth
+
+import javax.servlet.http.HttpServletRequest
+import unfiltered.netty.ReceivedMessage
+import java.util.Date
+import java.math.BigInteger
+import java.net.URL
+import unfiltered.request.{UserAgent, HttpRequest}
+import java.security.cert.{X509Certificate, Certificate}
+import java.security._
+import interfaces.RSAPublicKey
+import unfiltered.util.IO
+import sun.security.x509._
+
+
+object X509CertSigner {
+
+ def apply(keyStoreLoc: URL, keyStoreType: String, password: String, alias: String) = {
+ val keystore = KeyStore.getInstance(keyStoreType)
+
+ IO.use(keyStoreLoc.openStream()) { in =>
+ keystore.load(in, password.toCharArray)
+ }
+ val privateKey = keystore.getKey(alias, password.toCharArray).asInstanceOf[PrivateKey]
+ val certificate = keystore.getCertificate(alias).asInstanceOf[X509Certificate]
+ //one could verify that indeed this is the private key corresponding to the public key in the cert.
+
+ new X509CertSigner(certificate,privateKey)
+ }
+}
+
+class X509CertSigner(signingCert: X509Certificate, signingKey: PrivateKey ) {
+ val WebID_DN="""O=FOAF+SSL, OU=The Community of Self Signers, CN=Not a Certification Authority"""
+
+ /**
+ * Adapted from http://bfo.com/blog/2011/03/08/odds_and_ends_creating_a_new_x_509_certificate.html
+ * The libraries used here are sun gpled code. This is much lighter to use than bouncycastle. All VMs that already
+ * have these classes don't need to download the code. It should be easy in scala to create a build that can decide
+ * if these need to be added to the classpath. I think the code just looks better than bouncycastle too.
+ *
+ * WARNING THIS IS in construction
+ *
+ * Create a self-signed X.509 Certificate
+ * @param subjectDN the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
+ * @param pair the KeyPair
+ * @param days how many days from now the Certificate is valid for
+ * @param algorithm the signing algorithm, eg "SHA1withRSA"
+ */
+ def generate(subjectDN: String,
+ subjectKey: RSAPublicKey,
+ days: Int,
+ webId: URL): X509Certificate = { //todo: the algorithm should be deduced from private key in part
+
+
+ var info = new X509CertInfo
+ val from = new Date(System.currentTimeMillis()-10*1000*60) //start 10 minutes ago, to avoid network trouble
+ val to = new Date(from.getTime + days*24*60*60*1000)
+ val interval = new CertificateValidity(from, to)
+ val serialNumber = new BigInteger(64, new SecureRandom)
+ val subjectXN = new X500Name(subjectDN)
+ val issuerXN = new X500Name(signingCert.getSubjectDN.toString)
+
+ info.set(X509CertInfo.VALIDITY, interval)
+ info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serialNumber))
+ info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subjectXN))
+ info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuerXN))
+ info.set(X509CertInfo.KEY, new CertificateX509Key(subjectKey))
+ info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3))
+
+ //
+ //extensions
+ //
+ val extensions = new CertificateExtensions();
+
+ val san = new SubjectAlternativeNameExtension(true, new GeneralNames().add(new GeneralName(new URIName(webId.toExternalForm))))
+ extensions.set(san.getName,san)
+
+ val basicCstrExt = new BasicConstraintsExtension(false,1)
+ extensions.set(basicCstrExt.getName,basicCstrExt)
+
+ { import KeyUsageExtension._
+ val keyUsage = new KeyUsageExtension()
+ List(DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, KEY_AGREEMENT, KEY_CERTSIGN).foreach {
+ usage => keyUsage.set(usage,true)
+ }
+ extensions.set(keyUsage.getName,keyUsage)
+ }
+
+ { import NetscapeCertTypeExtension._
+ val netscapeExt = new NetscapeCertTypeExtension()
+ List(SSL_CLIENT, S_MIME) foreach { ext => netscapeExt.set(ext,true) }
+ extensions.set(netscapeExt.getName, new NetscapeCertTypeExtension(false,netscapeExt.getValue))
+ }
+
+ val subjectKeyExt = new SubjectKeyIdentifierExtension(new KeyIdentifier(subjectKey).getIdentifier)
+ extensions.set(subjectKeyExt.getName,subjectKeyExt)
+
+ info.set(X509CertInfo.EXTENSIONS,extensions)
+
+ val algo = signingCert.getPublicKey.getAlgorithm match {
+ case "DSA" => new AlgorithmId(AlgorithmId.sha1WithDSA_oid )
+ case "RSA" => new AlgorithmId(AlgorithmId.sha1WithRSAEncryption_oid)
+ case _ => throw new RuntimeException("Don't know how to sign with this type of key")
+ }
+
+ info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo))
+
+ // Sign the cert to identify the algorithm that's used.
+ val tmpCert = new X509CertImpl(info)
+ tmpCert.sign(signingKey,algo.getName)
+
+ //update the algorithm and re-sign
+ val sigAlgo = tmpCert.get(X509CertImpl.SIG_ALG).asInstanceOf[AlgorithmId]
+ info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgo)
+ val cert = new X509CertImpl(info)
+ cert.sign(signingKey,algo.getName)
+
+ cert.verify(signingCert.getPublicKey)
+ return cert
+ }
+
+
+}
+
+
+object Certs {
+
+
+ def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[IndexedSeq[Certificate]] = {
+ if (m <:< manifest[HttpServletRequest]) unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
+ else if (m <:< manifest[ReceivedMessage]) unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]])
+ else None //todo: should throw an exception here?
+ }
+
+
+ //todo: should perhaps pass back error messages, which they could in the case of netty
+
+ private def unapplyServletRequest[T <: HttpServletRequest](r: HttpRequest[T]):
+ Option[IndexedSeq[Certificate]] = {
+ r.underlying.getAttribute("javax.servlet.request.X509Certificate") match {
+ case certs: Array[Certificate] => Some(certs)
+ case _ => None
+ }
+ }
+
+ private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T]):
+ Option[IndexedSeq[Certificate]] = {
+
+ import org.jboss.netty.handler.ssl.SslHandler
+ r.underlying.context.getPipeline.get(classOf[SslHandler]) match {
+ case sslh: SslHandler => try {
+ //return the client certificate in the existing session if one exists
+ Some(sslh.getEngine.getSession.getPeerCertificates)
+ } catch {
+ case e => {
+ // request a certificate from the user
+ 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) {
+ if (future.isSuccess) try {
+ Some(sslh.getEngine.getSession.getPeerCertificates)
+ } catch {
+ case e => None
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
+ }
+ case _ => None
+ }
+
+ }
+
+ /**
+ * Some agents do not send client certificates unless required. This is a problem for them, as it ends up breaking the
+ * connection for those agents if the client does not have a certificate...
+ *
+ * It would be useful if this could be updated by server from time to time from a file on the internet,
+ * so that changes to browsers could update server behavior
+ *
+ */
+ def needAuth(agent: String): Boolean = {
+ agent.contains("Java")
+ }
+
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/X509Claim.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 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._
+import java.util.concurrent.TimeUnit
+import com.google.common.cache.{CacheLoader, CacheBuilder, Cache}
+import javax.servlet.http.HttpServletRequest
+import unfiltered.request.HttpRequest
+
+/**
+ * @author hjs
+ * @created: 13/10/2011
+ */
+
+object X509Claim {
+ final val logger = LoggerFactory.getLogger(classOf[X509Claim])
+
+ val idCache: Cache[X509Certificate, X509Claim] =
+ CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).
+ build(new CacheLoader[X509Certificate, X509Claim]() {
+ def load(x509: X509Certificate) = new X509Claim(x509)
+ })
+
+ def unapply[T](r: HttpRequest[T])(implicit webCache: WebCache,m: Manifest[T]): Option[X509Claim] =
+ r match {
+ case Certs(c1: X509Certificate, _*) => Some(idCache.get(c1))
+ case _ => None
+ }
+
+
+
+ /**
+ * Extracts the URIs in the subject alternative name extension of an X.509
+ * certificate
+ *
+ * @param cert X.509 certificate from which to extract the URIs.
+ * @return Iterator of URIs as strings found in the subjectAltName extension.
+ */
+ def getClaimedWebIds(cert: X509Certificate): Iterator[String] =
+ if (cert == null) Iterator.empty;
+ else cert.getSubjectAlternativeNames() match {
+ case coll if (coll != null) => {
+ for (sanPair <- coll
+ if (sanPair.get(0) == 6)
+ ) yield sanPair(1).asInstanceOf[String]
+ }.iterator
+ case _ => Iterator.empty
+ }
+
+}
+
+
+/**
+ * An X509 Claim maintains information about the proofs associated with claims
+ * found in an X509 Certificate. It is the type of object that can be passed
+ * into the public credentials part of a Subject node
+ *
+ * todo: think of what this would look like for a chain of certificates
+ *
+ * @author bblfish
+ * @created: 30/03/2011
+ */
+class X509Claim(val cert: X509Certificate) extends Refreshable {
+
+ import X509Claim._
+ val claimReceivedDate = new Date();
+ lazy val tooLate = claimReceivedDate.after(cert.getNotAfter())
+ lazy val tooEarly = claimReceivedDate.before(cert.getNotBefore())
+
+ /* a list of unverified principals */
+ lazy val webidclaims = getClaimedWebIds(cert).map {
+ webid =>new WebIDClaim(webid, cert.getPublicKey)
+ }.toSet
+
+
+
+ //note could also implement Destroyable
+ //
+ //http://download.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html#Credentials
+ //
+ //if updating validity periods can also take into account the WebID reference, then it is possible
+ //that a refresh could have as consequence to do a fetch on the WebID profile
+ //note: one could also take the validity period to be dependent on the validity of the profile representation
+ //in which case updating the validity period would make more sense.
+
+ override
+ def refresh() {
+ }
+
+ /* The certificate is currently within the valid time zone */
+ override
+ def isCurrent(): Boolean = !(tooLate||tooEarly)
+
+ lazy val error = {}
+
+ def canEqual(other: Any) = other.isInstanceOf[X509Claim]
+
+ override
+ def equals(other: Any): Boolean =
+ other match {
+ case that: X509Claim => (that eq this) || (that.canEqual(this) && cert == that.cert)
+ case _ => false
+ }
+
+ override
+ lazy val hashCode: Int = 41 * (41 +
+ (if (cert != null) cert.hashCode else 0))
+
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/auth/X509view.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.auth
+
+import unfiltered.request.Path
+import unfiltered.response.{Html, ContentType, Ok}
+import org.w3.readwriteweb.WebCache
+import unfiltered.Cycle
+
+/**
+ * This plan just described the X509 WebID authentication information.
+ * It works independently of the underlying Cycle.Intent implementations of Request and Response,
+ * so it can work with servlet filters just as well as with netty.
+ *
+ * This is a simple version. A future version will show EARL output, and so be useful for debugging the endpoint.
+ *
+ * @author hjs
+ * @created: 13/10/2011
+ */
+
+trait X509view[Request,Response] {
+ implicit def wc: WebCache
+ implicit def manif: Manifest[Request]
+
+ def intent: Cycle.Intent[Request, Response] = {
+ case req @ Path(path) if path startsWith "/test/auth/x509" =>
+ Ok ~> ContentType("text/html") ~> Html(
+ <html><head><title>Authentication Page</title></head>
+ { req match {
+ case X509Claim(xclaim) => <body>
+ <h1>Authentication Info received</h1>
+ <p>You were identified with the following WebIDs</p>
+ <ul>{xclaim.webidclaims.filter(cl=>cl.verified).map(p=> <li>{p.webId}</li>)}</ul>
+ <p>You sent the following certificate</p>
+ <pre>{xclaim.cert.toString}</pre>
+ </body>
+ case _ => <body><p>We received no Authentication information</p></body>
+ }
+ }</html>)
+
+ }
+
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.netty
+
+import org.clapper.argot.ArgotUsageException
+import scala.Console._
+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}
+
+/**
+ * ReadWrite Web for Netty server, allowing TLS renegotiation
+ *
+ * @author hjs
+ * @created: 21/10/2011
+ */
+
+object ReadWriteWebNetty extends ReadWriteWebArgs {
+
+ // regular Java main
+ def main(args: Array[String]) {
+
+ try {
+ parser.parse(args)
+ } catch {
+ case e: ArgotUsageException => err.println(e.message); sys.exit(1)
+ }
+
+ val filesystem =
+ new Filesystem(
+ rootDirectory.value.get,
+ baseURL.value.get,
+ lang=rdfLanguage.value getOrElse RDFXML)(mode.value getOrElse ResourcesDontExistByDefault)
+
+ val rww = new cycle.Plan with cycle.ThreadPool with ServerErrorResponse with ReadWriteWeb[ReceivedMessage,HttpResponse]{
+ val rm = filesystem
+ def manif = manifest[ReceivedMessage]
+ override val authz = new RDFAuthZ[ReceivedMessage,HttpResponse](webCache,filesystem)
+ }
+
+ //this is incomplete: we should be able to start both ports.... not sure how to do this yet.
+ val service = httpsPort.value match {
+ case Some(port) => new KeyAuth_Https(port)
+ case None => new KeyAuth_Https(httpPort.value.get)
+ }
+
+ // configures and launches a Netty server
+ service.plan( x509v ).
+ plan( rww ).run()
+
+ }
+
+ object x509v extends cycle.Plan with cycle.ThreadPool with ServerErrorResponse with X509view[ReceivedMessage,HttpResponse] {
+ def wc = webCache
+ def manif = manifest[ReceivedMessage]
+ }
+
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/netty/SslLoginTest.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.netty
+
+import org.jboss.netty.handler.ssl.SslHandler
+import unfiltered.request.Path
+import unfiltered.response.ResponseString
+
+/**
+ * A very light weight plan to test SSL login using TLS renegotiation in netty.
+ * This shows how easy it is to to this, and can be useful to try out different browsers' implementations
+ * The certificate should only be requested of the client on going to /test/login .
+ *
+ * Note: to get this to work on all browsers, and if security is just less of a concern for you, you should
+ * set the sun.security.ssl.allowUnsafeRenegotiation=true and sun.security.ssl.allowLegacyHelloMessages=true
+ * see:
+ *
+ * http://download.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#workarounds
+ *
+ *
+ * @author hjs
+ * @created: 21/10/2011
+ */
+object SslLoginTest extends NormalPlan {
+
+ def certAvailable(sslh: SslHandler): String = try {
+ sslh.getEngine.getSession.getPeerCertificateChain.head.toString
+ } catch {
+ case e => e.getMessage
+ }
+
+
+ def intent = {
+
+ case req @ Path("/test/login") => {
+
+ req.underlying.context.getPipeline.get(classOf[org.jboss.netty.handler.ssl.SslHandler]) match {
+ case sslh: SslHandler => {
+ sslh.setEnableRenegotiation(true)
+ sslh.getEngine.setNeedClientAuth(true)
+// sslh.getEngine.setWantClientAuth(true)
+ val future = sslh.handshake()
+ future.await(5000)
+ val res = if (future.isDone) {
+ var r ="We are in login & we have an https handler! "
+ if (future.isSuccess)
+ r += "\r\n"+"SSL handchake Successful. Did we get the certificate? \r\n\r\n"+certAvailable(sslh)
+ else {
+ r += "\r\n handshake failed. Cause \r\n" +future.getCause
+ }
+ r
+ } else {
+ "Still waiting for requested certificate"
+ }
+ ResponseString(res)
+ }
+ case _ =>ResponseString("We are in login but no https handler!")
+ }
+
+ }
+ case req => {
+ req.underlying.context.getPipeline.get(classOf[org.jboss.netty.handler.ssl.SslHandler]) match {
+ case sslh: SslHandler => {
+ ResponseString(certAvailable(sslh))
+ }
+ case null => ResponseString("Just a normal hello world")
+
+ }
+ }
+ }
+
+
+ def main(args: Array[String]) {
+ new KeyAuth_Https(8443).plan(SslLoginTest).run()
+ }
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/netty/server.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ * 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 org.w3.readwriteweb.netty
+
+
+import unfiltered.netty._
+import java.lang.String
+import org.jboss.netty.channel.{ChannelPipelineFactory, ChannelHandler}
+import java.security.cert.X509Certificate
+import javax.net.ssl.{SSLEngine, X509ExtendedTrustManager}
+import java.net.Socket
+
+trait NormalPlan extends cycle.Plan with cycle.ThreadPool with ServerErrorResponse
+
+
+class KeyAuth_Https(override val port: Int) extends Https(port) with KeyAuth_Ssl
+
+
+/**
+ * a class that trusts all ssl certificates - as long as the tls handshake crypto works of course.
+ * Ie: we don't care about who signed the certificate. All we know when the certificate is received
+ * is that the client knew the private key of the given public key. It is the job of other layers,
+ * to follow through on claims made in the certificate.
+ */
+trait KeyAuth_Ssl extends Ssl {
+
+ import java.security.SecureRandom
+ import javax.net.ssl.{SSLContext, TrustManager}
+
+ val nullArray = Array[X509Certificate]()
+
+ val trustManagers = Array[TrustManager](new X509ExtendedTrustManager {
+
+ def checkClientTrusted(chain: Array[X509Certificate], authType: String, socket: Socket) {}
+
+ def checkClientTrusted(chain: Array[X509Certificate], authType: String, engine: SSLEngine) {}
+
+ def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String) {}
+
+ def checkServerTrusted(chain: Array[X509Certificate], authType: String, socket: Socket) {}
+
+ def checkServerTrusted(chain: Array[X509Certificate], authType: String, engine: SSLEngine) {}
+
+ def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String) {}
+
+ def getAcceptedIssuers() = nullArray
+ })
+
+
+ override def initSslContext(ctx: SSLContext) = ctx.init(keyManagers, trustManagers, new SecureRandom)
+
+
+}
+
+//
+// Below is code mostly taken from unfiltered.netty, and so available under their licence. I am waiting
+// for them to make it easier to extend the netty.Https classes so that this code would not longer be
+// needed
+//
+
+object Https {
+
+ /** bind to a the loopback interface only */
+ def local(port: Int): Https =
+ new Https(port, "127.0.0.1")
+
+ /** bind to any available port on the loopback interface */
+ def anylocal = local(unfiltered.util.Port.any)
+}
+
+
+/** Http + Ssl implementation of the Server trait. */
+class Https(val port: Int,
+ val host: String,
+ val handlers: List[() => ChannelHandler],
+ val beforeStopBlock: () => Unit) extends HttpServer with KeyAuth_Ssl { self =>
+
+ def this(port: Int, host: String) = this(port, host, Nil, () => ())
+
+ def this(port: Int) = this(port, "0.0.0.0")
+
+ def pipelineFactory: ChannelPipelineFactory =
+ new SecureServerPipelineFactory(channels, handlers, this)
+
+ type ServerBuilder = Https
+ def handler(h: => ChannelHandler) = new Https(port, host, { () => h } :: handlers, beforeStopBlock)
+ def plan(plan: => ChannelHandler) = handler(plan)
+ def beforeStop(block: => Unit) = new Https(port, host, handlers, { () => beforeStopBlock(); block })
+
+}
--- a/src/main/scala/plan.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/plan.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,10 +1,8 @@
package org.w3.readwriteweb
+import auth.{AuthZ, NullAuthZ}
import org.w3.readwriteweb.util._
-import unfiltered.request._
-import unfiltered.response._
-
import scala.io.Source
import java.net.URL
@@ -18,29 +16,49 @@
QueryTypeConstruct => CONSTRUCT,
QueryTypeDescribe => DESCRIBE}
-import scalaz._
-import Scalaz._
+import scalaz.{Resource=>SzResource}
+import unfiltered.request._
+import unfiltered.Cycle
+import unfiltered.response._
//object ReadWriteWeb {
-//
+//
// val defaultHandler: PartialFunction[Throwable, HttpResponse[_]] = {
// case t => InternalServerError ~> ResponseString(t.getStackTraceString)
// }
-//
+//
//}
-class ReadWriteWeb(rm: ResourceManager) {
-
+/**
+ * The ReadWriteWeb intent.
+ * It is independent of jetty or netty
+ */
+trait ReadWriteWeb[Req,Res] {
+ val rm: ResourceManager
+ implicit def manif: Manifest[Req]
+ implicit val authz: AuthZ[Req,Res] = new NullAuthZ[Req,Res]
+ // a few type short cuts to make it easier to reason with the code here
+ // one may want to generalise this code so that it does not depend so strongly on servlets.
+// type Request = HttpRequest[Req]
+// type Response = ResponseFunction[Res]
+
val logger: Logger = LoggerFactory.getLogger(this.getClass)
- /** 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 ScalaZ Validation. For example of how to use it, see
+ /**
+ * The partial function that if triggered sends to the readwrite web code.
+ * It wraps the ReadWriteWeb function with the AuthZ passed in the argument
+ * ( Note that we don't want to protect this intent, since that would be to apply the security to all other applications,
+ * many of which may want different authorization implementations )
+ */
+ def intent : Cycle.Intent[Req,Res] = {
+ case req @ Path(path) if path startsWith rm.basePath => authz.protect(rwwIntent)(manif)(req)
+ }
+
+ /**
+ * The core ReadWrite web function
+ * ( This is not a partial function and so is not a Plan.Intent )
+ *
+ * This code makes use of ScalaZ Validation. 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]
@@ -53,99 +71,103 @@
* 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
+ * through another implicit conversion. It saves us the call to the Validation.fold() method
*/
- val plan = unfiltered.filter.Planify {
- case req @ Path(path) if path startsWith rm.basePath => {
- val Authoritative(uri, representation) = req
- val r: Resource = rm.resource(uri)
- req match {
- case GET(_) if representation == HTMLRepr => {
- 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 }
- lang = representation match {
- case RDFRepr(l) => l
- case _ => Lang.default
- }
- } yield {
- req match {
- case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
- case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+ def rwwIntent = (req: HttpRequest[Req]) => {
+
+ val Authoritative(uri: URL, representation: Representation) = req
+ val r: Resource = rm.resource(uri)
+ val res: ResponseFunction[Res] = req match {
+ case GET(_) if representation == HTMLRepr => {
+ val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
+ val body = source.getLines.mkString("\n")
+ Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
}
- }
- case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
- for {
- bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
- _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
- } yield Created
- }
- case PUT(_) & RequestLang(lang) =>
- for {
- bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
- _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
- } yield Created
- case PUT(_) =>
- BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
- case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
- Post.parse(Body.stream(req), uri, ct) match {
- case PostUnknown => {
- logger.info("Couldn't parse the request")
- BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
- }
- case PostUpdate(update) => {
- logger.info("SPARQL UPDATE:\n" + update.toString())
+ case GET(_) | HEAD(_) =>
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())
+ model <- r.get() failMap { x => NotFound }
+ lang = representation match {
+ case RDFRepr(l) => l
+ case _ => Lang.default
+ }
+ } yield {
+ val res = req match {
+ case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+ }
+ res ~> ContentLocation( uri.toString ) // without this netty (perhaps jetty too?) sends very weird headers, breaking tests
+ }
+ case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
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
+ bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+ _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+ } yield Created
}
- case PostQuery(query) => {
- logger.info("SPARQL Query:\n" + query.toString())
- lazy val lang = RequestLang(req) getOrElse Lang.default
+ case PUT(_) & RequestLang(lang) =>
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(lang.contentType) ~> ResponseModel(model, uri, lang)
- }
- case DESCRIBE => {
- val result: Model = qe.execDescribe()
- Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+ _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+ } yield Created
+ case PUT(_) =>
+ BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
+ case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
+ Post.parse(Body.stream(req), uri, ct) match {
+ case PostUnknown => {
+ logger.info("Couldn't parse the request")
+ BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
+ }
+ 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 lang = RequestLang(req) getOrElse Lang.default
+ 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(lang.contentType) ~> ResponseModel(model, uri, lang)
+ }
+ case DESCRIBE => {
+ val result: Model = qe.execDescribe()
+ Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ }
+ }
}
}
}
}
+ case POST(_) =>
+ BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
+ case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
}
+ res
}
- case POST(_) =>
- BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
- case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
- }
- }
-
- }
+
+
+
+
}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/util/SpyInputStream.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,58 @@
+/*
+ * 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 org.w3.readwriteweb.util
+
+import java.io.{IOException, OutputStream, InputStream}
+
+
+/**
+ * Wrap an inputstream and write everything that comes in here
+ * @author hjs
+ * @created: 30/10/2011
+ */
+
+class SpyInputStream(val in: InputStream, val out: OutputStream) extends InputStream {
+ var stopOut = false
+
+ def read() ={
+
+ val i = try {
+ in.read()
+ } catch {
+ case ioe: IOException => {
+ out.close()
+ stopOut=true
+ throw ioe;
+ }
+ }
+ if (!stopOut) try {
+ out.write(i)
+ } catch {
+ case ioe: IOException => {
+ stopOut = true
+ }
+ }
+ i
+ }
+}
\ No newline at end of file
--- a/src/main/scala/util/package.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/main/scala/util/package.scala Sat Nov 12 08:33:10 2011 -0500
@@ -1,14 +1,15 @@
package org.w3.readwriteweb
import java.io._
-import java.net.URL
import com.hp.hpl.jena.rdf.model._
-
import scalaz._
import Scalaz._
+import java.net.URL
package object util {
+
+
def modelFromInputStream(
is: InputStream,
base: URL,
Binary file src/test/resources/KEYSTORE.jks has changed
--- a/src/test/scala/ReadWriteWebSpecs.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/test/scala/ReadWriteWebSpecs.scala Sat Nov 12 08:33:10 2011 -0500
@@ -13,7 +13,7 @@
PutRDFXMLSpec, PostRDFSpec,
PutInvalidRDFXMLSpec, PostOnNonExistingResourceSpec,
// sparql query
- PostSelectSpec, PostConstructSpec, PostAskSpec,
+ PostSelectSpec, PostConstructSpec, PostAskSpec,
// sparql update
PostInsertSpec,
// delete content
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/scala/auth/CreateWebIDSpec.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,283 @@
+/*
+ * 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 org.w3.readwriteweb.auth
+
+import org.w3.readwriteweb.utiltest._
+
+import dispatch._
+import java.security.cert.X509Certificate
+import java.security._
+import interfaces.RSAPublicKey
+import java.net.{Socket, URL}
+import scala.collection.mutable
+import javax.net.ssl._
+import java.io.File
+import org.w3.readwriteweb.{Post, RDFXML, TURTLE}
+
+
+/**
+ * A key manager that can contain multiple keys, but where the client can take one of a number of identities
+ * One at a time - so this is not sychronised. It also assumes that the server will accept all CAs, which in
+ * these test cases it does.
+ */
+class FlexiKeyManager extends X509ExtendedKeyManager {
+ val keys = mutable.Map[String, Pair[Array[X509Certificate],PrivateKey]]()
+
+ def addClientCert(alias: String,certs: Array[X509Certificate], privateKey: PrivateKey) {
+ keys.put(alias,Pair(certs,privateKey))
+ }
+
+ var currentId: String = null
+
+ def setId(alias: String) { currentId = if (keys.contains(alias)) alias else null }
+
+ def getClientAliases(keyType: String, issuers: Array[Principal]) = if (currentId!=null) Array(currentId) else null
+
+ def chooseClientAlias(keyType: Array[String], issuers: Array[Principal], socket: Socket) = currentId
+
+ def getServerAliases(keyType: String, issuers: Array[Principal]) = null
+
+ def chooseServerAlias(keyType: String, issuers: Array[Principal], socket: Socket) = ""
+
+ def getCertificateChain(alias: String) = keys.get(alias) match { case Some(certNKey) => certNKey._1; case None => null}
+
+ def getPrivateKey(alias: String) = keys.get(alias).map(ck=>ck._2).getOrElse(null)
+
+ override def chooseEngineClientAlias(keyType: Array[String], issuers: Array[Principal], engine: SSLEngine): String = currentId
+}
+
+/**
+ * @author hjs
+ * @created: 23/10/2011
+ */
+
+object CreateWebIDSpec extends SecureFileSystemBased {
+ lazy val peopleDirUri = host / "wiki/people/"
+ lazy val webidProfileDir = peopleDirUri / "Lambda/"
+ lazy val webidProfile = webidProfileDir / "Joe"
+ lazy val joeProfileOnDisk = new File(root,"people/Lambda/Joe")
+ lazy val lambdaMetaURI = webidProfileDir/".meta.n3"
+
+ lazy val directory = new File(root, "people")
+ lazy val lambdaDir = new File(directory,"Lambda")
+ lazy val lambdaMeta = new File(lambdaDir,".meta.n3")
+
+
+
+ val foaf = """
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
+ @prefix : <#> .
+
+ <> a foaf:PersonalProfileDocument;
+ foaf:primaryTopic :me .
+
+ :jL a foaf:Person;
+ foaf:name "Joe Lambda"@en .
+ """
+
+ val webID = new URL(webidProfile.secure.to_uri + "#jL")
+
+
+ val updatePk = """
+ PREFIX cert: <http://www.w3.org/ns/auth/cert#>
+ PREFIX rsa: <http://www.w3.org/ns/auth/rsa#>
+ PREFIX : <#>
+ INSERT DATA {
+ :jL cert:key [ rsa:modulus "%s"^^cert:hex;
+ rsa:public_exponent "%s"^^cert:int ] .
+ }
+ """
+
+ val updateFriend = """
+ PREFIX foaf: <http://xmlns.com/foaf/0.1/>
+ PREFIX : <#>
+ INSERT DATA {
+ :jL foaf:knows <%s> .
+ }
+ """
+
+
+ "PUTing nothing on /people/" should {
+ "return a 201" in {
+ val httpCode = Http(peopleDirUri.secure.put(TURTLE, "") get_statusCode)
+ httpCode must_== 201
+ }
+ "create a directory on disk" in {
+ directory must be directory
+ }
+ }
+
+
+ "PUTing nothing on /people/Lambda/" should { // but should it really? Should it not create a resource too? Perhaps index.html?
+ "return a 201" in {
+ val httpCode = Http(webidProfileDir.secure.put(TURTLE, "") get_statusCode)
+ httpCode must_== 201
+ }
+ "create a directory on disk" in {
+ lambdaDir must be directory
+ }
+ }
+
+
+ "PUTing a WebID Profile on /people/Lambda/" should {
+ "return a 201" in {
+ val httpCode = Http( webidProfile.secure.put(TURTLE, foaf) get_statusCode )
+ httpCode must_== 201
+ }
+ "create a resource on disk" in {
+ joeProfileOnDisk must be file
+ }
+ }
+
+
+ "POSTing public key into the /people/Lambda/Joe profile" should {
+
+ "first create signed WebID certificate and add it to local SSL keystore" in {
+ val keystore = getClass.getClassLoader.getResource("KEYSTORE.jks")
+ val signer = X509CertSigner(
+ keystore,
+ "JKS",
+ "secret",
+ "localhost"
+ )
+
+ val rsagen = KeyPairGenerator.getInstance("RSA")
+ rsagen.initialize(512)
+ val rsaKP = rsagen.generateKeyPair()
+
+
+ val testCert = signer.generate("CN=JoeLambda, OU=DIG, O=W3C", rsaKP.getPublic.asInstanceOf[RSAPublicKey], 1, webID)
+
+ testCert mustNotBe null
+
+ testKeyManager.addClientCert("JoeLambda",Array(testCert),rsaKP.getPrivate)
+ }
+
+ "return a 200 when POSTing relations to profile" in {
+ val joeCert = testKeyManager.getCertificateChain("JoeLambda")
+
+ joeCert mustNotBe null
+
+ val joeKey = joeCert(0).getPublicKey.asInstanceOf[RSAPublicKey]
+
+ val updateQStr = updatePk.format(
+ joeKey.getModulus.toString(16),
+ joeKey.getPublicExponent()
+ )
+
+ val httpCode = Http(
+ webidProfile.secure.postSPARQL(updateQStr) get_statusCode )
+ httpCode must_== 200
+ }
+
+ "create 3 more relations" in {
+ val model = Http(webidProfile.secure as_model(baseURI(webidProfile.secure), RDFXML))
+ model.size() must_== 7
+
+ }
+ }
+
+ val aclRestriction = """
+ @prefix acl: <http://www.w3.org/ns/auth/acl#> .
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
+ @prefix : <#> .
+
+ :a1 a acl:Authorization;
+ acl:accessTo <Joe>;
+ acl:mode acl:Write;
+ acl:agent <%s>, <http://bblfish.net/people/henry/card#me> .
+
+ :allRead a acl:Authorization;
+ acl:accessTo <Joe>;
+ acl:mode acl:Read;
+ acl:agentClass foaf:Agent .
+ """
+
+
+ "PUT access control statements in directory" should {
+ "return a 201" in {
+ val httpCode = Http( lambdaMetaURI.secure.put(TURTLE, aclRestriction.format(webID.toExternalForm)) get_statusCode )
+ httpCode must_== 201
+ }
+
+ "create a resource on disk" in {
+ lambdaMeta must be file
+ }
+
+ "everybody can still read the profile" in {
+ testKeyManager.setId(null)
+ val model = Http(webidProfile.secure as_model(baseURI(webidProfile.secure), RDFXML))
+ model.size() must_== 7
+ }
+
+ "no one other than the user can change the profile" in {
+ val httpCode = Http.when(_ == 401)(webidProfile.secure.put(TURTLE, foaf) get_statusCode)
+ httpCode must_== 401
+ }
+
+ "access it as the user - allow him to add a friend" in {
+ testKeyManager.setId("JoeLambda")
+
+/* The code below was very useful to help me debug this.
+ Sometimes it helps to get back to basics. So I will leave this here.
+
+ val scon =webidProfile.secure.to_uri.toURL.openConnection().asInstanceOf[HttpsURLConnection]
+ scon.setSSLSocketFactory(sslContext.getSocketFactory)
+ scon.setRequestProperty("Content-Type",Post.SPARQL)
+ scon.setRequestProperty("User-Agent" , "Java/1.7.0")
+ scon.setRequestMethod("POST")
+ val msg = updateFriend.format("http://bblfish.net/#hjs").getBytes("UTF-8")
+ scon.setRequestProperty("Content-Length",msg.length.toString)
+ scon.setDoOutput(true)
+ scon.setDoInput(true)
+
+ val out = scon.getOutputStream
+ out.write(msg)
+ out.flush()
+ out.close()
+ scon.connect()
+
+ val httpCode = scon.getResponseCode
+*/
+
+ val req =webidProfile.secure.PUT <:< Map("User-Agent" -> "Java/1.7.0","Content-Type"->Post.SPARQL)
+ val req2 = req.copy(
+ method="POST",
+ body=Some(new RefStringEntity(updateFriend.format(webID.toExternalForm),Post.SPARQL,"UTF-8"))
+ )
+
+ val httpCode = Http( req2 get_statusCode )
+ httpCode must_== 200
+ }
+
+ "and so have one more relation in the foaf" in {
+ val model = Http(webidProfile.secure as_model(baseURI(webidProfile.secure), RDFXML))
+ model.size() must_== 8
+ }
+
+ }
+
+
+
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/scala/auth/SecureReadWriteWebSpec.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,42 @@
+/*
+ * 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 org.w3.readwriteweb.auth
+
+import org.specs.Specification
+
+/**
+ * @author hjs
+ * @created: 25/10/2011
+ */
+
+object SecureReadWriteWebSpec extends Specification {
+ try {
+ "The Secure Read Write Web".isSpecifiedBy(
+ CreateWebIDSpec
+ )
+ } catch {
+ case e => e.printStackTrace(System.out)
+ }
+
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/scala/auth/secure_specs.scala Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,134 @@
+/*
+ * 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 org.w3.readwriteweb.auth
+
+import unfiltered.spec.netty.Started
+import org.specs.Specification
+import unfiltered.netty.{ReceivedMessage, ServerErrorResponse, cycle}
+import java.io.File
+import org.w3.readwriteweb._
+import grizzled.file.GrizzledFile._
+
+import java.security.cert.X509Certificate
+import org.apache.http.conn.scheme.Scheme
+import dispatch.Http
+import org.apache.http.client.HttpClient
+import javax.net.ssl.{SSLContext, X509TrustManager, KeyManager}
+
+/**
+ * @author hjs
+ * @created: 24/10/2011
+ */
+
+
+trait SecureServed extends Started {
+ import org.w3.readwriteweb.netty._
+
+ //todo: replace this with non property method of setting this.
+ System.setProperty("netty.ssl.keyStore",getClass.getClassLoader.getResource("KEYSTORE.jks").getFile)
+ System.setProperty("netty.ssl.keyStoreType","JKS")
+ System.setProperty("netty.ssl.keyStorePassword","secret")
+
+ def setup: (Https => Https)
+ lazy val server = setup( new KeyAuth_Https(port) )
+
+
+}
+
+object AcceptAllTrustManager extends X509TrustManager {
+ def checkClientTrusted(chain: Array[X509Certificate], authType: String) {}
+ def checkServerTrusted(chain: Array[X509Certificate], authType: String) {}
+ def getAcceptedIssuers = Array[X509Certificate]()
+}
+
+/**
+ * Netty resource managed with access control enabled
+ */
+trait SecureResourceManaged extends Specification with SecureServed {
+ import org.jboss.netty.handler.codec.http._
+
+ def resourceManager: ResourceManager
+
+ /**
+ * Inject flexible behavior into the client ssl so that it does not
+ * break on every localhost problem. It returns a key manager which can be used
+ * to allow the client to take on various guises
+ */
+ def flexi(client: HttpClient, km: KeyManager): SSLContext = {
+
+ val sslContext = javax.net.ssl.SSLContext.getInstance("TLS");
+
+ sslContext.init(Array(km.asInstanceOf[KeyManager]), Array(AcceptAllTrustManager),null); // we are not trying to test our trust of localhost server
+
+ import org.apache.http.conn.ssl._
+ val sf = new SSLSocketFactory(sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
+ val scheme = new Scheme("https", 443, sf);
+ client.getConnectionManager.getSchemeRegistry.register(scheme)
+
+ sslContext
+ }
+
+
+
+ val webCache = new WebCache()
+ val serverSslContext = javax.net.ssl.SSLContext.getInstance("TLS");
+
+
+
+ flexi(webCache.http.client, new FlexiKeyManager)
+
+
+ val testKeyManager = new FlexiKeyManager();
+ val sslContext = flexi(Http.client,testKeyManager)
+
+
+
+
+ val rww = new cycle.Plan with cycle.ThreadPool with ServerErrorResponse with ReadWriteWeb[ReceivedMessage,HttpResponse] {
+ val rm = resourceManager
+ def manif = manifest[ReceivedMessage]
+ override val authz = new RDFAuthZ[ReceivedMessage,HttpResponse](webCache,resourceManager)
+ }
+
+ def setup = { _.plan(rww) }
+
+}
+
+trait SecureFileSystemBased extends SecureResourceManaged {
+ lazy val mode: RWWMode = ResourcesDontExistByDefault
+
+ lazy val lang = TURTLE
+
+ lazy val baseURL = "/wiki"
+
+ lazy val root = new File(new File(System.getProperty("java.io.tmpdir")), "readwriteweb")
+
+ lazy val resourceManager = new Filesystem(root, baseURL, lang)(mode)
+
+ doBeforeSpec {
+ if (root.exists) root.deleteRecursively()
+ root.mkdir()
+ }
+
+}
--- a/src/test/scala/util/specs.scala Sat Nov 12 08:29:00 2011 -0500
+++ b/src/test/scala/util/specs.scala Sat Nov 12 08:33:10 2011 -0500
@@ -2,48 +2,68 @@
import org.w3.readwriteweb._
+import auth.RDFAuthZ
import org.specs._
-import java.net.URL
-import unfiltered.response._
-import unfiltered.request._
import dispatch._
import java.io._
-import com.codecommit.antixml._
import grizzled.file.GrizzledFile._
-import com.hp.hpl.jena.rdf.model._
-import com.hp.hpl.jena.query._
-import com.hp.hpl.jena.update._
+import org.w3.readwriteweb.utiltest._
+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+import unfiltered.filter.Planify
+import unfiltered.netty.{ReceivedMessage, ServerErrorResponse, cycle}
+import unfiltered.spec.netty.Started
-import org.w3.readwriteweb.util._
-import org.w3.readwriteweb.utiltest._
-
-trait ResourceManaged extends Specification with unfiltered.spec.jetty.Served {
+trait JettyResourceManaged extends Specification with unfiltered.spec.jetty.Served {
def resourceManager: ResourceManager
-
- def setup = { _.filter(new ReadWriteWeb(resourceManager).plan) }
+
+ val rww = new ReadWriteWeb[HttpServletRequest,HttpServletResponse] {
+ val rm = resourceManager
+ def manif = manifest[HttpServletRequest]
+ }
+
+ def setup = { _.filter(Planify(rww.intent)) }
}
+/**
+ * Netty Resource managed.
+ **/
+trait ResourceManaged extends Specification with unfiltered.spec.netty.Served {
+ import org.jboss.netty.handler.codec.http._
+
+ def resourceManager: ResourceManager
+
+ val rww = new cycle.Plan with cycle.ThreadPool with ServerErrorResponse with ReadWriteWeb[ReceivedMessage,HttpResponse] {
+ val rm = resourceManager
+ def manif = manifest[ReceivedMessage]
+ }
+
+ def setup = { _.plan(rww) }
+
+}
+
+
+
trait FilesystemBased extends ResourceManaged {
-
+
lazy val mode: RWWMode = ResourcesDontExistByDefault
-
+
lazy val lang = RDFXML
-
+
lazy val baseURL = "/wiki"
-
+
lazy val root = new File(new File(System.getProperty("java.io.tmpdir")), "readwriteweb")
lazy val resourceManager = new Filesystem(root, baseURL, lang)(mode)
-
+
doBeforeSpec {
if (root.exists) root.deleteRecursively()
root.mkdir()
}
-
+
}
trait SomeRDF extends SomeURI {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test_www/.meta.n3 Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,14 @@
+@prefix acl: <http://www.w3.org/ns/auth/acl#> .
+@prefix foaf: <http://xmlns.com/foaf/0.1/> .
+@prefix : <#> .
+
+:a1 a acl:Authorization;
+ acl:accessTo <foaf.n3>;
+ acl:mode acl:Write;
+ acl:agent <http://bblfish.net/people/henry/card#me> .
+
+
+:readAll a acl:Authorization;
+ acl:accessTo <foaf.n3>;
+ acl:mode acl:Read;
+ acl:agentClass foaf:Agent .
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test_www/foaf.n3 Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,9 @@
+@prefix foaf: <http://xmlns.com/foaf/0.1/> .
+@prefix : <#> .
+
+<> a foaf:PersonalProfileDocument;
+ foaf:primaryTopic :joe .
+
+:joe a foaf:Person;
+ foaf:name "John Doe";
+ foaf:knows <http://bblfish.net/people/henry/card#me> .
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test_www/hello.n3 Sat Nov 12 08:33:10 2011 -0500
@@ -0,0 +1,4 @@
+@prefix foaf: <http://xmlns.com/foaf/0.1/> .
+
+<> a foaf:Document .
+