Packages
phoenix
0.12.0
1.8.9
1.8.8
1.8.7
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.8.0-rc.4
1.8.0-rc.3
1.8.0-rc.2
1.8.0-rc.1
1.8.0-rc.0
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.7.0-rc.3
1.7.0-rc.2
1.7.0-rc.1
1.7.0-rc.0
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.6.0-rc.1
1.6.0-rc.0
1.5.15
1.5.14
1.5.13
1.5.12
1.5.11
1.5.10
1.5.9
1.5.8
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.5.0-rc.0
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.3
1.4.0-rc.2
1.4.0-rc.1
1.4.0-rc.0
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.3.0-rc.3
1.3.0-rc.2
1.3.0-rc.1
1.3.0-rc.0
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.1
1.2.0-rc.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Productive. Reliable. Fast. A productive web framework that does not compromise speed or maintainability.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/phoenix/transports/long_poller.ex
defmodule Phoenix.Transports.LongPoller do
use Phoenix.Controller
@moduledoc """
Handles LongPoller clients for the Channel Transport layer.
## Configuration
The long poller is configurable via the Endpoint's transport configuration:
config :my_app, MyApp.Endpoint, transports: [
longpoller_window_ms: 10_000,
longpoller_pubsub_timeout_ms: 1000,
longpoller_crypto: [iterations: 1000,
length: 32,
digest: :sha256,
cache: Plug.Keys],
]
* `:longpoller_window_ms` - how long the client can wait for new messages
in it's poll request.
* `:longpoller_pubsub_timeout_ms` - how long a request can wait for the
pubsub layer to respond.
* `:longpoller_crypto` - configuration for the key generated to sign the
private topic used for the long poller session (see `Plug.Crypto.KeyGenerator`).
"""
alias Phoenix.Socket.Message
alias Phoenix.Transports.LongPoller
alias Phoenix.Channel.Transport
plug :check_origin
plug :allow_origin
plug :default_content_type
# We need to uncomment this when we remove channels from endpoints
# plug Plug.Parsers, parsers: [:json], json_decoder: Poison
plug :action
@doc """
Responds to pre-flight CORS requests with Allow-Origin-* headers.
"""
def options(conn, _params) do
send_resp(conn, :ok, "")
end
@doc """
Listens for `%Phoenix.Socket.Message{}`'s from `Phoenix.LongPoller.Server`.
As soon as messages are received, they are encoded as JSON and sent down
to the longpolling client, which immediately repolls. If a timeout occurs,
a `:no_content` response is returned, and the client should immediately repoll.
"""
def poll(conn, _params) do
case resume_session(conn) do
{:ok, conn, priv_topic} ->
listen(conn, priv_topic)
{:error, conn, :terminated} ->
new_session(conn)
end
end
defp listen(conn, priv_topic) do
ref = :erlang.make_ref()
:ok = broadcast_from(conn, priv_topic, {:flush, ref})
receive do
{:messages, msgs, ^ref} ->
:ok = ack(conn, priv_topic, msgs)
status_json(conn, %{messages: msgs, token: conn.params["token"], sig: conn.params["sig"]})
after
timeout_window_ms(conn) ->
:ok = ack(conn, priv_topic, [])
conn
|> put_status(:no_content)
|> status_json(%{token: conn.params["token"], sig: conn.params["sig"]})
end
end
defp new_session(conn) do
{conn, priv_topic, sig, _server_pid} = start_session(conn)
conn
|> put_status(:gone)
|> status_json(%{token: priv_topic, sig: sig})
end
@doc """
Publishes a `%Phoenix.Socket.Message{}` to a channel.
If the message was authorized by the Channel, a 200 OK response is returned,
otherwise a 401 Unauthorized response is returned.
"""
def publish(conn, message) do
case resume_session(conn) do
{:ok, conn, priv_topic} -> dispatch_publish(conn, message, priv_topic)
{:error, conn, :terminated} -> conn |> put_status(:gone) |> status_json(%{})
end
end
defp dispatch_publish(conn, message, priv_topic) do
msg = Message.from_map!(message)
case dispatch(conn, priv_topic, msg) do
:ok -> conn |> put_status(:ok) |> status_json(%{})
{:error, _reason} -> conn |> put_status(:unauthorized) |> status_json(%{})
end
end
## Client
@doc """
Starts the `Phoenix.LongPoller.Server` and stores the serialized pid in the session.
"""
def start_session(conn) do
router = router_module(conn)
priv_topic =
"phx:lp:"
|> Kernel.<>(Base.encode64(:crypto.strong_rand_bytes(16)))
|> Kernel.<>(:os.timestamp() |> Tuple.to_list |> Enum.join(""))
child = [router, timeout_window_ms(conn), priv_topic, endpoint_module(conn)]
{:ok, server_pid} = Supervisor.start_child(LongPoller.Supervisor, child)
{conn, priv_topic, sign(conn, priv_topic), server_pid}
end
@doc """
Finds the `Phoenix.LongPoller.Server` server from the session.
"""
def resume_session(conn) do
case verify_longpoll_topic(conn) do
{:ok, priv_topic} -> {:ok, conn, priv_topic}
:notopic -> {:error, conn, :terminated}
{:error, :terminated} -> {:error, conn, :terminated}
end
end
@doc """
Retrieves the serialized `Phoenix.LongPoller.Server` pid from the encrypted token.
"""
def verify_longpoll_topic(%Plug.Conn{params: %{"token" => token, "sig" => sig}} = conn) do
case verify(conn, token, sig) do
{:ok, priv_topic} ->
ref = :erlang.make_ref()
:ok = subscribe(conn, priv_topic)
:ok = broadcast_from(conn, priv_topic, {:subscribe, ref})
receive do
{:ok, :subscribe, ^ref} -> {:ok, priv_topic}
after
pubsub_timeout_ms(conn) -> {:error, :terminated}
end
_ -> :notopic
end
end
def verify_longpoll_topic(_conn), do: :notopic
@doc """
Ack's a list of message refs back to the `Phoenix.LongPoller.Server`.
To be called after buffered messages have been relayed to the client.
"""
def ack(conn, priv_topic, msgs) do
ref = :erlang.make_ref()
:ok = broadcast_from(conn, priv_topic, {:ack, Enum.count(msgs), ref})
receive do
{:ok, :ack, ^ref} -> :ok
after
pubsub_timeout_ms(conn) -> :error
end
end
@doc """
Dispatches deserialized `%Phoenix.Socket.Message{}` from client to
`Phoenix.LongPoller.Server`
"""
def dispatch(conn, priv_topic, msg) do
ref = :erlang.make_ref()
:ok = broadcast_from(conn, priv_topic, {:dispatch, msg, ref})
receive do
{:ok, :dispatch, ^ref} -> :ok
{:error, :dispatch, reason, ^ref} -> {:error, reason}
after
pubsub_timeout_ms(conn) -> {:error, :timeout}
end
end
defp timeout_window_ms(conn) do
get_in endpoint_module(conn).config(:transports), [:longpoller_window_ms]
end
defp pubsub_timeout_ms(conn) do
get_in endpoint_module(conn).config(:transports), [:longpoller_pubsub_timeout_ms]
end
defp pubsub_server(conn), do: endpoint_module(conn).__pubsub_server__()
defp subscribe(conn, priv_topic) do
Phoenix.PubSub.subscribe(pubsub_server(conn), self, priv_topic, link: true)
end
defp broadcast_from(conn, priv_topic, msg) do
Phoenix.PubSub.broadcast_from(pubsub_server(conn), self, priv_topic, msg)
end
defp check_origin(conn, _opts) do
Transport.check_origin(conn, send: &status_json(&1, %{}))
end
defp sign(conn, priv_topic) do
salt = derive_salt(conn, to_string(pubsub_server(conn)))
Plug.Crypto.MessageVerifier.sign(priv_topic, salt)
end
defp verify(conn, priv_topic, sig) do
salt = derive_salt(conn, to_string(pubsub_server(conn)))
case Plug.Crypto.MessageVerifier.verify(sig, salt) do
{:ok, ^priv_topic} -> {:ok, priv_topic}
_ -> :error
end
end
defp derive_salt(%Plug.Conn{secret_key_base: base}, _key)
when base == nil or byte_size(base) < 64 do
raise "conn.secret_key_base must be at least 64 bytes for longpoll token verification"
end
defp derive_salt(conn, key) do
crypto_opts = get_in(endpoint_module(conn).config(:transports), [:longpoller_crypto])
Plug.Crypto.KeyGenerator.generate(conn.secret_key_base, key, crypto_opts)
end
defp allow_origin(conn, _opts) do
headers = get_req_header(conn, "access-control-request-headers") |> Enum.join(", ")
conn
|> put_resp_header("access-control-allow-origin", "*")
|> put_resp_header("access-control-allow-headers", headers)
|> put_resp_header("access-control-allow-methods", "get, post, options")
|> put_resp_header("access-control-max-age", "3600")
end
# XDomainRequest doesn't allow you to set request headers so manually set
# Content-Type and run the JSON parser plug again
defp default_content_type(conn, _opts) do
if get_req_header(conn, "content-type") == [] do
update_in(conn.req_headers, &[{"content-type", "application/json"}|&1])
else
conn
end
end
defp status_json(conn, map) do
status = Plug.Conn.Status.code(conn.status || 200)
map = Map.put(map, :status, status)
conn
|> put_status(:ok)
|> json(map)
end
end