akka-http: complete request with flow - akka-stream

Assume I have set up an arbitrarily complex Flow[HttpRequest, HttpResponse, Unit].
I can already use said flow to handle incoming requests with
Http().bindAndHandle(flow, "0.0.0.0", 8080)
Now I would like to add logging, leveraging some existing directive, like logRequestResult("my-service"){...}
Is there a way to combine this directive with my flow? I guess I am looking for another directive, something along the lines of
def completeWithFlow(flow: Flow): Route
Is this possible at all?
N.B.: logRequestResult is an example, my question applies to any Directive one might find useful.

Turns out one (and maybe the only) way is to wire and materialize a new flow, and feed the extracted request to it. Example below
val myFlow: Flow[HttpRequest, HttpResponse, NotUsed] = ???
val route =
get {
logRequestResult("my-service") {
extract(_.request) { req ⇒
val futureResponse = Source.single(req).via(myFlow).runWith(Sink.head)
complete(futureResponse)
}
}
}
Http().bindAndHandle(route, "127.0.0.1", 9000)

http://doc.akka.io/docs/akka/2.4.2/scala/http/routing-dsl/overview.html
Are you looking for route2HandlerFlow or Route.handlerFlow ?
I believe Route.handlerFlow will work based on implicits.
eg /
val serverBinding = Http().bindAndHandle(interface = "0.0.0.0", port = 8080,
handler = route2HandlerFlow(mainFlow()))

Related

Gatling switch protocols during scenario

I'm trying to create a Gatling scenario which requires switching the protocol to a different host during the test. The user journey is
https://example.com/page1
https://example.com/page2
https://accounts.example.com/signin
https://example.com/page3
so as part of a single scenario, I need to ether switch the protocol defined in the scenario set up, or switch the baseUrl defined on the protocol but I can't figure out how to do that.
A basic scenario might look like
package protocolexample
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class Example extends Simulation {
val exampleHttp = http.baseURL("https://example.com/")
val exampleAccountsHttp = http.baseURL("https://accounts.example.com/")
val scn = scenario("Signin")
.exec(
http("Page 1").get("/page1")
)
.exec(
http("Page 2").get("/page2")
)
.exec(
// This needs to be done against accounts.example.com
http("Signin").get("/signin")
)
.exec(
// Back to example.com
http("Page 3").get("/page3")
)
setUp(
scn.inject(
atOnceUsers(3)
).protocols(exampleHttp)
)
}
I just need to figure out how to ether switch the host or protocol for the 3rd step. I know I can create multiple scenarios but this needs to be a single user flow across multiple hosts.
I've tried directly using the other protocol
exec(
// This needs to be done against accounts.example.com
exampleAccountsHttp("Signin").get("/signin")
)
which results in
protocolexample/example.scala:19: type mismatch;
found : String("Signin")
required: io.gatling.core.session.Session
exampleAccountsHttp("Signin").get("/signin")
and also changing the base URL on the request
exec(
// This needs to be done against accounts.example.com
http("Signin").baseUrl("https://accounts.example.com/").get("/signin")
)
which results in
protocolexample/example.scala:19: value baseUrl is not a member of io.gatling.http.request.builder.Http
You can use an absolute URI (inclusive protocol) as a parameter for Http.get, Http.post and so on.
class Example extends Simulation {
val exampleHttp = http.baseURL("https://example.com/")
val scn = scenario("Signin")
.exec(http("Page 1").get("/page1"))
.exec(http("Page 2").get("/page2"))
.exec(http("Signin").get("https://accounts.example.com/signin"))
.exec(http("Page 3").get("/page3"))
setUp(scn.inject(atOnceUsers(3))
.protocols(exampleHttp))
}
see: https://gatling.io/docs/current/cheat-sheet/#http-protocol-urls-baseUrl
baseURL: Sets the base URL of all relative URLs of the scenario on which the configuration is applied.
I am currently working in gatling.
SOLUTION:
=> WHAT WE USE IF WE HAVE ONE HTTPBASE:
var httpConf1= http.baseUrl("https://s1.com")
=>FOR MULTIPLE HTTPBASE:
var httpConf1=http.**baseUrls**("https://s1.com","https://s2.com","https://s3.com")
You can use baseUrls function which takes a list of httpBase urls.
ANOTHER METHOD:
Reading from csv file all httpbase and storing it in list buffer in scala language and then converting it into list while passing it in http.baseUrls
var listOfTenants:ListBuffer[String] = new ListBuffer[String] //buffer
var httpConf1=http.**baseUrls**(listOfTenants.toList) //converting buffer to list.
Hope it helps!.
Thank you.😊

