Apache Drillはuserに厳しい

mongodbに対して、userというキーをそのまま使うと、anonymousになってしまう。

0: jdbc:drill:zk=local> select * from mongo.test.actions t;
+--------------------------------------+---------+-------------+------------------+
|                 _id                  | action  | created_at  |       user       |
+--------------------------------------+---------+-------------+------------------+
| {"$oid":"5686bacac3220d960f34589c"}  | add     | 1451664185  | Walter Lynch     |
| {"$oid":"5686bad1c3220d960f34589d"}  | add     | 1451668314  | Irene Armstrong  |
| {"$oid":"5686badac3220d960f34589e"}  | add     | 1451671227  | Paul Bailey      |
| {"$oid":"5686bae1c3220d960f34589f"}  | remove  | 1451671349  | Walter Lynch     |
+--------------------------------------+---------+-------------+------------------+
4 rows selected (0.315 seconds)

0: jdbc:drill:zk=local> select user from mongo.test.actions;
+------------+
|    user    |
+------------+
| anonymous  |
| anonymous  |
| anonymous  |
| anonymous  |
+------------+
4 rows selected (0.171 seconds)

これはバッククォートだけでもダメで、collectionとセットで指定しないと正しく取得できない。

0: jdbc:drill:zk=local> select `user`, t.`user` from mongo.test.actions t;
+------------+------------------+
|    user    |      user0       |
+------------+------------------+
| anonymous  | Walter Lynch     |
| anonymous  | Irene Armstrong  |
| anonymous  | Paul Bailey      |
| anonymous  | Walter Lynch     |
+------------+------------------+
4 rows selected (0.133 seconds)

このanonymousは接続ユーザ名というわけでもないようだし謎なのだけれど、サブクエリ内で集計して結果出力していたら、何故か結果が常に1行になってしまい、無駄に時間を費やしてしまった。

Apache Drill 1.2.0までは、(dfs経由での)JSONMySQLに対してuserというキーを使うとエラーになる問題もあったけれど、1.3.0以降で解消されていた。

0: jdbc:drill:zk=local> select user from dfs.`/tmp/actions.json`;
Error: DATA_READ ERROR: Error parsing JSON - Cannot read from the middle of a record. Current token was START_ARRAY

0: jdbc:drill:zk=local> select * from mysql.mysql.`user`;
Error: VALIDATION ERROR: java.lang.NullPointerException

そもそもキー名として微妙な感じもするけれど、外部データだと如何ともし難い場合があったりではあるので悩ましい。

OpenConnectによるVPN接続

概要

  • Cisco AnyConnectではなく、互換クライアントのOpenConnectを利用してVPN接続を行う方法についてのメモ
  • Cisco AnyConnectはVirtualboxの仮想NICと相性が悪く、VPN接続中はルーティングを全てトンネル側に向けてしまうため、仮想環境のネットワークに接続できない問題がある
  • Virtualboxのネットワークに接続できないと、Vagrant上の開発環境や、Genymotion(Androidエミュレータ)が利用できなくなるので困る
  • 問題の報告は以前から上がっているようだけれど、改善の気配はないみたい
  • 動作確認環境は Mac OS X 10.10.5 (Yosemite)

事前準備

必要なアプリケーションのインストール

仮想ネットワークデバイス TUN/TAP
  • TunTap - Home
  • Yosemite以降、未署名のKernel Extensionがインストールできなくなったため sudo nvram boot-args="kext-dev-mode=1" などの回避手段が取られていたが、2014/11/18から署名済パッケージが配布されているため、現在はインストール上の問題は発生しない
$ brew cask install tuntap
Cisco互換のVPNクライアント(?) vnpc
  • OpenConnectはVPNサーバとの通信機能だけを持っており、ルーティングやDHCPなどの諸々はサポートしないため、vpncのスクリプトを補助的に利用する(多分)
$ brew install vpnc
OpenConnect
$ brew install openconnect

各種証明書のエクスポート

  • クライアント証明をKeyChainからエクスポートする
    • KeyChain Access.app の login にあるユーザ名の証明書を選択し、右クリックメニューなどからExportする
    • 出力先は適当な場所でよいけれど、自分は ~/.cisco/certificate/client/cert.p12 などに設置した
    • 証明書の出力時にパスワード入力を求められるので、任意のパスワード文字列を設定する
  • サーバ証明書をKeyChainからエクスポートする

接続方法

以下の様なコマンドで接続できる。

