In Vapor, I can easily secure routes in a login session with this:
drop.group(protect) {
secure in
secure.get("secureRoute", handler: )
secure.post("securePostRoute", handler: )
//and so forth
}
And the handler proceeds as usual, no checking for sessions, as it's already done by drop.group(protect).
However, in Kitura, it seems as though if I want to achieve the same thing, I'd have to do this:
router.get("/") {
request, response, next in
//Get the current session
sess = request.session
//Check if we have a session and it has a value for email
if let sess = sess, let email = sess["email"].string {
try response.send(fileName: pathToFile).end()
} else {
try response.send(fileName: pathToAnotherFile).end()
}
}
I'll have to manually check for the session in every secure route. This will end up being very redundant.
Is there any solution as elegant as Vapor's?
If you have common logic that is needed in multiple routes, you can set up a middleware and have it execute before each route. Kitura supports Node express-style route handling; you can register middleware in sequence, and the middleware will be processed (assuming their mount paths match the request URL) in the same order that they were registered.
For example:
router.get("/private/*", handler: handler1)
router.get("/private/helloworld", handler: handler2)
In this case, a request matching "/private/helloworld" will be processed by handler1 and then by handler2, as long as handler1 invokes next() at the end of its processing.
Related
I'm wondering if there's a way to do an internal redirect, re-route, or response forwarding inside Ktor.
call.respondRedirect("relative/url")
sends a HTTP 302 or 301 depending on the permantent: Boolean flag. I'm looking for something that would do the same without using HTTP, just internally in Ktor. Here's some pseudo-routing of what I want to achieve:
get("/foo") {
if (call.parameters["something"] != null) {
call.respondText("Yay, something!")
} else {
call.respondRedirect("/bar") // except without relying on client to handle HTTP redirects.
}
}
get("/bar") {
call.respondText("No thing :(")
}
The goal is that the client shouldn't make 2 requests, and shouldn't be aware of the redirection happening.
NB: I'm aware I can extract a function for /bar's body and invoke it, instead of responsdRedirect. However, I want to make Ktor handle it so that it goes through all the necessary lifecycle and pipeline with all the interceptors. This is to make sure it is handled as if it was an external request, except the network roundtrip.
I'm looking for something like Express.js' req.app.handle(req, res) as shown in the first half of this answer: https://stackoverflow.com/a/48790319/253468. A potential solution I couldn't understand yet is something like TestApplicationEngine.handleRequest (in io.ktor:ktor-server-test-host) is doing with pipeline.execute. I guess I could invoke call.application.execute(), the question is how to construct the ApplicationCall object then. Note this is for production use, so no TestApplicationCall.
You can do similar thing in Ktor by using call.application.execute function with cloned call object. For convenience let's define extension function for doing internal redirects:
suspend fun ApplicationCall.redirectInternally(path: String) {
val cp = object: RequestConnectionPoint by this.request.local {
override val uri: String = path
}
val req = object: ApplicationRequest by this.request {
override val local: RequestConnectionPoint = cp
}
val call = object: ApplicationCall by this {
override val request: ApplicationRequest = req
}
this.application.execute(call)
}
Here it creates a copy of an ApplicationCall object with the replaced path for a request. I use delegates to avoid boilerplate code. You can use redirectInternally function like this:
get("/foo") {
call.redirectInternally("/bar")
}
Due to some url versioning, we try to map multiple paths to the same handler.
I tried to achieve this via rerouting but the query parameters get lost in the process.
// reroute if the path contains apiv3 / api v3
router.route("/apiv3/*").handler( context -> {
String path = context.request().path();
path = path.replace("apiv3/", "");
LOG.info("Path changed to {}", path);
context.reroute(path);
});
What is the most elegant way around this problem?
There are some discussions on google groups but surprisingly nothing quick and simple to implement.
The reroute documentation says:
It should be clear that reroute works on paths, so if you need to
preserve and or add state across reroutes, one should use the
RoutingContext object.
So you could create a global catch-all route that stores any query param in the RoutingContext:
router.route().handler(ctx -> {
ctx.put("queryParams", ctx.queryParams());
ctx.next();
});
Then your apiv3 catch-all route:
router.route("/apiv3/*").handler( context -> {
String path = context.request().path();
path = path.replace("apiv3/", "");
LOG.info("Path changed to {}", path);
context.reroute(path);
});
Finally an actual route handler:
router.get("/products").handler(rc -> {
MultiMap queryParams = rc.get("queryParams");
// handle request
});
I'm using Apama v10.3.1. I'm using the built-in Apama container of a Cumulocity installation, that is all I'm uploading is a monitor, not an entire Apama project. In my Apama monitor I'm executing an HTTP GET request against the Cumulocity REST API to obtain additional parameters I need for my monitor processing.
My problem is that when executing the HTTP request I need to provide a user and password, otherwise I get a 401 error. Since I do not want to hard code the user and password in my monitor, is there any way to use the credentials the built-in Apama container uses for communicating with Cumulocity? Since Apama communicates with Cumulocity under the hood for exchanging events, measurements and alike, I would assume that there are credentials available somewhere. Is that the case and if so, how can I tell my Apama monitor to use these credentials?
Here is an extract of the code:
monitor SampleAlarmRules {
action onload() {
monitor.subscribe(Alarm.CHANNEL);
monitor.subscribe(FindManagedObjectResponse.CHANNEL);
on all Alarm(type = "ax_UnavailabilityAlarm") as a {
onAlarm(a.source);
}
}
action onAlarm(string source) {
integer reqId := integer.getUnique();
send FindManagedObject(reqId, source, new dictionary<string,string>) to FindManagedObject.CHANNEL;
on FindManagedObjectResponse(reqId = reqId) as resp
and not FindManagedObjectResponseAck(reqId) {
ManagedObject dev := resp.managedObject;
dictionary<string, string> httpConfig := {
HttpTransport.CONFIG_AUTH_TYPE:"HTTP_BASIC"
//HttpTransport.CONFIG_USERNAME:"someUser",
//HttpTransport.CONFIG_PASSWORD:"somePassword"
};
HttpTransport httpTransport := HttpTransport.getOrCreateWithConfigurations("someBaseUrl", 80, httpConfig);
Request request := httpTransport.createGETRequest("/inventory/managedObjects/5706999?withParents=true");
request.execute(handleResponse);
}
}
action handleResponse(Response response) {
JSONPlugin json := new JSONPlugin;
if response.isSuccess(){
switch (response.payload.data as payload) {
case string: {
}
default: {
}
}
}
else {
print "###Request failed. Response status is: " + response.statusCode.toString() + " | " + response.statusMessage;
}
}
}
With this configuration (user and password commented) I get the following print statement:
Request failed. Response status is: 401 | Unauthorized
When enabling the user and password, the request executes successfully. However, I do not want to hard code the user and password in here.
Also, is there a way to get the current tenant from an environment variable or anything like that so that I don't have to hard code the base URL?
Thanks
Mathias
Yes, it is possible to do that because Cumulocity passes these as environment variables to all microservice including the Apama microservice.
You should be able to use the com.apama.correlator.Component event to access the environment variables. Use
Component.getInfo("envp") to get the dictionary of environment properties and then lookup the interested variables. You can see the list of environment variables at http://www.cumulocity.com/guides/reference/microservice-runtime/#environment-variables
So for your use case something like following will work:
using com.apama.correlator.Component;
...
monitor test {
action onload() {
dictionary<string,string> envp := Component.getInfo("envp");
dictionary<string, string> httpConfig := {
HttpTransport.CONFIG_AUTH_TYPE:"HTTP_BASIC",
HttpTransport.CONFIG_USERNAME:envp["C8Y_USER"],
HttpTransport.CONFIG_PASSWORD:envp["C8Y_PASSWORD"]
};
HttpTransport httpTransport := HttpTransport.getOrCreateWithConfigurations("someBaseUrl", 80, httpConfig);
Request request := httpTransport.createGETRequest("/inventory/managedObjects/5706999?withParents=true");
request.execute(handleResponse);
}
}
Similarly, you can access the tenant name using environment variable C8Y_TENANT.
Please note that these environment variables are available only in the cloud. If you want to do the same or test the code locally when using it with Cumulocity transport added by yourself, without changing the code, you can manually define the same environment variables in the run configuration of the Designer so that they are available there as well.
Trying out GetStream for Swift, I am not able to add an activity.
class MyActivity : Activity {}
...
let client = Client(apiKey: <MyApiKey>, appId: <MyApiKey>, token: <Token>)
let ericFeed = client.flatFeed(feedSlug: "user", userId: "eric")
let activity = MyActivity(actor: "eric", verb: "waves", object: "picture:10", foreignId: "picture:10")
ericFeed.add(activity) { result in
print("!result!")
print(result)
}
The token is generated server-side, is in form eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZXJpYyJ9.AAAAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAA-AAAAAAAA and:
client.currentUserId returns eric (So, the token is correct?)
The Callback of ericFeed.add(activity) is not called
On my app's dashboard log, I see the attempt of adding the activity as failed, with error 403
I tried different ids (with different tokens), both actor: "eric" and actor: "user:eric". What could have gone wrong?
The code to generate the token (php server) is:
$userId = "eric";
$client = new GetStream\Stream\Client(<MyApiKey>, <MyAppSecret>);
$userToken = $client->createUserSessionToken($userId);
And I receive to logs on my dashboard:
There is a couple things that needs to keep in mind.
First of all, probably your client was deallocated when the request was ended and that's why the callback wasn't called, but logs could show you that the request was done. I suggest you to use a shared Client instance and it would be easy to use. To setup a shared Client you need to write this:
Client.config = .init(apiKey: "<MyApiKey>", appId: "<MyApiKey>", token: "<Token>")
More about the Client setup in wiki page.
The second important thing, that you have to create/update your Stream user. From the server side you are getting your Token with the Stream userId and can request the Stream user. The easiest way is to call Client.shared.create(user:) where user would be created/updated. So, it's still a part of the Stream Client setup:
Client.shared.create(user: GetStream.User(id: Client.shared.currentUserId!)) { result in
// Update the client with the current user.
Client.shared.currentUser = try? result.get()
// Do all your requests from here. Reload feeds and etc.
}
More info in docs.
I suggest you to create feeds with only feedSlug parameter and the Stream userId would be taken from the Token. But it would be Optional, because the currentUserId is optional. For example:
let ericFeed = Client.shared.flatFeed(feedSlug: "user")
ericFeed?.add(activity)
And for your activities, Stream clients should always use the current Stream user as an actor. So, we need to update the definition of your MyActivity.
Finally, here is your code that should works:
// Define your activity.
class MyActivity: EnrichedActivity<GetStream.User, String, DefaultReaction> {
// ...
}
// Setup Stream Client.
Client.config = .init(apiKey: <MyApiKey>, appId: <MyApiKey>, token: <Token>)
// Setup the current user.
Client.shared.getCurrentUser {
let ericFeed = Client.shared.flatFeed(feedSlug: "user")
let activity = MyActivity(actor: Client.shared.currentUser!, verb: "waves", object: "picture:10", foreignId: "picture:10")
ericFeed?.add(activity) { result in
print("!result!")
print(result)
}
}
What is the correct way to alter a Request performing an asynchronous task before the Request happens?
So any request Rn need to become transparently Tn then Rn.
A little of background here: The Task is a 3rd party SDK that dispatch a Token I need to use as Header for the original request.
My idea is to decorate the Rn, but in doing this I need to convert my Tn task into a Siesta Request I can chain then.
So I wrapped the Asynchronous Task and chained to my original request.
Thus any Rn will turn into Tn.chained { .passTo(Rn) }
In that way, this new behaviour is entirely transparent for the whole application.
The problem
Doing this my code end up crashing in a Siesta internal precondition:
precondition(completedValue == nil, "notifyOfCompletion() already called")
In my custom AsyncTaskRequest I collect the callbacks for success, failure, progress etc, in order to trigger them on the main queue when the SDK deliver the Token.
I noticed that removing all the stored callback once they are executed, the crash disappear, but honestly I didn't found the reason why.
I hope there are enough informations for some hints or suggests.
Thank you in advance.
Yes, implementing Siesta’s Request interface is no picnic. Others have had exactly the same problem — and luckily Siesta version 1.4 includes a solution.
Documentation for the new feature is still thin. To use the new API, you’ll implement the new RequestDelegate protocol, and pass your implementation to Resource.prepareRequest(using:). That will return a request that you can use in a standard Siesta request chain. The result will look something like this (WARNING – untested code):
struct MyTokenHandlerThingy: RequestDelegate {
// 3rd party SDK glue goes here
}
...
service.configure(…) {
if let authToken = self.authToken {
$0.headers["X-Auth-Token"] = authToken // authToken is an instance var or something
}
$0.decorateRequests {
self.refreshTokenOnAuthFailure(request: $1)
}
}
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = $0.response, // Did request fail…
error.httpStatusCode == 401 else { // …because of expired token?
return .useThisResponse // If not, use the response we got.
}
return .passTo(
self.refreshAuthToken().chained { // If so, first request a new token, then:
if case .failure = $0.response { // If token request failed…
return .useThisResponse // …report that error.
} else {
return .passTo(request.repeated()) // We have a new token! Repeat the original request.
}
}
)
}
}
func refreshAuthToken() -> Request {
return Request.prepareRequest(using: MyTokenHandlerThingy())
.onSuccess {
self.authToken = $0.jsonDict["token"] as? String // Store the new token, then…
self.invalidateConfiguration() // …make future requests use it
}
}
}
To understand how to implement RequestDelegate, you best bet for now is to look at the new API docs directly in the code.
Since this is a brand new feature not yet released, I’d greatly appreciate a report on how it works for you and any troubles you encounter.