Skip to content

Commit

Permalink
Support cross domain long polling on IE8-9
Browse files Browse the repository at this point in the history
Use XDomainRequest for the AJAX requests on IE8-9.
Include the status code in the JSON response because XDomainRequest
does not expose the status code.
Generate phoenix.js with the “Loose Mode” option on es6to5 which in
conjunction with es5-shim makes the javascript work on IE8.
Always respond with 200 for long polling, XDomainRequest handles
all 4** codes as errors and we can’t retrieve the response body.
Add a `longpoller_timeout` option to `Phoenix.Socket`.
Don't have multiple heartbeat timers after timeout.
Don't send heartbeats for long polling.
  • Loading branch information
ericmj committed Mar 12, 2015
1 parent a803dd7 commit 41bfcf3
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 58 deletions.
3 changes: 2 additions & 1 deletion brunch-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ exports.config = {
plugins: {
ES6to5: {
// Do not use ES6 compiler in vendor code
ignore: [/^(web\/static\/vendor)/]
ignore: [/^(web\/static\/vendor)/],
loose: "all"
}
}
};
12 changes: 9 additions & 3 deletions lib/phoenix/channel/transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,21 @@ defmodule Phoenix.Channel.Transport do
configured it will return the given `Plug.Conn`. Otherwise a 403 Forbidden
response will be send and the connection halted.
"""
def check_origin(conn) do
def check_origin(conn, opts \\ []) do
import Plug.Conn

endpoint = Phoenix.Controller.endpoint_module(conn)
allowed_origins = Dict.get(endpoint.config(:transports), :origins)
origin = Plug.Conn.get_req_header(conn, "origin") |> List.first
origin = get_req_header(conn, "origin") |> List.first

send = opts[:send] || &send_resp(&1)

if origin_allowed?(origin, allowed_origins) do
conn
else
Plug.Conn.send_resp(conn, :forbidden, "") |> Plug.Conn.halt
resp(conn, :forbidden, "")
|> send.()
|> halt
end
end

Expand Down
9 changes: 5 additions & 4 deletions lib/phoenix/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -709,10 +709,11 @@ defmodule Phoenix.Router do
@phoenix_socket_mount mount
@phoenix_transports opts[:via]
@phoenix_channel_alias opts[:alias]
get @phoenix_socket_mount, Phoenix.Transports.WebSocket, :upgrade, Dict.take(opts, [:as])
post @phoenix_socket_mount, Phoenix.Transports.WebSocket, :upgrade
get @phoenix_socket_mount <> "/poll", Phoenix.Transports.LongPoller, :poll
post @phoenix_socket_mount <> "/poll", Phoenix.Transports.LongPoller, :publish
get @phoenix_socket_mount, Phoenix.Transports.WebSocket, :upgrade, Dict.take(opts, [:as])
post @phoenix_socket_mount, Phoenix.Transports.WebSocket, :upgrade
options @phoenix_socket_mount <> "/poll", Phoenix.Transports.LongPoller, :options
get @phoenix_socket_mount <> "/poll", Phoenix.Transports.LongPoller, :poll
post @phoenix_socket_mount <> "/poll", Phoenix.Transports.LongPoller, :publish
unquote(chan_block)
@phoenix_socket_mount nil
@phoenix_transports nil
Expand Down
52 changes: 45 additions & 7 deletions lib/phoenix/transports/long_poller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ defmodule Phoenix.Transports.LongPoller do
alias Phoenix.Channel.Transport

plug :check_origin
plug :allow_origin
plug :default_content_type
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`.
Expand All @@ -55,13 +65,13 @@ defmodule Phoenix.Transports.LongPoller do
receive do
{:messages, msgs, ^ref} ->
:ok = ack(conn, priv_topic, msgs)
json(conn, %{messages: msgs, token: conn.params["token"], sig: conn.params["sig"]})
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)
|> json(%{token: conn.params["token"], sig: conn.params["sig"]})
|> status_json(%{token: conn.params["token"], sig: conn.params["sig"]})
end
end

Expand All @@ -70,7 +80,7 @@ defmodule Phoenix.Transports.LongPoller do

conn
|> put_status(:gone)
|> json(%{token: priv_topic, sig: sig})
|> status_json(%{token: priv_topic, sig: sig})
end

@doc """
Expand All @@ -82,16 +92,16 @@ defmodule Phoenix.Transports.LongPoller do
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) |> json(%{})
{: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) |> json(%{})
{:error, _reason} -> conn |> put_status(:unauthorized) |> json(%{})
:ok -> conn |> put_status(:ok) |> status_json(%{})
{:error, _reason} -> conn |> put_status(:unauthorized) |> status_json(%{})
end
end

