すぎゃーんメモ

2013-01-12

Heroku上からService Account認証でGoogle APIにアクセスする

Googleの様々なAPIを叩く際、認証にOAuth 2.0を用いる。

Using OAuth 2.0 to Access Google APIs - Google Accounts Authentication and Authorization — Google Developers

使用する場面やパターンによって以下のような6つのシナリオが想定されている。

  • Login
  • Web Server Applications
  • Client-side Applications
  • Installed Applications
  • Devices
  • Service Accounts

多くの場合は「ユーザごとに認証させて個別のtokenを発行しリクエストに利用する」という流れなのだけど、中にはたとえばURL短縮APIとか、必ずしもユーザ個別にtokenを発行させる必要がないこともある。

そういう場合には「サービス固有のtoken」だけあれば良い。ということで使えるのが「Service Accounts」という方式。

Using OAuth 2.0 for Server to Server Applications - Google Accounts Authentication and Authorization — Google Developers

サービスアカウントに発行されたprivate keyを使って署名したJWTリクエストを生成してAPIを叩く、という仕組みのようだ。


rubygoogle-api-clientを使う例

GoogleAPIを叩くためのライブラリとして、Ruby Gemsではgoogle-api-clientというのがある。

google-api-client | RubyGems.org | your community gem host

JWTを使ったリクエストや認証なども対応してくれている。

これを使って実際にやってみる。

準備

Google Accounts

にて、Application typeを"Service account"と選択してClient IDを生成する。

f:id:sugyan:20130112213142p:image

すると、"Email address"が発行され、private keyダウンロードできるようになるので、これを使う。

書く

Gemfileに

source :rubygems
gem 'google-api-client', '0.6.0'

と書いてbundle installして、以下のようなスクリプトを書く。

require 'google/api_client'

client = Google::APIClient.new(:application_name => '')
key = Google::APIClient::PKCS12.load_key('/Users/sugyan/Downloads/...-privatekey.p12', 'notasecret')
client.authorization = Signet::OAuth2::Client.new(
  :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
  :audience             => 'https://accounts.google.com/o/oauth2/token',
  :scope                => 'https://www.googleapis.com/auth/urlshortener',
  :issuer               => <発行されたemail address>
  :signing_key          => key,
)
client.authorization.fetch_access_token!

shortener = client.discovered_api('urlshortener')
result = client.execute(
  :api_method  => shortener.url.insert,
  :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' },
)
puts result.data.id

ダウンロードしたprivate keyファイルを読みこんでkeyを生成し、email addressやscopeを指定し、認証。

これで、GoogleURL短縮APIを叩いて短縮URLを得ることができる。

$ bundle exec ruby shorten.rb
http://goo.gl/zLVjD

Herokuに上げるために

で、こんなものをHeroku上で動かそうとすると、API Consoleからダウンロードしたprivate keyファイルgit repositoryに含める必要が出てしまう。それはイヤだ。

幸いにも、Google::APIClient::PKCS12でloadしたkey文字列として得られる。

$ bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")'
-----BEGIN RSA PRIVATE KEY-----
MIICXQ...

...

-----END RSA PRIVATE KEY-----

なので、これを丸ごとheroku configで渡してしまえばいい。ついでに発行されたemail addressも。

$ heroku config:set GOOGLE_API_KEY="$(bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")')"
$ heroku config:set GOOGLE_API_EMAILADDRESS=...@developer.gserviceaccount.com

で、これを使用してKeyを生成するようにコードを変更する。

require 'google/api_client'

client = Google::APIClient.new(:application_name => '')
key = OpenSSL::PKey::RSA.new(ENV['GOOGLE_API_KEY'])
client.authorization = Signet::OAuth2::Client.new(
  :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
  :audience             => 'https://accounts.google.com/o/oauth2/token',
  :scope                => 'https://www.googleapis.com/auth/urlshortener',
  :issuer               => ENV['GOOGLE_API_EMAILADDRESS'],
  :signing_key          => key,
)
client.authorization.fetch_access_token!

shortener = client.discovered_api('urlshortener')
result = client.execute(
  :api_method  => shortener.url.insert,
  :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' },
)
puts result.data.id

これで、このへんをcommitしてherokupushすると、設定したconfig文字列を使用してheroku上からService Accounts認証を使ってAPIを叩けるようになる。