Client session storage in an akka-http server

In an akka-http service, how does one cache some information, per client session? This is not quite obvious in the docs. I would for example like to create an actor for each connection.
Where should I create the actor, and how do I get reference to it from inside my stages?
My service is bound something like this:
val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] =
Http().bind(interface = bindAddress, port = bindPort)
val bindingFuture: Future[Http.ServerBinding] =
serverSource
.to(Sink.foreach { connection =>
connection.handleWithSyncHandler (requestHandler)
// seems like I should set up some session state storage here,
// such as my actor
})
.run()
...
and later on:
val packetProcessor: Flow[A, B, Unit] = Flow[A]
.map {
case Something =>
// can i use the actor here, or access my session state?
}
I suspect I'm probably misinterpreting the whole paradigm in trying to make this fit. I can't tell if there is anything built in or how much I need to implement manually.
I have found Agent to be a very convenient mechanism for concurrent caching.
Say, for example, you want to keep a running Set of all the remote addresses that you have been connected to. You can setup an Agent to store the values and a Flow to write to the cache:
import scala.concurrent.ExecutionContext.Implicits.global
import akka.agent.Agent
import scala.collection.immutable
val addressCache = Agent(immutable.Set.empty[java.net.InetSocketAddress])
import akka.stream.scaladsl.Flow
val cacheAddressFlow = Flow[IncomingConnection] map { conn =>
addressCache send (_ + conn.remoteAddress) //updates the cache
conn //forwards the connection to the rest of the stream
}
This Flow can then be made part of your Stream:
val bindingFuture: Future[Http.ServerBinding] =
serverSource.via(cacheAddressFlow)
.to(Sink.foreach { connection =>
connection.handleWithSyncHandler (requestHandler)
})
.run()
You can then "query" the cache completely outside of the binding logic:
def somewhereElseInTheCode = {
val currentAddressSet = addressCache.get
println(s"address count so far: ${currentAddressSet.size}")
}
If your goal is to send all IncomingConnection values to an Actor for processing then this can be accomplished with Sink.actorRef:
object ConnectionStreamTerminated
class ConnectionActor extends Actor {
override def receive = {
case conn : IncomingConnection => ???
case ConnectionStreamTerminated => ???
}
}
val actorRef = actorSystem actorOf Props[ConnectionActor]
val actorSink =
Sink.actorRef[IncomingConnection](actorRef, ConnectionStreamTerminated)
val bindingFuture: Future[Http.ServerBinding] =
serverSource.runWith(actorSink)
For the reason that the suggested Agents have been deprecated. I would suggests to use akka-http-session. It makes sure session data is secure and cannot be tampered with.

Google App Engine Channel API with custom domains

