Created
December 10, 2021 04:38
-
-
Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.
Revisions
-
sntran created this gist
Dec 10, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,172 @@ #!/usr/bin/env elixir help = ~S""" HTTP-server to execute shell commands. The CLI takes a pair of path and the shell commands and generates the routing. Upon requests to a matched path, the corresponding shell command is executed, and the output is responded to the client. The routing is generated by Plug.Router so it is really fast, and only handles the routes the user specifies. By default, the server listens on 127.0.0.1 and port 4000, which can be changed with `--host` and `--port` switches. The shell command is executed with `sh` shell, which can be changed with `--shell` switch. ## Usage chmod +x shell2http.exs # Command with no param. ./shell2http.exs /hello 'echo "World"' # Optional param `word` with empty default value. ./shell2http.exs /hello 'echo "Hello ${word-}"' # Optional param `word` with default value of "World". ./shell2http.exs /hello 'echo "Hello ${word-World}"' # Command with required params. ./shell2http.exs /mirror 'curl "${url}" > "${outfile}"' ## Examples ./shell2http.exs --host 127.0..1 --port 4000 --shell sh \ /top 'top -l 1 | head -10' \ /date date \ /ps 'ps aux' """ version = "0.0.1" {switches, commands, _invalid} = OptionParser.parse(System.argv(), [ strict: [ host: :string, port: :integer, shell: :string, version: :boolean, help: :boolean, ], aliases: [ h: :host, p: :port, v: :version, ] ]) defaults = [ host: "127.0.0.1", port: 4000, shell: "sh", help: commands === [], ] switches = Keyword.merge(defaults, switches) if switches[:version] do IO.puts version System.halt(0) end if switches[:help] do IO.puts help System.halt(0) end # Parses IP tuple from string host flag. {:ok, ip} = switches[:host] |> :erlang.binary_to_list() |> :inet.parse_address() Application.put_env(:phoenix, :json_library, Jason) Application.put_env(:shell2http, Shell2HTTP, [ http: [ip: ip, port: switches[:port]], server: true, secret_key_base: String.duplicate("a", 64) ]) Application.put_env(:shell2http, :shell, switches[:shell]) # Maps the command pairs. commands = commands |> Enum.chunk_every(2) |> Enum.reduce(%{}, fn([name, action], acc) -> Map.put(acc, name, action) end) # Installs the dependencies. Mix.install([ {:plug_cowboy, "~> 2.5"}, {:jason, "~> 1.2"}, {:phoenix, "~> 1.6"} ]) defmodule Shell2HTTP do @moduledoc help defmodule Controller do use Phoenix.Controller @commands commands @regex ~r/\${(?<param>\w+)(?:-(?<default>[^}]*))?}/ # This action is guaranteed to be called on an existing command. def index(conn, params) do name = conn.request_path command = @commands[name] # Replaces variables in command with request params. command = Regex.replace(@regex, command, fn (_, param, default) -> params[param] || default end) # Executes the final command. {output, _exit_code} = System.cmd(shell(), ["-c", command]) send_resp(conn, 200, output) end defp shell() do Application.fetch_env!(:shell2http, :shell) end end defmodule Router do use Phoenix.Router use Plug.ErrorHandler pipeline :browser do plug :accepts, ["html"] end scope "/" do pipe_through :browser # Generates routes for each commands. for {path, _command} <- commands do get path, Controller, :index end end def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do send_resp(conn, conn.status, "Unavailable") end end use Phoenix.Endpoint, otp_app: :shell2http plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:shell2http] plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library() plug Plug.MethodOverride plug Router def start() do {:ok, _} = Supervisor.start_link([__MODULE__], strategy: :one_for_one) Process.sleep(:infinity) end end Shell2HTTP.start()