ヤマハルーターのGUIでPingを実行する

# 前置き

ヤマハルーターはブラウザからコマンドを実行するメニューもありますが、ping や traceroute は実行できません。禁止と表示されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180964/ed80d969-4e9f-0d28-8d25-f8124b15fc0a.png)

カスタムGUI からも実行できないコマンドとして ping, ping6, traceroute, traceroute6 が入っています[^1]。

カスタムGUI を利用するのが前提ですが、やや強引に pingなど を実行して結果を取得するコードを書いてみました。

# 仕組み

カスタムGUIから ping は実行できませんが、luaコマンド経由の ping は禁止されていないので、luaスクリプトで`rt.command("ping ～")` を実行します。
ただし、そのままでは戻り値が取得できず結果は不明になります[^2]。
そのためLUAの実行結果は rt.syslogコマンドで残した後に、カスタムGUIからsyslogを取り込むことにしました。

# 実装
### ルーターコマンド

カスタムGUIの設定

```
login user sample *
httpd custom-gui use on
httpd custom-gui user sample directory=/gui/sample index=index.html
```

### HTML

``` HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>GUI Sample</title>
<script type="text/javascript" src="/custom/custom_gui_lib.js"></script>
<script type="text/javascript">
<!--

function createHttpRequest() {
  if (window.ActiveXObject) {
    try {
      return new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      try {
        return new ActiveXObject("Microsoft.XMLHTTP");
      } catch (e2) {
         return null;
      }
    }
  }
  else if (window.XMLHttpRequest)
    return new XMLHttpRequest();
  else
    return null;
}

function sendRequest(data, async) {
  return new Promise((resolve, reject) => {
    let httpoj = createHttpRequest();
    httpoj.open("POST", "/custom/execute", async);
    httpoj.onreadystatechange = function() {
      if (httpoj.readyState == 4) {
        if (httpoj.status == 200) {
          resolve(httpoj);
        } else {
          reject(new Error("/custom/execute 実行失敗"));
        }
      }
    };
    httpoj.send(data);
  });
}

function inputCheck() {
  const command = document.getElementById("command").value.trim();
  if ( command === "") {
    alert("コマンドを選択してください。");
    document.getElementById("command").focus();
    return false;
  }

  execUserCommand();
}

function startLoadingAnimation() {
  let dots = '';
  const resultElement = document.getElementById('result');
  let counter = 0;

  const intervalId = setInterval(() => {
    dots += '.';
    counter++;
    resultElement.innerHTML = `コマンド実行中 ${dots}`;

    if (counter > 5) {
      dots = '';
      counter = 0;
    }
  }, 500);

  return intervalId;
}

async function execUserCommand() {
  const loadingIntervalId = startLoadingAnimation();

  try {
    const command = document.getElementById("command").value.trim();
    const terget = document.getElementById("terget").value.trim();
    document.getElementById("execButton").disabled = true;
    document.getElementById("result").innerHTML = "コマンド実行中 .";

    const sessionId = getSessionId();
    const execId = sessionId + Math.random().toString(36).substring(2, 7);
    let order = "#" + sessionId + "\r\n";
    order += "lua /lua/command.int " + execId + " " + command + " " + terget + "\r\n";
    await sendRequest(order, true);

    order = "#" + sessionId + "\r\n";
    order += "show log\r\n";
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    const syslog = await sleep(['traceroute', 'traceroute6'].includes(command) ? 30000 : 3000)
      .then(() => sendRequest(order, true));

    const syslogArray = syslog.responseText.split('\r\n');
    const keyword = "[" + execId + "]";
    let sessionLog = ""

    for (let i = 0; i < syslogArray.length; i++) {
      const line = syslogArray[i];
      if (!line.includes(keyword)) continue;
      sessionLog += line.replace(keyword, "")
                        .replace(/Error: \/lua\/command\.lua:\d+: /, "")
                        + "<br>\r\n";
    }
    if (sessionLog.length === 0) {
      sessionLog = "待機時間内に応答が返りませんでした";
    }

    clearInterval(loadingIntervalId);
    document.getElementById("execButton").disabled = false;
    document.getElementById("result").innerHTML = sessionLog;

  } catch (error) {
    clearInterval(loadingIntervalId);
    document.getElementById("execButton").disabled = false;
    console.error(error);
  }
}
//-->
</script>
</head>

<body>

<form name="formCommand" action="#">
  <label for="command">コマンド: </label>
  <select id="command">
    <option value="" selected>選択してください</option>
    <option value="ping">ping</option>
    <option value="ping6">ping6</option>
    <option value="traceroute">traceroute</option>
    <option value="traceroute6">traceroute6</option>
    <option value="nslookup">nslookup</option>
  </select>

  <label for="terget">対象: </label>
  <input id="terget" name="terget" type="text">

  <input id="execButton" type="button" value="実行" onclick="inputCheck();"/>

  <div id="result"></div>
</form>

</body>
</html>

```

