x-powered-byを消す

悪意ある攻撃への対策として、Responseのヘッダから'x-powered-by'を削ると良いという。

Expressでは何もしなければ、サーバ実装を示すこのヘッダ値に'Express'と正直に返してしまいます。そうなると悪い人が「そうかNodeでExpress使ってるのね」とすぐわかってしまうので攻撃前の一手間が省かれてしまうという。いや、しかし、Node+Expressは一定以上使われてるメジャーなプラットフォームだと思うので悪意ある人の前では無駄な努力だとは思うけど...とりあえずおまじないとしてやっておく方がより良いということでしょう。

var app = express();
app.disable('x-powered-by');

これだけ。

そういえば、Node5.1.0にアップデートされていました。$sudo n stableではまだ対応されてなかったので$sudo n 5.1.0で上げました。npmは3.5.0です。

PassportによるFacebook認証(第一歩)

var Passport = require('passport');
var Facebook = require('passport-facebook');
// Facebook Strategyの設定を'fb'で登録。省略すると'facebook'で設定される
Passport.use('fb', new Facebook.Strategy({
        clientID: '1513567018936647',
        clientSecret: '2596-xxxxxxxxxsecretxxxxxxx-d126',
        callbackURL: "http://localhost:8080/auth/facebook/callback"
    },
    function(accessToken, refreshToken, profile, done) {
        // 2)Facebook認証のVerifyイベント
        console.log('Passport.verify');
        return done(null, profile);
    }
));
Passport.serializeUser(function(user, done) {
    // 3)セッションへ認証ユーザーを保存
    console.log('Passport.serializeUser');
    done(null, user);
});
Passport.deserializeUser(function(obj, done) {
    console.log('Passport.deserializeUser');
    done(null, obj);
});

var Express = require('express');
var app = Express();
app.use(Passport.initialize());
app.use(Passport.session());
// 1)認証始め
app.get('/auth/facebook', Passport.authenticate('fb'));
// 4)コールバック
app.get('/auth/facebook/callback',
    Passport.authenticate('fb', { failureRedirect: '/index.html'}),
    function(req, res) {
        console.log('success login');
        res.redirect('/success.html');
    }
);
app.use(Express.static('public'));
app.listen(8080);

Passportを利用してFacebookで認証するのに、ハマリどころは満載ですがコード自体はシンプルに書けます。シンプルにそぎ落としてみて最低限必要なことは以上の通りになりました。一見いらなそうなものも削るとエラー出ます。基本構造としては、1)'/auth/facebook'へのルーティングに仕込まれている、passport.authenticateミドルウェアで認証を始めます。引数に'fb'を渡すことで先に設定したFacebookStrategyを利用することを宣言しています。そしてFacebookに移動してそちらのログイン処理を行う最中に2)Strategyが持つVerifyイベントハンドラがコールされてAccess Tokenが手に入ります。その後に3)Strategyに設定するハンドラで認証ユーザーをセッションへ保存します。

ここまでの処理が成功していると、コールバックが戻ってきます。3)こちらも同じpassport.authenticateミドルウェアで、成功失敗の別に応じてリダイレクト。この後に取得したAccess Tokenを利用してFacebookAPIをコツコツ呼び出すことになります。

設定としてpassportのinitializeミドルウェアとsessionミドルウェアルーター登録するのが必須ですし、認証ユーザーのシリアライズがないとエラーを出しました。

f:id:masataka_k:20151117084100p:plain

Facebook側では、コールバックURLを登録する必要があります。localhost:8080をサンプルでは用いてるので、それはそれとしてFacebookアプリ設定ページにそのまま書いておかないと、指定コールバックURLが既知では無いということですぐに認証エラーを出す、いわば正しい動きをします。

オレオレHTTPSサーバを立てる

実際には証明書を買うとしても、開発時にはオレオレ証明書

$ openssl req -nodes -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 90
Generating a 2048 bit RSA private key
...........................+++
.........................+++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
$ ls
cert.pem    key.pem

opensslを使って証明書を作ってますが、パラメータの「-nodes」が必要です。これを付けないとhttpsを上げた時に「error:0906A068:PEM routines:PEM_do_header:bad password」とエラーが出ちゃう。最後の90は有効期間で、伸ばしたければ365とか。途中のopensslとの対話ではパスフレーズを4文字以上で適当に入力する以外、リターンキーで省略OK。

var fs = require('fs');
var https = require('https');
var app = express();
var privateKey  = fs.readFileSync('key.pem');
var certificate = fs.readFileSync('cert.pem');
app.use(express.static('public'));
https.createServer({key: privateKey, cert: certificate}, app).listen(443 function() {
    process.setuid('masataka_k');
});

平民で実行するとデカいポート番号(8443とか)にしなければ「listen EACCES 0.0.0.0:443」というようにエラーで上がらない。ルートで実行。

$ sudo node server.js

ルートで上げっぱだとセキュリティ上ナニなので、process.setuid()をするってNodeクックブックに書いてあった。が、これでいいのかな?psするとこんな状態。最後によくわかってなくておまじない状態。本番あげるまでには識者を見つけねば。