$ sudo openconnect \
    --user=username \
    --authgroup=vpn \
    --script=/usr/local/etc/vpnc-script \
    --cafile=~/.cisco/certificate/server/ca.example.com.cer \
    --certificate=~/.cisco/certificate/client/cert.p12 \
    vpn.example.com
  • --user は、各自接続ユーザ名に置き換える
  • --script は、vpncでインストールされたパスを指定する
  • --cafile--certificate は、前項でエクスポートした各証明書パスを指定する
  • vpn.example.com は、接続先のVPNサーバ名に置き換える
  • --authgroupは環境によって不要かもしれない(指定値は接続先VPN管理者に確認する)
  • 設定項目はファイルに切り出して接続時に読み込ませることもできる

接続終了

  • 接続終了は Ctrl + C などで切断する
  • 起動時に -b オプションを付加するとバックグランドで実行されるので、その場合は kill などで殺す

問題点など

signer not found のエラーが出る

  • CA証明書指定が間違っているかもしれない
  • とりあえず yes とか答えてスルーすることもできるが、当然微妙
  • 雑に済ますなら --no-cert-check オプションでスキップすることもできる
SSL negotiation with vpn.example.com
Server certificate verify failed: signer not found
 
Certificate from VPN server "vpn.example.com" failed verification.
Reason: signer not found
Enter 'yes' to accept, 'no' to abort; anything else to view: yes

パスワード入力を最大3回求められる

  • 「sudo」「クライアント証明書」「VPNサーバへのログイン」のそれぞれで、パスワード入力が求められる
  • セキュリティ上のリスクを承知の上で、以下の様なパスワード回避手段を採用することができる
openconnect実行にsudoパスワードを省略する
$ sudo visudo -f /etc/sudoers
---
 
# 下記の1行を追加する
%admin  ALL=(ALL) NOPASSWD: /usr/local/bin/openconnect
クライアント証明書からパスワードを除去する
$ cd ~/.cisco/certificate/client/
$ openssl pkcs12 -in cert.p12 -nodes -out temp.pem
Enter Import Password: (エクスポート時のパスワードを入力)
MAC verified OK
 
$ openssl pkcs12 -export -in temp.pem -out cert_unprotected.p12
Enter Export Password: (何も入力せずにEnter)
Verifying - Enter Export Password: (何も入力せずにEnter)
 
$ rm temp.pem
VPNサーバへのログインパスワードを標準入力で渡す
  • 標準入力からパスワードを受け取ることができるので、起動時のコマンドに含めてしまう
echo -n "password" | sudo openconnect --passwd-on-stdin vpn.example.com

切断後にネット接続できなくなることがある

  • 何故か自然復旧することもあるけれど、たいてい接続できない状態のままになる
  • pingを打っても下記のようなエラーが表示される感じ
$ ping 192.168.0.1
PING 192.168.0.1 (192.168.0.1): 56 data bytes
ping: sendto: No route to host
  • 原因はDefault GatewayRTF_IFSCOPEが立っているので、通常の接続時にgatewayとして認識してくれないみたい
    • ルーティングテーブルを出力すると、FlagsIが含まれていることが確認できる
$ netstat -nr
Routing tables

Internet:
Destination        Gateway            Flags        Refs      Use   Netif Expire
default            192.168.211.254    UGScI           0        0     en1
127                127.0.0.1          UCS             0        0     lo0
127.0.0.1          127.0.0.1          UH              2   327379     lo0
...
  • netstatのドキュメントによると、IRTF_IFSCOPEを示すとのこと

    I RTF_IFSCOPE Route is associated with an interface scope

  • 説明はちゃんと理解できていないけど、そのインターフェース専用のgatewayとして設定されるということなのかな…

    A route which is marked with the RTF_IFSCOPE flag is instantiated for the corresponding interface.

  • いずれにしてもこれがあると都合が悪いので正常に戻したい
  • 復旧手段
    • RTF_IFSCOPEのついたgatewayを削除して、再度登録しなおせば復旧する
    • 現在のgatewayの値は、以下のコマンドで確認できる
$ route -n get -ifscope en1 default
   route to: default
destination: default
       mask: default
    gateway: 192.168.211.254
  interface: en1
      flags: <UP,GATEWAY,DONE,STATIC,PRCLONING>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1500         0 
  • 削除〜追加のコマンドは、このような感じ(en1は各環境のインターフェース名にあわせる)
$ DEFAULT_GATEWAY=`route -n get -ifscope en1 default | grep gateway | awk '{ print $2 }'`
$ sudo route delete default -ifscope en1
$ sudo route add default $DEFAULT_GATEWAY
  • ルーティングテーブルを出力すると、FlagsからIがなくなっていることが分かる
$ netstat -nr
Routing tables

Internet:
Destination        Gateway            Flags        Refs      Use   Netif Expire
default            192.168.211.254    UGSc          210        2     en1
127                127.0.0.1          UCS             0        0     lo0
127.0.0.1          127.0.0.1          UH              2      104     lo0
...

