2014/10

Jenkins(+Nginx) - Hipchat - Hubot 環境構築メモ

タイトルの通りの環境を作ってみたのでメモを残しておきます。

(昨日やったことを思い出しながら書いているので参考までに)

この環境でできることは

1.HipchatからHubotに命令する。
2.HubotがJenkinsのJOB実行APIを叩く
3.JenkinsがJOBを実行する
4.JenkinsがHipchatに結果を通知する

ができるようになりました。

0.目次

  • 1.Hipchatの設定
  • 2.EC2インスタンスの準備
  • 3.Jenkinsのインストール
  • 4.Jenkinsの初期設定とプラグインのインストール
  • 5.Hubotのインストール
  • 6.HipchatからHubot経由でJenkinsのJOBを実行する

1.Hipchatの設定

参考にしたのはこちらの記事
-> Jenkins+HipChat+Hubotをチーム開発に導入してお手軽CI

やることは以下の2つです。

  • Hubot用のJenkinsユーザー作成をGroupAdminから作成します。
  • 通知用のAPIトークンを生成しておく。

あとでこのAPIや、作成したユーザーのJabber IDが必要になります。
ユーザー登録時にEメール設定が必要で、gmailの拡張エイリアスを使って <メールアドレス>+jenkins@gmail.comとしておいた。
作成したユーザーをhubotを呼びたいルームに招待しておく。

2.EC2インスタンスの準備

参考にしたのはこの記事
-> EC2にJenkinsによるCI環境を作成する

Amazon Linux AMI (64bit)をM3.Mediumで作成。

必要そうなモジュールのインストールと初期起動設定

インスタンスができたらEC2-userでログインして、yumで必要そうなモジュール追加
実際の順序は異なったり、平行してRuby環境も作ってたのでJenkinsには不要なものもあるかもしれないけど
Node.jsのビルドや、Hubotの実行やらなんやらで以下のものがhisoryコマンドの結果にありました。

sudo yum update
sudo yum -y install git
sudo yum -y install build-essential
sudo yum -y install make glibc-devel gcc
sudo yum -y install openssl
sudo yum -y install openssl-devel
sudo yum -y install libicu-devel
sudo yum -y install nginx
sudo yum -y install httpd-tools
sudo yum --enablerepo=epel install redis
 
sudo /etc/init.d/redis start
sudo /sbin/chkconfig --levels 235 redis on
 
sudo /etc/init.d/nginx start
sudo  /sbin/chkconfig --levels 235 nginx on

3.Jenkinsのインストール

まずはJavaのインストール。

今回はJava8にしてみた。Oracleからダウンロードするには以下のコマンドでOK

wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u25-b17/jdk-8u25-linux-x64.rpm -q -O /tmp/jdk-8u25-linux-x64.rpm
sudo yum localinstall /tmp/jdk-8u25-linux-x64.rpm
sudo update-alternatives --install /usr/bin/java java /usr/java/default/bin/java 200000
ll /usr/java/jdk1.8.0_25/bin/
sudo update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk1.8.0_25/bin/java" 1

Jenkinsはyumでインストール

sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
sudo yum install jenkins
sudo service jenkins start
sudo  /sbin/chkconfig --levels 235 jenkins on

これでとりあえずJenkinsは動く。

4.Jenkinsの初期設定とプラグインのインストール

初期ユーザー設定

あとで少し変更するがEC2にJenkinsによるCI環境を作成するにそって、アドミン権限的なユーザーを作成しておく。

プラグインのインストール

Manage Jenkins > Manage Plugins から必要そうなのと使いそうなプラグインを選んでインストールして再起動
いま確認したら以下のプラグインが有効になってた。

  • Ant Plugin 1.2
  • Build With Parameters 1.1
  • Config File Provider Plugin 2.7.5
  • Confluence Publisher 1.8
  • Credentials Plugin 1.18
  • External Monitor Job Type Plugin 1.2
  • GIT client plugin 1.11.0
  • GIT plugin 2.2.7
  • GitBucket Plugin 0.5.1
  • Gradle plugin 1.24
  • HipChat Plugin 0.1.8
  • Ivy Plugin 1.24
  • Javadoc Plugin 1.2
  • JDK Parameter Plugin 1.0
  • JUnit Plugin 1.1
  • Mailer Plugin 1.11
  • Matrix Authorization Strategy Plugin 1.2
  • Matrix Project Plugin 1.4
  • Maven Integration plugin 2.7
  • NodeJS Plugin 0.2.1
  • OWASP Markup Formatter Plugin 1.2
  • PAM Authentication plugin 1.2
  • Rake plugin 1.8.0
  • rbenv plugin 0.0.16
  • ruby-runtime 0.12
  • RubyMetrics plugin for Jenkins 1.6.2
  • sbt plugin 1.4
  • Scala JUnit Name Decoder 1.0
  • SCM API Plugin 0.2
  • SSH Credentials Plugin 1.10
  • SSH Slaves plugin 1.8
  • TestFairy 1.0
  • Testflight Plugin 1.3.9
  • Token Macro Plugin 1.10
  • Translation Assistance plugin 1.11
  • Windows Slaves Plugin 1.0
  • Xcode integration 1.4.2