In my GAE app (Python) I have implemented multitenancy and multi-site support based on the host part of the request object.
For example, www.foo.com/index.html and www.bar.com/index.html are both handled by the same app (e.g. myapp.appspot.com). The app reads the host-value and then decides which namespace and site-configuration to use. This works great as long as the app receives the request directly from the user-agent.
However, I would like to use the Channel API, but there is an issue because requests to /_ah/channel/connected/ and /_ah/channel/disconnected/ are not originating from the original user-agent. Instead, the request has Host: myapp.appspot.com and the parameter to=myapp.appspot.com. (The from parameter is the token I expect. Also www.foo.com/_ah/channel/jsapi is redirected to a talkgadget server which is not documented but seems to be as expected.)
I assume, the problem is caused by the code in channel.js which doesn't call my app using the original host, e.g. www.foo.com/_ah/channel/connected. Instead it uses a talkgadget.google.com host which (as far as I can tell) will then call my app, but using myapp.appspot.com, ignoring the original host and so I cannot use the request's host value for my purpose.
As a workaround, I can figure out a way of including the host information into the channel token, so when my connected and disconnected handlers receive the token, they can use the token instead.
However, I would like to know if there is a better approach, where I could still get the original host name (e.g. www.foo.com) requests to /_ah/channel/connected/ and /_ah/channel/disconnected/. Any ideas?
This is what I have tried so far (with-out any success):
Adding the custom domain host name to the JS src attribute:
<script type="text/javascript" src="//www.foo.com/_ah/channel/jsapi"></script>
I also tried to manually override the base-url of the channel socket, suggested here: https://stackoverflow.com/questions/16558776/google-app-engine-channel-api-same-origin-policy
<script type="text/javascript">
onOpened = function() {
// TODO
};
onMessage = function() {
// TODO
};
onError = function() {
// TODO
};
onClose = function() {
// TODO
};
goog.appengine.Socket.BASE_URL = "https://www.foo.com/_ah/channel/";
channel = new goog.appengine.Channel('{{channelToken}}');
socket = channel.open();
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onerror = onError;
socket.onclose = onClose;
</script>
I couldn't find any official documentation for channel.js and I don't want to implement something that is going to break easily with the next update by Google.
Short of a proxy, I don't see a better way than including the information in-band. The problem is that the library/infrastructure (can't be certain without looking deeper) is stripping the HTTP-layer information (the Host header), and indeed you don't have any control of the HTTP layer to pass custom headers, etc. So, you either need to have the info at a lower layer (TCP doesn't even provide a means to do this, and since the entrypoint of your code is through the browser running channel.js, rather than a system-level process running on the bare network interface, this is out of the picture decisively), or at a higher layer, ie. within the channel.

WMS GetFeatureInfo; multiple layers, different sources

I'm developing a web application using GeoExt, OpenLayers and having my own GeoServer to serve various maps. Still, I want to let the user add other WMS's if needed, to be able to play around with all desired layers.
Thus, my problem with the GetFeatureInfo request. Right now I have a toolbar button attached to geoext's map panel,
new GeoExt.Action({
iconCls: "feature",
map: map,
toggleGroup: "tools",
tooltip: "Feature",
control: featureControl
})
its control attribute being
var featureControl = new OpenLayers.Control.WMSGetFeatureInfo({
queryVisible: true,
drillDown: true,
infoFormat:"application/vnd.ogc.gml"
});
I've also defined an event listener to do what I really want once I receive the responses, but that is not relevant here. My problem is the following:
Considering the user clicks on a point where there are 2+ visible layers and at least one of them is from a different source, OpenLayers will have to do one AJAX request per different source and, from OpenLayers own documentation,
Triggered when a GetFeatureInfo response is received. The event
object has a text property with the body of the response (String), a
features property with an array of the parsed features, an xy property
with the position of the mouse click or hover event that triggered the
request, and a request property with the request itself. If drillDown
is set to true and multiple requests were issued to collect feature
info from all layers, text and request will only contain the response
body and request object of the last request.
so, yeah, it will obviously wont work like that right away. Having a look at the debugger I can clearly see that, giving two layers from different sources, it actually DOES the request, it's just that it doesn't wait for the first's response and jumps for the next one (obviously, being asynchronous). I've thought about doing the requests one-by-one, meaning doing the first one as stated above and once it's finished and the response saved, go for the next one. But I'm still getting used to the data structure GeoExt uses.
Is there any API (be it GeoExt or OpenLayers) option/method I'm missing? Any nice workarounds?
Thanks for reading :-)
PS: I'm sorry if I've not been clear enough, english is not my mother tongue. Let me know if something stated above was not clear enough :)
i Hope this help to someone else, I realized that: you're rigth this control make the request in asynchronous mode, but this is ok, no problem with that, the real problem is when the control handle the request and trigger the event "getfeatureinfo" so, i modified 2 methods for this control and it works!, so to do this i declare the control first, and then in the savage mode i modified the methods here is de code:
getInfo = new OpenLayers.Control.WMSGetFeatureInfo({ drillDown:true , queryVisible: true , maxFeatures:100 });
//then i declare a variable that help me to handle more than 1 request.....
getInfo.responses = [];
getInfo.handleResponse=function(xy, request) { var doc = request.responseXML;
if(!doc || !doc.documentElement) { doc = request.responseText; }
var features = this.format.read(doc);
if (this.drillDown === false) {
this.triggerGetFeatureInfo(request, xy, features);
} else {
this._requestCount++;
this._features = (this._features || []).concat(features);
if( this._numRequests > 1){
//if the num of RQ, (I mean more than 1 resource ), i put the Request in array, this is for maybe in a future i could be need other properties or methods from RQ, i dont know.
this.responses.push(request);}
else{
this.responses = request;}
if (this._requestCount === this._numRequests) {
//here i change the code....
//this.triggerGetFeatureInfo(request, xy, this._features.concat());
this.triggerGetFeatureInfo(this.responses, xy, this._features.concat());
delete this._features;
delete this._requestCount;
delete this._numRequests;
// I Adding this when the all info is done 4 reboot
this.responses=[];
}
}
}
getInfo.triggerGetFeatureInfo= function( request , xy , features) {
//finally i added this code for get all request.responseText's
if( isArray( request ) ){
text_rq = '';
for(i in request ){
text_rq += request[i].responseText;
}
}
else{
text_rq = request.responseText;
}
this.events.triggerEvent("getfeatureinfo", {
//text: request.responseText,
text : text_rq,
features: features,
request: request,
xy: xy
});
// Reset the cursor.
OpenLayers.Element.removeClass(this.map.viewPortDiv, "olCursorWait");}
Thanks, you bring me a way for discover my problem and here is the way i solved, i hope this can help to somebody else.
saheka's answer was almost perfect! Congratulations and thank you, I had the same problem, and with it I finally managed to solve it.
What I would change in your code:
isArray() does not work, change it like this: if(request instanceof Array) {...} at the first line of getInfo.triggerGetFeatureInfo()
to show the results in a popup this is the way:
My code:
getInfo.addPopup = function(map, text, xy) {
if(map.popups.length > 0) {
map.removePopup(map.popups[0]);
}
var popup = new OpenLayers.Popup.FramedCloud(
"anything",
map.getLonLatFromPixel(xy),
null,
text,
null,
true
);
map.addPopup(popup);
}
and in the getInfo.triggerGetFeatureInfo() function, after the last line, append:
this.addPopup(map, text_rq, xy);
A GetFeatureInfo request is send as a JavaScript Ajax call to the external server. So, the requests are likely blocked for security reasons. You'll have to send the requests to the external servers by a proxy on your own domain.
Then, configure this proxy in openlayers by setting OpenLayers.ProxyHost to the proper path. For example:
OpenLayers.ProxyHost = "/proxy_script";
See http://trac.osgeo.org/openlayers/wiki/FrequentlyAskedQuestions#ProxyHost for more background information.

