0

SinatraでGitHub OAuthする

Webアプリに「GitHubで認証」ボタンを付けてログインさせる方法。


アプリを登録

https://github.com/settings/applications
からRegister new Applicationする。

ローカルで試すので、URLとコールバックURLはこうしておく

自分のアプリケーションのIDとsecret keyがもらえるのでメモしておく。(あとで使う)


OAuthする

ここに手順が書いてある。この通りにやればOAuthのtokenが手に入る。
OAuth | GitHub API
tokenが手に入れば、あとはoctokitなどにtokenを渡せばそのユーザーの権限でAPIを使わせてもらえる。


手順はおおまかに

1. GET https://github.com/login/oauth/authorize

ユーザーのWebブラウザをGETパラメータにアプリケーションID付けて上のURLにredirectする。
OAuthの承認画面が表示され、OKされればアプリ登録時に設定したURLにcallbackされる。

2. コールバックから”code”を取り出す。

callback URLに指定した通り、ユーザーのWebブラウザで http://localhost:5000/auth.callback が開かれる。
その時にGETパラメータで”code”が付いているので、実際のURLは
http://localhost:5000/auth.callback?code=1234abcd56
こうなる。
codeを取っておく。

3. POST https://github.com/login/oauth/access_token

Sinatraから上のURLにPOSTする。
“code”とアプリケーションIDとsecretをPOSTすると、ようやくOAuthのtokenが得られる。

4. tokenでOAuth認証

tokenで認証して、ようやく「この人がGitHub上でなんというアカウント名なのか」「持っているリポジトリ一覧」などが取得できるようになる。
ここからはoctokit使えばいいと思う。

なお1の時にscope(読み書き権限の詳細)やstate(クロスサイトリクエスト対策の文字列)を付ける事もできる。
無くても一応動く。


実装

起動前にRegister new Applicationした時に得たアプリケーションIDとsecretを環境変数に入れておく。
export GITHUB_APP_ID=a1b2cdef344565677asdf
export GITHUB_APP_SECRET=asdfhujikohujiko123456


あとはこのアプリを起動して、 /auth に移動させれば認証できる。

require "sinatra"
require "uri"
require "httparty"

get '/auth' do
query = {
:client_id => ENV["GITHUB_APP_ID"],
:redirect_uri => "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/auth.callback",
}.map{|k,v|
"#{k}=#{URI.encode v}"
}.join("&")
redirect "https://github.com/login/oauth/authorize?#{query}"
end

get '/auth.callback' do
code = params["code"]
halt 400, "bad request (code)" if code.to_s.empty?

## get oauth token
query = {
:body => {
:client_id => ENV["GITHUB_APP_ID"],
:client_secret => ENV["GITHUB_APP_SECRET"],
:code => code
},
:headers => {
"Accept" => "application/json"
}
}
res = HTTParty.post("https://github.com/login/oauth/access_token", query)
halt 500, "github auth error" unless res.code == 200
begin
token = JSON.parse(res.body)["access_token"] ## tokenを取得!
rescue
halt 500, "github auth error"
end
## sessionに保存するなど自由に
redirect '/'
end


取得したtokenはoctokitで使える。
require 'octokit'

client = Octokit::Client.new :oauth_token => token
user = client.user
user.avatar_url # アイコン画像

0

Sinatraプラグインを作って踏んだ地雷

いくつかプラグインを作ってみて(RocketIOLindaなど)踏んだ地雷について書いておく。


ちなみに作るにあたって読んだドキュメントは
Sinatra: Writing Extensions
Sinatra Extension を書くお作法、というか我流 #Ruby #Sinatra – Qiita [キータ]
Sinatra でカスタムセッター/ゲッターを定義したり、それにより処理をフックしたりする #Ruby #Sinatra – Qiita [キータ]
sinatra-contribとeventmachineとrackのコード
だけです。


modular styleで動かない / classic styleで動かない

これはもう両方で試すしかないですね。
片方では動くのにもう片方では動かないとか、5回ぐらいあったのでもうこれはサンプル2種類作って試すしかないのではないかと思う。


他のpluginから呼び出された時に動かない

自作pluginをさらに自作pluginから呼び出した時に起こった。
変な事をするpluginばかり作っていたからだと思う。(rackと別にem-websocketでサーバーを建てるとか)
でもpluginの完成度を上げるには、そのpluginを使うplugin書いてみないと絶対わからないのではないかという不安が充満している。


サーバーが起動するまで待つ必要ある場合もある

EventMachineなサーバー(thinなど)と一緒に、他のEventMachineなライブラリ(em-websocketなど)を使う時は
自分でEM::runしてはならない。thinが起動しなくなってしまう。

