2011/04/28

種文字列から一定の乱数列を生成する

NILScriptのCipherユニットには、種文字列を指定して乱数列を得るRandomクラスが用意されています。
通常のMath.random()と違うのは、同じ種文字列を指定すると常に同じ乱数列が生成されるという点です。
この機能を利用すれば、ゲームのステージ生成などで、生成されたステージを丸ごと保存しなくても、種文字列を保存しておくだけで過去に生成したステージを再現できるようになります。

下記の例は、Cipher::Randomを利用して迷路を生成するというスクリプトです。
コマンドライン引数に横の広さ、縦の広さ、種文字列を指定すると、迷路が生成され表示されます。
種文字列が省略された場合は、String.random()で生成したランダムな文字列が使用されます。
var w=(Main.params[0]||32)*2-1, h=(Main.params[1]||18)*2-1;
var r=new (require('Cipher').Random)(Main.params[2]||String.random(16)); //(1)
var map=[],joints=[];
//周辺の枠の生成
map.push(('-'.times(w-1)+'+').split(''));
for(var x=2,m=w-2;x<m;x+=2){
    joints.push({x:x,y:0});
    joints.push({x:x,y:h-1});
}
for(var y=1;y<(h-1);++y){
    map.push(('|'+' '.times(w-2)+'|').split(''));
    if(!(y&1)){
        joints.push({x:0,y:y});
        joints.push({x:w-1,y:y});
    }
}
map.push(('+'+'-'.times(w-1)).split(''));

//壁の生成
var j;
while(joints.length){
 //新たに生やし始める箇所を選択する
    if(!j){
        j=joints[r.next(joints.length)]; //(3)
    }
    //壁を延ばす方向の候補を列挙
    var c=[];
    if((map[j.y-2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y-1}, next:{x:j.x  ,y:j.y-2}, mark:'|'});
    }
    if((map[j.y+2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y+1}, next:{x:j.x  ,y:j.y+2}, mark:'|'});
    }
    if((map[j.y]||[])[j.x-2]==' '){
        c.push({via:{x:j.x-1,y:j.y  }, next:{x:j.x-2,y:j.y  }, mark:'-'});
    }
    if((map[j.y]||[])[j.x+2]==' '){
        c.push({via:{x:j.x+1,y:j.y  }, next:{x:j.x+2,y:j.y  }, mark:'-'});
    }
    //延ばせる方向があれば、そのうちのどこかに延ばす
    if(c.length){
        var m=c[r.next(c.length)]; //(4)
        map[m.via.y][m.via.x]=map[m.next.y][m.next.x]=m.mark;
        if(map[j.y][j.x]!=m.mark){
            map[j.y][j.x]='+';
        }
        joints.push(j=m.next);
    }else{
        joints.remove(j);
        j=null;
    }
}
map[1][0]=map[h-2][w-1]=' ';
//表示
for(var y=0;y<h;++y){
    var l=[];
    for(var x=0;x<w;++x){
        l.push(map[y][x]);
        if(x&1){
            l.push(map[y][x]);
        }
    }
    println(' '+l.join(''));
}
println('Seed='+r.seed);
free(r); //(2)

最初に「r」変数にRandomオブジェクトを格納します。(1)
Randomオブジェクトは、不要になったら「free()」でリソースを解放する必要があります。(2)

迷路の生成には、既存の壁から壁を生やして、他の壁にぶつからないように延ばしていくという方式を使用しています。
新たに壁を生やし始める箇所を選択する処理(3)と、壁を延ばす方向を選択する処理(4)で乱数を使用しています。
Randomオブジェクトのnext()メソッドは、Number.random()と同じように、指定した範囲の整数を返します。