プラグインの設定

インストールしたプラグインの設定をManage Jenkins > Configure System から行う。
ビルドツール系のパスの設定などを行う。
Hipchatプラグインの設定はこちらの記事のキャプチャにあるとおり
-> Jenkinsのビルド結果をHipChatで通知する方法

適当なビルドJOBを作成して、
[HipChat Notifications]のところでルーム名を指定すればビルド開始や完了時にHipchatに通知が届くようになりました。

5.Hubotのインストール

次はHubotを使ってHipchatからJenkinsへのリクエストを行えるようにします。
参考にしたのは公式doc
-> Getting Started With Hubot

Node.jsはnodebrewを使って準備

wget -O git.io/nodebrew
perl nodebrew setup
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH"' >> ~/.bash_profile
source ~/.bashrc
nodebrew install v0.10.32
nodebrew alias default v0.10.32
nodebrew use default
npm install -g hubot coffee-script

Hubotのワークディレクトリの作成

次のReadMEを参考に
-> hipchat/hubot-hipchat

ただし、この記事はHerokuで動かす前提で書かれているのでProcfileとかは不要です。

hubot --create jenkins
npm install
npm install --save hubot-hipchat
git add .
git commit -m "Initial commit"

hubotに渡す変数は全て環境変数経由のようなので、bin/hubotを修正します。
あと、PORT番号をJenkinsとぶつからないように変更します。

#!/bin/sh 
 
set -e
 
npm install
export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH"
 
export HUBOT_HIPCHAT_JID="<作成したHipchatユーザーのJobberID>"
export HUBOT_HIPCHAT_PASSWORD="<作成したHipchatユーザーのログインパスワード>"
 
# ここはあとで設定する 
# export HUBOT_JENKINS_URL="<JenkinsのURL>" 
# export HUBOT_JENKINS_AUTH="<Jenkinsのベーシック認証ユーザ>:<Jenkinsのベーシック認証パスワード>" 
 
 
export PORT="8081"
 
exec node_modules/.bin/hubot "$@" --adapter hipchat

この状態でHubotを起動(start-stopデーモンがうまく動かなかったのでとりあえずnohupでバックグランド起動)すると、

nohup bin/hubot &

以下のようなログがでていればOK

[Mon Oct 20 2014 09:49:46 GMT+0000 (UTC)] INFO Connected to hipchat.com as @<Hubotユーザー名>
[Mon Oct 20 2014 09:49:46 GMT+0000 (UTC)] INFO Data for hubot brain retrieved from Redis
[Mon Oct 20 2014 09:49:47 GMT+0000 (UTC)] INFO Joining <ルームID>@conf.hipchat.com

hipchatのhubotユーザーがルームに入ってきます。

@<Hubotユーザー名> hi
@<Hubotユーザー名> translate me こんにちは

みたいなhubotのデフォルトのスクリプトに反応してくれるようになります。

6.HipchatからHubot経由でJenkinsのJOBを実行する

jenkins用hubotコマンドの入手

さて、最後はHubotに命令することでJenkinsのJOBが実行できるようにします。
Jenkinsを起動するスクリプトはこれ
github/hubot-scriptsにはほかにもたくさん便利コマンドがありそうなので、いろいろ試してみたい。

使いたいコマンドをscriptsディレクトリに保存します。

wget -O https://raw.githubusercontent.com/github/hubot-scripts/master/src/scripts/jenkins.coffee

jenkins.coffeeのヘッダーコメントにあるように、このコマンドに必要な引数を
bin/hubot内で環境変数にセットします。

export HUBOT_JENKINS_URL="<JenkinsのURL>"
export HUBOT_JENKINS_AUTH="<Jenkinsのベーシック認証ユーザ>:<Jenkinsのベーシック認証パスワード>"

一度hubotを止めて、bin/hubotで再度起動します。

Jenkinsのベーシック認証設定

Jenkinsに対してリモートからHTTPリクエストを投げる場合は、Jenkinsの認証はベーシック認証で行う必要があるようです。
ググるとJenkins単体でベーシック認証をするのではなく、ApacheやNginxで行うのが一般的なようでした。

参考にしたのはこの記事
-> JenkinsサーバのSSL対応とBasic認証