How to know if a Kohana request is an internal one?

I'm writing an API using Kohana. Each external request must be signed by the client to be accepted.
However, I also sometime need to do internal requests by building a Request object and calling execute(). In these cases, the signature is unnecessary since I know the request is safe. So I need to know that the request was internal so that I can skip the signature check.
So is there any way to find out if the request was manually created using a Request object?
Can you use the is_initial() method of the request object? Using this method, you can determine if a request is a sub request.
Kohana 3.2 API, Request - is_initial()
It sounds like you could easily solve this issue by setting some sort of static variable your app can check. If it's not FALSE, then you know it's internal.
This is how I ended up doing it: I've overridden the Request object and added a is_server_side property to it. Now, when I create the request, I just set this to true so that I know it's been created server-side:
$request = Request::factory($url);
$request->is_server_side(true);
$response = $request->execute();
Then later in the controller receiving the request:
if ($this->request->is_server_side()) {
// Skip signature check
} else {
// Do signature check
}
And here is the overridden request class in application/classes/request.php:
<?php defined('SYSPATH') or die('No direct script access.');
class Request extends Kohana_Request {
protected $is_server_side_ = false;
public function is_server_side($v = null) {
if ($v === null) return $this->is_server_side_;
$this->is_server_side_ = $v;
}
}
Looking through Request it looks like your new request would be considered an internal request but does not have any special flags it sets to tell you this. Look at 782 to 832 in Kohana_Request...nothing to help you.
With that, I'd suggest extending the Kohana_Request_Internal to add a flag that shows it as internal and pulling that in your app when you need to check if it is internal/all others.
Maybe you are looking for is_external method:
http://kohanaframework.org/3.2/guide/api/Request#is_external
Kohana 3.3 in the controller :
$this->request->is_initial()
http://kohanaframework.org/3.3/guide-api/Request#is_initial

Resources