$ git push heroku master
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 1.10 KiB, done.
Total 6 (delta 0), reused 0 (delta 0)
-----> Ruby app detected
-----> Installing dependencies using Bundler version 1.3.0.pre.5
       Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin --deployment
       Fetching gem metadata from http://rubygems.org/.........
       Fetching gem metadata from http://rubygems.org/..
       Installing addressable (2.3.2)
       Installing extlib (0.9.16)
       Installing multi_json (1.5.0)
       Installing autoparse (0.3.2)
       Installing multipart-post (1.1.5)
       Installing faraday (0.8.4)
       Installing jwt (0.1.5)
       Installing launchy (2.1.2)
       Installing signet (0.4.4)
       Installing uuidtools (2.1.3)
       Installing google-api-client (0.6.0)
       Using bundler (1.3.0.pre.5)
       Your bundle is complete! It was installed into ./vendor/bundle
       Cleaning up the bundler cache.

...

 * [new branch]      master -> master
$ heroku run bundle exec ruby shorten.rb
http://goo.gl/WnqP5

まとめ

…と、そういうようなことが、以下の記事に書いてあった。

How To Store Private Key Files In Heroku - arzumy md

2012-08-14

数十行の簡単なスクリプトでWebコンテンツの更新を検知し通知する

以前にも似たようなのは書いたのだけど。

某アイドルグループに、ついに待望のファンクラブが作られた。もちろんすぐに入会しました。

そこではその子たちのマネージャーさんが不定期に日記を更新してくれるのだけど、残念なことにRSSとかも無いし、自分でログインして覗いてみないと更新されているかどうかを知ることができない。


自動化せずにはいられない。


ということで、更新チェックするプログラムを書くのだけど、、、

コンテンツが「更新されたか否か」を判定するためには、「一定間隔でコンテンツの内容をチェック」し、「前回チェックしたときと比較して差異があるか否か」を調べる必要がある。

「一定間隔で内容をチェック」すること自体は、cronなどを使えば簡単にできる(ここではWWW::MechanizeログインWeb::ScraperでコンテンツHTMLの解析を行う)。

けど、「前回チェックしたときと比較して差異があるか否か」を調べるためには、前回チェックしたときの結果を何らかの形で残しておく必要がある。ファイルに書き出すだとか、DBに保存するだとかの形で。

そうするのは面倒なので、そもそもプログラムを終了させる形で書かずに「一定間隔で内容チェック」だけを行うよう実行させ続け、チェックした結果はメモリに残しておくようにする。

AnyEventのtimerで一定間隔ごとにMechanizeを走らせるようにする。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use AnyEvent;
use Config::Pit;
use Encode 'encode_utf8';
use Log::Minimal;
use Try::Tiny;
use Web::Scraper;
use WWW::Mechanize;

my $conf = +{
    angeleyes => pit_get('fc.momoclo.net', require => {
        login_id => '会員番号',
        password => 'パスワード',
    }),
};
my $previous = undef;

my $cv = AE::cv;
my $w = AE::timer 0, 100, sub {
    try {
        my $mech = WWW::Mechanize->new;
        $mech->get('https://fc.momoclo.net/pc/login.php');
        $mech->submit_form(
            form_id => 'loginForm',
            fields  => $conf->{angeleyes},
        );
        my $latest = scraper {
            process '#topdiary .cont li', 'contents[]' => scraper {
                process 'a',      'title' => 'TEXT';
                process 'p.date', 'date'  => 'TEXT';
            };
        }->scrape($mech->content)->{contents}[0];
        $latest->{date} =~ s/更新//;
        my $current = sprintf '(%s) %s', $latest->{date}, encode_utf8 $latest->{title};
        infof('latest: %s', $current);
        if (defined $previous && $previous ne $current) {
            # 何らかの通知を行う
        }
        $previous = $current;
    } catch {
        warnf('error: %s', $_);
    };
};
$cv->recv;

https://gist.github.com/516f6d3f7297d2865a93


プログラムはAnyEvent::timerによって回り続けるので終了せず、定期的にコンテンツをチェックした内容は$previous変数に保持され続け、更新されたときだけ通知処理を行う、ということがこの単一のスクリプトだけで実現できる。cronを使う必要は無い。


こういうのはNode.jsとかでsetIntervalを使っても一緒だと思うけど、Mechanize的なモジュールをよく知らないのでPerlで書いた。

WWW::Mechanize的なのってどういうのがあるんだろう?


ニッポン笑顔百景

ニッポン笑顔百景

Z女戦争(初回限定盤A)(DVD付)

Z女戦争(初回限定盤A)(DVD付)


追記

少なくともPerlの場合はwhile文でループさせつつsleepで、という方法でも良かったはず。Nodeで最初にsetIntervalで書こうとしていてこういう形になってしまったのかも…

2012-07-03

Test::Flattenでsubtestの初っ端で死ぬときのエラーが出力されない (追記あり)

@さんのTest::Flattenを使ってみている。

use Test::More;
use Test::Flatten;

subtest 'hoge' => sub {
    ok 1, 'fuga';
    piyo();
};

done_testing;