次の行のところで、pingやtraceroute実行後の待ち時間設定を行っていますが、設定内容は適当に決め打ちです。
```
const syslog = await sleep(['traceroute', 'traceroute6'].includes(command) ? 30000 : 3000)
      .then(() => sendRequest(order, true));
```


### LUAスクリプト

``` /lua/command.lua
local COMMANDS = {
  ping = {
    cmd = "ping",
    args = {"-c", "3"}
  },
  ping6 = {
    cmd = "ping6",
    args = {"-c", "3"}
  },
  traceroute = {
    cmd = "traceroute",
    args = {"noresolv"}
  },
  traceroute6 = {
    cmd = "traceroute6",
    args = {"noresolv"}
  },
  nslookup = {
    cmd = "nslookup",
    args = {}
  }
}

local function validateInput(command, destination)
  if not command or not destination then
    error("Usage: script.lua <id> <command> <destination>")
  end

  if not COMMANDS[command] then
    error(string.format("Invalid command: %s. Supported commands: %s", 
      command, table.concat(getTableKeys(COMMANDS), ", ")))
  end

  if destination:match("[;|`&$]") then
    error("Invalid destination: contains forbidden characters")
  end
end

local function getTableKeys(t)
  local keys = {}
  for k in pairs(t) do
    table.insert(keys, k)
  end
  return keys
end

local function buildCommand(command, destination)
  local cmdConfig = COMMANDS[command]
  local cmd = cmdConfig.cmd
  local args = cmdConfig.args

  if command == "traceroute" or command == "traceroute6" then
    return string.format("%s %s %s", cmd, destination, table.concat(args, " "))
  end

  return string.format("%s %s %s", cmd, table.concat(args, " "), destination)
end

local function executeCommand(order)
  local success, flag, outputs = pcall(function()
    return rt.command(order)
  end)
  
  if not success then
    error("Command execution failed: " .. tostring(flag))
  end
  
  if not flag then
    error(string.format("Command execution failed with status false: %s", order))
  end

  return outputs
end

local function logOutput(sessionId, result)
  for line in result:gmatch("[^\r\n]+") do
    if line and line:trim() ~= "" then
      rt.syslog("info", "[" .. sessionId .. "] " .. line)
    end
  end
end

string.trim = function(s)
  return s:match("^%s*(.-)%s*$")
end

local function main()
  local sessionId = arg[1]
  local command = arg[2]
  local destination = arg[3]

  local function logError(err)
    rt.syslog("info", "[" .. sessionId .. "] Error: " .. tostring(err))
  end

  local status, err = pcall(validateInput, command, destination)
  if not status then
    logError(err)
    os.exit(1)
  end

  local order = buildCommand(command, destination)

  local status, result = pcall(executeCommand, order)
  if not status then
    logError(result)
    os.exit(1)
  end
  
  logOutput(sessionId, result)
end

main()
```

# 実行画面

![Result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180964/3c1371ab-5de1-f06d-4757-29ea9f635df0.png)


# 後書き

先日、セキュリティの都合上 Webインターフェイスしか触れない端末で通信調査をする際に困ったので、pingは実行できるといいですね。カスタムGUIの設定を入れておくのも面倒ですが、Qiitaのネタとして作成しました。
補足ですが、有料サービスのYNO[^3]のインターフェイスからpingの実行はできました。もちろんコマンドラインからのpingも実行できます。

nslookupも勢いでつけてしまいましたが、禁止コマンドではなかったのでLUAを通す意味は薄いかもしれません。



[^1]: [ヤマハネットワーク周辺機器 技術情報ページ カスタムGUI](https://www.rtpro.yamaha.co.jp/RT/docs/custom-gui/index.html)

[^2]: 即座にレスポンスが返ってきていた。

[^3]: [Yamaha Network Organizer](https://network.yamaha.com/products/software_service/ysl-yno/index)