Skip to content

Instantly share code, notes, and snippets.

@sntran
Created December 10, 2021 04:38
Show Gist options
  • Select an option

  • Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.

Select an option

Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.

Revisions

  1. sntran created this gist Dec 10, 2021.
    172 changes: 172 additions & 0 deletions shell2http.exs
    Original 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()