Node.jsで、cgiの使える簡易webサーバーを作成する

## **はじめに**

私は組み込みメインの開発ばかりで、Web周りはインフラ側、触ったとしてもAjaxのXMLHttpRequestでレスポンスパースするような画面とは関わらない処理を書くくらいでした。
なのでちょっと視界を広げようと思い、Webフロントエンド側の開発もちょっとやってみようかと思い立ちました。
今回はまずnode.jsを利用して、cgiが利用可能な簡易webサーバーを作成します。

## **Node.jsのインストール**

Node.jsは、サーバーサイドで動作させることが出来るツールです。
パッケージはNode.jsのパッケージ管理ツールであるnpmをインストールすれば一緒にインストールされます。

```
sudo apt install npm
or
sudo yum install npm
```

## **作成webサーバーの機能仕様**

以下のような構成で動作するwebサーバーとなっています。`topdir`はコード内で/usr/local/www/にべた書き。

```
topdir --- html => index.html達の格納する場所
       |
       `-- cgi-bin => cgiコマンドを格納する場所
```

### **機能**

- 8080番ポートでlistenするwebサーバー
- /index.html(or /)アクセス or /cgi-binアクセスをサポート
    - /index.html(or /)アクセス時は、`topdir/html/index.html`が開かれる。cssやjsもファイルがあれば取得可能です。
    - /cgi-binアクセス時は、/cgi-bin/`コマンド名で指定したコマンドと対応する`topdir/cgi-bin`内の同コマンドを実行し、標準出力の結果がそのままHTTPレスポンスとなるよう結果をパースして処理します。

### **備考**

- index.htmlが読み込むファイル達は、ファイルの種類に応じてmime typeを変更する必要があります。(コード内`mime`変数でべた書き)
- htmlファイル以外でサポートしたいuriは、`ResArray`に定義してます。追加する場合は、`var ResArray = ['cgi-bin'];`の中にurlを追加し、Responseオブジェクトに処理を記載してあげます。
- このコードでは、cgiはpython3.6で動作するもの限定としています。`execSync`の`python3.6`部分を適宜変更してください。

### **動作確認環境**

Ubuntu 18.04 Desktop

## **コード**

[こちらの記事](https://qiita.com/Sekky0905/items/31214fdeae477468fd83)をベースに、このような実装にしました。

```webServer.js
// 必要なファイルを読み込み
var http = require('http');
var url = require('url');
var fs = require('fs');
var path = require('path');

//server定義
var server = http.createServer();

//serverのtop directory定義
var topdir = "/usr/local/www/"

//mime type定義
var mime = {
    ".html": "text/html",
    ".css":  "text/css; charset=utf-8",
    ".js":  "application/javascript",
    ".png":  "image/png"
    //必要に応じてmime typeを追加
};

// http.createServerがrequestを受け取った場合の処理
server.on('request', function (req, res) {
    // ファイル読み込みじゃないURIはここに記載
    var ResArray = ['cgi-bin'];

    // Responseオブジェクトを作成し、その中に必要な処理を書いていき、条件によって対応させる
    var Response = {
        // ファイル読み込みURI
        "other": function (req_path) {
            try {
                //html内のファイルなら読み込み
                var fpath = topdir + 'html' + req_path.split("?")[0]
                if ( fpath === topdir + 'html/' ) {
                    fpath += "index.html"
                }

                fs.statSync(fpath);
                data = fs.readFileSync(fpath)
                // HTTPレスポンスヘッダを出力する
                res.writeHead(200, {
                    'content-type': mime[path.extname(fpath)] || "text/plain",
                    'content-length': data.length
                });

                // HTTPレスポンスボディを出力する
                res.write(data);
                res.end()
            } catch(err) {
                //ファイルが無いならNot Foundにする
                res.writeHead(404, {"content-type": "text/plain"});
                res.write("404 Not Found\n");
                res.end()
            }
        },

        //cgi実処理
        "cgi-bin": function (req_path) {
            //コマンド、query設定
            var command = req_path.split("/")[2].split("?")[0]
            var query_string = req_path.split("/")[2].split("?")[1]
            const execSync = require('child_process').execSync;
            //これはpython3.6で実行するcommand限定。queryは環境変数QUERY_STRINGに設定
            var result =  execSync("cd " + topdir + "cgi-bin; QUERY_STRING=\""+ query_string + "\" python3.6 " + command).toString();
            separate_head_body = result.split("\n\n")
            if ( separate_head_body.length === 1 ) {
                head={'content-Type': 'text/plain'}
                body=separate_head_body[0]
            } else {
                head = ParseHeader(separate_head_body[0])
                body=separate_head_body[1]
            }

            console.log("head:" + head.toString())
            res.writeHead(200, head)

            // HTTPレスポンスボディを出力する
            console.log("body:" + body)
            res.write(body);
            res.end()
        }
    }

    // cgiのレスポンスパース処理
    function ParseHeader(header_string) {
        // HTTPヘッダーは連想配列で表現する。
        var head_hash={}
        // 各HTTPヘッダーは改行で分割
        heads=header_string.split("\n")
        for(let i = 0; i < heads.length; i++) {
            // HTTPヘッダーをさらに:で分ける。
            part = heads[i].split(":")

            //execSyncでの実行結果で改行コードが変わるケースがあったので、Content-Lengthは指定せずchunkedにする
            if ( part[0].toLowerCase() !== "content-length" ) {
                //HTTPヘッダーをhashに追加
                head_hash[part[0].toLowerCase()] = part[1]
            }
        }

        return head_hash
    }

    // URI取得、
    function GetURI (uri) {
        uri_part = uri.split("/")
        if ( ResArray.indexOf(uri_part[1]) === -1 ) {
            return "other"
        } else {
            return uri_part[1]
        }
    }

    // server.onのメイン処理
    var uri = url.parse(req.url).pathname;
    var req_path = url.parse(req.url).path;

    // urlと対応するResponseオブジェクト内関数を実行
    Response[GetURI(uri)](req_path);
});

// 指定されたポート(8080)でコネクションの受け入れを開始する
server.listen(8080)
```

## **実行**

html、cgiを所定の位置に格納して以下を実行するだけです。

```
node webServer.js
```

 `http://ip:8080/index.html`等該当のurlに対する応答が帰ってくればOK

cgiは[こちら](https://github.com/developer-kikikaikai/lighttpd_setting)のcgi-binを/usr/local/wwwにコピーして動作確認。
index.htmlはDoxygenで生成したファイル([自分はここのdocsフォルダ内](https://github.com/developer-kikikaikai/design_pattern_for_c_doc))を/usr/local/www/htmlに配置して動作確認をしました。

cgiの実行は[こちら](http://tkybpp.hatenablog.com/entry/2016/04/25/163246)を参考に、`require('child_process').execSync`を利用してコマンド実行しています。


## **参考**

コードの参考: [ExpressなしでNode.jsで簡単なWebサーバを作ってみる、HTTPを学ぶ](https://qiita.com/Sekky0905/items/31214fdeae477468fd83)
HTTPリクエストパス取得: [Node.jsでURLをパースする](http://info-i.net/url-parse)
mime    : [Node.jsでhttpサーバを立てた際にCSSが読み取れない場合の対処法について](https://qiita.com/Suibari_cha/items/48da8519d6f363925b6a)
cgiの実行: [Node.jsからシェルコマンドを実行する](http://tkybpp.hatenablog.com/entry/2016/04/25/163246)