Expand Down Expand Up @@ -193,7 +203,7 @@ defmodule Phoenix.Transports.LongPoller do
end

defp check_origin(conn, _opts) do
Transport.check_origin(conn)
Transport.check_origin(conn, send: &status_json(&1, %{}))
end

defp sign(conn, priv_topic) do
Expand All @@ -219,4 +229,32 @@ defmodule Phoenix.Transports.LongPoller do

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
3 changes: 2 additions & 1 deletion priv/static/brunch/brunch-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ exports.config = {
plugins: {
ES6to5: {
// Do not use ES6 compiler in vendor code
ignore: [/^(web\/static\/vendor)/]
ignore: [/^(web\/static\/vendor)/],
loose: "all"
}
}
};
2 changes: 1 addition & 1 deletion priv/static/phoenix.js

Large diffs are not rendered by default.

37 changes: 19 additions & 18 deletions test/phoenix/integration/channel_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -140,45 +140,46 @@ defmodule Phoenix.Integration.ChannelTest do
session = Map.take(resp.body, ["token", "sig"])
assert resp.body["token"]
assert resp.body["sig"]
assert resp.status == 410
assert resp.body["status"] == 410
assert resp.status == 200

# join
resp = poll :post, "/ws/poll", session, %{
"topic" => "rooms:lobby",
"event" => "join",
"payload" => %{}
}
assert resp.status == 200
assert resp.body["status"] == 200

# poll with messsages sends buffer
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 200
assert resp.body["status"] == 200
[status_msg] = resp.body["messages"]
assert status_msg["payload"] == %{"status" => "connected"}

# poll without messages sends 204 no_content
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 204
assert resp.body["status"] == 204

# messages are buffered between polls
Endpoint.broadcast! "rooms:lobby", "user:entered", %{name: "José"}
Endpoint.broadcast! "rooms:lobby", "user:entered", %{name: "Sonny"}
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 200
assert resp.body["status"] == 200
assert Enum.count(resp.body["messages"]) == 2
assert Enum.map(resp.body["messages"], &(&1["payload"]["name"])) == ["José", "Sonny"]

# poll without messages sends 204 no_content
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 204
assert resp.body["status"] == 204

resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 204
assert resp.body["status"] == 204

# generic events
Phoenix.PubSub.subscribe(:int_pub, self, "rooms:lobby")
Expand All @@ -187,11 +188,11 @@ defmodule Phoenix.Integration.ChannelTest do
"event" => "new:msg",
"payload" => %{"body" => "hi!"}
}
assert resp.status == 200
assert resp.body["status"] == 200
assert_receive {:socket_broadcast, %Message{event: "new:msg", payload: %{"body" => "hi!"}}}
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 200
assert resp.body["status"] == 200

# unauthorized events
capture_log fn ->
Expand All @@ -201,7 +202,7 @@ defmodule Phoenix.Integration.ChannelTest do
"event" => "new:msg",
"payload" => %{"body" => "this method shouldn't send!'"}
}
assert resp.status == 401
assert resp.body["status"] == 401
refute_receive {:socket_broadcast, %Message{event: "new:msg"}}


Expand All @@ -213,12 +214,12 @@ defmodule Phoenix.Integration.ChannelTest do
"event" => "join",
"payload" => %{}
}
assert resp.status == 200
assert resp.body["status"] == 200
Endpoint.broadcast! "rooms:lobby", "new:msg", %{body: "Hello lobby"}
# poll
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 200
assert resp.body["status"] == 200
assert Enum.count(resp.body["messages"]) == 2
assert Enum.at(resp.body["messages"], 0)["payload"]["status"] == "connected"
assert Enum.at(resp.body["messages"], 1)["payload"]["body"] == "Hello lobby"
Expand All @@ -230,37 +231,37 @@ defmodule Phoenix.Integration.ChannelTest do
:timer.sleep @ensure_window_timeout_ms
resp = poll(:get, "/ws/poll", session)
session = Map.take(resp.body, ["token", "sig"])
assert resp.status == 410
assert resp.body["status"] == 410

# join
resp = poll :post, "/ws/poll", session, %{
"topic" => "rooms:lobby",
"event" => "join",
"payload" => %{}
}
assert resp.status == 200
assert resp.body["status"] == 200
Phoenix.PubSub.subscribe(:int_pub, self, "rooms:lobby")
:timer.sleep @ensure_window_timeout_ms
resp = poll :post, "/ws/poll", session, %{
"topic" => "rooms:lobby",
"event" => "new:msg",
"payload" => %{"body" => "hi!"}
}
assert resp.status == 410
assert resp.body["status"] == 410
refute_receive {:socket_reply, %Message{event: "new:msg", payload: %{"body" => "hi!"}}}

# 410 from crashed/terminated longpoller server when publishing
# create new session
resp = poll :post, "/ws/poll", %{"token" => "foo", "sig" => "bar"}, %{}
assert resp.status == 410
assert resp.body["status"] == 410
end
end

test "longpoller refuses unallowed origins" do
conn = call(Endpoint, :get, "/ws/poll", [], headers: [{"origin", "https://example.com"}])
assert conn.status == 410
assert Poison.decode!(conn.resp_body)["status"] == 410

conn = call(Endpoint, :get, "/ws/poll", [], headers: [{"origin", "http://notallowed.com"}])
refute conn.status == 410
assert Poison.decode!(conn.resp_body)["status"] == 403
end
end
15 changes: 8 additions & 7 deletions test/phoenix/router/console_formatter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ defmodule Phoenix.Router.ConsoleFormatterTest do

test "format multiple routes" do
assert draw(RouterTestSingleRoutes) == """
web_socket_path GET /ws Phoenix.Transports.WebSocket.upgrade/2
web_socket_path POST /ws Phoenix.Transports.WebSocket.upgrade/2
long_poller_path GET /ws/poll Phoenix.Transports.LongPoller.poll/2
long_poller_path POST /ws/poll Phoenix.Transports.LongPoller.publish/2
page_path GET / Phoenix.PageController.index/2
upload_image_path POST /images Phoenix.ImageController.upload/2
remove_image_path DELETE /images Phoenix.ImageController.delete/2
web_socket_path GET /ws Phoenix.Transports.WebSocket.upgrade/2
web_socket_path POST /ws Phoenix.Transports.WebSocket.upgrade/2
long_poller_path OPTIONS /ws/poll Phoenix.Transports.LongPoller.options/2
long_poller_path GET /ws/poll Phoenix.Transports.LongPoller.poll/2
long_poller_path POST /ws/poll Phoenix.Transports.LongPoller.publish/2
page_path GET / Phoenix.PageController.index/2
upload_image_path POST /images Phoenix.ImageController.upload/2
remove_image_path DELETE /images Phoenix.ImageController.delete/2
"""
end

Expand Down
Loading

0 comments on commit 41bfcf3

Please sign in to comment.