EventMachineが起動するまで待つにはこうする
def self.start
return if @@running
@@running = true
EM::defer do
while !EM::reactor_running? do
sleep 1
end
puts "Sinatra::WebSocketIO.start port:#{options[:port]}"
EM::WebSocket.run :host => "0.0.0.0", :port => options[:port] do |ws|
ws.onopen do |handshake|
## (略)
どういうわけか、EventMachineが起動する前でもEM::deferは使えるので、そこでEM::reactor_running?を待ってやるとよい。
それから他のEventMachineなライブラリを起動してあげる。


modular styleとclassic styleでsetやenableのタイミングが違う

これもハマる場所で、どっちがどっちだったか忘れたけど
sinatra読み込まれる→プラグイン読み込まれる→setやenable→サーバー起動
だったり
sinatra読み込まれる→プラグイン読み込まれる→サーバー起動→setやenable
だったりした。
最終的に、上のEM::reactor_running?を待つ方法を使って、サーバーが起動するまでプラグインの中でsetでオプション渡されるような機能に処理が行かないようにした。


気軽にSinatra::Base.routesいじってるとよくわからない事起こりうる

ドヤ顔でroutesをいじっていたのですが
Sinatraのroutesを直接操作する
これも、modular styleとclassic styleで
プラグイン読み込まれる→ユーザーの書いたrouteが登録される→サーバー起動
だったり
ユーザーの書いたrouteが登録される→プラグイン読み込まれる→サーバー起動
と順番が違ったりして、やらかしましたね。

RocketIOは何バージョンかの間、これが動かない状態だった。このroute削除してた。
get '/*' do
## 略
end


require名とgem名が違うのでBundler.requireが通らない

これよくわからないんですけど、どうすればいいんですかね・・?

0

Ruby上に並列言語拡張Lindaを実装してWebSocket/Cometで使えるようにした

gemにしてある

linda | RubyGems.org | your community gem host

Linda

Lindaは1990年ごろに出来た言語。
タプルスペースという共有メモリ空間でタプル(オブジェクト)を共有して、in/out/rd/inp/rdpという命令で操作する事で大抵の並列処理が記述できるという物。
仕様は単純なんだけどセマフォなどのロック機構やジョブキューも超簡単に記述できる。いわば分散並列処理のための最小セット。

既にCやJavaの実装があり、Rubyでも咳さんのRindaがある。

で、今回作ったLinda gemではタプルのマッチング判定とオンメモリのタプルスペースの読み書き機能と、それぞれのテストコードしか実装されていない。
複数のプロセスからタプルスペースに接続して読み書きする機能は実装されていない。
マッチングは配列タプルは要素の前方一致、Hashタプルは自身のKeyとValueが相手に全部含まれていればtrueとしている。


Linda RocketIO

Linda gemをさらにSinatra::RocketIOに乗せた。
RocketIOはSocket.ioをパクって俺が作ったSinatra用のWebSocket/Comet実装です。
sinatra-rocketio-linda | RubyGems.org | your community gem host

Sinatraがタプルスペースつまり共有メモリ空間になって、RocketIOで接続されたクライアントがLindaで操作できる。
今のところRubyとJavaScriptがタプルの読み書きに参加できる。

命令はin/out/rd/inp/rdpがそれぞれtake/write/read/take(コールバック)/read(コールバック)になった。
RocketIOは非同期通信なので全部非同期で実行される。


LindaBase

Webサービスを作った。LindaBaseという。
http://linda.shokai.org/
http://linda.masuilab.org/
http://linda-base.herokuapp.com/
それぞれ物理的に別のサーバーだけどアプリは同じ。
ソースコードは http://github.com/shokai/linda-base
自分のサーバーやHerokuなどにインストールも簡単にできる。



いくつかサンプルアプリを作った。
世界観的にはユビキタスコンピューティング、つまり生活環境にセンサーやコンピュータが埋め込まれていて陰ながら色々サービスしてくれるし、スマホとかで操作もできるしWebサービスとも連携したりする環境を考えている。


しゃべる

http://linda.shokai.org/myhome/say/hello
タプル[“say”, “hello”]をタプル空間”myhome”に書き込むページ。
https://github.com/shokai/linda-mac-say
[“say”, 文字列] をwatchして、Macのsayコマンドに投げるアプリ。タプル[“say”, String, “success”]を書き込む。
Rubyで書かれていて、適当なMac上で起動してlinda.shokai.orgに接続して使う。


温度や明るさをArduinoでセンシング、寒かったら警告など