今回はSSL化は行いませんでしたが、SSL設定はJenkinsはあまり関係なく、keyとcertファイルを用意してNginxだけでやれば良さそうです。
Nginxでリバースプロキシします。
今回EC2のセキュリティグループで8080しかポートを開けていなかったのでNginxで8080をListenしてJenkinsは別のポートを使うように
/etc/sysconfig/jenkinsを修正する必要がありました。

sudo su - root
cd /etc/nginx/conf.d
cp virtual.conf jenkins.conf
vi jenkins.conf
htpasswd -c /etc/nginx/.htpasswd jenkins_admin
cp -rp /etc/sysconfig/jenkins /etc/sysconfig/jenkins.bak
vi /etc/sysconfig/jenkins
service nginx stop
service jenkins restart
service nginx start

/etc/sysconfig/jenkinsでPORTを8090に変更して、
jenkins.confは以下の通りにしました。

server {
    listen       8080;
    server_name  jenkins;
 
    location / {
      proxy_pass http://127.0.0.1:8090/;
      auth_basic "jenkins server";
      auth_basic_user_file /etc/nginx/.htpasswd;
      proxy_set_header Authorization "";
    }
}

これでHipchatからhubotに対してjenkins build ジョブ名と語りかけると指定したジョブが実行されます。
ビルド結果はJenkinsから直接Hipchatに対して通知されます。


今後は、JIRA,bitbucket連携を進めていきたいです。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: CSRF対策

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

7. リクエストとスコープ:

今回はリクエストに直接アクセスする方法を取り上げたいと思います。

公式ドキュメントは以下のページが参考になります。

7-4. CSRF対策

7-3-1. XitrumのCSRF対策の仕組み