$ ps -ax | grep node
 4020 ttys000    0:00.01 sudo node server.js
 4021 ttys000    0:00.33 node server.js
 4037 ttys001    0:00.00 grep node

Nodeクックブック

Nodeクックブック

SuperAgentとMulterでファイルアップロード

ファイルアップロードの機能を作るのに、二日ハマりました。前提としてReactで作ったFormから投げ、Express4で書いたサーバで受けるというところでの通信部分だけです。まずガチなところとしてサーバを作ります。

var express = require('express');
var multer  = require('multer');
var server = express();
var upload = multer({ dest: "./uploads/" });
server.post("/confirm", upload.single("contact_attachment"), function(req, res) {
    console.log(req.headers);
    console.log(req.params);
    console.log(req.body);
    console.log(req.file);
    res.sendStatus(200);
});
server.use(express.static('public'));
server.listen(process.env.PORT || 8080);

multerはマルチパートのリクエストボディを処理してくれるExpressのMiddlewareで、npmでサクッとインストール。いろんな書き方がありますが、uploadというインスタンスを作っておいてExpressのルーターチェーンに差し込みます。メソッド引数に並べて渡すだけでチェーンできるのは発見だった。違う書き方としては、以下もあります。

server.use("/confirm", upload.single("contact_attachment"));
server.post("/confirm", function(req, res) {
    console.log(req.body);
    res.sendStatus(200);
});

ルーティングの書き方は同じことでもいろんな書き方できるから、ググってる時に混乱多い。みんなマニアックなテクニック使いまくってること多くて慣れるまでルーティングがどうなってるかなかなか掴めないじゃないか!

一方で、ドハマりしたクライアント。迷走の先に気がついたのは、簡単なものとしてプレーンHTMLさえ書けばOKなんでまずそれ作って、後でもっと難しいことをやる時に、それぞれヘッダやパラメータやボディなどに何を書かれているかを見比べることでした。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data" action="/confirm">
        <div><input type="text" name="contact_name"></div>
        <div><input type="file" name="contact_attachment"></div>
        <div><button type="submit">UPLOAD</button></div>
    </form>
</body>
</html>

タグだけで書いたこれが動かないとしょうがない。だから一番初めにサーバがちゃんと動くことを確認すべきだった(私はやらなかったためサーバもクライアントも変更し続けて迷走が長引いた)。multerもたった数行のコードなのに、一発では動くものを書けなかったからねー。

クライアントはReactの中から、初めはjQueryの$.ajax()でやってたけど、どうにも動かなかった。普通にフォームPOSTするだけならすぐだったけど、ファイル添付されたmultipartができなかった。そんな中、ReactではDOMを直に触るの邪道っぽいこともあって通信にしか使わないので、ダメ元で通信専門のライブラリを持ってきました。SuperAgentです。

github.com

まあ、やることは$.ajax()と大差ない。

var React = require("react");
var SuperAgent = require("superagent");
var ContactForm = React.createClass({
    handleSubmit: function(event) {
        event.preventDefault();
        this.setState({
            valid_button: false
        });
  // var formData = new FormData() <==FormDataを使って迷走
        SuperAgent
            .post("/confirm")
            //.type("multipart/form-data") <==これをやるとboundaryが設定されなくなって迷走
            //.set({contact_name: this.state.contact_name}) <==set使って迷走
            .field("contact_name", this.state.contact_name)
            .field("contact_kana", this.state.contact_kana)
            .field("contact_email", this.state.contact_email)
            .field("contact_company", this.state.contact_company)
            .field("contact_message", this.state.contact_message)
            .attach("contact_attachment", this.state.contact_attachment)
            .end(function(err, res) {
                if(res.statusCode == 200) {
                    this.setState({
                        text_guidance: GUIDANCE_3,
                        style_form: {"display": "none"}
                    });
                } else {
                    this.setState({
                        valid_button: true,
                        text_guidance: GUIDANCE_4
                    });
                }
            }.bind(this));
        return false;
    },
   //以下、renderとか色々ごっそり省略
});

頭で"superagent"をrequireしてから、あとはfieldとattachでコツコツ積み上げます。multipartの場合は他のことをしちゃダメだった。より一般的なsetメソッドでパラメータを積んだりすると自動的にcontent-typeがjsonにされちゃう。また、typeで明示的にcontent-typeをmultipart/form-dataに設定すると続いてほしいboundaryのぐちゃぐちゃ文字列が設定されなくなるので死亡。

ライブラリのソース読んで理屈と癖がわかって、動くようになってみるとSuperAgentとMulterはなかなか良い。捗る。

ReactでinputのonChangeが効かなくて焦った話など

Reactでちょこちょこ書いていて、嵌ったところをメモ。ちなみにECMAScript6は、ExpressもReactもあまりメリットない割に、参考資料が少なくてどう書いていいのか悩む時もあるのでやめた。Babelでreact-presetだけ使ってます。

inputのonChangeが効かなくなった