上記のスクリプトを「20 10 XYZ」を引数にして実行すると、以下のような迷路が表示されるはずです。
 ---------------------------------------------------------+
                                                          |
 |  ---+  +-----------+  +-----+---  +--------+  +--+  +--+
 |     |  |           |  |     |     |        |  |  |  |  |
 |  +--+  |  +-----+  |  |  |  +-----+  +--+  |  |  |  |  |
 |  |     |  |     |     |  |           |  |  |  |  |  |  |
 |  +--+  +--+---  +-----+  +--------+  |  |  +--+  +--+  |
 |     |                             |  |  |           |  |
 |  +--+  +--------+  +-----+-----+--+  |  +--+  +--+  |  |
 |  |     |        |  |     |     |     |     |  |  |     |
 |  +-----+  +-----+  +--+  |  +--+  +--+--+  +--+  +--+  |
 |           |           |  |  |     |     |           |  |
 +--+  ---+  +--------+  |  |  +-----+  +--+  +-----+  |  |
 |  |     |           |  |  |           |     |     |  |  |
 |  +--+  |  +---  +--+  |  |  |  +-----+  +--+  +--+  |  |
 |     |  |  |     |  |     |  |  |        |  |  |     |  |
 |  ---+--+  +-----+  +-----+--+  +---  +--+  |  +-----+  |
 |                                      |
 +--------------------------------------+------------------

2011/04/14

FTPでログファイルなどをレジュームダウンロードする

NILScriptのFTPユニットを利用すれば、自分の管理するWebサイトにFTPで接続してアクセスログなどのファイルを定期的に自動ダウンロードさせたりすることも可能です。

末尾に追記されていくタイプのログファイルの場合、毎回丸ごとダウンロードするのではなく、取得済みのバイト数だけスキップして未取得の部分だけをダウンロードし、ローカルの取得済みファイルに追記するレジュームダウンロードを行うと、無駄な転送を減らせて効率的です。

NILScriptのFTPユニットでは、下記の例のように、FTPオブジェクトのfile()メソッドで取得したFTPFileオブジェクトのdownload()メソッドを呼び出すとき、第2引数にtrueを指定すれば、第1引数に指定した保存先ファイルが存在した場合にそのファイルにレジュームダウンロードが行われます。

try{
    var ftp=new (require('FTP').FTP)({
        host:'ftp.example.com',
        user:'aaa',
        password:'bbb',
    });
 var f=ftp.file('/public_html/access.log');
 f.download('R:\\access.log',true);
}finally{
    free(ftp);
}

第2引数を省略したりfalseを指定した場合は、既存の保存先ファイルの内容は上書きされます。
画像などの追記型でないファイルをレジュームダウンロードしてしまうと、ファイル破損などの問題が生じるので注意して下さい。

FTPでファイルをアップロードする

Webサーバへのファイルアップロードなどで用いられているFTPによるファイル転送やファイル情報の取得機能を提供する「FTP」ユニットが新たに追加されました。
この機能を利用すれば、NILScript上でコンテンツを生成してアップロードまで行うことが出来ます。

以下は、FTPによる基本的なファイルアップロードの例です。

try{
    var ftp=new (require('FTP').FTP)({
        host:'ftp.example.com',
        user:'aaa',
        password:'bbb',
    });
    var d=ftp.directory('/public_html');
    d.upload('C:\\www\\index.rdf');
}finally{
    free(ftp);
}

FTPクラスのコンストラクタに、接続先サーバのホスト名やログインユーザー名、パスワードなどのメンバを持つオブジェクトを引数として与えると、サーバへの接続とログイン処理が行われ、インスタンスが生成されます。
FTPインスタンスオブジェクトの「directory()」メソッドを、サーバ上の絶対パスを引数にして呼び出すと、そのディレクトリに関する操作を提供するFTPDirectoryオブジェクトが得られます。
このFTPDirectoryオブジェクトの「upload()」メソッドをローカルファイルのパスやFileオブジェクトを引数にして呼び出すことで、ファイルのアップロードが行われます。

FTPDirectoryオブジェクトには、他にもディレクトリ内のファイルやディレクトリを列挙するchildrenメンバなどの機能が用意されています。各機能の説明は、同梱のdoc/FTP.txtを参照して下さい。

2011/04/08

Mailユニットで送受信するメールを暗号化する