[Xitrum.Action](https://github.com/xitrum-framework/xitrum/blob/561d214d2af847c7ad4ab7bf1b3b1b9835f0f9a0/src/main/scala/xitrum/Action.scala#L85-L90)は、
リクエストメソッドが、POSTPUTPATCHDELETEの場合、デフォルトでCSRF対策トークンチェックを行います。

if ((request.getMethod == HttpMethod.POST ||
     request.getMethod == HttpMethod.PUT ||
     request.getMethod == HttpMethod.PATCH ||
     request.getMethod == HttpMethod.DELETE) &&
    !isInstanceOf[SkipCsrfCheck] &&
    !Csrf.isValidToken(this)) throw new InvalidAntiCsrfToken

では、このCSRFトークンチェックの内容はというと、
[Csrf.isValidToken](https://github.com/xitrum-framework/xitrum/blob/46f330ac6c360688417406dcc1539ebb8704b721/src/main/scala/xitrum/scope/session/Csrf.scala#L18-L29)の処理は次の用に、
リクエストヘッダーあるいはリクエストボディに指定されたキーでセットされたトークンと、セッション内のトークンが一致しているかを判定しています。
トークン自体はランダムな文字列ですが、シリアライズされた形式でセッションに保存されています。

def isValidToken(action: Action): Boolean = {
  // The token must be in the request body for more security
  val bodyTextParams = action.handlerEnv.bodyTextParams
  val headers        = action.handlerEnv.request.headers
  val tokenInRequest = Option(headers.get(X_CSRF_HEADER)).getOrElse(action.param(TOKEN, bodyTextParams))
 
  // Cleaner for application developers when seeing access log
  bodyTextParams.remove(TOKEN)
 
  val tokenInSession = action.antiCsrfToken
  tokenInRequest == tokenInSession
}

7-3-2. AntiCSRFトークンのセット

クライアントはリクエストヘッダーまたはリクエストボディにセッション内のトークンと一致する値を含める必要があります。

HTMLメタタグとXitrum.jsを用いてリクエストヘッダーにトークンを含める方法

レイアウト内で、{antiCsrfMeta}を使用することでメタタグが生成されます。
Scaffoldプロジェクトにあるように通常デフォルトレイアウトで用います。

!!! 5
html
  head
    != antiCsrfMeta
    != xitrumCss
    meta(content="text/html; charset=utf-8" http-equiv="content-type")

こう書くことで、以下のようにHTMLに展開されます。

<!DOCTYPE html>
<html>
  <head>
    <meta name="csrf-token" content="9f16c39b-3456-4020-9695-484d101908ca"/>
    <link href="/webjars/xitrum/3.18/xitrum.css?mhIAFrxv3tBMQXtHcoYT7w" type="text/css" rel="stylesheet" media="all"/>
    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>

Xitrum.jsを使用した場合AJAXリクエスト送信時に、このメタタグの値をリクエストヘッダーの値に自動的に含めてくれます。
Xitrum.jsのインポートの仕方は以下のとおりです。

!= jsDefaults
 
または、
 
script(type="text/javascript" src={url[xitrum.js]})

リクエストボディにトークンを含める方法

form(method="post" action={url[SiteIndex]})
  != antiCsrfInput
 
#または
 
form(method="post" action={url[SiteIndex]})
  input(type="hidden" name="csrf-token" value={antiCsrfToken})

いずれも以下のように展開されます。

<form method="post" action="/">
  <input type="hidden" name="csrf-token" value="9f16c39b-3456-4020-9695-484d101908ca"/>
</form>

7-3-3. CSRF対策をスキップする

debug時やcurlクライントなどCSRF対策を省略したい場合があります。
ActionとHTTPメソッドアノテーションの回で確認したように、
SkipCsrfCheckを継承したActionを使用することで前述の!isInstanceOf[SkipCsrfCheck]という条件にあてはまらなくなるなるので、
CSRF対策チェックは実行されません。


次回は、CSRFトークンも保存されているセッションおよびクッキーについて掘り下げたいと思います。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: FullHttpRequest

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

7. リクエストとスコープ:

今回はリクエストに直接アクセスする方法を取り上げたいと思います。

公式ドキュメントは以下のページが参考になります。

7-3. FullHttpRequest

7-3-1. リクエストに直接アクセスする

作成したActionクラスのexecuteメソッドが呼ばれた時、生のリクエストFullHttpRequest
requestという変数に、リクエストボディはrequestContentStringという変数で文字列として取得することができます。

RequestRawExample.scala
@GET("/requestraw")
class RequestRawIndex extends Action {
  def execute() {
    val whaleRequest = request
    log.debug("Request:" + whaleRequest.toString)
    respondText(
s"""
Request:${whaleRequest.toString}
"""
    )
  }
}
 
@GET("/requestbody")
class RequestBodyIndex extends Action {
  def execute() {
    val body = requestContentString
    log.debug("body:" + requestContentString)
    respondText(
s"""
body:${requestContentString}
"""
    )
  }
}

これらのURLにアクセスすると以下のような結果となります。

curl -X GET http://localhost:8000/requestraw\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
Request:DefaultFullHttpRequest(decodeResult: success)
GET /requestraw?query=Hello HTTP/1.1
User-Agent: curl/7.32.0
Host: localhost:8000
Accept: */*
X-MyHeader: World
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
 
 
curl -X GET http://localhost:8000/requestbody\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
body:message=xxx

7-3-2. リクエストヘッダーにアクセスする

HTTP Headerにアクセスするにはparamparamoは使用することができません。
直接リクエストから取得する必要があります。

RequestHeaderExample.scala
@GET("/requestheader")
class RequestHeaderIndex extends Action {
  def execute() {
    val headers = request.headers
    log.debug("Header:" + headers.toString)
 
    val entries = headers.entries
    log.debug("Entries:" + entries.toString)
 
    val myHeader = headers.get("X-MyHeader")
    log.debug("X-MyHeader:" + myHeader.toString)
 
 
    respondText(
s"""
Header:${headers.toString}
Entries:${entries.toString}
X-MyHeader:${myHeader.toString}
"""
    )
  }
}

結果は以下のようになります。

curl -X GET http://localhost:8000/requestheader\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
Header:io.netty.handler.codec.http.DefaultHttpHeaders@2f1bc1e1
Entries:[User-Agent=curl/7.32.0, Host=localhost:8000, Accept=*/*, X-MyHeader=World, Content-Length=11, Content-Type=application/x-www-form-urlencoded]
myHeader:World

ヘッダーパラメーターがparamparamoの対象外である理由としては、
通常のActionからはヘッダーパラメータにアクセスする機会が少ないからであるといえます。
ヘッダーパラメータをアプリケーションが利用するユースケースとして、認証処理などが考えられます。
たとえば、X-APP_TOKENなどといった形式でアプリケーションのトークンを全てのリクエストのヘッダーに含めて認証を行うアプリの場合、
認証処理は全てのActionに共通であるため、通常はフィルターが使用されます。
リクエストへの直接のアクセスはフィルターに限定して、通常のActionはそのActionのためのリクエスト(クエリー、パス、ボディ)のみを参照する設計がよいと考えられます。
フィルターを使ったサンプルはまた次の機会にやってみます。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: リクエストパラメーター

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

7. リクエストとスコープ:

今回はリクエストパラメーターの扱い方を取り上げたいと思います。

公式ドキュメントは以下のページが参考になります。

7-2. リクエストパラメーター

7-2-1. リクエストパラメータにアクセスする

XitrumのActionで扱えるリクエストパラメータは以下の通りです。

リクエストパラメーターには2種類あります:

テキストパラメータ
ファイルアップロードパラメーター(バイナリー)
テキストパラメーターは scala.collection.mutable.Map[String, List[String]] の型をとる3種類があります:

queryParams: URL内の?以降で指定されたパラメーター 例: http://example.com/blah?x=1&y=2
bodyTextParams: POSTリクエストのbodyで指定されたパラメーター
pathParams: URL内に含まれるパラメーター 例: GET("articles/:id/:title")
これらのパラメーターは上記の順番で、 textParams としてマージされます。 (後からマージされるパラメーターは上書きとなります。)

bodyFileParams は scala.collection.mutable.Map[String, List[ FileUpload ]] の型をとります。

今回は、テキストパラメータについてのみ試します。
テキストパラメータは、paramおよび、paramoメソッドで取得することができます。

paramで指定したキーが存在しない場合、Xitrumが自動で400 Bad Requestをレスポンスします。
paramoは指定したキーをOption型として取得し、存在しない場合はNoneとなります

また、bodyTextParamsについては、POST、PUT,PATCHメソッドの場合のみ取得されます。

RequestParamExample.scala
@GET("/requestparam/:path1/:path2")
@POST("/requestparam/:path1/:path2")
class RequestParamIndex extends Action with SkipCsrfCheck {
  def execute() {
 
    // From path param
    val path1  = param("path1")
    val path2  = param("path2")
    log.debug("path1"+path1)
    log.debug("path2"+path2)
 
    // From query param
    val query1  = param("query1")
    val query2  = param("query2")
    log.debug("query1"+query1)
    log.debug("query2"+query2)
 
    // From body param when HTTP method is POST, PUT, PATCH
    val body1   = param("body1")
    val body2   = param("body2")
    log.debug("body1"+body1)
    log.debug("body2"+body2)
 
    respondText(
s"""
textParams:${textParams}
queryParams:${queryParams}
bodyTextParams:${bodyTextParams}
pathParams:${pathParams}
path1:${path1}
path2:${path2}
query1:${query1}
query2:${query2}
"""
    )
  }
}
 
@GET("/requestparamoption/:path1/:path2")
@POST("/requestparamoption/:path1/:path2")
class RequestParamOptionIndex extends Action with SkipCsrfCheck {
  def execute() {
 
    // From path param
    val path1  = paramo("path1")
    val path2  = paramo("path2")
    log.debug("path1"+path1)
    log.debug("path2"+path2)
 
    // From query param
    val query1  = paramo("query1")
    val query2  = paramo("query2")
    log.debug("query1"+query1)
    log.debug("query2"+query2)
 
    // From query param
    val body1   = paramo("body1")
    val body2   = paramo("body2")
    log.debug("body1"+body1)
    log.debug("body2"+body2)
 
    respondText(
s"""
textParams:${textParams}
queryParams:${queryParams}
bodyTextParams:${bodyTextParams}
pathParams:${pathParams}
path1:${path1}
path2:${path2}
query1:${query1}
query2:${query2}
body1:${body1}
body2:${body2}
"""
    )
  }
}

それぞれのURLにGETとPOSTでアクセスすると以下の様な結果となります。

curl -X GET http://localhost:8000/requestparam/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
Missing param: body1
 
 
curl -X POST http://localhost:8000/requestparam/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), body2 -> List(b2), query2 -> List(q2), body1 -> List(b1), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map(body2 -> List(b2), body1 -> List(b1))
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:x
path2:y
query1:q1
query2:q2
 
 
curl -X GET http://localhost:8000/requestparamoption/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), query2 -> List(q2), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map()
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:Some(x)
path2:Some(y)
query1:Some(q1)
query2:Some(q2)
body1:None
body2:None
 
 
curl -X POST http://localhost:8000/requestparamoption/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), body2 -> List(b2), query2 -> List(q2), body1 -> List(b1), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map(body2 -> List(b2), body1 -> List(b1))
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:Some(x)
path2:Some(y)
query1:Some(q1)
query2:Some(q2)
body1:Some(b1)
body2:Some(b2)

7-2-2 型を指定してパラメータを取得する

リクエストパラメータのデフォルトはString型です。
paramおよび、paramoに型を指定することで任意の型でパラメータを取得することができます。
デフォルトのコンバータは以下の用に定義されており、独自の型に変換する場合は、Action内でconvertTextParamをオーバーライドします。
なおconvertTextParamをオーバーライドする際に、Scala 2.10ではTypeの代わりにManifestを使用することに注意してください。

RequestParamConvertExample.scala
@GET("/requestparamconvert/:one")
class RequestConvertIndex extends Action {
  def execute() {
 
      val one_as_String   = param[String]("one")
      val one_as_Char     = param[Char]("one")
      val one_as_Byte     = param[Byte]("one")
      val one_as_Short    = param[Short]("one")
      val one_as_Int      = param[Int]("one")
      val one_as_Long     = param[Long]("one")
      val one_as_Float    = param[Float]("one")
      val one_as_Double   = param[Double]("one")
      val one_as_Implicit = param("one")
 
 
    respondText(
s"""
one_as_String =>   Class:${one_as_String.getClass.toString}, Value:${one_as_String}
one_as_Char =>     Class:${one_as_Char.getClass.toString},   Value:${one_as_Char}
one_as_Byte =>     Class:${one_as_Byte.getClass.toString},   Value:${one_as_Byte}
one_as_Short =>    Class:${one_as_Short.getClass.toString},  Value:${one_as_Short}
one_as_Int =>      Class:${one_as_Int.getClass.toString},    Value:${one_as_Int}
one_as_Long =>     Class:${one_as_Long.getClass.toString},   Value:${one_as_Long}
one_as_Float =>    Class:${one_as_Float.getClass.toString},  Value:${one_as_Float}
one_as_Double =>   Class:${one_as_Double.getClass.toString}, Value:${one_as_Double}
one_as_Implicit => Class:${one_as_Implicit.getClass.toString}, Value:${one_as_Implicit}
"""
    )
  }
}
 
case class MyClass(value:String)
 
@GET("/requestparamconvertcustome/:one")
class RequestConvertCustomeIndex extends Action {
  override  def convertTextParam[T: TypeTag](value: String): T = {
    val t = typeOf[T]
    val any: Any =
           if (t <:< typeOf[String])  value
      else if (t <:< typeOf[MyClass]) MyClass(value)
      else if (t <:< typeOf[Int])    value.toInt
      else throw new Exception("convertTextParam cannot covert " + value + " to " + t)
    any.asInstanceOf[T]
  }
 
  def execute() {
 
      val one_as_MyClass  = param[MyClass]("one")
      val one_as_String   = param[String]("one")
      val one_as_Int      = param[Int]("one")
 
 
    respondText(
s"""
one_as_String =>   Class:${one_as_String.getClass.toString},  Value:${one_as_String}
one_as_MyClass=>   Class:${one_as_MyClass.getClass.toString}, Value:${one_as_MyClass}
one_as_Int =>      Class:${one_as_Int.getClass.toString},     Value:${one_as_Int}
"""
    )
  }
}

実行結果は以下のようになります。

oshidatakeharu@oshida [~] curl -X GET http://localhost:8000/requestparamconvert/1
 
one_as_String =>   Class:class java.lang.String, Value:1
one_as_Char =>     Class:char,   Value:1
one_as_Byte =>     Class:byte,   Value:1
one_as_Short =>    Class:short,  Value:1
one_as_Int =>      Class:int,    Value:1
one_as_Long =>     Class:long,   Value:1
one_as_Float =>    Class:float,  Value:1.0
one_as_Double =>   Class:double, Value:1.0
one_as_Implicit => Class:class java.lang.String, Value:1
 
 
oshidatakeharu@oshida [~] curl -X GET http://localhost:8000/requestparamconvertcustome/1
 
one_as_String =>   Class:class java.lang.String,  Value:1
one_as_MyClass=>   Class:class quickstart.action.MyClass, Value:MyClass(1)
one_as_Int =>      Class:int,     Value:1

7-2-3. リクエストボディをJSONとして扱う

Restful APIサーバなど、クライアントとのインターフェイスをJSONで定義したアプリケーションの場合など、
JSONをそのままMapとして扱えると便利です。Xitrumでは以下の方法で実現することができます。

RequestJSONExample.scala
@GET("/requestbodyjson")
class RequestBodyJsonIndex extends Action {
  def execute() {
    val bodyJson = requestContentJson[Map[String, Any]]
    log.debug("body as Json:" + bodyJson)
 
    bodyJson match {
      case Some(v) => log.debug("Successfully parsed")
      case None =>    log.debug("Failed to parse")
    }
    respondText(
s"""
body as Json:${bodyJson}
"""
    )
  }
}

requestContentJsonはパースに失敗した場合はNoneを返します。

curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "{\"message\":\"xxx\",\"code\":1,\"list\":[1,2,3],\"bool\":true}"
 
body as Json:Some(Map(message -> xxx, code -> 1, list -> List(1, 2, 3), bool -> true))
 
 
curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "{\"json\":{\"nest\":1}}"
 
body as Json:Some(Map(json -> Map(nest -> 1)))
 
 
curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "invalidjson"
 
body as Json:None

次回は生のリクエストにアクセスする方法をやります。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: チャネルパイプライン

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

7. リクエストとスコープ:

今回からはリクエストおよびスコープについて勉強します。
クライアントからのリクエストがどのように処理されるか、リクエストパラメーターの扱い方などを取り上げたいと思います。

公式ドキュメントは以下のページが参考になります。

7-1. チャネルパイプラインとActionのライフサイクル

アプリケーションのロジックでリクエストを処理するうえで、Xitrumの処理の流れの概要を掴んでおきたいと思います。
Xitrumアプリケーションを起動時に

xitrum.Server.start()

と呼び出す必要があります。xitrum.Serverは、
NettyのChannelPipelineを構築し、Nettyサーバを起動しています。

Xitrumがデフォルトで構築するチャネルパイプラインは、
次のようなイメージです

                                        +~~~~~~~~~~~~~~~~~~~~~~+
                                ------->|         Action       |
                                |       +~~~~~~~~~~~~~~~~~~~~~~+
                                |                   |
+-------------------------------|-------------------+---------------+
| ChannelPipeline               |                   |               |
|                               |                   |               |
|    +---------------------+    |                   |               |
|    |  BadClientSilencer  |    |                   |               |
|    +----------+----------+    |                   |               |
|              /|\              |                   |               |
|               |          _____|                  \|/     OutBound |
|    +--------------------/+            +-----------+----------+    |
|    |      Dispatcher     |            |     XSendResource    |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +---------------------+            +-----------+----------+    |
|    |   MethodOverrider   |            |       XSendFile      |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |      UriParser      |            |   FixiOS6SafariPOST  |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  .               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |    WebJarsServer    |            |    OPTIONSResponse   |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |   PublicFileServer  |            |        SetCORS       |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |    BaseUrlRemover   |            |     Env2Response     |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |     Request2Env     |            | ChunkedWriteHandler  |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    | HttpRequestDecoder  |            | HttpResponseEncoder  |    |
|    +----------+----------+            +-----------+----------+    |
| InBound      /|\                                  |               |
+---------------+-----------------------------------+---------------+
                |                                  \|/
+---------------+-----------------------------------+---------------+
|               |                                   |               |
|       [ Socket.read() ]                    [ Socket.write() ]     |
|                                                                   |
|  Netty Internal I/O Threads (Transport Implementation)            |
+-------------------------------------------------------------------+

クライアントからのリクエストに対して、NettyのI/Oスレッド上でそれぞれパイプラインが生成・実行されます。
リクエストはInboundハンドラーを経て、DispacherにてルーティングにマッチしたActionへディスパッチされます。
このパイプラインを経ることで、Actionが実行される段階では生のリクエストはアプリケーション開発者にとって利用しやすい形になっています。
ActionはrespondViewなどを実行した段階で処理を終え、処理の流れはOutBoundへと進みます。

クライアントからのリクエスト毎に割り当てられるこのスレッド上でActionクラスのインスタンスは生成されます。
一般にアプリケーション開発者が意識すべきActionのライフサイクルは
Actionのexecuteメソッド内に限られており、
アノテーションで指定されたルーティングにマッチするリクエストを受け付けた時に、
Actionインスタンスは生成され、executeメソッドが実行されます。
executeメソッドの最後にrespondXXXを実行することoutBoundハンドラーへの電文を作成しActionは役目を終えます。

WebSocketおよびSockJSクライアントのように、接続を持続するリクエストに対しては、
Action以降の不要なハンドラーが削除されます

なお、XitrumにはActionの実行をFutureやAkkaのスレッドで実行する仕組みも用意されています。
その話はまた今度。

クライアントからのリクエストがActionに届く流れがは大体こんな感じなので、
次回は実際にHTTPリクエストをAction内で扱うための便利機能について使ってみたいと思います。

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: ポストバック、リダイレクト、フォーワード

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

6. レスポンスとビュー:

今回は、Actionによるレスポンスのパターンとして、
ポストバック、リダイレクト、フォーワードを取り上げます。

公式ドキュメントは以下のページが参考になります。

6-6-1. ポストバック

Xitrumのポストバックは、Nitrogenという、Erlangライブラリを参考に実装されています。
参考:Nitrogenのポストバックチュートリアル

ポストリクエストに対してインタラクティブにページを生成するための機能と言えます。
Xitrumが提供するポストバックは、Formのポスト処理と、ポスト処理に対するサーバサイドのレスポンスを
クライアントサイドのxitrum.jsがポストバックイベントとしてハンドリングすることで実現されています。

ただし、AngularやBackboneなどのJSフレームワークとJSONレスポンスを用いることでより柔軟なページ生成ができることが
多いため、この機能を使う機会は少ないかもしれません。

Postback.scala

@GET("postback")
class PostbackIndex extends DefaultLayout {
  def execute() {
    respondInlineView(
      <form data-postback="submit" action={url[PostbackAction]}>
        <label>Title</label>
        <input type="text" name="title" class="required" /><br />
 
        <label>Body</label>
        <textarea name="body" class="required"></textarea><br />
 
        <input type="submit" value="Postback" />
      </form>
      <hr>
      <form data-postback="submit" action={url[PostbackAction2]}>
        <label>Title</label>
        <input type="text" name="title" class="required" /><br />
 
        <label>Body</label>
        <textarea name="body" class="required"></textarea><br />
 
        <input type="submit" value="Postback2" />
      </form>    )
  }
}
 
@POST("postback")
class PostbackAction extends DefaultLayout {
  def execute() {
    val title   = param("title")
    val body    = param("body")
    flash("Posted.")
    jsRedirectTo[PostbackIndex]()
  }
}
 
@POST("postback2")
class PostbackAction2 extends DefaultLayout {
  def execute() {
    val title   = param("title")
    val body    = param("body")
    jsRespond(s"""$$('body').append('title:$title\\nbody:$body')""")
  }
}

この例では、
Postbackボタン、Postback2ボタン共にポストバックリクエストを行います。
違いは、レスポンスにあり1の場合はjsRedirectToを通じてクライアントから再びPostbackIndexへリクエストが行われます。
Postしてもとのページヘバックすることになります。
2の場合はレスポンスしたjavascriptがクライアント側で実行されます。

6-6-2. リダイレクト

リダイレクトを行う場合、redirectToを使用します。
クライアントには、HTTPステータス 302が返却され、レスポンスヘッダーには次のリダイレクト先URLが追加されます。

Redirect.scala

@GET("/redirect")
class RedirectIndex extends Action {
  def execute() {
    log.debug("RedirectIndex")
    log.debug(textParams.toString)
    redirectTo[RedirectedPage]()
  }
}
 
@GET("/redirected")
class RedirectedPage extends Action {
  def execute() {
    log.debug("RedirectedPage")
    log.debug(textParams.toString)
    respondText(getClass)
  }
}

上記のActionに対してcurlクライアントからリクエストを行った場合、
以下のような結果となります。

curl -v http://localhost:8000/redirect\?x\=1
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* Adding handle: conn: 0x7fb089003000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fb089003000) send_pipe: 1, recv_pipe: 0
* Connected to localhost (::1) port 8000 (#0)
> GET /redirect?x=1 HTTP/1.1
> User-Agent: curl/7.32.0
> Host: localhost:8000
> Accept: */*
>
< HTTP/1.1 302 Found
< Connection: keep-alive
< Location: /redirected
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Methods: OPTIONS, GET, HEAD
< Content-Length: 0
<
* Connection #0 to host localhost left intact 

XitrumログにはRedirectIndexへのアクセスのみ表示されます。

[DEBUG] RedirectIndex
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /redirect?x=1 -> quickstart.action.RedirectIndex, queryParams: {x: 1} -> 302, 1 [ms]

ブラウザから上記のURLを表示した場合、ブラウザがリダイレクト用のレスポンスヘッダーを解釈して次のURLへリクエストを自動で作成します。
この時、RedirectIndexへのリクエストとは全く別のリクエストがRedirectedPageへ行われるため、リクエストパラメータは引き継がれません。

Xitrumログは以下のように成ります。

[DEBUG] RedirectIndex
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /redirect?x=1 -> quickstart.action.RedirectIndex, queryParams: {x: 1} -> 302, 86 [ms]
[DEBUG] RedirectedPage
[DEBUG] Map()
[INFO] 0:0:0:0:0:0:0:1 GET /redirected -> quickstart.action.RedirectedPage -> 200, 43 [ms]

6-6-3. フォーワード

フォーワードの場合は、処理の流れはクライアントにレスポンスは戻らずに、直接次のアクションへ処理が移ります。
リクエストパラメータはそのままフォーワード先のアクションへと引き継がれます。

Forward.scala

@GET("/forward")
class ForwardIndex extends Action {
  def execute() {
    log.debug("ForwardIndex")
    log.debug(textParams.toString)
    forwardTo[ForwardedPage]()
  }
}
 
@GET("/forwarded")
class ForwardedPage extends Action {
  def execute() {
    log.debug("ForwardedPage")
    log.debug(textParams.toString)
    respondText(getClass)
  }
}

Forwardの場合、クライアントがcurlでもブラウザでも同じ結果となります。

curl -v http://localhost:8000/forward\?x\=1
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* Adding handle: conn: 0x7fdf82006e00
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fdf82006e00) send_pipe: 1, recv_pipe: 0
* Connected to localhost (::1) port 8000 (#0)
> GET /forward?x=1 HTTP/1.1
> User-Agent: curl/7.32.0
> Host: localhost:8000
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain; charset=UTF-8
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Methods: OPTIONS, GET, HEAD
< Content-Length: 37
< ETag: "Bz-NYDptSQMU_PgZeFPu0w"
<
* Connection #0 to host localhost left intact 
class quickstart.action.ForwardedPage%
[DEBUG] ForwardIndex
[DEBUG] Map(x -> List(1))
[DEBUG] ForwardedPage
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /forward?x=1 -> quickstart.action.ForwardedPage, queryParams: {x: 1} -> 200, 2 [ms]


今回はこれまで。
レスポンスのパターンはだいたい把握できたので、
次回からは、リクエストパラメーターやスコープについて勉強します。