Skip to content

Instantly share code, notes, and snippets.

@sntran
Created December 10, 2021 04:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.
Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.
HTTP-server to execute shell commands in a single file
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment