Skip to content

Instantly share code, notes, and snippets.

@bblfish
Last active December 20, 2015 00:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bblfish/6041149 to your computer and use it in GitHub Desktop.
Save bblfish/6041149 to your computer and use it in GitHub Desktop.
simple CORSProxy with Play2.2 - does not quite work. It blocks the HTTP connection, even though the URL is correctly fetched...
/*
* Copyright 2012 Henry Story, http://bblfish.net/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.www.readwriteweb.play
import play.api.mvc._
import akka.util.Timeout
import scala.concurrent.{ExecutionContext, Future, Promise}
import play.api.mvc.SimpleResult
import play.api.mvc.ResponseHeader
import akka.actor.ActorSystem
import play.api.libs.iteratee._
import play.api.libs.ws.WS
/**
* A <a href="http://www.w3.org/TR/cors/">CORS</a> proxy that allows a client to fetch remote RDF
* resources that do not have the required CORS headers.
*
* Currently this only permits GET operations. It is unclear what PUT, POST, DELETE operations would
* look like for a CORS proxy.
*
* note: graphSelector used to be an IterateeSelector[Rdf#Graph], should try to get write non blocking parsers
*/
class CORSProxy extends Controller {
implicit val system = ActorSystem("MySystem")
implicit val executionContext = scala.concurrent.ExecutionContext.global //todo: make sure this is right
// turn a header map into an (att,val) sequence
private implicit def sequentialise(headers: Map[String,Seq[String]]) = headers.toSeq.flatMap(pair=>pair._2.map(v=>(pair._1,v)))
def get(url: String) = EssentialAction { request =>
System.out.println("in CORSProxy.get(" + url + ")")
implicit val timeout = Timeout(10 * 1000)
val res: Iteratee[Array[Byte],SimpleResult] = Iteratee.ignore[Array[Byte]].mapM { Unit =>
val resultPromise = Promise[SimpleResult]()
WS.url(url) //todo: develop a WS for each client, so that one can separate their views
.withHeaders(request.headers.toSimpleMap.toSeq: _*) //todo: this looses headers, must fix WS.withHeaders method
.get { response =>
println("in get received headers =" + response.headers)
val code = if (response.status == 200) 203
else response.status
val hdrs = response.headers.map{case (key,value)=>(key,value.head)} //todo: we loose info here too!
val corsHeaders = if (!hdrs.contains("Access-Control-Allow-Origin")) {
val origin = hdrs.get("Origin")
hdrs + ("Access-Control-Allow-Origin" -> origin.getOrElse("*")) - ("Content-Length")
} else {
hdrs
}
println("will send the following headers to client for cors "+corsHeaders)
val (getIteratee, actionResultEnum) = joined[Array[Byte]]
val print = Enumeratee.map[Array[Byte]] { in =>
println(new String(in))
in
}
resultPromise.trySuccess(SimpleResult(ResponseHeader(code, corsHeaders), actionResultEnum.through(print)))
getIteratee
}
println("WS.url done, returing resultPromise.future")
resultPromise.future
}
println("calculated res: Iteratee[Array[Byte,SimpleResult]]")
res
}
/**
* Create a joined iteratee enumerator pair.
* found in http://jazzy.id.au/default/2013/06/12/call_response_websockets_in_play_framework.html
*
* When the enumerator is applied to an iteratee, the iteratee subsequently consumes whatever the iteratee in the pair
* is applied to. Consequently the enumerator is "one shot", applying it to subsequent iteratees will throw an
* exception.
*/
def joined[A]: (Iteratee[A, Unit], Enumerator[A]) = {
val promisedIteratee = Promise[Iteratee[A, Unit]]()
val enumerator = new Enumerator[A] {
def apply[B](finalIteratee: Iteratee[A, B]): Future[Iteratee[A,B]] = {
val doneIteratee = Promise[Iteratee[A, B]]()
// Equivalent to map, but allows us to handle failures
def wrap(delegate: Iteratee[A, B]): Iteratee[A, B] = new Iteratee[A, B] {
def fold[C](folder: (Step[A, B]) => Future[C])(implicit ec: ExecutionContext): Future[C] = {
print("in a wrapped fold --> ")
val toReturn = delegate.fold {
case done @ Step.Done(a, in) => {
println(s"Done($a,$in)")
doneIteratee.success(done.it)
folder(done)
}
case Step.Cont(k) => {
println(s"Cont($k)")
folder(Step.Cont(k.andThen(wrap)))
}
case err @ Step.Error(msg,in)=> {
println(s"Err($msg, $in)")
doneIteratee.failure(new Exception(msg))
folder(err)
}
}
toReturn.onFailure {
case e => doneIteratee.failure(e)
}
toReturn
}
}
if (promisedIteratee.trySuccess(wrap(finalIteratee).map(_ => ()))) {
doneIteratee.future
} else {
throw new IllegalStateException("Joined enumerator may only be applied once")
}
}
}
(Iteratee.flatten(promisedIteratee.future), enumerator)
}
}
@bblfish
Copy link
Author

bblfish commented Jul 19, 2013

The problem seems to be linked to the length of the file that is being proxies. It does for example download everything on a longer file such as:

$ curl -i -H "Accept: text/turtle" http://localhost:9000/srv/cors?url=http://bblfish.net/people/henry/card

So presumably this would be something to do with flushing...

@bblfish
Copy link
Author

bblfish commented Jul 19, 2013

note: I have just updated to the latest Play 2.2 master diffs, just to make sure this was not the problem...

@bblfish
Copy link
Author

bblfish commented Jul 20, 2013

I tried adding an Enumerator.eof

resultPromise.trySuccess(
   SimpleResult(ResponseHeader(code, corsHeaders), 
                actionResultEnum andThen Enumerator.eof)
   )

but that does not seem to help.

@bblfish
Copy link
Author

bblfish commented Jul 21, 2013

Oh dear. The answer was really simple.
I was not removing the Content-Lenght header! I had checked that but ended up putting on the wrong
part of the if clause.

This works:

val corsHeaders = if (!hdrs.contains("Access-Control-Allow-Origin")) {
       hdrs + ("Access-Control-Allow-Origin" -> request.headers.get("Origin").getOrElse("*"))
    } else {
       hdrs
    } - ("Content-Length")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment