眠気

戯言とメモ

njsを使ってnginxで数独をする

njs

この記事はkb Advent Calendar 2020 12日目の記事です。 https://adventar.org/calendars/5280

軽く動的なサーバサイドのコードを動かしたいけど、サーバーを立ち上げるのにnodejsをインストールしてデーモン化とか面倒… とはいえphp/php-fpmで書くのもな…

そういうときにもしかすると便利かもしれないのがnjsです。

njsはnginx上で ECMAScript5.1 相当のJavascriptを動かすことが出来るサブセットで、 modules/ngx_http_js_module.so をload_moduleすることで使用する事ができます。

使い方は簡単、

  1. 動かしたいjavascriptのコードを用意し、
  2. nginx.conf にload_module modules/ngx_http_js_module.so; を追加
  3. httpモジュール内で js_include [jsファイルパス]; で呼び出す
  4. location モジュール内で js_content [includeしたjsファイル内のfuction]; で該当のfunctionを呼び出す。

以上です。

Hello World

使い方を見ていきます。

├── Dockerfile
├── nginx.conf
└── src
    └── hello_world.js

上記の構成で hello_world.js を作成します。

hello_world.js

function hello(r) {
    r.return(200, "Hello world!");
}

function params(r) {
    var h = r.headersIn["test"];
    r.headersOut['test'] = "[header]Hello " + h;
    r.status = 200;
    r.contentType = 'text/plain';
    r.sendHeader();
    var b = r.args["test"];
    var msg = '[body]Hello '+ b;
    r.send(msg);
    r.finish();
}

helloは Hello world! を出力し、 params はリクエストパラメータを読み取り、Helloのあとにつけてレスポンスを行うfunctionです。

nginx.conf

worker_processes  1;
load_module modules/ngx_http_js_module.so;

events {
    worker_connections  1024;
}
http {
    js_include src/hello_world.js;

    server {
        server_name _;

        location = /favicon.ico { access_log off; log_not_found off; }

        location /hello {
            js_content hello;
        }

        location /params {
            js_content params;
        }
    }
}

nginx.conf で先程作成したjsをimportし、実行したいlocation内でjs_contentとfunction名を指定します。

今回はDocker内で実行させるため、以下の様なDockerfileを書きます。

Dockerfile

FROM nginx:latest
COPY nginx.conf   /etc/nginx/nginx.conf
COPY src /etc/nginx/src
EXPOSE 80

あとはbuild/runしてnginxを起動します。

$ docker build . -t docker-njs && docker run -p 80:80 -it docker-njs

curlでアクセスしてみると動作してることがわかります。

$ curl localhost/hello
Hello world!

$ curl -v -H test:fuga "localhost/params?test=hoge"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET /params?test=hoge HTTP/1.1
> Host: localhost
> User-Agent: curl/7.64.1
> Accept: */*
> test:fuga
>
< HTTP/1.1 200 OK
< Server: nginx/1.19.5
< Date: Tue, 15 Dec 2020 15:39:14 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< test: [header]Hello fuga
<
* Connection #0 to host localhost left intact
[body]Hello hoge* Closing connection 0

数独を動かす

やはり動的なコンテンツを出力するのであればオウム返しでは物足りないので、何か便利なサービスを動かしたいです。自分は数独がとても苦手なのですが、数独を出題してくれるやつを動かしてみます。

とはいえこの記事は数独アルゴリズムを解説する記事ではないので、すでにあるものを持ってきて使います。

探すとsudoku.jsなるものがあったので、これをそのまま使ってみます。

sudoku.js の使い方は簡単で、jsを読み込むだけで

  • sudoku.generate()数独の生成
  • sudoku.solve()数独の解答

を行えるようです。

ここで、njsはjs_importを用いることでesmoduleのimport/exportのような機能を使うことが出来るのですが、js_importjs_includeを同時に使うことが出来ないため、どちらかに統一する必要があります。

ここで、sudoku.jsがあまりexportしやすい作りになっておらず、sudoku.jsの1ファイルのみをjs_importでimportする方向にします。

curl https://raw.githubusercontent.com/robatron/sudoku.js/master/sudoku.js > src/sudoku.js

sudoku.jsを落としてきて、以下のようなpatchを書きます。

sudoku.js自体はconsole.log が無いためreturnに変更した部分以外は基本的に変更せず、generate()とsolve()を追加します。

sudoku.js.patch

diff --git a/sudoku/src/sudoku.js b/sudoku/src/sudoku.js
index 65039da..46a5f70 100644
--- a/sudoku/src/sudoku.js
+++ b/sudoku/src/sudoku.js
@@ -670,7 +670,7 @@
             }
         }

-        console.log(display_string);
+        return (display_string);
     };

     sudoku.validate_board = function(board){
@@ -804,4 +804,30 @@
     initialize();

 // Pass whatever the root object is, lsike 'window' in browsers
-})(this);
\ No newline at end of file
+})(this);
+
+function generate(r) {
+    var level = "easy";
+    switch (r.args["level"]) {
+        case 'easy':
+            level = "easy";
+            break;
+        case 'medium':
+            level = "medium";
+            break;
+        case 'hard':
+            level = "hard";
+            break;
+
+    }
+
+    var q = sudoku.generate(level);
+    var v = sudoku.print_board(q);
+
+    r.return(200, q + '\n\n' + v );
+}
+
+function solve(r) {
+    var q = r.args["q"]
+    r.return(200, sudoku.print_board(sudoku.solve(q)));
+}
\ No newline at end of file

上記のpatchを適用してnginx.confを記述し、nginxを起動させます。

worker_processes  1;
load_module modules/ngx_http_js_module.so;

events {
    worker_connections  1024;
}
http {
    js_include src/sudoku.js;

    server {
        server_name _;

        location = /favicon.ico { access_log off; log_not_found off; }

        location /generate {
            js_content generate;
        }
        location /solve {
            js_content solve;
        }
    }
}
$ patch -u src/sudoku.js < sudoku.js.patch
$ docker build . -t docker-njs && docker run -p 80:80 -it docker-njs

クエリパラメータにlevelを入れることでレベルが変えられているかを確認してみます。

$ curl "http://localhost/generate?level=easy"
7.193.8.42834.59679..768.323.5.46791.16.9728387.3216.56.8...51.192.53478547189326

7 . 1   9 3 .   8 . 4
2 8 3   4 . 5   9 6 7
9 . .   7 6 8   . 3 2

3 . 5   . 4 6   7 9 1
. 1 6   . 9 7   2 8 3
8 7 .   3 2 1   6 . 5

6 . 8   . . .   5 1 .
1 9 2   . 5 3   4 7 8
5 4 7   1 8 9   3 2 6
$ curl "http://localhost/generate?level=hard"
.2..9..5..7...4..9.59......71298.46.936.4....8451...9.5648293713874..9262917.....

. 2 .   . 9 .   . 5 .
. 7 .   . . 4   . . 9
. 5 9   . . .   . . .

7 1 2   9 8 .   4 6 .
9 3 6   . 4 .   . . .
8 4 5   1 . .   . 9 .

5 6 4   8 2 9   3 7 1
3 8 7   4 . .   9 2 6
2 9 1   7 . .   . . .

以上、generateが実行され、数独が生成されています。

github.com

ちなみに、ドキュメントにはtypescriptをnjsに変換して使う方法なども載っているので、多少ちゃんと使う方法もあるのではないかなと思われます。

参考: