playのRequest#remoteAddressで取れるのはIPアドレスではない
ちゃんと細かくドキュメントを読まず http://www.playframework.com/documentation/2.1.0/HTTPServer の書くままに設定してると痛い目にあう
突然カンマ区切りのIPアドレスがやってくる
FugaLogging.write(id, "投稿しました", request.remoteAddress)
こんな感じでログを残していたのだが突然IPアドレスがカンマ区切りで記録されていることが…
2014-02-10 23:36:41,933 XXXXXX A.B.C.D 2014-02-10 23:36:47,432 XXXXXX A.B.C.D 2014-02-10 23:37:47,142 XXXXXX A.B.C.D, E.F.G.H 2014-02-10 23:38:15,870 XXXXXX A.B.C.D 2014-02-10 23:38:20,336 XXXXXX A.B.C.D
うっ…
Request#remoteAddressはX-Forwarded-Forを返す
http://www.playframework.com/documentation/2.1.0/HTTPServerの下に記載されている通り次の場合はRequest#remoteAddressの返り値は接続元のIPアドレスではなくX-Forwarded-Forとなる。これは、nginxやApache等のリバースプロキシ存在下を考慮した仕様。
- 127.0.0.1からのアクセス
- application.conf等にtrustxforwarded=trueが書かれている
ドキュメントの通り次のようにnginxでproxy_set_headerを設定しているとX-Forwarded-Forには "接続元のIPアドレス" もしくは "接続元のIPアドレス, nginxにアクセスした時点で付加されていたX-Forwarded-For" という形になる。
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
アクセス元のIPアドレスだけを取る方法
今回のケースでは迷惑行為防止としてログに記録するのが目的なのでX-Forwarded-Forが全て記録されているのは好都合だったが、本当に「アクセス元のIPアドレス」がほしい場合があるかもしれない。
そういう場合は次のいずれかの方法をとれば良い。
- Request#remoteAddress の結果を分解して一番右のIPアドレスを得る (おすすめ)
- メリット: 確実
- デメリット: ちょっとだけ面倒
- (僕の頭でplayが勝手にX-Forwarded-Forの一番右のIPアドレスを取り出すのかと思い込んでいた…)
- 上記のドキュメント通りのnginxの設定だった場合、X-Real-IPヘッダを取り出し使う
- メリット: 簡単
- デメリット: playには直接アクセスできないことが条件(X-Real-IPヘッダが信頼できる状況)
- パフォーマンスの関係上、クライアントが直接playにリクエストを送りたい場合がある(一部の通信はnginxを通して, 一部の通信は直接playに…とか)と、X-Real-IPヘッダが信頼できるのかどうか判定できない。
- proxy_set_header X-Forwarded-For $remote_addr としてしまう
- メリット: 簡単
- デメリット: X-Forwarded-Forの意味からズレてる
Request#remoteAddress が playへの直接アクセスのときとリバースプロキシ通したときとで意味が変わるのはややこしいなあ…
play frameworkでPUT, DELETE, PATCH等のリクエストを<form>から受け取る
RESTfulなパスにしたいとき、<form>タグでmethod=PUT, DELETE等を送れない問題*1にぶち当たることがある。
全部POSTで送ればいいのだが、どうしてもPUTやDELETEを使いたいRESTful脳な人のための解決方法。
解決方法
Railsはこの願いを叶えるために_methodというパラメータを渡した場合はそのメソッドとしてリクエストを解釈するという仕様がある。(formヘルパーは、勝手に<input type="hidden" name="_method" value="put">というhiddenパラメータをつけてくれる *2 )play frameworkでもこれとほぼ同様の手段で解決できるよう、@helper.formの代わりになるヘルパーと、HTTPリクエストの処理の仕方を変えるようにした。
app/views/helper/formExtended.scala.html
GET or POSTでない場合のみGETパラメータに"_method=METHOD"をつけるヘルパー。@helper.formの代わりに@helper.formExtendedとしてあげれば良い。
@(action: Call, args: (Symbol, String)*)(body: => Html) @availableFormMethod(method: String) = @{ method.toUpperCase match { case "GET" | "POST" | "" => true case _ => false } } @appendMethod(action: Call) = @{ val (url, method) = (action.url, action.method) if(availableFormMethod(method)) { url } else { url + (if(url.contains('?')) "&" else "?") + "_method=" + method } } <form action="@appendMethod(action)" method="@if(availableFormMethod(action.method)) {@action.method} else {POST}" @toHtmlArgs(args.toMap)> @body </form>
(大抵のケースだとCSRF対策でtokenを送っていると思うので、送るロジックをついでにここに書いちゃえばいい)
app/Global.scala
あとはGlobal.scalaでリクエストのメソッドを書き換えてあげれば完成
object Global extends GlobalSettings with Results { .... override def onRouteRequest(request: RequestHeader): Option[Handler] = { val requestRewrited = request.method.toUpperCase match { case "POST" => request.copy(method = request.getQueryString("_method").getOrElse("POST")) case _ => request } super.onRouteRequest(requestRewrited) } }
(追記) @formExtended というヘルパーを作るより、単純にCallを書き換えてくれるやつ作ったほうが良いかも
play framework 2.2でテスト用configを設定する
FakeApplicationで指定のconfigを読み込ませたい場合(例えばconf/test.conf)は次のようにすれば読み込むことができる。
"テストケース" should { "ふがほげ" in { running(new FakeApplication with DevSettings { def devSettings: Map[String, String] = Map( "config.file" -> "conf/test.conf" ) }) { // テストコード } } }
JSONをHTML/CSSで表現する part2
再チャレンジしていいかんじにコピペできる版を作った。
コピペ例
{ "a": "fuga", "b": [ 1, 2, true ] }
実現方法
見えない文字列を挿入することによりインデントを実現…。結局泥臭い方法になっちゃったなあ。
JSONをHTML/CSSで表現する
突然JSONをHTML/CSSでいい感じに表現したくなったのでやってみた。
例
<div class="json"> <div class="js-value js-object"> <div class="element"> <div class="js-key">key1</div> <div class="js-value js-string">value1</div> </div> <div class="element"> <div class="js-key">key2</div> <div class="js-value js-object"> <div class="element"> <div class="js-key">key3</div> <div class="js-value js-number">12345</div> </div> <div class="element"> <div class="js-key">key4</div> <div class="js-value js-array"> <div class="element"> <div class="js-value js-string">A</div> </div> <div class="element"> <div class="js-value js-string">B</div> </div> <div class="element"> <div class="js-value js-number">3</div> </div> <div class="element"> <div class="js-value js-object"> <div class="element"> <div class="js-key">fuga</div> <div class="js-value js-number">12345</div> </div> <div class="element"> <div class="js-key">hoge</div> <div class="js-value js-number">12345</div> </div> </div> </div> <div class="element"> <div class="js-value js-string">4</div> </div> </div> </div> <div class="element"> <div class="js-key">key6</div> <div class="js-value js-string">めっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFG</div> </div> </div> </div> </div> </div>
表示結果
解説
箇条書きで簡潔に
- 1つの要素は<div class="element">で表現している
- キーは<div class="js-key">, 値は<div class="js-value 型の名前">で表現。型の名前は以下の通り
- js-string ... 文字列型
- js-number ... 数値型
- js-boolean ... 真偽型
- js-array ... 配列
- js-object ... オブジェクト
- js-nullを忘れてた…(あとでやる)
ここがすごいの
CSSのafter, before要素を使ってるので「"」や「,」「{」「[」をつけなくてもいい
誰が得するの?
...
UnicornでUnix domain socketを使う場合は絶対パスで指定しなければならない
listen 8080 # TCP listen "tmp/unicorn.sock" # Unix Domain Socketのつもり
と指定するとそんなListenの方法ないよー、と怒られた
I, [2013-06-26T09:22:21.178723 #11457] INFO -- : listening on addr=0.0.0.0:8080 fd=8 F, [2013-06-26T09:22:21.179525 #11457] FATAL -- : error adding listener addr=tmp/unicorn.sock /u/apps/..../gems/unicorn-4.6.2/lib/unicorn/socket_helper.rb:149:in `bind_listen': Don't know how to bind: tmp/unicorn.sock (ArgumentError)
Unicornのソースコード
https://github.com/schneems/unicorn/blob/master/lib/unicorn/socket_helper.rb
def bind_listen(address = '0.0.0.0:8080', opt = {}) return address unless String === address sock = if address[0] == ?/ if File.exist?(address) if File.socket?(address) begin UNIXSocket.new(address).close
1文字目がスラッシュからはじまってるかどうかで判定してるんですね...そうですか...
つまり、絶対パスじゃないと無理
Githubからやってくるhookの内容メモ
Githubのhook
https://help.github.com/articles/post-receive-hooks
タグが作成され、pushされたときは
{ "ref": "refs/tags/testtag", "after": "a011d5d157f769b39b99d2c10333e936eeab6bed", "before": "0000000000000000000000000000000000000000", "created": true, "deleted": false, "forced": true, "base_ref": "refs/heads/master", "compare": "https://github.com/mashijp/gitstudy/compare/testtag", "commits": [ ], "head_commit": { "id": "a011d5d157f769b39b99d2c10333e936eeab6bed", "distinct": true, "message": "delete", "timestamp": "2013-04-25T09:10:23-07:00", "url": "https://github.com/mashijp/gitstudy/commit/a011d5d157f769b39b99d2c10333e936eeab6bed", "author": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "committer": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "added": [ ], "removed": [ "fugahoge" ], "modified": [ ] }, "repository": { "id": 9674385, "name": "gitstudy", "url": "https://github.com/mashijp/gitstudy", "description": "", "watchers": 0, "stargazers": 0, "forks": 0, "fork": false, "size": 108, "owner": { "name": "mashijp", "email": "(ry" }, "private": false, "open_issues": 0, "has_issues": true, "has_downloads": true, "has_wiki": true, "created_at": 1366902142, "pushed_at": 1366906300, "master_branch": "master" }, "pusher": { "name": "mashijp", "email": "(ry" } }
ブランチがきられ、pushされたときは
{ "ref": "refs/heads/develop", "after": "0ce9e6e68576a18c073aaf6fadd5b93aef850811", "before": "0000000000000000000000000000000000000000", "created": true, "deleted": false, "forced": true, "compare": "https://github.com/mashijp/gitstudy/commit/0ce9e6e68576", "commits": [ { "id": "0ce9e6e68576a18c073aaf6fadd5b93aef850811", "distinct": true, "message": "doyaa", "timestamp": "2013-04-25T09:13:55-07:00", "url": "https://github.com/mashijp/gitstudy/commit/0ce9e6e68576a18c073aaf6fadd5b93aef850811", "author": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "committer": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "added": [ "pyo" ], "removed": [ ], "modified": [ ] } ], "head_commit": { "id": "0ce9e6e68576a18c073aaf6fadd5b93aef850811", "distinct": true, "message": "doyaa", "timestamp": "2013-04-25T09:13:55-07:00", "url": "https://github.com/mashijp/gitstudy/commit/0ce9e6e68576a18c073aaf6fadd5b93aef850811", "author": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "committer": { "name": "mashijp", "email": "(ry", "username": "mashijp" }, "added": [ "pyo" ], "removed": [ ], "modified": [ ] }, "repository": { "id": 9674385, "name": "gitstudy", "url": "https://github.com/mashijp/gitstudy", "description": "", "watchers": 0, "stargazers": 0, "forks": 0, "fork": false, "size": 108, "owner": { "name": "mashijp", "email": "(ry" }, "private": false, "open_issues": 0, "has_issues": true, "has_downloads": true, "has_wiki": true, "created_at": 1366902142, "pushed_at": 1366906447, "master_branch": "master" }, "pusher": { "name": "mashijp", "email": "(ry" } }
こんな通知がきたよ。というメモ。
"created": true が来たら新しいタグ or ブランチ作成かな?