それでは、快適なVPNライフをお過ごしください。

CVE-2015-4024.patch (PHP5.2/5.3)

今更感ではあるけれど、公式にはPHP5.4以降しか対応されなかったCVE-2015-4024のPHP5.2/5.3向けpatchを当時用意した。ソースを追った限りでは該当部分の実装はPHP5.2時代から変わっていなかったため、そのままバックポートしただけではある。

商用環境で数ヶ月稼働させて問題がないので、一応貼るだけ貼っておこうかなと思った。未だに何かの呪いでバージョンを上げられない環境はあるのだ。

https://gist.github.com/castor4bit/ec28eebc6df551c69c0f

CtrlP matcherをcpsmに変更する

Big Sky :: Vim の CtrlP matcher、cpsm がヤバイくらいに速すぎる を読んで、CtrlPのmatcherをcpsmに変更した。

インストールにはNeoBundleを使うけれど、インストール時にPythonモジュールのビルドが必要となるので、以下のように記述する。

.vimrc

NeoBundle 'nixprime/cpsm', {
  \ 'build': {
  \   'others': 'sh install.sh'
  \}}

let g:ctrlp_match_func = {'match': 'cpsm#CtrlPMatch'}

インストールでエラーが発生する場合には、ログの内容を確認する。

:NeoBundleLog

自分の環境では、このようなエラーが出力されていた。

...
CMake Error at /usr/local/Cellar/cmake/3.2.2/share/cmake/Modules/FindBoost.cmake:1182 (message):
  Unable to find the requested Boost libraries.

  Unable to find the Boost header files.  Please set BOOST_ROOT to the root
  directory containing Boost or BOOST_INCLUDEDIR to the directory containing
  Boost's headers.
Call Stack (most recent call first):
  CMakeLists.txt:17 (find_package)
...

公式の Requirements をみると、確かに色々と必要であることが記載されているので、インストールされていないものは事前に準備しておく必要があるのであった。自分の場合は、エラーの内容からBoostが未導入だったので、Homebrewで追加した。

$ brew install boost

変更してすこぶる快適になった気分で使っていたけれど、一度アンインストールしてみても意外と遅くならなくて、プラシーボ効果だった気がしないでもない。とはいえ、遅くなるわけではないし、候補の選ばれ方も多少違うようなので、しばらく試してみる。

MEAN(mean.io)ではpassport-twitterが動作しない話

まとめ

MEAN(mean.io)ではpassport-twitterが(そのままでは)動作しないので、強引に回避するか諦めるかする必要がある。

何があったのか

MEAN(mean.io) を触っていたのだけれど、passport-twitterで認証すると下記のようなエラーが発生していた。

Error: Failed to find request token in session

よくあるミスで、localhostにアクセスしているのに、Twitterのcallback URLは127.0.0.1にしていてCookieが共有されていないという原因で発生しがちなエラーのようだけど、当然それも踏んだ上でまだ発生し続ける。

コンソールにエラー出力させてみると、別のエラーが原因でセッションからトークンが削除されているみたいだった。曰く、emailがないとのこと。

ValidatorError: Path `email` is required.

問題の箇所は、ログイン認証後にユーザ情報を格納する部分で、Mongooseのスキーマ上はemailフィールドは必須になっているけれど、Twitterの認証後処理にはemailを保存する処理が書かれていない

これが実装ミスかというとそうでもなくて、Twitterのユーザ情報取得にはGET users/showを参照しているけれど、そもそもTwitter APIではメールアドレス情報は返ってこないので保存する項目に記載がないのはある意味で正しいとも言えるのだった。

つまり、MEAN(mea.io)のユーザ管理上はメールアドレスを必須としているけれど、メールアドレスを取得できないTwitterでは、認証連携はうまく動作しないということらしい。しれっと対応している風に実装されているけれど、確実に動作しないという罠であって5時間くらい潰れた。猫のうんこ踏め。

傾向と対策

Twitterでメールアドレスを取得する方法はないのかというと、残念ながら存在しない。

メールアドレスや電話番号の扱いについては、少なくとも直接公開されることはない旨がヘルプに記載されているAPI経由での取得については数年前から要望には上がっていて、つい先日、許可されたアプリにのみaccount/verify_credentialsのオプションで取得が可能になったと公式からの告知があった。なんとなくヘルプの記載内容とは矛盾している気もするけれど。

いずれにしても、他のソーシャル連携と比較してスコープが雑なTwitterでは、現状の仕様のままメールアドレスが取得できるようにはならないだろうし、なったら危険な感じはする。

