unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.46k stars 176 forks source link

How can I get a login form with POST method? #499

Closed ghiboz closed 3 years ago

ghiboz commented 3 years ago

hi all!

I'm in trouble finding a simple sample that let me to retrieve POST values from a form.. is there a sample to get this?

thank you

tiziano-morgia commented 3 years ago

Hi, you can take example from main readme: https://github.com/unosquare/embedio#reading-from-a-post-body-as-a-dictionary-applicationx-www-form-urlencoded or you need something else?

ghiboz commented 3 years ago

grazie @tiziano-morgia, but the sample above doesn't work... I wish a simple sample to manage the post values (and maybe the html used) thanks in advance

rdeago commented 3 years ago

Hello @ghiboz, thanks for using EmbedIO!

(Also, thanks a lot @tiziano-morgia for the link)

If your form contains only a few fields, each of which can always be deserialized to a .NET type, you may also find the FormField attribute handy:

[Route(HttpVerbs.Post, "/data")]
    public async Task PostData([FormField] string name, [FormField] int age) 
    {
        // Here you can use name and age directly.
    }

You can also decide whether or not a 400 Bad Request is automatically sent if a field is missing, and associate a field to a parameter with a different name. Read the relevant documentation for more details.

ghiboz commented 3 years ago

Hello @ghiboz, thanks for using EmbedIO!

(Also, thanks a lot @tiziano-morgia for the link)

If your form contains only a few fields, each of which can always be deserialized to a .NET type, you may also find the FormField attribute handy:

[Route(HttpVerbs.Post, "/data")]
    public async Task PostData([FormField] string name, [FormField] int age) 
    {
        // Here you can use name and age directly.
    }

You can also decide whether or not a 400 Bad Request is automatically sent if a field is missing, and associate a field to a parameter with a different name. Read the relevant documentation for more details.

thank you! and the html form as action should have /data ?? I'll try

rdeago commented 3 years ago

and the html form as action should have /data?

Of course. Also, method="POST".

I'm curious, though, to know what didn't work for you in the example linked by @tiziano-morgia.

ghiboz commented 3 years ago

I made a clean project: this is my code:

        private static WebServer CreateWebServer(string url)
        {

            var server = new WebServer(o => o
                    .WithUrlPrefix(url)
                    .WithMode(HttpListenerMode.EmbedIO))
                // First, we will configure our web server by adding Modules.
                .WithLocalSessionManager()
                .WithWebApi("/api", m => m.WithController<DummyController>())
                //.WithModule(new WebSocketChatModule("/chat"))
                //.WithModule(new WebSocketTerminalModule("/terminal"))
                .WithStaticFolder("/", @"D:\Temp\BootStrap", true, m => m
                    .WithContentCaching(true)) // Add static files after other modules to avoid conflicts
                .WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" })));

            // Listen for state changes.
            server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info();

            return server;
        }

my DummyController class:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.Utilities;
using EmbedIO.WebApi;

namespace wwwServer
{
    public sealed class DummyController : WebApiController
    {
        [Route(HttpVerbs.Post, "/login")]
        public async Task PostData([FormData] NameValueCollection data)
        {
            foreach (var item in data)
            {
                Console.WriteLine(item);
            }
            return;
        }
    }
}

and in the static folder into index.html there's a form with this action:

<form action="/api/login" method="POST">

here is the value of the data...

rdeago commented 3 years ago

May I also see your HTML, at least the form tag and its contents?

ghiboz commented 3 years ago

here is the code:

<main class="form-signin">
    <form action="/api/login" method="POST">
      <img class="mb-4" src="../assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">
    <h1 class="h3 mb-3 fw-normal">Inserisci le credenziali</h1>
    <label for="inputEmail" class="visually-hidden">Utente MHS</label>
    <input type="text" id="inputName" name="inputName" class="form-control" placeholder="Utente MHS" required autofocus>
    <label for="inputPassword" class="visually-hidden">Password</label>
    <input type="password" id="inputPassword" name="inputPassword" class="form-control" placeholder="Password" required>
    <button class="w-100 btn btn-lg btn-primary" type="submit">Entra</button>
    <p class="mt-5 mb-3 text-muted">&copy; 2017-2020</p>
  </form>