http://linda.shokai.org/myhome/sensor
タプル空間”myhome”内の[“sensor”]から始まるタプルがwatchできるページ。
https://github.com/shokai/linda-arduino-sensor
Arduinoに接続した照度と気温センサーを読んで、タプル[“sensor”, “light”, 120]と[“sensor”, “temperature”, 25]をwriteするアプリ。毎秒書き込む。
適当なUNIXマシンで動かして、linda.shokai.orgに接続して使う。(以降のgithubはlindaに接続して使うアプリです)
https://github.com/shokai/linda-temperature-alert
気温をアラートする。タプル[“sensor”, “temperature”]をreadして、タプル[“say”, “現在の気温 23度”]や[“say”, “現在の気温 8度、お体に障りますよ”]などの警告を流すアプリ。


スマホとPhidgetsサーボモータでドアロックを開ける

http://linda.shokai.org/myhome/door/open
タプル[“door”, “open”]をタプル空間”myhome”に書き込むページ。
https://github.com/shokai/linda-door-phidgets-servo
タプル[“door”, “open”]が来たら、Phidgetsのサーボモータを使ってドアの錠を回す。タプル[“door”, “open”, “success”]を書き込む。
https://github.com/shokai/linda-door-open-goldfish
タプル[“door”, “open”]をAjax POSTとGoldFishで書き込む例


Twitter

http://linda.shokai.org/myhome/twitter/tweet/hello
タプル[“twitter”, “tweet”, “hello”]をタプル空間”myhome”に書き込むページ。
https://github.com/shokai/linda-twitter
タプル[“twitter”, “tweet”, 文字列]をwatchして、Tweetするアプリ。


このように共有メモリを介して小さいアプリが連携できる事がおわかりか。
URLがタプル空間名/タプルの内容になっていて、その場でダミーデータを投入できたり通信を見たりできるのもとても便利。


利点

最後にこのシステムの利点をまとめておく。

分散並列プログラミングがシンプルに書ける

4種類の命令の組み合わせで何でもできる。共有メモリに接続して読み書きするだけ。
ユビキタスコンピューティング、特にお家ハック系はノード間の連携がとても多い。
センサAが反応してから→センサBの反応を待って→モーターCを動かす、のようなプログラムは常人にはなかなか難しい。
Linda使うと簡単。


拡張が簡単

全通信が覗き見できる。処理をフックできるとも言えるし、最初からLinda全体がプラグインシステムみたいになっている。
“明るくなったら”→”カーテンを開ける”というシステムをLinda上に作った後で、”明るくなったら”→”ご飯を炊く”というシステムを追加したいとする。
共有メモリなので、太陽が出た[“sun”, “available”]というタプルを監視すればご飯が炊ける。
Lindaを使わずに明るさセンサとカーテンを直結した場合、ご飯炊くプログラムがデータを取得するには明るさセンサのプログラムを修正しなければならない。
最初からLindaを使っていれば、既存システムを一切修正せずに拡張できる。


デバッグが楽

“明るくなったら”→”カーテンを開ける”というシステムが動かなくなった場合
\突然の死/
明るさセンサとカーテンを直結していた場合、どこが悪いかわからない。
通信がwebページで見れるので、どの部分が壊れたか特定しやすい。


分担して同時にシステムを作れる

普通のシステムでは、センサー側を先に作成しなければアクチュエータ側を作れない。
入力値が無いと出力側を作れないのだが、LindaBaseのwebページ上でダミーデータを投入できる。
ボタン押すだけでタプルが書き込める
入力側が未完成でも、出力側を実装できる。


接続を確立し、維持するのが楽

RocketIOを使っているのでWebSocketで高速な通信ができる。
WebSocketが使えない環境の場合、自動的にcometで接続される。
サーバーより先にクライアントが起動しても、クライアントは自動的に接続をリトライする。
e-mobileの一番下のプランとか海外の怪しい回線でも多分動くので、海外での学会展示でも有用な気がする。
どちらを使っているかは全く意識せずok
切断されても自動再接続される。


オブジェクトが送れる

タプル、実は配列だけではなくHashも送れる。
配列の入れ子も送れるし、複雑なデータ構造も送れる
[“door”, [“open”, “stop”, “close”], {speed: 30, delay: 1}] とか
JSONに変換できる物なら何でも送れる。


なぜLindaというgemがあるか

タプルのマッチングと、オンメモリのタプルスペースの操作APIとそのテストだけが書かれたgemを作ったのは
DBやファイルシステム上で互換のある物を作ればLinda RocketIOで使うLinda実装を差し替えれるようにしたかったから。(まだその機構作ってない)
一応MongoDBで実装してみる予定。


色々便利なのでモリモリ開発していきます。

0

foremanでSinatraアプリをUbuntuのupstartにインストール

参考

橋本商会 » foremanでSinatraアプリをMacのLaunchdにインストール
foreman(1) – manage Procfile-based applications


upstartにインストール


shokai/rocketio-chat-sample · GitHub をインストールする

