Drab: remote controlled frontend framework for Phoenix

So,

  1. create a Drab.Plug to add a cookie, containing token + some config (Drab JS templates depends on config values as well); the question is if I can have a controller name in the plug (from conn)
  2. create an NPM bundle with a static drab JS

Like that way, as it will be more transparent (no more adding <%= Drab.Client.run %> to the layout template). Also, there will be normal caching and minimization of the drab JS.

How can I ensure that token is a single use only?

Wasn’t the controller name in the conn?

Or just do it like Phoenix’s JS, you just include it from the deps directory straight so it is always properly updated (although an NPM entry can be useful for if someone is using javascript frameworks).

Need some kind of backing store combined with a quick timeout (along with the usual things to get a new token when reconnecting else you still run into the 24-hr timeouts or so that you do now). A good default for single-servers is just an ETS table with a janitor over it (Cachex library makes that so simple), can go up to mnesia for a multi-server backing store as default, although make it pluggable of course then someone could use their PostgreSQL database or whatever they want. And there are tons and tons of other methods too, even just a super-short timeout of 30 seconds or so would prevent any of the usual attacks as well, or just don’t have a timeout on the token and instead really just use an immediate timeout cookie, so when the page loads, it gets the cookie data, but the cookie doesn’t get permanently stored since it’s only valid for that one request anyway (no backing store then, and you can have tokens with multi-day or longer timeouts without issue, just leave that configurable). Tons of styles to choose from. :slight_smile:

True! This is the best solution.

But you need to store it somewhere anywyay, in case of disconnection?

The page’s javascript itself would hold on to it, and if the page gets reloaded anyway then the server will end up sending a new one regardless. :slight_smile:

1 Like

This is cool, I like it.

On the load, Drab reads the token cookie (max-age of few minutes) and immediately destroys it. The only where it is stored, is a closure for connect.

Moving all the stuff to the plug may also solve my issue with reading the session cookie, I just need to ensure that Drab plug is called before the Plug session!

2 Likes

Which will not be a very good practice.

1 Like

Did you miss another post in this short thread? :slight_smile: yeah, me too. I have a survey to the community. The changes belowe would be quite fundamental, so they should be implemented (or not!) before 1.0.

1. Shall we make Drab JS static?

Now, Drab javascripts are eex templates, so they are generated each time depending on config, action, controller, etc etc. The benefit is that we can shrink the whole stuff when not using some of Drab Modules.
But, if we make JSes static, they could be cacheable and shrinkable.

2. Shall we pass the token via cookie? (and have Drab.Plug)

You can see this discussion few pages above. The whole idea is to create a plug which creates a cookie with token, instead of passing it in the page source with Drab.Client.run in app.html.eex

3. Shall we enable Drab on all pages in the application?

If we do 1. and 2., Drab.Client.run in app.html.eex will be obsolete, as Drab will run on each page. Now, Drab.Client.js checks, for example, if the commander exists, and if not, does not inject JS and does not run Drab client.
By the other hand, I think it is not impossible to check the existence of the commander (and other checks) in the plug, so we could keep at least part of the functionality (Drab JS will be loaded, as it is static, but not try to connect).

4. Shall we create own structure and use it instead of %Phoenix.Socket{}?

Drab now base on the socket structure, so socket is passed to every handler function, and all drab functions rely on the socket. But the “Drab socket” contains more information than the ordinary one. So far we store it in assigns field as underscore__underscore fields, eg. __drab_pid. This works, but is not very elegant obviously.
I’ve tried to use private field in the socket struct, which requires a small change in Phoenix, but @chrismccord suggested to make the own structure. Obviously this struct will store a socket as well, something like:

%Drab.Drut{socket: Phoenix.Socket.t(), pid: pid, controller: atom, action: atom, store: map, session: map, ...}

I don’t know what to think about it, it’s elegant, but I still like the idea of two worlds, render-time (conn and controller) and run-time (socket and commander).

5. Shall we have a full read/write access to the Plug Session?

It can be done, if we have a plug (might be the same from point 2.) which is called before :fetch_session, as it destroys the session cookie name and access method. It is risky, as Drab handlers may be run in parallel. One function may overwrite the session and you’re not aware of it.

6. Shall Drab support user_ids, disconnections?

Very recent discussion. Now Drab does not care about which user is connected. Shall we create a support for users? And the most important question, how? I would be grateful for any API propositions :slight_smile:

2 Likes

At least support for session and/or assign data in the socket connection (before channels) so we can auth at that point, I think that’s (mostly?) done? :slight_smile:

It is done, but read-only so far.

As you may use your own connect callback, you can.

But, you don’t have an access to the session in connect callback.

Ah so that was the issue I had, yeah we just need some way to specify information to be passed in to the connect callback.

You can only use the additional params in Drab.Client function:

<%= Drab.Client.run(@conn, user_id: 42) %>
def connect(%{"user_id" => id} = params, socket) do
  ...
end

Yep, I wish we could use the cookie. ^.^;

1 Like

v0.9.2 is out

