RpcElixir.Plug (elixir_ts_rpc v0.0.2)

Copy Markdown View Source

HTTP transport adapter for RpcElixir, implemented as a Plug.

It mounts a router at a path prefix. It decodes JSON bodies and dispatches to the procedure pipeline. It drains response cookies and headers onto the conn, then renders JSON. See Getting started for the mounting walkthrough.

Options

  • :router (required) — a module using RpcElixir.Router.
  • :path_prefix (optional, default "/rpc") — stripped from the request path before dispatch. POST /rpc/users.get dispatches "users.get".
  • :ctx_builder (optional) — (Plug.Conn.t() -> RpcElixir.Context.t()). Its result is the base context. The transport always overwrites :req with conn-derived metadata (cookies, headers, remote_ip, session). Those fields are always present, whatever the builder returns.
  • :max_body_size (optional, default 8 MB) — request body cap in bytes. Larger bodies are rejected with 413 :payload_too_large.
  • :max_body_depth (optional, default 64) — nesting-depth cap on the decoded JSON body. Deeper payloads are rejected with 400 :input_validation_failed, before validation runs. This bounds stack and CPU use that the byte cap alone cannot.
  • :require_content_type (optional, default true) — the request must carry content-type: application/json. A charset and other params are allowed. Other types are rejected with 415 :unsupported_media_type. See ## Security / CSRF.
  • :allowed_origins (optional, default nil = disabled) — allow-list of origin strings. An origin header outside the list is rejected with 403 :forbidden. See ## Security / CSRF.

Security / CSRF

This adapter dispatches state-changing RPC over POST. It can pair with cookie-based sessions (see ## Session integration). That combination is a CSRF surface. A cross-site page can auto-submit a form to an RPC endpoint. The browser then attaches the session cookie. Undefended, that lets an attacker trigger authenticated calls. Two mitigations are enforced here:

  • Content-Type enforcement (:require_content_type, default true). Requiring application/json stops the request being a "simple" cross-site request. Browsers must then send a CORS preflight, which the server never approves. HTML forms can only send application/x-www-form-urlencoded, multipart/form-data, or text/plain. They are blocked outright. This is the primary CSRF defense.
  • Origin allow-listing (:allowed_origins, default disabled). When configured, a present but non-allow-listed origin is rejected with 403. This is defense-in-depth for browsers that send Origin on state-changing requests.

The bundled JS client always sends Content-Type: application/json. So the default-on enforcement does not break legitimate use.

Session integration

Configure Plug.Session earlier in your pipeline. The adapter reads it into ctx.req.session. It drains Resolution.resp_session back after dispatch.

defmodule MyApp.Router do
  use Plug.Builder

  plug Plug.Session,
    store: :cookie, key: "_my_app_session", signing_salt: "my_salt"

  plug :fetch_session
  plug RpcElixir.Plug, router: MyApp.RpcRouter
end

In middleware, use Resolution.put_session/3, delete_session/2, or clear_session/1 to change the session. Read it via res.ctx.req[:session].

Response draining order

After dispatch, session mutations, cookies, and headers are applied to the conn first. The response body is written only after that. Plug requires this order: session and cookie mutations must precede the response.