アプリケーションの要件にもよるけれど、MEAN(mean.io)のユーザ情報で何故メールアドレスが必要かといえば、パスワード紛失時の再発行などの用途以外で必須とする理由はあまりない(内部的にはユーザをユニークに特定するために使っているけれど、これはメールアドレス変更などを考えると若干筋が悪い気もする)。他サービス連携においては、パスワード紛失などは書くサービスに任せればよいので、単純にこの必須条件を外してしまってもよいとも思ったけれど、どこで参照しているかチェックするのも面倒だったので、ダミーの値を捏造して格納するようなことを考えた。

根本的には、アプリ自体をメールアドレス必須でなくして、メールアドレス取得可能なほかサービスでも不要な情報を保存しないようにするべきではあるのだろうなあ。

とりあえずの対応

実はこの問題、以前にIssueにも上がっていたようなのだけれど、議論が中途半端なまま半年くらいが経過して閉じられてしまっている。

回避派の人のだいたい考えることは同じで、必須条件を外すかダミーの値を生成するような感じである。ドメイン部分以外はほぼ同様で、ダミーの値をこういう感じに生成するようにして回避した。解決した気分じゃない。

packages/users/passport.js

  user = new User({
    name: profile.displayName,
    username: profile.username,
    email: profile.username + '_twitter@example.com',    // この行を追加
    provider: 'twitter',
    twitter: profile._json,
    roles: ['authenticated']
  });

最近のVimFilerの挙動変更への対応

いくつか変更点があって、意図しない挙動ではなくなったので対応した。

VimFilerからファイルを選択しても新しいbufferで開かれなくなった

-quitオプションの名前が変わって-force-quitになったらしいので変更した。(4ca27f4

- nnoremap <silent> ,vf :<C-u>VimFilerBufferDir -split -simple -winwidth=30 -quit<CR>
- nnoremap <silent> ,vp :<C-u>VimFiler -project -split -simple -winwidth=30 -quit<CR>
+ nnoremap <silent> ,vf :<C-u>VimFilerBufferDir -split -simple -winwidth=30 -force-quit<CR>
+ nnoremap <silent> ,vp :<C-u>VimFiler -project -split -simple -winwidth=30 -force-quit<CR>

buftabsでバッファ一覧に表示されなくなった

あまり因果関係を理解していないけれど、g:vimfiler_restore_alternate_file のデフォルトが1になったことが影響している感じだった。(198eb0e)

+ let g:vimfiler_restore_alternate_file=0

git bisect便利。

momonga.vim #6でinside-motion.vimを作った

momonga.vim #6に参加してきた。

vimを本格的に使うようになってから2年足らずだけれど、Vim Scriptはちゃんと書いたことがなかったので、よい機会だと思ってもくもくすることにした。

そもそも基本文法からほぼ分からない状態だったので、Vimスクリプト基礎文法最速マスター - 永遠に未完成や、「実践Vim」を読んでみた感想と Vim script 初心者講座 | MBA-HACKなどを読みながら1時間くらい。なんとなく分かったところで何を作ろうか考えて、あまり効率よくできていない移動周りをよしなにしてくれるモーションプラグインを書こうと思った。

HTMLタグだったり関数呼び出しの引数部分だったりキーだったり、とかく中身だけ編集したいというシーンが結構多いのだけれど、fとかで毎回先頭文字を指定して移動するのがいけてないと思っていたので、そういう内側に一気に飛んでくれるためのプラグインが欲しかったのだった。(↓完成したスクリーンショット

image

image

camelcasemotionのソースを中心にいろいろプラグインとかドキュメントを読んで書き始める。

NeoBundleでローカル環境のプラグインを読み込むにはどうすればよいのか悩んだけれど、NeoBundleLocalを使えばいける感じだったので、.vim/bundle下に直接ファイルを置いて開発することにした。

$ mkdir -p ~/.vim/bundle/inside-motion.vim && $_
$ mkdir plugin autoload
$ touch README.md
$ vim -c ':NeoBundleLocal ~/.vim/bundle' 

既存プラグインを参考に大枠はだいたいできたものの、正規表現エスケープがいつもの感覚と違うで変に時間を取られてしまった。検索するときは\vを使ってしまうけれど、プラグインでもカジュアルに使ってよいのか悩ましかったので、基本的には使わない方向でやろうと思った。他にはデフォルトのキーマップで迷ったり、一度githubにあげてインストールしてからローカルファイルを編集して反映されないとか悩んだりなどして時間が過ぎていった。

結局時間内にはすべて実装終えることができなかったので、日曜日のプリキュア観るつもりだった時間に残った作業をしてgithubにあげた。

castor4bit/inside-motion.vim

Vim Scriptの書き方少し分かった気がするし、一応今回の目的は達成したような感じがする。 あとは、暗黒美夢王のライブが聴けたのはとてもよかった。4曲ってWake Up, Girlsよりも持ち歌が多い。