This is a slow evolution release, containing a number of bug fixes and a few new features:

  • Cookies function in Drab.Browser
iex> set_cookie(socket, "mycookie", "value", max_age: 10)
{:ok, "mycookie=value; expires=Thu, 19 Jul 2018 19:47:09 GMT"}


iex> Drab.Browser.cookies(socket, decoder: Drab.Coder.Cipher)
{:ok, %{"other" => 42, "mycookie" => "value"}}
  • Drab.{Live, Core, Element, Modal} functions accept %Phoenix.HTML.Safe{}

That means you don’t need to call safe_to_string/1 anymore, just pass the safe:

html = ~E"<strong><%= nick %>:</strong> <%= message %><br>"
broadcast_insert socket, "#chat", :beforeend, html
  • Tested with Elixir 1.7

Drab is now blessed to work with Elixir 1.7 and OTP 21.

3 Likes
  1. In regard to making it static
I would say no.

In ANY other language, I would say yes
but EEX templates are so efficient that I would be surprised if it’s worth it. As long as you include some type of version hash on the URL, a user who wanted their to be static could point to it with a CDN domain.

1 Like

Actually, it is impossible now. Drab’s JSes are generated each time and they are different for literally each request. This is what I want to change.

I guess the big question would be the tradeoff then.

After a long discussion with myself, my answers to the questions are:

1. Yes

They will be cacheable, shrinkable and easier to maintain.

"dependencies": {
  "drab": "file:deps/drab",
  "drab_query": "file:deps/drab" // TBD

2. Yes

Very short max_age. Drab client will grab it, and delete it from document.cookies immediately.

3. Yes

Standard Drab installation will just add plug to generate a token cookie, and Drab will start on load. We just need to solve the case, when you want to add something, like authentication token, to the connect callback. Proposition:

plug(Drab.Client, connect: false)

and

if (window.Drab) Drab.connect({auth_token: window.my_token});

Drab will not try to start if the commander is not present.

4. No

%Phoenix.Socket{} is enough. I will just do some housekeeping, moving all drab’s underscore assign to the one struct, and keep all under __drab assign.

5. No

We can’t. Session cookie is http only.

6. Yes

But I still have no idea how :slight_smile:

1 Like

Hello Grych - just trying drab-example and also starting from scratch with a new phoenix app using your instructions.

using OSX
I started off with elixir 1.6 and erlang 20.0 and could not get this working.
I then upgraded to 1.7 and 21.0 after scanning latest comments.

still no luck.

drab-example gave me this error in the browser

mix phx.server
warning: found quoted keyword “test” but the quotes are not required. Note that keywords are always atoms, even when quoted, and quotes should only be used to introduce keywords with foreign characters in them
mix.exs:58

Compiling 14 files (.ex)
Generated drabstuff app
[info] Running DrabstuffWeb.Endpoint with Cowboy using http://0.0.0.0:4000
17:20:16 - info: compiling
17:20:17 - info: compiled 6 files into 2 files, copied 3 in 6.2 sec
[info] GET /
[debug] Processing with DrabstuffWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 500 in 368ms
[error] #PID<0.544.0> running DrabstuffWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
** (RuntimeError) the algorithm :aes_gcm is not supported by your Erlang/OTP installation. Please make sure it was compiled with the correct OpenSSL/BoringSSL bindings
(plug) lib/plug/crypto/message_encryptor.ex:114: Plug.Crypto.MessageEncryptor.raise_notsup/1
(plug) lib/plug/crypto/message_encryptor.ex:58: Plug.Crypto.MessageEncryptor.aes128_gcm_encrypt/3
(drabstuff) lib/drabstuff_web/templates/page/index.html.drab:1: DrabstuffWeb.PageView.“index.html”/1
(drabstuff) lib/drabstuff_web/templates/layout/app.html.eex:29: DrabstuffWeb.LayoutView.“app.html”/1
(phoenix) lib/phoenix/view.ex:332: Phoenix.View.render_to_iodata/3
(phoenix) lib/phoenix/controller.ex:740: Phoenix.Controller.do_render/4
(drabstuff) lib/drabstuff_web/controllers/page_controller.ex:1: DrabstuffWeb.PageController.action/2
(drabstuff) lib/drabstuff_web/controllers/page_controller.ex:1: DrabstuffWeb.PageController.phoenix_controller_pipeline/2
(drabstuff) lib/drabstuff_web/endpoint.ex:1: DrabstuffWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:278: Phoenix.Router.call/1
(drabstuff) lib/drabstuff_web/endpoint.ex:1: DrabstuffWeb.Endpoint.plug_builder_call/2
(drabstuff) lib/plug/debugger.ex:122: DrabstuffWeb.Endpoint.“call (overridable 3)”/2
(drabstuff) lib/drabstuff_web/endpoint.ex:1: DrabstuffWeb.Endpoint.call/2
(plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
(cowboy) /Users/elay/projects/phoenix/drabstuff/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Sorry - error was in terminal and browser

That must be a root of an issue. Please ensure your erlang installation contains the latest openSSL. For OSX, I am using OTP installed forom both brew and port, and both are OK.