cgi

pico-args

rustのexampleに、 引数のパース なんてのが掲載されてる。が、もう少し手軽にならないか。ってんで、前回見付ておいた、pico-argsなんてのを試してみる。

     Running `target/debug/zpiko --number 123 --opt-number 987 --output myoutput ZIN`
Args { inner: ["target/debug/zpiko", "--number", "123", "--opt-number", "987", "
--output", "myoutput", "ZIN"] }
AppArgs {
    number: 123,
    opt_number: Some(
        987,
    ),
    width: 10,
    input: "ZIN",
    output: Some(
        "myoutput",
    ),
}

下記は例にあったのに些細な間違えがあったので、ちょいと訂正してる。

struct AppArgs {
    number: u32,
    opt_number: Option<u32>,
    width: u32,
    input: std::path::PathBuf,
    output: Option<std::path::PathBuf>,
}

fn parse_args() -> Result<AppArgs, pico_args::Error> {
    let mut pargs = pico_args::Arguments::from_env();
     :
    let args = AppArgs {
        number: pargs.value_from_str("--number")?,
        opt_number: pargs.opt_value_from_str("--opt-number")?,
        width: pargs.opt_value_from_fn("--width", parse_width)?.unwrap_or(10),
        output: pargs.opt_value_from_os_str("--output", parse_path)?,
        input: pargs.free_from_str()?,
    };

fn main() {
    let xxxx = std::env::args();
    println!("{:?}",  xxxx);

    let args = match parse_args() {
        Ok(v) => v,
        Err(e) => {
            eprintln!("myError: {}.", e);
            std::process::exit(1);
        }
    };
    println!("{:#?}", args);
}

引数を全て、構造体に押し込めてしまうとな。して、メンバーを埋めていく時に、解析の順番で必須引数とオプション引数を使い分ているのか。これぐらいが手頃でいいな。

git版には、

ob$ ls test-apps/
argh-app/       gumdrop-app/    pico-args-app/
clap-app/       null-app/       structopt-app/

有名なclap(拍手)を始めその派生版のstructoptとかの定義比べがでてる。けど、たかがパースするだけで、とんでもない数の木箱を使うって、将来に悔過を残さないか? 気の小さいオイラーは、心配になっちゃうぞ。

Web server

以前やったsqlite3の英日辞書をWEBからアクセスしてみたい。今迄便利に使っていたPythonの簡易サーバー python3 -m http.server 8080 は、残念ながら使えない。だって、バックエンドでsqliteを使う必要があるからだ。

そこで、アラウンドY2K(2000年前後)時代に使ってた、本物のWEBサーバーが必要。あの頃は、apaache 1.3.XXを使ってたな。2.X時代になった頃には、足を洗ってしまったから、20年もブランクがある。

今なら、nginxが主流? それともapacheが健在? OpenBSDのportsを調べてみたら、nginx 1.18.0, apache 2.4.46 なんてのが提供されてた。luaがサポートされてたりLDAPが追加されたりして、隔世の感があるなあ。

仮にapacheを使うとして、cgiをやるには、面倒な設定が必要だったはず。手元に何か資料がないものかと探してみたら、Gauche本で、cgiの要件が説明されてた。

mod_cgi.so をロードして、cgiを使えるサーバーにしろ。拡張子 .cgi で、cgiスクリプトだと認識させろ。それからWEBサーバーからgoshが起動出来るようになってる亊、ですってさ。 面倒なこった。で、続きを読むと、text.html-liteとかwww.cgiを使ってくださいなんて説明があった。残念ながらsqliteへのアクセスモジュールは標準提供されていないようだ。GaucheでRDBプログラミングに解説が出てたぞ。また、括弧まぎれになってみるか?

そんな亊より問題が。Gauche download にあるgitの案内では、いつの間にかソースが取れなくなっている。

ob$ git clone git://github.com/shirok/Gauche.git
Cloning into 'Gauche'...
fatal: remote error:
  The unauthenticated git protocol on port 9418 is no longer supported.
Please see https://github.blog/2021-09-01-improving-git-protocol-security-github/ for more information.

案内をみると、時代が変っていたぞ。

ob$ git clone https://github.com/shirok/Gauche.git

これでOK。既に利用中ならば、下記のように修整すればよい。

sakae@deb:~/src/Gauche$ cat .git/config
   :
[remote "origin"]
        url = https://github.com/shirok/Gauche.git
        fetch = +refs/heads/*:refs/remotes/origin/*

で、昔の亊を思い出した。これって年寄の特技です。OpenBSDには備付の家具ならぬ、備付のWEBサーバーが用意されてるじゃん。2018年に、根ほり葉ほり調べているぞ。 OpenBSD httpd

折角あるなら有効活用しよう。既に常人の道を外れています。

static?

で、問題になりそうなのが、セキュリティーに五月蝿いOpenBSDの常で、WEBエリアはjail環境になってる亊。FreeBSDでもjailを売りにしてる。jailって刑務所の亊、世間一般から隔離されてるのよ。いわゆるサンドボックス。unix言葉で言ったらchrootね。

これの何が問題かと言うと、アプリが必要とするライブラリィーをOSが用意した物と共有出来無いって亊。だから、アプリは必要なライブラリィーを全て組み込んだスタチックな物が欲しい。

そんな都合良くrustが振る舞ってくれるか。ええ、C言語に変ってこの所ずっとrustですからな。軽く調べてみると、

RustのLinux muslターゲット (その1:Linux向けのポータブルなバイナリを作る)

Go と Rust における静的リンクのビルド方法(+ Dockerfile サンプル)

どうも、特殊な亊をやらないと無理みたい。世間一般に流通してるリナなら、何とかなりそうだけど、OpenBSDを選んだ時点に荒野に放り出されたと覚悟ですよ。

まあ、野生の証明ですな。最近、森村誠一さんの、野生の証明を読んだ所だから、ぴったりなのさ。

昔の設定をちょっと変えて、動くか試運転。

http://localhost/cgi-bin/now

vbox$ cat /etc/httpd.conf
ext_ip = "0.0.0.0"
prefork 1
server "default" {
    listen on $ext_ip port 8080
    root "/htdocs/"
    location "/cgi-bin/*" {
        fastcgi socket "/run/slowcgi.sock"
        root "/"
    }
}
types {
    include "/usr/share/misc/mime.types"
}

httpd.confを設定して起動するだけ。pythonサーバーに習って、8080で待受。スタチックエリアは、htdocs以下。動的アプリはcgi-binの下に配置するっていう、簡単明瞭な設定。slowcgiで、昔風のcgiをサポートすると言う、Y2K時代の再現です。

尚、WEBエリアのトップdirは、固定で var/www になってる。

cgi and html

昔の亊ですっかり忘れているので、資料収集。

Ruby ビギナーのための CGI 入門

フォームのデータのCGIへの受け渡し

フォームからの入力を受け取る basic by perl

それから、結果表示の為の知恵を少し仕入れた。

<dl>
  <dt>りんご</dt>
    <dd>赤い色をした丸い果物。</dd>
  <dt>バナナ</dt>
    <dd>黄色い色をした細長い果物。</dd>
</dl>

りんご
    赤い色をした丸い果物。
バナナ
    黄色い色をした細長い果物。 

バックエンドの試作

バックエンドとして、検索ワードが渡ってきた時、辞書ファイルを検索してhtmlを出力するのを作成する。

use sqlite::Value;

fn search(sw: &str) {
    let ew = format!("{}{}", sw, "%");
    let connection = sqlite::open("ejdict.sqlite3").unwrap();
    let mut cursor = connection
        .prepare("SELECT word,mean FROM items WHERE word like ? limit 3")
        .unwrap()
        .into_cursor();
    cursor.bind(&[Value::String(ew.to_string())]).unwrap();

    println!("Content-type: text/html; charset=UTF-8\n");
    while let Some(row) = cursor.next().unwrap() {
        println!("<dt>{}</dt>", row[0].as_string().unwrap());
        println!("<dd>{}</dd>", row[1].as_string().unwrap());
    }
}

fn main(){
    let sw = "rust";
    search(sw);
}

面倒そうなcgiデータのパースをサボった版だ。検索ワードに '%' を追加してるのは、検索ワード* っていう正規表現のsql版だ。

配置と試運転

cgi-bin/sqlを駆動する為の入力用htmlファイルを作る。場所がhtdocs/ej/ の下にしてるのは、有名なトリックだ。こうすると、面倒なってか無様な長ったらしいURLを入力しなくてもよくなる。WEBサーバーのお約束として、http://localhost/ej と指定するだけで、自動でej/の中にあるindex.htmlが参照される。

vbox$ cat /var/www/htdocs/ej/index.html
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head><body>
English word to Japanese
<br><br>

<form method=POST action="/cgi-bin/sql">
<input type="text" name="ew">
<br><br>
<input type="submit" value="trans">
</form></body></html>

次はバックエンドのsqlの確認。

vbox$ ldd target/debug/sql
target/debug/sql:
        Start    End      Type  Open Ref GrpRef Name
        167c6000 367c9000 exe   2    0   0      target/debug/sql
        0ec30000 2ec35000 rlib  0    1   0      /usr/local/lib/libsqlite3.so.37.13
        00afc000 20afe000 rlib  0    2   0      /usr/lib/libpthread.so.26.1
        054bf000 254c3000 rlib  0    1   0      /usr/lib/libc++abi.so.5.0
        043e0000 243f1000 rlib  0    1   0      /usr/lib/libc.so.96.1
        0b16b000 2b16d000 rlib  0    1   0      /usr/lib/libz.so.6.0
        0bc93000 2bc95000 rlib  0    1   0      /usr/lib/libm.so.10.1
        0734a000 0734a000 ld.so 0    1   0      /usr/libexec/ld.so

必要なライブラリィー類を、/var/www/usr/ 以下へコピーする。まあ二重生活になるけど我慢。usr以下は6.5Mになった。それから、大事なsqlと辞書ファイルを/var/www/cgi-bin以下に配置する。本来なら、辞書ファイルはWEBからアクセス出来るエリアを避けて/var/www/usr/share/dictあたりに配置するのが正しい処置だけど、大目にみてね。

これで準備が整ったんで、http://localhost/ej へアクセス。 がーん、 500 internal error エラーですって。こういう時はエラーログの確認だな。

vbox# cat logs/error.log
ld.so: sql: can't load library 'libsqlite3.so.37.13'

しばし考える。ライブラリィーが見付からない。 LD_LIBRARY_PATH の出番か? それらしいコマンドの ldconfig を見付た。キャッシュが必要なのね。本来はldconfigするのが正しい処置だろうけど、本体OS側で使っているものを流用させて貰う亊にする。

vbox# cd /var/www
vbox# mkdir -p var/run
vbox# cd var/run/
vbox# cp /var/run/ld.so.hints .

w3mを使っても、ちゃんと機能した

English word to Japanese

[                    ]

[trans]

結果画面

rust
    (金属の)『さび』 / さび病;さび菌 / さび色 / さびる / 〈能力などが〉(使わず
    に)鈍る / 〈植物が〉さび病にかかる / 〈金属〉‘を'さびさせる / さび色の
rustic
    『いなか』(『田舎』)『の』,田舎ふうの,田園生活の(rural) / 《良い意味で》素
    ぼくな,飾り気のない(simple);《しばしば軽べつして》田舎者の,田舎っぽい / 《
    名詞の前にのみ用いて》粗木(あらき)作りの,丸木作りの / 《しばしば軽べつして
    》田舎者,農夫
rusticate
    《おもに文》…‘を'田舎へ行かせる / …‘を'田舎ふうにする / 《英》〈大学生〉‘を
    '停学処分する / 《おもに文》田舎へ行く / 田舎ふうになる

なお、libsqlite3.soは、pythonに連られて自動でインストールされてる。OpenBSDでは、何故か、OS側にあるやつが使われた。何故かは深くは追求しない。

get and post

ブラウザーから送られるデータをcgiアプリケーションに届ける方法として、 入力フォーム編(GETとPOSTデータを受け取るには) のように2種類がある。英語に詳しくなくても、get/postって真逆だろう。getは、サーバーからファイルを取ってくる。そんなに長大なデータを想定してないので環境変数で渡してしまえ。初期のcgiはこの考えだな。

そのうちにファイルをアップロードしたいって要求が生れた。だからサーバーにpostするって命令語が選ばれた。当然、長大なものになると予想されるので、環境変数に載せるんでは役たたず。サーバー側では標準入力からのストリーミングで受けて立つって塩梅。

POST対応限定のコードを書いていこう。登場人物はハッシュになるので、少し調べておく。

キーとそれに紐づいた値をハッシュマップに格納する

そして、出来上がったもの。標準入力からデータを読むので stdin().read_line するんだけど、こやつ最後の改行も含めて取得されちゃう。だから、pop()で、この改行を削除してる。

無闇に削除するんじゃなくて、 trim_end() を使うのが本当は正解。行末にホワイトスペースが有ると削除してくれる。

で、実は重大なCGIの規格違反をしている。perl言語で書かれたものは、 read (STDIN, $PostData, $ENV{'CONTENT_LENGTH'}); の様に、指定されたサイズだけ読み込んでいる。が、面倒なので誤魔化している。それでも、firefox,chroneでは動いた。w3mは駄目だった。本当は、takeとかを使うんだろうね。

use std::collections::HashMap;

fn main() {
    let mut cgiin = String::new();
    std::io::stdin().read_line(&mut cgiin).unwrap();
    // cgiin.pop(); // delete '\n'
    let mut pd = HashMap::new();
    let ary: Vec<&str> = cgiin.split('&').collect();
    for el in ary {
        let kv: Vec<&str> = el.split('=').collect();
        pd.insert(kv[0], kv[1]);
    }
    let sw = pd.get("ew").unwrap();
    search(sw);
}

オフライン試験するなら

vbox$ echo 'name=Rich&ew=rust&state=KY&side=Monkey' | target/debug/sql

いちいち、出来たアプリを転送するの面倒くさいからね。

perlの真似は大変だ

上で出て来た不満を解消する手立て。 これは取りも直さず、perlで実現してる read (STDIN, $PostData, $ENV{'CONTENT_LENGTH'}) を実現する亊だ。

Y2Kの時代はperlしかなくて、そこへrubyが殴り込みをかけたんだな。pythonなんて片鱗もなかったぞ。オイラーはrubyの物珍しさに引かれて使っていたけどね。

時代が変って、先進のrustでやったら、どうなるかって試み。

バイト配列を文字列に変換する方法を事前に調べておく。 Rustでバイト列から文字列へ

fn main() {
    let bytes = vec![0x41, 0x42, 0x43];
    let s = format!("{:?}", &bytes);
    println!("{}", s);
}

こんな技も有るんだね。

use std::io::Read;

fn main() {
    let cs: u64 = std::env::var("CONTENT_LENGTH").unwrap().parse().unwrap();
    let len = 512;
    let mut buf = Vec::<u8>::with_capacity(len);
    unsafe {
        buf.set_len(cs as usize);
    }
    let f = std::io::stdin();
    let mut handle = f.take(cs);
    handle.read(&mut buf).unwrap();
    println!("{:?}", buf);
    let cgiin: String = String::from_utf8(buf.to_vec()).unwrap();
    println!("{:?}", cgiin);
}

環境変数を設定してから、実行。

sakae@pen:/tmp/cgi$ export CONTENT_LENGTH=5
sakae@pen:/tmp/cgi$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/cgi`
123456789
[49, 50, 51, 52, 53]
"12345"

確かに指定した長さだけ、取り込まれているな。こういう需要は、余りないのかな?

どうやらOKなんで、前記のやつと組み合わせたら、今度はちゃんとw3mからもアクセスできた。 これにて、目的達成!! clap clap と喜んで拍手しつつ、辞書を引いてみたら、平手打ちって意味も載ってた。かしわでで変換したら柏手になった。そういうものなんですか? 金田一先生。

by golang

掟破りで、golangに触ってみる。

5.3 SQLiteデータベースの使用 sqlite3で調べていたら、もっと広い枠であるWEBサーバーにも言及してる本に出会った。グーグル流のモダンなやつって認識でいいのかな? それとも、これがY2K時代のcgiから脱却する標準的な枠組みなのかな。

vbox$ cd zz
vbox$ go mod init
go: cannot determine module path for source directory /tmp/zz (outside GOPATH, m
odule path must be specified)

Example usage:
        'go mod init example.com/m' to initialize a v0 or v1 module
        'go mod init example.com/m/v2' to initialize a v2 module

Run 'go help mod init' for more information.

オイラーの大好きな、/tmpの下でゴニョゴニョするのは、拒否された。

vbox$ cd go/
vbox$ mkdir -p src/gosql
vbox$ cd src/gosql/
vbox$ go mod init
go: creating new go.mod: module gosql

由緒正しく、所定の場所に店を作れとな。面倒っぽいな。

vbox$ go build
main.go:7:5: no required module provides package github.com/mattn/go-sqlite3; to
 add it:
        go get github.com/mattn/go-sqlite3
vbox$ go get github.com/mattn/go-sqlite3
go: downloading github.com/mattn/go-sqlite3 v1.14.12
go get: added github.com/mattn/go-sqlite3 v1.14.12
vbox$ go build
vbox$ ldd gosql
gosql:
        Start    End      Type  Open Ref GrpRef Name
        00400000 20033000 exe   2    0   0      gosql
        09b7a000 29b7c000 rlib  0    1   0      /usr/lib/libpthread.so.26.1
        0fd24000 2fd35000 rlib  0    1   0      /usr/lib/libc.so.96.1
        0e8ee000 0e8ee000 ld.so 0    1   0      /usr/libexec/ld.so
vbox$ ls -lh gosql
-rwxr-xr-x  1 sakae  wheel   9.4M Apr 10 06:11 gosql*

こちらは、libsqlを内蔵してるね。そんなものなのか。

そうだ、golangにこだわる亊なく、rubyのそれでも調べてみればいいのか。


This year's Index

Home