Packages
livebook
0.14.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/teams/web_socket.ex
defmodule Livebook.Teams.WebSocket do
alias Mint.WebSocket.UpgradeFailureError
@ws_path "/user/websocket"
@type conn :: Mint.HTTP.t()
@type websocket :: Mint.WebSocket.t()
@type frame :: Mint.WebSocket.frame() | Mint.WebSocket.shorthand_frame()
@type ref :: Mint.Types.request_ref()
defguard is_frame(value) when value in [:close, :ping] or elem(value, 0) == :binary
@doc """
Connects to the WebSocket server with given headers.
"""
@spec connect(list({String.t(), String.t()})) ::
{:ok, conn(), websocket(), ref()}
| {:transport_error, String.t()}
| {:server_error, String.t()}
def connect(headers \\ []) do
uri = URI.parse(Livebook.Config.teams_url())
{http_scheme, ws_scheme} = parse_scheme(uri)
state = %{status: nil, headers: [], body: []}
transport_opts =
if http_scheme == :https do
[cacerts: :public_key.cacerts_get()]
else
[]
end
transport_opts =
if String.ends_with?(Livebook.Config.teams_url(), ".flycast"),
do: Keyword.put(transport_opts, :inet6, true),
else: transport_opts
opts = [protocols: [:http1], transport_opts: transport_opts]
with {:ok, conn} <- Mint.HTTP.connect(http_scheme, uri.host, uri.port, opts),
{:ok, conn, ref} <- Mint.WebSocket.upgrade(ws_scheme, conn, @ws_path, headers) do
receive_upgrade(conn, ref, state)
else
{:error, exception} ->
{:transport_error, Exception.message(exception)}
{:error, conn, exception} ->
Mint.HTTP.close(conn)
{:transport_error, Exception.message(exception)}
end
end
defp parse_scheme(uri) when uri.scheme in ["http", "ws"], do: {:http, :ws}
defp parse_scheme(uri) when uri.scheme in ["https", "wss"], do: {:https, :wss}
defp receive_upgrade(conn, ref, state) do
with {:ok, conn} <- Mint.HTTP.set_mode(conn, :passive),
{:ok, conn, responses} <- Mint.WebSocket.recv(conn, 0, 5_000) do
handle_upgrade_responses(responses, conn, ref, state)
else
{:error, exception} ->
Mint.HTTP.close(conn)
{:transport_error, Exception.message(exception)}
{:error, _websocket, exception, []} ->
Mint.HTTP.close(conn)
{:transport_error, Exception.message(exception)}
end
end
defp handle_upgrade_responses([{:status, ref, status} | responses], conn, ref, state) do
handle_upgrade_responses(responses, conn, ref, %{state | status: status})
end
defp handle_upgrade_responses([{:headers, ref, headers} | responses], conn, ref, state) do
handle_upgrade_responses(responses, conn, ref, %{state | headers: headers})
end
defp handle_upgrade_responses([{:data, ref, body} | responses], conn, ref, state) do
handle_upgrade_responses(responses, conn, ref, %{state | body: [body | state.body]})
end
defp handle_upgrade_responses([{:done, ref} | responses], conn, ref, state) do
case state do
%{status: 101} ->
start_websocket(conn, ref, state)
%{body: []} ->
handle_upgrade_responses(responses, conn, ref, state)
%{status: _} ->
Mint.HTTP.close(conn)
{:server_error, state.body |> Enum.reverse() |> IO.iodata_to_binary()}
end
end
defp handle_upgrade_responses([], conn, ref, state) do
receive_upgrade(conn, ref, state)
end
defp start_websocket(conn, ref, state) do
with {:ok, conn, websocket} <- Mint.WebSocket.new(conn, ref, state.status, state.headers),
{:ok, conn} <- Mint.HTTP.set_mode(conn, :active) do
{:ok, conn, websocket, ref}
else
{:error, exception} ->
Mint.HTTP.close(conn)
{:transport_error, Exception.message(exception)}
{:error, conn, %UpgradeFailureError{}} ->
Mint.HTTP.close(conn)
{:server_error, state.body |> Enum.reverse() |> IO.iodata_to_binary()}
{:error, conn, exception} ->
Mint.HTTP.close(conn)
{:transport_error, Exception.message(exception)}
end
end
@doc """
Disconnects from the given connection, WebSocket and reference.
If there's no WebSocket connection yet, it'll only close the HTTP connection.
"""
@spec disconnect(conn(), websocket() | nil, ref()) ::
{:ok, conn(), websocket() | nil}
| {:error, conn(), websocket(), term()}
def disconnect(conn, nil, _ref) do
{:ok, conn} = Mint.HTTP.close(conn)
{:ok, conn, nil}
end
def disconnect(conn, websocket, ref) do
with {:ok, conn, websocket} <- send(conn, websocket, ref, :close) do
{:ok, conn} = Mint.HTTP.close(conn)
{:ok, conn, websocket}
end
end
@doc """
Receive the message from the given HTTP connection.
If the WebSocket isn't connected yet, it will try to get the connection
response to start a new WebSocket connection.
"""
@spec receive(conn(), ref(), websocket(), term()) ::
{:ok, conn(), websocket(), list(binary())}
| {:error, conn(), websocket(), String.t()}
def receive(conn, ref, websocket, message \\ receive(do: (message -> message))) do
with {:ok, conn, [{:data, ^ref, data}]} <- Mint.WebSocket.stream(conn, message),
{:ok, websocket, frames} <- Mint.WebSocket.decode(websocket, data),
{:ok, response} <- handle_frames(frames) do
{:ok, conn, websocket, response}
else
{:close, response} ->
handle_disconnect(conn, websocket, ref, response)
{:error, conn, exception} when is_exception(exception) ->
{:error, conn, websocket, Exception.message(exception)}
{:error, conn, exception, []} when is_exception(exception) ->
{:error, conn, websocket, Exception.message(exception)}
end
end
defp handle_disconnect(conn, websocket, ref, response) do
with {:ok, conn, websocket} <- disconnect(conn, websocket, ref) do
{:ok, conn, websocket, response}
end
end
defp handle_frames(frames), do: handle_frames([], frames)
defp handle_frames(binaries, [{:binary, binary} | rest]),
do: handle_frames([binary | binaries], rest)
defp handle_frames(binaries, [{:close, _, _} | _]),
do: {:close, binaries}
defp handle_frames(binaries, [_ | rest]), do: handle_frames(binaries, rest)
defp handle_frames(binaries, []), do: {:ok, binaries}
@doc """
Sends a message to the given HTTP Connection and WebSocket connection.
"""
@spec send(conn(), websocket(), ref(), frame()) ::
{:ok, conn(), websocket()}
| {:error, conn(), websocket(), term()}
def send(conn, websocket, ref, frame) when is_frame(frame) do
with {:ok, websocket, data} <- Mint.WebSocket.encode(websocket, frame),
{:ok, conn} <- Mint.WebSocket.stream_request_body(conn, ref, data) do
{:ok, conn, websocket}
else
{:error, %Mint.HTTP1{} = conn, exception} when is_exception(exception) ->
{:error, conn, websocket, Exception.message(exception)}
{:error, websocket, exception} when is_exception(exception) ->
{:error, conn, websocket, Exception.message(exception)}
end
end
end