Mailユニットに用意されているPOP3クラスを使用すれば、定期的に新着メールをチェックして特定の書式のメールがあったときに本文に記述された命令を実行するというスクリプトを起動しておき、外出先からメールで自宅のPCを操作することも可能です。
しかし、何者かに偽の命令メールで勝手に操作されてしまっては困ります。
命令メールの送信をPC上から行うのであれば、送信時に内容を暗号化するようにすればよいでしょう。

メールを暗号化するには、Cipherユニットを使用し、以下のようにします。
var subject='NILScript:Command';
var addr='test@example.com';
var msg="HELLO";
var key='password';
var opt={cipher:"AES-128-CFB",salt:true,encoding:"utf8"};
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    smtp.send({
        subject:subject,
        from:addr,
        to:addr,
        message: require('Cipher').Cipher.encode(JSON.stringify(
            {time:now().getTime(),text:msg}
        ),key,opt),
    });
}finally{
    free(smtp,cp);
}
Cipherクラスのencode()メソッドを、平文データとパスワード、暗号化オプションを引数にして呼び出すことで、暗号化されたBASE64文字列を取得できます。
これをSMTPオブジェクトのsend()メソッドの引数オブジェクトのmessageメンバに指定して、メール本文として送信します。
なお、BASE64文字列ではなくバイト列へのPointerオブジェクトを返すCipher.encodeToBytes()などのメソッドも用意されています。

受信側では、以下のようにスレッドを生成して定期的にPOP3サーバに接続し、規定の件名のメールがあったときに、本文をCipher.decode()で復号化します。
この例では「Main.notifyIcon.showInfo()」で本文を表示しているだけですが、eval()関数に渡せば、任意のNILScript文を実行させたりもできます。
var subject='NILScript:Command';
var addr='test@example.com';
var msg="HELLO";
var key='password';
var opt={cipher:"AES-128-CFB",salt:true,encoding:"utf8"};
Main.createNotifyIcon();
var last=now().getTime();
Thread.create(function(){
    while(true){
        try{
            var pop3=new (require('Mail').POP3)({
                host:'vm01xp',
                user:'test',
                password:'asdf',
            });
            
            for(let m in pop3.items){
                if(m.subject==subject){
                    var {text,time}=JSON.parse(
                        require('Cipher').Cipher.decode(m.message,key,opt)
                    );
                    if(text && time && (time>last)){
                        Main.notifyIcon.showInfo(text);
                        last=time;
                    }
                    m.remove();
                }
            }
        }catch(e){
            println(e);
        }finally{
            free(pop3);
        }
        sleep(60000);
    }
});

セキュリティを強化するため、単に命令本文を暗号化するのではなく、実際の命令本体と命令の送信日時の2つのメンバからなるオブジェクトのJSON文字列を暗号化して、受信側では前回実行した命令よりも新しい命令でないと実行しないようにしています。
こうしておかないと、メールを傍受した何者かが、後でそのメールを送りつけることで過去に送信された命令を任意のタイミングで再実行できてしまいます。
ハッシュなどによる改竄チェックは行っていないので、出鱈目なBASE64文字列が送りつけられても復号化してJSON文字列の解析まで行われてしまいますが、偶然timeとtextのメンバを持つオブジェクトのJSON文字列として正しくデコード出来てしまう可能性は低いので問題ないでしょう。

POP3でメールを受信する

最近追加されたMailユニットには、メールの受信を行うPOP3クラスが用意されています。
この機能を利用すれば、新着メールをチェックするツールや、メールで受信した命令に従って処理を自動実行するツールなどを実現できます。

POP3クラスの基本的な使用方法は以下のようになります。
try{
    var pop3=new (require('Mail').POP3)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
    });
    for(let m in pop3.items){
        println(m.subject);
    }
}finally{
    free(pop3);
}
SMTPの場合と同じように、new POP3()でインスタンスを生成すると、サーバとの接続やログイン処理が行われ、free()でログアウトと切断が行われます。
POP3オブジェクトは、「count」プロパティでメールボックス内のメールの件数が取得したり、「items」プロパティで各メールの情報取得を行うRemoteMailオブジェクトを列挙できます。
RemoteMailオブジェクトには、ヘッダを受信して件名を返す「subject」や、本文テキストを返す「message」などのプロパティが用意されています。また、「remove()」メソッドで、サーバ上からメールを削除することも可能です。
上記の例では、サーバ上のメールの本文を表示しています。

