Skip to content

Commit

Permalink
Check origin in transports by default
Browse files Browse the repository at this point in the history
  • Loading branch information
José Valim committed Aug 4, 2015
1 parent 2fdda3e commit bbebb81
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 175 deletions.
37 changes: 20 additions & 17 deletions lib/phoenix/channel/transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -250,31 +250,34 @@ defmodule Phoenix.Channel.Transport do
Otherwise a otherwise a 403 Forbidden response will be sent and
the connection halted. It is a noop if the connection has been halted.
"""
def check_origin(conn, allowed_origins, sender \\ &Plug.Conn.send_resp/1)
def check_origin(conn, endpoint, check_origin, sender \\ &Plug.Conn.send_resp/1)

def check_origin(%Plug.Conn{halted: true} = conn, _allowed_origins, _sender) do
conn
end
def check_origin(%Plug.Conn{halted: true} = conn, _endpoint, _check_origin, _sender),
do: conn

def check_origin(conn, allowed_origins, sender) do
def check_origin(conn, endpoint, check_origin, sender) do
import Plug.Conn
origin = get_req_header(conn, "origin") |> List.first

if origin_allowed?(origin, allowed_origins) do
conn
else
resp(conn, :forbidden, "")
|> sender.()
|> halt()
cond do
is_nil(origin) ->
conn
origin_allowed?(check_origin, origin, endpoint) ->
conn
true ->
resp(conn, :forbidden, "")
|> sender.()
|> halt()
end
end

defp origin_allowed?(nil, _) do
true
end
defp origin_allowed?(_, nil) do
true
end
defp origin_allowed?(false, _, _),
do: true
defp origin_allowed?(true, origin, endpoint),
do: compare?(URI.parse(origin).host, endpoint.config(:url)[:host])
defp origin_allowed?(check_origin, origin, _endpoint) when is_list(check_origin),
do: origin_allowed?(origin, check_origin)

defp origin_allowed?(origin, allowed_origins) do
origin = URI.parse(origin)

Expand Down
58 changes: 30 additions & 28 deletions lib/phoenix/transports/long_poll.ex
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
defmodule Phoenix.Transports.LongPoll do
@moduledoc """
Handles LongPoll clients for the Channel Transport layer.
Socket transport for long poll clients.
## Configuration
The long poller is configurable in your Socket's transport configuration:
The long poll is configurable in your socket:
transport :longpoll, Phoenix.Transports.LongPoll,
window_ms: 10_000,
pubsub_timeout_ms: 1000,
crypto: [iterations: 1000,
length: 32,
digest: :sha256,
cache: Plug.Keys],
log: false,
check_origin: true,
crypto: [max_age: 1209600]
* `:window_ms` - how long the client can wait for new messages
in it's poll request.
in its poll request
* `:pubsub_timeout_ms` - how long a request can wait for the
pubsub layer to respond.
* `:crypto` - configuration for the key generated to sign the
private topic used for the long poller session (see `Plug.Crypto.KeyGenerator`).
pubsub layer to respond
* `:crypto` - options for verifying and signing the token, accepted
by `Phoenix.Token`. By default tokens are valid for 2 weeks
* `:log` - if the transport layer itself should log and, if so, the level
* `:check_origin` - if we should check the origin of requests when the
origin header is present. It defaults to true and, in such cases,
it will check against the host value in `YourApp.Endpoint.config(:url)[:host]`.
It may be set to `false` (not recommended) or to a list of explicitly
allowed origins
"""

## Transport callbacks

@behaviour Phoenix.Channel.Transport

@doc """
Provides the deault transport configuration to sockets.
* `:serializer` - The `Phoenix.Socket.Message` serializer
* `:pubsub_timeout_ms` - The timeout to wait for the LongPoll.Server ack
* `:log` - The log level, for example `:info`. Disabled by default
* `:timeout` - The connection timeout in milliseconds, defaults to `:infinity`
* `:crypto` - The list of encryption options for the `Plug.Session`
"""
def default_config() do
[window_ms: 10_000,
pubsub_timeout_ms: 1000,
serializer: Phoenix.Transports.LongPollSerializer,
log: false,
crypto: [iterations: 1000, length: 32,
digest: :sha256, cache: Plug.Keys]]
check_origin: true,
crypto: [max_age: 1209600]]
end

def handler_for(:cowboy), do: Plug.Adapters.Cowboy.Handler
Expand All @@ -58,10 +58,12 @@ defmodule Phoenix.Transports.LongPoll do
alias Phoenix.Transports.LongPoll
alias Phoenix.Channel.Transport

@doc false
def init(opts) do
opts
end

@doc false
def call(conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport)

Expand All @@ -70,7 +72,7 @@ defmodule Phoenix.Transports.LongPoll do
|> Plug.Conn.fetch_query_params
|> Transport.transport_log(opts[:log])
|> Transport.force_ssl(handler, endpoint)
|> Transport.check_origin(opts[:origins], &status_json(&1, %{}))
|> Transport.check_origin(endpoint, opts[:check_origin], &status_json(&1, %{}))
|> dispatch(endpoint, handler, transport, opts)
end

Expand Down Expand Up @@ -167,14 +169,14 @@ defmodule Phoenix.Transports.LongPoll do

child = [socket, opts[:window_ms], priv_topic]
{:ok, server_pid} = Supervisor.start_child(LongPoll.Supervisor, child)
{priv_topic, sign_token(endpoint, priv_topic), server_pid}
{priv_topic, sign_token(endpoint, priv_topic, opts), server_pid}
end

# Retrieves the serialized `Phoenix.LongPoll.Server` pid
# by publishing a message in the encrypted private topic.
@doc false
def resume_session(%{"token" => token}, endpoint, opts) do
case verify_token(endpoint, token) do
case verify_token(endpoint, token, opts) do
{:ok, priv_topic} ->
ref = :erlang.make_ref()
:ok = subscribe(endpoint, priv_topic)
Expand Down Expand Up @@ -225,12 +227,12 @@ defmodule Phoenix.Transports.LongPoll do
Phoenix.PubSub.broadcast_from(endpoint.__pubsub_server__, self, priv_topic, msg)
end

defp sign_token(endpoint, priv_topic) do
Phoenix.Token.sign(endpoint, Atom.to_string(endpoint.__pubsub_server__), priv_topic)
defp sign_token(endpoint, priv_topic, opts) do
Phoenix.Token.sign(endpoint, Atom.to_string(endpoint.__pubsub_server__), priv_topic, opts[:crypto])
end

defp verify_token(endpoint, signed) do
Phoenix.Token.verify(endpoint, Atom.to_string(endpoint.__pubsub_server__), signed)
defp verify_token(endpoint, signed, opts) do
Phoenix.Token.verify(endpoint, Atom.to_string(endpoint.__pubsub_server__), signed, opts[:crypto])
end

defp status_json(conn, data) do
Expand Down
59 changes: 23 additions & 36 deletions lib/phoenix/transports/long_poll/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ defmodule Phoenix.Transports.LongPoll.Supervisor do
end

defmodule Phoenix.Transports.LongPoll.Server do
use GenServer

@moduledoc false

use GenServer

alias Phoenix.Channel.Transport
alias Phoenix.PubSub
alias Phoenix.Socket.Broadcast
Expand All @@ -38,14 +38,15 @@ defmodule Phoenix.Transports.LongPoll.Server do
GenServer.start_link(__MODULE__, [socket, window_ms, priv_topic])
end

@doc false
## Callbacks

def init([socket, window_ms, priv_topic]) do
Process.flag(:trap_exit, true)

state = %{buffer: [],
socket: socket,
sockets: HashDict.new,
sockets_inverse: HashDict.new,
channels: HashDict.new,
channels_inverse: HashDict.new,
window_ms: trunc(window_ms * 1.5),
pubsub_server: socket.endpoint.__pubsub_server__(),
priv_topic: priv_topic,
Expand All @@ -59,23 +60,18 @@ defmodule Phoenix.Transports.LongPoll.Server do
{:ok, state}
end

@doc """
Stops the server
"""
def handle_call(:stop, _from, state), do: {:stop, :shutdown, :ok, state}

@doc """
Dispatches client message back through Transport layer.
"""
# Handle client dispatches
def handle_info({:dispatch, msg, ref}, state) do
msg
|> Transport.dispatch(state.sockets, self, state.socket)
|> Transport.dispatch(state.channels, self, state.socket)
|> case do
{:ok, socket_pid, reply_msg} ->
{:ok, channel_pid, reply_msg} ->
:ok = broadcast_from(state, {:ok, :dispatch, ref})

new_state = %{state | sockets: HashDict.put(state.sockets, msg.topic, socket_pid),
sockets_inverse: HashDict.put(state.sockets_inverse, socket_pid, msg.topic)}
new_state = %{state | channels: HashDict.put(state.channels, msg.topic, channel_pid),
channels_inverse: HashDict.put(state.channels_inverse, channel_pid, msg.topic)}
publish_reply(reply_msg, new_state)

{:ok, reply_msg} ->
Expand All @@ -92,39 +88,31 @@ defmodule Phoenix.Transports.LongPoll.Server do
end
end

@doc """
Forwards replied/broadcasted message from Channels back to client.
"""
# Forwards replied/broadcasted message from Channels back to client.
def handle_info(%Message{} = msg, state) do
publish_encoded_reply(msg, state)
end

@doc """
Detects disconnect broadcasts and shuts down
"""
# Detects disconnect broadcasts and shuts down
def handle_info(%Broadcast{event: "disconnect"}, state) do
{:stop, {:shutdown, :disconnected}, state}
end

@doc """
Crash if pubsub adapter goes down
"""
# Crash if pubsub adapter goes down
def handle_info({:EXIT, pub_pid, :shutdown}, %{pubsub_server: pub_pid} = state) do
{:stop, :pubsub_server_terminated, state}
{:stop, {:shutdown,:pubsub_server_terminated}, state}
end

@doc """
Trap channel process exits and notify client of close or error events
`:normal` exits and shutdowns indicate the channel shutdown gracefully from
return. Any other exit reason is treated as an error.
"""
def handle_info({:EXIT, socket_pid, reason}, state) do
case HashDict.get(state.sockets_inverse, socket_pid) do
# Trap channel process exits and notify client of close or error events
#
# Normal exits and shutdowns indicate the channel shutdown gracefully
# from return. Any other exit reason is treated as an error.
def handle_info({:EXIT, channel_pid, reason}, state) do
case HashDict.get(state.channels_inverse, channel_pid) do
nil -> {:noreply, state}
topic ->
new_state = %{state | sockets: HashDict.delete(state.sockets, topic),
sockets_inverse: HashDict.delete(state.sockets_inverse, socket_pid)}
new_state = %{state | channels: HashDict.delete(state.channels, topic),
channels_inverse: HashDict.delete(state.channels_inverse, channel_pid)}
case reason do
:normal ->
publish_reply(Transport.chan_close_message(topic), new_state)
Expand All @@ -138,7 +126,6 @@ defmodule Phoenix.Transports.LongPoll.Server do

def handle_info({:subscribe, ref}, state) do
:ok = broadcast_from(state, {:ok, :subscribe, ref})

{:noreply, state}
end

Expand Down
Loading

0 comments on commit bbebb81

Please sign in to comment.