From b4ac5d6f74b043ee68933c704cc9731e0c630988 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Tue, 21 May 2024 16:06:28 -0300 Subject: [PATCH 01/10] Add `Kino.Proxy` module --- lib/kino/proxy.ex | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/kino/proxy.ex diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex new file mode 100644 index 00000000..97d2ece4 --- /dev/null +++ b/lib/kino/proxy.ex @@ -0,0 +1,22 @@ +defmodule Kino.Proxy do + @moduledoc """ + A kino for handling proxy requests from the host. + + ## Examples + + Kino.Proxy.listen(fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/text;charset=utf-8") + |> Plug.Conn.send_resp(200, "used " <> conn.method <> " method") + end) + """ + + @doc """ + Persists the function to be listened by the proxy handler. + """ + @spec listen(atom(), module(), (Plug.Conn.t() -> Plug.Conn.t())) :: + DynamicSupervisor.on_start_child() + def listen(name \\ __MODULE__, mod \\ Livebook.Proxy.Handler, fun) when is_function(fun, 1) do + Kino.start_child({mod, name: name, listen: fun}) + end +end From c037a821ed01017e2facdb3c5374f10b566aa506 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Thu, 23 May 2024 15:05:04 -0300 Subject: [PATCH 02/10] Apply review comments --- lib/kino/bridge.ex | 9 +++++++++ lib/kino/proxy.ex | 18 ++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/kino/bridge.ex b/lib/kino/bridge.ex index eb80d873..e6737ad4 100644 --- a/lib/kino/bridge.ex +++ b/lib/kino/bridge.ex @@ -250,6 +250,15 @@ defmodule Kino.Bridge do match?({:ok, _}, io_request(:livebook_get_evaluation_file)) end + @doc """ + Requests the child spec for proxy handler with the given function. + """ + @spec get_proxy_handler_child_spec((Plug.Conn.t() -> Plug.Conn.t())) :: + Supervisor.child_spec() | {module(), term()} | module() + def get_proxy_handler_child_spec(fun) do + with {:ok, reply} <- io_request({:livebook_get_proxy_handler_child_spec, fun}), do: reply + end + defp io_request(request) do gl = Process.group_leader() ref = Process.monitor(gl) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 97d2ece4..e5986b4a 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -1,22 +1,16 @@ defmodule Kino.Proxy do @moduledoc """ - A kino for handling proxy requests from the host. + Functionality for handling proxy requests forwarded from Livebook. - ## Examples - - Kino.Proxy.listen(fn conn -> - conn - |> Plug.Conn.put_resp_header("content-type", "application/text;charset=utf-8") - |> Plug.Conn.send_resp(200, "used " <> conn.method <> " method") - end) + TODO: Write an extensive docs here. """ @doc """ Persists the function to be listened by the proxy handler. """ - @spec listen(atom(), module(), (Plug.Conn.t() -> Plug.Conn.t())) :: - DynamicSupervisor.on_start_child() - def listen(name \\ __MODULE__, mod \\ Livebook.Proxy.Handler, fun) when is_function(fun, 1) do - Kino.start_child({mod, name: name, listen: fun}) + @spec listen((Plug.Conn.t() -> Plug.Conn.t())) :: DynamicSupervisor.on_start_child() + def listen(fun) when is_function(fun, 1) do + child_spec = Kino.Bridge.get_proxy_handler_child_spec(fun) + Kino.start_child(child_spec) end end From a51459697db557e9e636e9f1b842ed960cb99e26 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Thu, 23 May 2024 15:25:46 -0300 Subject: [PATCH 03/10] Apply review comments --- lib/kino/bridge.ex | 4 ++-- lib/kino/proxy.ex | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/kino/bridge.ex b/lib/kino/bridge.ex index e6737ad4..e43e1abb 100644 --- a/lib/kino/bridge.ex +++ b/lib/kino/bridge.ex @@ -254,9 +254,9 @@ defmodule Kino.Bridge do Requests the child spec for proxy handler with the given function. """ @spec get_proxy_handler_child_spec((Plug.Conn.t() -> Plug.Conn.t())) :: - Supervisor.child_spec() | {module(), term()} | module() + {:ok, {module(), term()}} | request_error() def get_proxy_handler_child_spec(fun) do - with {:ok, reply} <- io_request({:livebook_get_proxy_handler_child_spec, fun}), do: reply + io_request({:livebook_get_proxy_handler_child_spec, fun}) end defp io_request(request) do diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index e5986b4a..f6be86c6 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -10,7 +10,12 @@ defmodule Kino.Proxy do """ @spec listen((Plug.Conn.t() -> Plug.Conn.t())) :: DynamicSupervisor.on_start_child() def listen(fun) when is_function(fun, 1) do - child_spec = Kino.Bridge.get_proxy_handler_child_spec(fun) - Kino.start_child(child_spec) + case Kino.Bridge.get_proxy_handler_child_spec(fun) do + {:ok, child_spec} -> + Kino.start_child(child_spec) + + {:request_error, reason} -> + raise "failed to access the proxy handler child spec, reason: #{inspect(reason)}" + end end end From 21d7f523bae3cbc6a9c6ad775c3a0a518e978e88 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Fri, 24 May 2024 17:18:11 -0300 Subject: [PATCH 04/10] Add docs --- lib/kino/proxy.ex | 48 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index f6be86c6..274e6185 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -2,7 +2,53 @@ defmodule Kino.Proxy do @moduledoc """ Functionality for handling proxy requests forwarded from Livebook. - TODO: Write an extensive docs here. + This functionality will make the requests forwarded from Livebook + through a proxy handler and send the response to the incoming connection + according to user definition. + + The proxy handler supports the routes below to perform this proxy + between the `Livebook` and current runtime: + + * `/sessions/:id/proxy/*path` - for notebook sessions. + + * `/apps/:slug/:session_id/proxy/*path` - for app sessions. + + Only certain fields will be forwarded through the proxy handler, which + builds a new `%Plug.Conn{}` and sends to the listener function. + + * `:host` + * `:method` + * `:owner` + * `:port` + * `:remote_ip` + * `:query_string` + * `:path_info` + * `:scheme` + * `:script_name` + * `:req_headers` + + It is possible to create notebooks and apps as APIs, allowing the user to fetch the + request data and send a proper response. + + data = <<...>> + token = "auth-token" + + Kino.Proxy.listen(fn + %{path_info: ["export", "data"]} = conn -> + ["Bearer " <> ^token] = Plug.Conn.get_req_header(conn, "authorization") + + conn + |> Plug.Conn.put_resp_header("content-type", "application/csv") + |> Plug.Conn.send_resp(200, data) + + conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/text") + |> Plug.Conn.send_resp(200, "use /export/data to get extract the report data") + end) + + So you would need to access the `/sessions/:id/proxy/export/data` to extract the data from + your session and return as a body response. """ @doc """ From d668cb818f59c832b447f31702864659e0d6aa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 15:19:40 +0700 Subject: [PATCH 05/10] Up --- lib/kino/proxy.ex | 42 ++++++++++++++++++++++++++++-------------- mix.exs | 1 + mix.lock | 4 ++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 274e6185..69e99524 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -2,19 +2,24 @@ defmodule Kino.Proxy do @moduledoc """ Functionality for handling proxy requests forwarded from Livebook. - This functionality will make the requests forwarded from Livebook - through a proxy handler and send the response to the incoming connection - according to user definition. + Livebook proxies requests at the following paths: - The proxy handler supports the routes below to perform this proxy - between the `Livebook` and current runtime: + * `/sessions/:id/proxy/*path` - a notebook sessions - * `/sessions/:id/proxy/*path` - for notebook sessions. + * `/apps/:slug/:session_id/proxy/*path` - a specific app session - * `/apps/:slug/:session_id/proxy/*path` - for app sessions. + * `/apps/:slug/proxy/*path` - generic app path, only supported for + single-session apps - Only certain fields will be forwarded through the proxy handler, which - builds a new `%Plug.Conn{}` and sends to the listener function. + You can define a custom listener to handle requests at these paths. + The listener receives a `Plug.Conn` and should use the `Plug` API + to send the response, for example: + + Kino.Proxy.listen(fn conn -> + Plug.Conn.send_resp(conn, 200, "hello") + end + + The following `Plug.Conn` fields are set: * `:host` * `:method` @@ -27,8 +32,14 @@ defmodule Kino.Proxy do * `:script_name` * `:req_headers` - It is possible to create notebooks and apps as APIs, allowing the user to fetch the - request data and send a proper response. + > #### Plug {: .info} + > + > In order to use this feature, you need to add `:plug` as a dependency. + + ## Examples + + Using the proxy feature, you can use Livebook apps to build APIs. + For example, we could provide a data export endpoint: data = <<...>> token = "auth-token" @@ -47,12 +58,15 @@ defmodule Kino.Proxy do |> Plug.Conn.send_resp(200, "use /export/data to get extract the report data") end) - So you would need to access the `/sessions/:id/proxy/export/data` to extract the data from - your session and return as a body response. + Once deployed as an app, the user would be able to export the data + by sending a request to `/apps/:slug/proxy/export/data`. """ @doc """ - Persists the function to be listened by the proxy handler. + Registers a request listener. + + Expects the listener to be a function that handles a request + `Plug.Conn`. """ @spec listen((Plug.Conn.t() -> Plug.Conn.t())) :: DynamicSupervisor.on_start_child() def listen(fun) when is_function(fun, 1) do diff --git a/mix.exs b/mix.exs index 322f5eec..53e6bcf3 100644 --- a/mix.exs +++ b/mix.exs @@ -35,6 +35,7 @@ defmodule Kino.MixProject do {:table, "~> 0.1.2"}, {:fss, "~> 0.1.0"}, {:nx, "~> 0.1", optional: true}, + {:plug, "~> 1.0", optional: true}, {:ex_doc, "~> 0.28", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 429d0058..d032afb6 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,11 @@ "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nx": {:hex, :nx, "0.4.0", "2ec2cebec6a9ac8a3d5ae8ef79345cf92f37f9018d50817684e51e97b86f3d36", [:mix], [{:complex, "~> 0.4.2", [hex: :complex, repo: "hexpm", optional: false]}], "hexpm", "bab955768dadfe2208723fbffc9255341b023291f2aabcbd25bf98167dd3399e"}, + "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } From 1b33215db93f84abbda1c14ccc213d005f784647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 10:27:04 +0200 Subject: [PATCH 06/10] Update lib/kino/proxy.ex --- lib/kino/proxy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 69e99524..10e771e2 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -4,7 +4,7 @@ defmodule Kino.Proxy do Livebook proxies requests at the following paths: - * `/sessions/:id/proxy/*path` - a notebook sessions + * `/sessions/:id/proxy/*path` - a notebook session * `/apps/:slug/:session_id/proxy/*path` - a specific app session From d436404bf0983e7cf4e5b5581d40b4de5691027e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 10:27:45 +0200 Subject: [PATCH 07/10] Update lib/kino/proxy.ex --- lib/kino/proxy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 10e771e2..819601dc 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -12,7 +12,7 @@ defmodule Kino.Proxy do single-session apps You can define a custom listener to handle requests at these paths. - The listener receives a `Plug.Conn` and should use the `Plug` API + The listener receives a `Plug.Conn` and it should use the `Plug` API to send the response, for example: Kino.Proxy.listen(fn conn -> From 306dd67baa058d6fb5930ab335ab839ee3c7194f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 10:28:35 +0200 Subject: [PATCH 08/10] Update lib/kino/proxy.ex --- lib/kino/proxy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 819601dc..9588b9be 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -38,7 +38,7 @@ defmodule Kino.Proxy do ## Examples - Using the proxy feature, you can use Livebook apps to build APIs. + Using the proxy feature, we can use Livebook apps to build APIs. For example, we could provide a data export endpoint: data = <<...>> From 71b1b19af33e36d75ead481ce6a2ac1cf3ffe730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 11:43:10 +0200 Subject: [PATCH 09/10] Update lib/kino/proxy.ex --- lib/kino/proxy.ex | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index 9588b9be..cf4c05fd 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -19,19 +19,6 @@ defmodule Kino.Proxy do Plug.Conn.send_resp(conn, 200, "hello") end - The following `Plug.Conn` fields are set: - - * `:host` - * `:method` - * `:owner` - * `:port` - * `:remote_ip` - * `:query_string` - * `:path_info` - * `:scheme` - * `:script_name` - * `:req_headers` - > #### Plug {: .info} > > In order to use this feature, you need to add `:plug` as a dependency. From 33e40d0cc30abe792e33cbef4a076e72b6b70725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 May 2024 11:44:35 +0200 Subject: [PATCH 10/10] Update lib/kino/proxy.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/kino/proxy.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/kino/proxy.ex b/lib/kino/proxy.ex index cf4c05fd..60cf4db4 100644 --- a/lib/kino/proxy.ex +++ b/lib/kino/proxy.ex @@ -9,7 +9,8 @@ defmodule Kino.Proxy do * `/apps/:slug/:session_id/proxy/*path` - a specific app session * `/apps/:slug/proxy/*path` - generic app path, only supported for - single-session apps + single-session apps. If the app has automatic shutdowns enabled + and it is not currently running, it will be automatically started You can define a custom listener to handle requests at these paths. The listener receives a `Plug.Conn` and it should use the `Plug` API