% sudo foreman export upstart /etc/init --app rocketio-chat --port 4000 -d `pwd` -c web=1 -u `whoami`
起動時にwebsocketのportや、RACK_ENVなどを環境変数で渡したいのだが
無理なので生成された設定ファイルを後で直す。


起動と終了

% sudo service rocketio-chat start
% sudo service rocketio-chat stop
% sudo service rocketio-chat restart
killしたり、OSを再起動しても自動的にプロセスが立ち上がる事を確認する。


設定


/var/log/rocketio-chat/ にログがでる。


upstartの設定として
/etc/init/rocketio-chat.conf
/etc/init/rocketio-web.conf
/etc/init/rocketio-web-1.conf
が生成される。

/etc/init/rocketio-web-1.conf に起動オプションが書かれている。

start on starting rocketio-chat-web
stop on stopping rocketio-chat-web
respawn

exec su - sho -c 'cd /home/sho/src/sinatra/rocketio-chat-sample; export PORT=4000; bundle exec rackup config.ru -p $PORT >> /var/log/rocketio-chat/web-1.log 2>&1'


rbenvのrubyを使う


bundle の代わりに ~/.rbenv/shims/bundle を使ったら、rbenvのrubyを使ってくれる。
アプリのディレクトリ内で
rbenv local 2.0.0-p0
してruby2.0を使うようにした。

rocketio-web-1.conf を編集。websocketのポートを18080にしたり、sinatraをproductionモードで起動するようにした。
start on starting rocketio-chat-web
stop on stopping rocketio-chat-web
respawn

exec su - sho -c '
cd /home/sho/src/sinatra/rocketio-chat-sample;
export RACK_ENV=production;
export WS_PORT=18080;
export PORT=4000;
/home/sho/.rbenv/shims/bundle exec rackup config.ru -p $PORT >> /var/log/rocketio-chat/web-1.log 2>&1
'


再起動。
% sudo service rocketio-chat restart

問題あったら /var/log/rocketio-chat/ のログでわかる。

0

foremanでSinatraアプリをMacのLaunchdにインストール

同様の方法でNodeなどのアプリもインストールできる。


例として、このSinatraアプリをMacで自動的に起動するようにインストールする。
shokai/rocketio-chat-sample · GitHub


Herokuを使っていたら普通こういうProcfileをアプリと同じディレクトリに置いてあるはず。

web: bundle exec rackup config.ru -p $PORT


foremanはinittabやupstartに書き出せる
foreman(1) – manage Procfile-based applications


ドキュメントに書かれてないが、Macのlaunchdにも書き出せるようになっている。
% sudo foreman export launchd ~/Library/LaunchAgents/ --app rocketio-chat -c web=1 -u `whoami`

~/Library/LaunchAgents/rocketio-chat-web-1.plist が作られる。
ログは /var/log/rocketio-chat/ に書き出されるようになる。chownされて権限は自分になっていた。

–port 5000とか書くと$PORTが5000になるはずなんだけどならなかったので省略した。
仕方ないので生成されたplistを手で直した。


あとはlaunchctrl loadすれば起動する。プロセスをkillしても自動的に再起動してくれるし、Macを再起動しても勝手に立ち上がるようになった。
% launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist

アンインストール
% launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist


rbenvのRubyを使う

アプリのディレクトリで
% rbenv local 2.0.0-p0
して、.ruby-vesionを作っておく。plistの設定でWorkingDirectoryが指定してあれば.ruby-versionを読んでくれるようだ。

あとは
橋本商会 » Ubuntu12.04にrbenvインストールして、crontabやdaemontoolsも設定した
のdaemontoolsの時みたいにshell scriptを一度経由すればいいかと思ったが、
bundle execを.rbenv/shims/bundle execにするだけでパスが通った。


最終的にこうなった。
上のチャットのアプリは環境変数WS_PORTを見てwebsocketのportを決定するので、dictを追加して環境変数も渡した。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>rocketio-chat-web-1</string>
<key>EnvironmentVariables</key>
<dict>
<key>WS_PORT</key>
<string>33100</string>
<key>PATH</key>
<string>/bin:/usr/bin:/usr/local/bin:/usr/local/sbin</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Users/sho/.rbenv/shims/bundle</string>
<string>exec</string>
<string>rackup</string>
<string>config.ru</string>
<string>-p</string>
<string>5000</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/rocketio-chat/rocketio-chat-web-1.log</string>
<key>StandardErrorPath</key>
<string>/var/log/rocketio-chat/rocketio-chat-web-1.log</string>
<key>UserName</key>
<string>sho</string>
<key>WorkingDirectory</key>
<string>/Users/sho/projects/rocketio/rocketio-chat-sample</string>
</dict>
</plist>

一度unloadしてからloadしないと有功にならないので注意。