という、subtest内で死んでしまうようなテストの場合、proveを実行すると

$ prove -v
t/01_hoge.t ..
# ------------------------------------------------------------------------------
# hoge
# ------------------------------------------------------------------------------
ok 1 - fuga
Undefined subroutine &main::piyo called at t/01_hoge.t line 6.
# Tests were run but no plan was declared and done_testing() was not seen.
Dubious, test returned 255 (wstat 65280, 0xff00)
All 1 subtests passed

Test Summary Report
-------------------
t/01_hoge.t (Wstat: 65280 Tests: 1 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.01 cusr  0.00 csys =  0.03 CPU)
Result: FAIL

とエラーを吐いて失敗を教えてくれるけど、

use Test::More;
use Test::Flatten;

subtest 'hoge' => sub {
    piyo();
    ok 1, 'fuga';
};

done_testing;

と、subtest内の最初で死ぬようなパターンだと、

$ prove -v
t/01_hoge.t ..
# ------------------------------------------------------------------------------
# hoge
# ------------------------------------------------------------------------------
No tests run for subtest hoge at t/01_hoge.t line 7.
Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run

Test Summary Report
-------------------
t/01_hoge.t (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
Files=1, Tests=0,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.01 cusr  0.00 csys =  0.03 CPU)
Result: FAIL

となり、"No tests run for subtest $caption"がcroakされてしまい、何故死んでしまったのかが分からない。

どうすればいいんだろう


追記

上記記事を書いてみたところ、xaicronさんがあっという間に直してくれました。

0.09であれば上記の問題は起こらないようです。ありがとうございます!

2012-03-22

Crypt::SSLeayがLANGの関係でインストールできないとき

あるLinux環境でCrypt::SSLeayをインストールしようとするとテストが通らない。

$ cpanm -v Crypt::SSLeay
...
t/00-basic.t .... ok     
t/01-connect.t .. 1/8 
#   Failed test 'Net::SSL->new'
#   at t/01-connect.t line 28.
# Connect failed: connect: 接続を拒否されました; 接続を拒否されました at t/01-connect.t line 11
# Looks like you failed 1 test of 8.
t/01-connect.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/8 subtests 
        (less 7 skipped subtests: 0 okay)
t/02-live.t ..... 1/4 # config on linux
# ssl OpenSSL 1.0.0-beta3 in /usr;
# lib -L -lssl -lcrypto -lgcc
# inc -I/usr/include
# cc cc
t/02-live.t ..... ok   