var App = React.createClass({
    // getInitialStateとか、handleChangeとか、省略
    render: function() {
        return (
            <form>
                <input type="text" value={this.state.data} onChange={this.handleChange}/>
            </form>
        );
    }
});
ReactDom.render(<App/>, $("form")[0]);

上記のようなコードだったとします。コンポーネントがformタグを返すのに、ReactDom.renderの第二引数にformタグを入れてた。第二引数のDOM要素の内側をごっそり第一引数のコンポーネントで置き換える動作だったので、出来上がったHTMLはformタグが二重になってる状態です。こうなると、getInitialStateで設定した初期値は何事もなく描画されるけど、onChangeだけ効かないって不思議状態になる。HTMLの方をいじらないで済まそうとせずに、formを囲うdivを加えて修正しました。

大文字のHTMLタグはダメ

var App = React.createClass({
    render: function() {
        return (<P>ホゲホゲ</P>);
    }
});
ReactDom.render(<App/>, $("#app")[0]);

Pが大文字でクラッシュ。小文字に直せばOK。

タグにインラインのStyle指定

facebook.github.io

ドキュメントにちゃんと書いてありました。

Node.js 5.0へ

5.0!ほんの一年ぐらい前は0.10とか0.11とか名乗ってたのになあ。

www.publickey1.jp

運用モードでも全くないので、バシッと最新にしちゃいます。これまではnodejs.orgから落としてきたインストーラを使ったので「n」を入れて、そこからアップデートします。

$ sudo npm install -g n
$ n --latest
$ n latest

サクッと、結果は以下。

install : node-v5.0.0
mkdir : /usr/local/n/versions/node/5.0.0
fetch : https://nodejs.org/dist/v5.0.0/node-v5.0.0-darwin-x64.tar.gz
installed : v5.0.0

続いてnpmもアップデート。

$ sudo npm update -g npm
$ npm -v

結果は「3.3.12」ということです。

おいおい、babel周りがガラッと変わってるぞ

$ sudo npm update -g
$ npm update

何気なくGlobalもLocalも一切合切アップデートしちゃったんですよ。そうしたらもう、Babelifyが動かなくなってちょっとハマりました。ほんの数日前に書いたエントリが嘘八百。

babeljs.io

babel/babelify · GitHub に"> As of Babel 6.0.0 there are no plugins included by default. For babelify to be useful, you must also include some presets and/or plugins."って書かれてる通りBabelが変わって、Babelifyも当然変わる。プラグインのインストールとTranspileコマンドの書き換えを行います。

$ npm install --save-dev babel-preset-es2015 babel-preset-react
$ browserify App.jsx -o public/js/bundle.js -t [ babelify --presets [ es2015 react ] ]

-tスイッチの引数はスペースもちゃんと入れないと、[babelifyなんて知らん!と怒られます。パラメータのトークナイズがちょっと足りてないみたい。これで10/30に書いたのと同じことが再び出来るようになってます。babel-nodeもダメなんで、そちらはpackage.jsonの中にBabel設定を書きました。

// package.jsonの一部
{
  "devDependencies": {
    "babelify": "^7.2.0",
    "babel-preset-es2015": "^6.0.15",
    "babel-preset-react": "^6.0.15"
  },
  "babel": {
    "presets": [ "es2015" ]
  },
  "scripts": {
    "start_es6": "babel-node server.es6",
    "build_server": "babel server.es6 -o server.js",
    "bundle_client": "browserify App.jsx -o public/js/bundle.js -t [ babelify --presets [ es2015 react ] ]"
  }
}

WebStorm 11も製品版リリースしてた

リリースラッシュです。WebStrom も11.0になってました。EA版じゃなくなったし、そろそろ課金すっかな。

今はbabel-nodeで動かす

ECMAScript6で、node.js/express.jsのサーバ側を書く段になるとes6的メリットを享受できていません。Reactの方はclassを用いて書くといい感じなんですけどね。

// server.es6
import express from 'express';
const server = express();
server.get('/', async (req, res, next) => {
    res.status(200).send("Hello World!");
});
server.listen(8080);

$ node server.es6

で、こんなHello Worldでもそのままでは動かない。node.jsの今のバージョンであるv4.1.1だとModulesがデフォで対応していないのでimportでランタイムに文法エラーが出てきちゃう。そのうち対応するでしょうけど今は残念ながら--harmonyオプションでもダメなので、babel-nodeの出番です。babelをインストールしたら、/usr/local/binにいます。

$ babel-node server.es6

はい。動きました。しかし今使ってるv11プレビュー版のWebStormであるバージョンWS-143.380.11のデバッグ機構ではbabel-nodeを設定して一度目の実行はちゃんと動くけど、二度目以降はツールの中でポートを掴んでる何かにアプリケーションをちゃんとアタッチできないのか、WebStormが出す独自の404画面になっちゃう。これは多分WebStormのバグだな。babel-nodeでの実行をやめ、Transpileしてからnodeでの実行に戻しても同じ現象だし。コンソールでの起動ならば何の問題もない。

f:id:masataka_k:20151031095730p:plain

ちなみにこれはWebStormのデバッグ機構にbabel-nodeを設定するところ。一度目はちゃんとデバッグできるんだけどなー。