Created
December 10, 2021 04:38
-
-
Save sntran/31c85b7271a4f386e5c484101b784da6 to your computer and use it in GitHub Desktop.
HTTP-server to execute shell commands in a single file
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 characters
#!/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