Test Summary Report
-------------------
t/01-connect.t (Wstat: 256 Tests: 8 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=3, Tests=24,  0 wallclock secs ( 0.05 usr  0.01 sys +  0.22 cusr  0.03 csys =  0.31 CPU)
Result: FAIL
Failed 1/3 test programs. 1/24 subtests failed.
make: *** [test_dynamic] エラー 255
FAIL
! Installing Crypt::SSLeay failed. See /home/sugyan/.cpanm/build.log for details.

どうやらNet::SSL->newが失敗したときのエラーメッセージが日本語で出てきてしまっているかららしい。テスト内部では英語のエラーメッセージが期待されているためコケてしまうようだ。

ということで

$ LANG=C cpanm -v Crypt::SSLeay

とすればインストールできるようになるらしい。

このエラーメッセージの言語ってどこで決まるんだろう?手元のMac OS X環境ではLANG=ja_JP.UTF-8でも問題ないのだけど…。

2012-03-20

gitoliteでプライベートリポジトリ作成

月額980円のさくらVPSを個人用に使い倒す - ゆーすけべー日記を読んで、そういえばgitのprivate repositoryを持ってないし何かのときのために設定しておくか、と思ってさくらVPSのサーバにgitoliteを入れてみた。

Hosting git repositories

以下、手順メモ。


gitoliteのインストール

yumでサクっと。

sugyan@remote $ sudo yum install gitolite

gitユーザを作成

専用ユーザを作ってそいつの$HOME以下でリポジトリ管理するようにした方がなにかとラクなので

sugyan@remote $ sudo useradd -m git

adminになるため公開鍵をコピー
sugyan@remote $ sudo cp $HOME/.ssh/id_dsa.pub /home/git/admin.pub

setup

コピーしてきた公開鍵を使ってsetup。使うコマンドはgl-setupだけ

sugyan@remote $ su - git
git@remote $ gl-setup -q admin.pub
creating gitolite-admin...
Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
creating testing...
Initialized empty Git repository in /home/git/repositories/testing.git/
[master (root-commit) 9a57094] start
 2 files changed, 6 insertions(+), 0 deletions(-)
 create mode 100644 conf/gitolite.conf
 create mode 100644 keydir/admin.pub
git@remote $ ls repositories/
gitolite-admin.git  testing.git

repositoryが出来上がってる。


gitolite-adminをclone

adminとしてgitolite-adminをcloneしてくる。同一サーバ内から持ってくるのでhost名はlocalhost指定で良い

sugyan@remote $ mkdir ~/temp
sugyan@remote $ cd ~/temp
sugyan@remote $ git clone git@localhost:gitolite-admin
Initialized empty Git repository in /home/sugyan/temp/gitolite-admin/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
sugyan@remote $ ls
gitolite-admin

cloneできた。


ローカルからも使えるようにする

ローカル環境の公開鍵を持ってきて登録する。

sugyan@local $ scp $HOME/.ssh/id_dsa.pub vps:temp/gitolite-admin/keydir/sugyan-local.pub

再びsshでvpsサーバに入ってgitolite-admin上で公開鍵をaddしてpush

sugyan@local $ ssh vps
sugyan@remote $ cd ~/temp/gitolite-admin
sugyan@remote $ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       keydir/sugyan-local.pub
nothing added to commit but untracked files present (use "git add" to track)
sugyan@remote $ git add keydir/sugyan-local.pub
sugyan@remote $ git commit -m 'add sugyan-local'
[master a193304] add sugyan-local
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 keydir/sugyan-local.pub
sugyan@remote $ git push
Counting objects: 6, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 842 bytes, done.
Total 4 (delta 0), reused 0 (delta 0)
remote: 
remote:                 ***** WARNING *****
remote:         the following users (pubkey files in parens) do not appear in the config file:
remote: sugyan-local(sugyan-local.pub)
To git@localhost:gitolite-admin
   9a57094..a193304  master -> master

confに何も書き足していないので警告は出るけど、まぁ今は問題無し。

ローカルから確認。gitolite-adminは権限ないからcloneできないけど、デフォルトでtestingは@allに対してRW+になっているのでcloneできるしpushもできる。

sugyan@local $ git clone git@vps:gitolite-admin
Cloning into gitolite-admin...
R access for gitolite-admin DENIED to sugyan-local
(Or there may be no repository at the given path. Did you spell it correctly?)
fatal: The remote end hung up unexpectedly
sugyan@local $ git clone git@vps:testing
Cloning into testing...
warning: You appear to have cloned an empty repository.
sugyan@local $ cd testing
sugyan@local $ echo hoge > hoge
sugyan@local $ git add hoge
sugyan@local $ git commit -m 'add hoge'
[master (root-commit) 3285627] add hoge
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hoge
sugyan@local $ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 205 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@vps:testing
 * [new branch]      master -> master

大丈夫げ。


グループとリポジトリを作る

gitolite-adminにてconf/gitolite.confを編集。@sugyanというグループを作り、そこにsugyan-localを加える

sugyan@remote $ emacs conf/gitolite.conf
sugyan@remote $ git diff
diff --git a/conf/gitolite.conf b/conf/gitolite.conf
index 2d55bf8..202384a 100644
--- a/conf/gitolite.conf
+++ b/conf/gitolite.conf
@@ -1,5 +1,10 @@
+@sugyan = sugyan-local
+
 repo    gitolite-admin
         RW+     =   admin
 
 repo    testing
         RW+     =   @all
+
+repo    my-private
+        RW+     =   @sugyan
sugyan@remote $ git add conf/gitolite.conf
sugyan@remote $ git commit -m 'add new group and repository'
[master e3c35ed] add new group and repository
 1 files changed, 5 insertions(+), 0 deletions(-)
sugyan@remote $ git push
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 409 bytes, done.
Total 4 (delta 0), reused 0 (delta 0)
remote: creating my-private...
remote: Initialized empty Git repository in /home/git/repositories/my-private.git/
To git@localhost:gitolite-admin
   a193304..e3c35ed  master -> master

my-privateレポジトリが新しく作成された。

ローカルから確認。

sugyan@local $ git clone git@vps:my-private
Cloning into my-private...
warning: You appear to have cloned an empty repository.
sugyan@local $ cd my-private
sugyan@local $ echo fuga > fuga
sugyan@local $ git add fuga
sugyan@local $ git commit -m 'add fuga'
[master (root-commit) 5955bdb] add fuga
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 fuga
sugyan@local $ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 207 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@vps:my-private
 * [new branch]      master -> master

testing同様に読み書きできた。

別のマシンからも使いたい場合は公開鍵を登録して@sugyanグループに追加してやれば良いし、他の人と共同で使うことになれば都度グループなり権限なりを追加していけば良い。


感想

最初なんだかよく分からずちょっと詰まったけど、一度設定できてしまえば結構ラクに管理できて良さそう。もっと細かい権限設定なども出来るらしい。必要になったら調べて設定してみよう