Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

私は組み込みメインの開発ばかりで、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で動作するもの限定としています。execSyncpython3.6部分を適宜変更してください。

動作確認環境

Ubuntu 18.04 Desktop

コード

こちらの記事をベースに、このような実装にしました。

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はこちらのcgi-binを/usr/local/wwwにコピーして動作確認。
index.htmlはDoxygenで生成したファイル(自分はここのdocsフォルダ内)を/usr/local/www/htmlに配置して動作確認をしました。

cgiの実行はこちらを参考に、require('child_process').execSyncを利用してコマンド実行しています。

参考

コードの参考: ExpressなしでNode.jsで簡単なWebサーバを作ってみる、HTTPを学ぶ
HTTPリクエストパス取得: Node.jsでURLをパースする
mime : Node.jsでhttpサーバを立てた際にCSSが読み取れない場合の対処法について
cgiの実行: Node.jsからシェルコマンドを実行する

developer-kikikaikai
元CのLinux組み込み開発者→201904からとある会社でGo言語バックエンドのアーキテクトとして活動しています。 組み込み時代はミドルウェアより上位層が主戦場でした。たまにRubyやpython、Java/Androidも若干触ります。 技術の幅を増やすのはもちろんだけど、それ以上にチーム構築・チーム開発への貢献力を磨きたい
https://github.com/developer-kikikaikai
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away