スクリーンショットをメール送信する

NILScriptのMailユニットのSMTPクラスでは、添付ファイルの送信も可能です。
添付ファイルには、Fileオブジェクトの他、メモリ上のバイト列を指すPointerオブジェクトを指定することも可能です。
画像を扱うImageオブジェクトには、ファイルに書き込むことなくPNGやJPEGなどの画像ファイルのバイト列を生成してPointerオブジェクトで得るtoBytes()というメソッドが用意されているので、以下のようにデスクトップのスクリーンショットなどをファイルに保存することなくメール送信することが可能です。
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    var img=require('Image').Image.capture();
    var buf=img.toBytes('.png');
    smtp.send({
        subject:"ScreenShot",
        from:'test@example.com',
        to:'test@example.com',
        message:"test",
        files:{
            'image/png:ss.png':buf
        },
    });
}finally{
    free(smtp,buf,img);
}
「require('Image').Image.capture()」でデスクトップのスクリーンショットをImageオブジェクトとして取得し、img変数に代入します。
次に、img.toBytes('.png')でPNGファイルのバイト列を生成しています。
これらのオブジェクトは、最後にfree()関数で解放する必要があります。
そして、SMTPオブジェクトのsend()メソッドの引数で、filesメンバに添付したいファイルの情報を与えます。
添付するのがFileオブジェクトの場合は、FileオブジェクトそのものやFileオブジェクトの配列として指定するだけで、元のファイル名で添付できますが、Pointerオブジェクトの場合はファイル名を指定する必要があるので、上記のように「"content-type:filename":data」のような形式のオブジェクトにして指定します。
Content-Typeの部分を省略して「"filename":data」のようにすると、Content-Typeは「application/octet-stream」として添付されますが、メールソフトの添付画像表示機能などが効かなくなる場合があるので、形式が分かるならなるべく指定しておいた方がいいでしょう。

処理完了などのイベントをメールで知らせる

最近のアップデートで、SMTP/POP3サーバに接続してメールの送受信を行う「Mail」ユニットが追加されました。
この機能を使えば、自動処理の完了など何らかのイベントの発生時にメールを送信して知らせたり、外出先からメールで送信した指令に応じて自動処理を行うことなどが可能になります。

SMTPクラスによるメール送信の基本的な手順は以下の通りです。
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    
    smtp.send({
        subject:'テストメール',
        from:'test@example.com',
        to:"test@example.com',
        encoding:'iso-2022-jp',
        message:"テスト本文",
    });
}finally{
    free(smtp);
}

まず、SMTPクラスのコンストラクタにホスト名やユーザー、パスワードなどのオプションを与えて呼び出し、インスタンスオブジェクトを得ます。この時、サーバとの接続が確立され、ログイン処理などが行われます。
POP before SMTPを使用するには、オプションに「pop:true」を指定します。未指定時はSMTPのコマンドによる認証が行われます。

こうして得たSMTPオブジェクトのsend()メソッドに、件名や宛先、本文などを指定するオプションオブジェクトを与えて実行することで、メールの送信が実行されます。
「encoding:'iso-2022-jp'」は、本文や件名を日本語の7ビット文字コードとして一般的なiso-2022-jpでエンコードすることを指定しています。
未指定時はutf-7でエンコードされますが、古い日本製のメールソフトなどではデコードできないかもしれないので、人間に読ませる日本語メールを送信する場合はこのオプションを指定しておいた方がよいでしょう。

メールの送信が終ったら、「free(smtp)」のようにしてサーバからの切断を行います。
この処理は、エラー発生時でも確実に実行されるように、try...finallyのfinally説で実行するようにします。

その他の細かな機能の説明などは、NILScriptに同梱のdoc\Mail.txtを参照して下さい。