</main>

I forgot to set the name instead of id, and now something goes on, but the NameValueCollection contains only the names, not the values of the POST data...

thank you!

rdeago commented 3 years ago

As a matter of fact your PostData method only shows the names in the NameValueCollection . Try this:

        [Route(HttpVerbs.Post, "/login")]
        public void PostData([FormData] NameValueCollection data)
        {
            foreach (var key in data.AllKeys)
            {
                Console.WriteLine($"{key} => {data[key]}");
            }
        }

Also notice that WebApi controller methods do not need to be asynchronous: a void method is perfectly acceptable and will cause EmbedIO to send an empty 200 OK response, just as a method returning a Task.

ghiboz commented 3 years ago

🙏 it works now!

Just another thing: in the old v2 I used in my WebApiController class the method

this.HtmlResponseAsync(myHtmlPage);

now this method doesn't exist... how can I manage a navigation between my html pages? thank you!!

(stupid example: I have:

in index.html I have the form to check my user/password (and the flow comes to my class correctly, now I wish redirect to ok.html if the user is valid or ko.html if the user is not valid..

thank you! 👏

rdeago commented 3 years ago

throw HttpException.Redirect("/nextPage");

Note that this will work because the WebApi method is responding to a form, so the browser expects some HTML to load. For a "normal" API call, made by a Javascript XmlHttpRequest object, you'll have to manage the redirection client-side, probably assigning an URL to window.location.href after you receive the response.

ghiboz commented 3 years ago

thank! and

throw HttpException.Redirect("/nextPage");

Note that this will work because the WebApi method is responding to a form, so the browser expects some HTML to load. For a "normal" API call, made by a Javascript XmlHttpRequest object, you'll have to manage the redirection client-side, probably assigning an URL to window.location.href after you receive the response.

great! and if I wish 'compile' the html, parsing some stuff? (like the old function, where I pass the html code inside the function)? thank you

rdeago commented 3 years ago

Short answer: you don't. 😜 This is not what Web APIs are for. Actually, this is not how the Web works.

Long answer:

HTTP was born as a stateless protocol, i.e. lacking any inherent mechanism to retain information between requests. This soon proved to be a problem for web sites requiring authentication: sure, you could just "compile" some HTML in response to a login form, but if different pages require authentication (as is most often the case), the user will have to fill in and send a login form every time they want to access each of them. Not very convenient, to say the least.

Enter cookies, small pieces of data (sort of a Dictionary<string, string>) that can be embedded in a response stored by the client and sent along with every subsequent request to the same server. Note that this is a shamefully stripped-down description: the point is, cookies are a possible way to retain information about the user, such as its username, which pages they have access to, etc. Cookies may be read and written by both the server and client (unless they have a special flag called httpOnly that hides a cookie from Javascript code on the client, leaving it for exclusive access by the server).

So do we just store the user's login name in a cookie, send it back along with the redirection to ok.html, then examine it and act accordingly? Maybe using Javascript, maybe server-side templating?

No we don't, for a series of reasons, some of which are detailed below.

OK, so cookies are not the whole solution, but they may be part of it. Instead of storing all user-related data in cookies, we can:

This is known as session management, where a session is the life span of our data set (our session data); the associated GUID is called a session ID; and the cookie we use is called a session cookie.

Practically every language or framework used to run web servers provides some form of session management, so you don't have to rethink it from scratch; fortunately, EmbedIO is no exception. The IHttpContext interface gives you access to an automatically managed Session object, that acts similarly to a Dictionary<string, object> but does not store null values (null is equivalent to "no value present" in an EmbedIO session).

I see you already have a LocalSessionManager in your server. All you need to do is:

Since you seem to want to generate HTML on the server, the main advice I can give you is to put JSON-returning Web API methods in a different WebAPiModule from HTML-returning methods, otherwise your returned HTML will be serialized as a JSON string.

To initialize a WebApiModule with no serialization (where you just return string or Task<string> from a controller method and it gets sent as-is to the client) you need a ResponseSerializerCallback, i.e. a method that serializes (or in this case does not serialize) returned data and sends it to the client:

        private static async Task Html(IHttpContext context, object? data)
        {
            if (data is null)
            {
                // Send an empty response
               return;
            }

            context.Response.ContentType = MimeType.Html;
            using var text = context.OpenResponseText(new UTF8Encoding(false));
            // string.ToString returns the string itself
            await text.WriteAsync(data.ToString()).ConfigureAwait(false);
        }

        // ...then, in your server initialization code...
        .WithWebApi("/pages", Html, m => m.WithController<PagesController>())
        // Methods of PagesController may use session data etc. and return HTML as a string

Mind that you'll have to redirect to /pages/ok.html upon successful login. Your PagesController class must have a method with a [Route(HttpVerbs.Get, "/ok.html")] attribute.

It is also possible to mix static web pages (returned by FileModule) with dynamic pages (generated by a WebApiModule) on the same path, but it requires subclassing WebApiModule, overriding the IsFinalHandler property with a getter that always returns false. This was a design glitch on my part, for which I'm sorry.


Some relevant API documentation links:

xxzl0130 commented 3 years ago

@rdeago Sorry for disturbing you, but I still don't know how to set session.
I directly set session data through HttpContext.Session, but browser can't get the data.
Code here:

server.WithModule(new ActionModule("/test", HttpVerbs.Get, async ctx => {
  ctx.Response.SetCookie(new Cookie("test", "hello"));
  ctx.Session["testSession"] = "hello";
  using (var writer = ctx.OpenResponseText())
  {
      await writer.WriteAsync("Hello!");
  }
}));
rdeago commented 3 years ago

@xxzl0130 no problem. 😉

Your code reveals some confusion on the subject of sessions and session data, so let's clear that up first.

A session is a sequence of logically grouped interactions (request + response) between a web server and a client. All interactions in the same session share some state: it may be the ID of the logged-in user, the choice of color theme... anything you want both the client and server to remember from request to request. This information is collectively called session data.

The HTTP protocol has no concept of state, but it gives us cookies, small pieces of information that can be read and/or written by both the server and client. Cookies are stored on the client machine and "remembered" from request to request. In theory, we could just use a cookie to store all session data in some serialized form, e.g. JSON.

The problem with cookies is that they can be modified, and even completely made up, by a malicious client. You don't want someone to be able to transfer money from someone else's account just because they sent a cookie with the other user's name.

Then someone came up with a solution:

This is what a session manager does in EmbedIO. You don't have to worry about cookies or expired sessions. Session data, in the form of a key/value dictionary, is available in every HTTP context.

When a request arrives, the session manager looks for a particular cookie, identified by means of various properties of LocalSessionManager. If that cookie exists, and its value is a valid session ID, session data is automatically retrieved and made available through context.Session.

When you set a value in HttpContext.Session, the session manager sets it in the session's dictionary if there is one; otherwise, it automatically creates a new session and adds a cookie with the ID of the newly-created session to the response.

When you retrieve a value from HttpContext.Session and there is no session, the result is null.

HttpContext.Session is an ISessionProxy interface: besides giving you access to session data by acting more or less like a Dictionary<string, object>, it can tell you whether a session exists, delete a session completely (useful, for example, when a user logs out), or delete the current session and create a new one.

All you can do with HttpContext.Session is in EmbedIO's documentation, here and here.

To recap:

xxzl0130 commented 3 years ago

@rdeago Thank you very much for your detailed explanation!
And now I know that, what made me confused is that HttpContext.Session retrieve data only after TryGetValue or TakeSnapshot being called and I just observed the value from the debugging interface before.

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.