Hatena::ブログ(Diary)

知らないけどきっとそう。 RSSフィード

 

2018-03-05 Have I been pwned? にパスワードを入力するとどうなるか このエントリーを含むブックマーク

Have I been pwned? Pwned Passwords (以下 HIBP と呼ぶ)が公開されました。ここでは、過去に情報漏えいで実際に暴露された 5 億個のパスワードのオンライン検索とダウンロードが提供されています。セキュリティに関心のある方は、検索するために入力したパスワードを HIBP の管理者に知られることに懸念を感じるでしょう。I’ve Just Launched ”Pwned Passwords” V2 With Half a Billion Passwords for Download では、検索されたパスワードの秘密がどのように守られるかが解説されています。

ダウンロードオフライン検索

その前に、安全にオフラインで検索する方法を説明します。

HIBP では、パスワードはソルトなしの SHA-1 ハッシュのリストで管理されています。利用者が自らのパスワードを検索したい場合、そのパスワードSHA-1 ハッシュを求める必要があります。利用者が HIBP から 5 億個のパスワードハッシュリストの全体をダウンロードし、それを検索すれば、管理者に自らのパスワードを知られるおそれはありません。ですが、そのリスト全体のサイズは約 9 ギガバイトあり、配布や更新にコストがかかります。

オンライン検索

それでは、利用者がパスワードハッシュのみを HIBP に送信し、一致しているか結果を返す方法はどうでしょうか。

残念ながら、HIBP はハッシュする前の生のパスワードリストを持っているので、一致している場合、管理者に利用者のパスワードを知られてしまいます。一致していない場合も、レインボーテーブル - Wikipedia により、総当たり攻撃が可能です。

そこで、HIBP はパスワードハッシュの最初の 5 文字のみを送信するというアイデアを採用しました。

例えば "P@ssw0rd" というパスワードを検索するには、そのハッシュである "21BD12DC183F740EE76F27B78EB39C8AD972A757" の先頭の ”21BD1” を HIBP に送信 します。HIBP はプレフィックス "21BD1" に一致するハッシュの残り 35 文字のリストを返します。

1. (21BD1) 0018A45C4D1DEF81644B54AB7F969B88D65:1 (password "lauragpe")

2. (21BD1) 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2 (password "alexguo029")

3. (21BD1) 011053FD0102E94D6AE2F8B83D76FAF94F6:1 (password "BDnd9102")

4. (21BD1) 012A7CA357541F0AC487871FEEC1891C49C:2 (password "melobie")

5. (21BD1) 0136E006E24E7D152139815FB0FC6A50B15:2 (password "quvekyny")

6. ...

https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/

利用者は、このリストに "(21BD1) 2DC183F740EE76F27B78EB39C8AD972A757" が含まれているかを調べ、あれば漏えいしていると判断できます。

実際のところ Have I been pwned? Pwned Passwords の検索フォームは上記のように動作します。また、同様のことをおこなう Bash スクリプト も存在します。

1. Every hash prefix from 00000 to FFFFF is populated with data (16^5 combinations)

2. The average number of hashes returned is 478

3. The smallest is 381 (hash prefixes "E0812" and "E613D")

4. The largest is 584 (hash prefixes "00000" and "4A4E8")

https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/

それぞれのプレフィックスは、最小でも 381 個のハッシュが存在しており、そこに利用者のパスワードが含まれていたとしても、管理者にはどれに一致するかわかりません。この HIBP が返すデータが持つ性質を k-匿名性 - Wikipedia と呼びます。

管理者が最悪でパスワード候補を 381 個までしか絞り込めないという意味であり、前述の完全なハッシュ送信するケースと比較して、リスクが緩和されています。利用者のパスワードが含まれていない場合は、ハッシュの残り 35 文字が不明なため 16^35 パターンの可能性があり、総当たり攻撃は現実的ではありません。

パスワードを推測されやすいシナリオ

ここからは筆者の考えですが、管理者がより正確にパスワードを推測できる利用シナリオがあります。

HIBP はハッシュしか返さないため、説明のために小規模なパスワードリストから生成したハッシュと、生のパスワードのペアを返すエンドポイントを用意しました。HIBP は 5 億ですが、ここでベースにしたパスワードリストは 100 万と小さいため、プレフィックス 3 文字で平均 244 個(最小 193 個)のハッシュパスワードを返します。

https://asannou.github.io/badpasswords/21b.txt

利用者

あるサービスのアカウントを作るにあたって、新しく作成したパスワードが漏えい済みかを確かめる。

新しいパスワード "trustme" をチェックするため、そのハッシュ "b6a34a9f8b81a6964ff5b983bcc739ff2efb569f" のプレフィックス送信する。

$ curl -s https://asannou.github.io/badpasswords/b6a.txt | grep ^b6a34a9f8b81a6964ff5b983bcc739ff2efb569f
b6a34a9f8b81a6964ff5b983bcc739ff2efb569f  trustme

リストに含まれていたので、続いて "trustme2", "trustme3" のハッシュ "30d22e311391fb2da70422f7402eb240908cf8e5", "42c540d61d64f38f334b2f350ea0e32d3a208a2e" をチェックする。

$ curl -s https://asannou.github.io/badpasswords/30d.txt | grep ^30d22e311391fb2da70422f7402eb240908cf8e5
30d22e311391fb2da70422f7402eb240908cf8e5  trustme2
$ curl -s https://asannou.github.io/badpasswords/42c.txt | grep ^42c540d61d64f38f334b2f350ea0e32d3a208a2e
$

"trustme3" はリストになかったため、アカウントパスワードとして決定する。

管理者

このとき、送られたプレフィックス "b6a", "30d", "42c" をみて、以下の推測ができる。

まず "b6a" と "30d" に含まれるパスワードで前方一致するものを探す。

$ wget -q https://asannou.github.io/badpasswords/b6a.txt https://asannou.github.io/badpasswords/30d.txt
$ cut -f 2 b6a.txt | while read password; do cut -f 2 30d.txt | grep ^$password; done
trustme2

この結果から、最初のリクエストで "trustme" 次のリクエストで "trustme2" が検索された可能性が高い。最後のリクエストを "trustme3" と仮定してハッシュを計算すると "42c..." が一致する。

考察

是非は別として、作成したパスワードが漏えい済みだった場合、文字を付加して回避することは、利用者の行動としてありえるでしょう。特に Nextcloud will check passwords against database of HaveIBeenPwned のように導入する場合は、パスワードが推測されないよう、適宜 HIBP にダミーのリクエストを送るなどの対策が必要と考えます。

2017-11-06 ジェネリックトレネ このエントリーを含むブックマーク

振動を検知して荷物の盗難を防ぐデバイス「トレネ」のクラウドファンディングが、目標金額を達成しています。

要素技術が、振動センサーと Bluetooth ということで、Raspberry Pi Zero W と少しのパーツで似た機能の実現を目指しました。盗られたことがわかるので、「トラレ」と呼ぶことにします。



緑色のミンティアカテキンミント)の中に収まっているのが Raspberry Pi で、ケーブル接続されている黒い物体は一般的なモバイルバッテリーです。(ミンティアから飛び出している赤外線 LED は今回関係ありません)

まずは振動センサーですが、15 円/個くらいの 水銀スイッチ - WikipediaRaspberry Pi の GPIO18 ピンと GND にはんだ付けしました。水銀スイッチは取り扱いを誤ると、環境に悪影響を及ぼすため 金属球で感知するタイプ もあります。GPIO18 がオフからオン、または逆に変化したとき、傾きが発生したとみなすことができます。

「トラレ」と iOS 端末の距離は、Raspberry PiBluetooth の RSSI 値を定期的に取得することで監視します。今回は no title を利用しました。

iOS 端末が一定以上離れていて、Raspberry Pi が傾きを検知したとき、iOS 端末に通知をおこないます。本家「トレネ」では、本体が警告音を発する仕様ですが、誤動作で注目を集めるのは望むところではないので、そのようにしました。Bluetooth による通知といえば、一時期ブームだった iBeacon - Wikipedia という仕組みがあるため、それを用います。

これらを組み合わせて「トラレ」として動作させるために Raspberry Pi 上で実行する no title を作成しました。さらに、iOS 端末で iBeacon を受信するためのアプリインストールが必要です。iBeacon advertisement を受信できるアプリは数多くありますが、任意の UUID を指定でき、バックグラウンドでも通知可能な 「Find My Stuff - 鍵やお財布から車まで、あなたの所持品を即座に位置確認!」をApp Storeで を採用しました。


f:id:asannou:20171105212011p:image:w590


Bluetooth が届く距離にいないと通知が受け取れないという懸念はあります。試した限りでは、壁を隔てた隣の部屋くらいであれば問題ないようです。圏外にいたとしても iBeacon advertisement は発信され続けるため、圏内に入った時点で通知されます。

Raspberry Pi Zero W は $10 なので、「トレネ」の予定価格 6,800 円と比較すると、コストパフォーマンスの高さが際立ちます。

2017-09-26 Siri に本当の愛を問う このエントリーを含むブックマーク

f:id:asannou:20170925214058g:image

  • カメラがドッタンバッタンしてるのと iMovie 編集丸出しなのはご容赦ください

f:id:asannou:20170925152130j:image:w320

begin remote

  name            SIGMA
  bits            16
  flags           SPACE_ENC
  eps             30
  aeps            100
  header          9094  4425
  one             642   1608
  zero            642   481
  ptrail          646
  pre_data_bits   16
  pre_data        0xAA55
  gap             39822
  toggle_bit_mask 0x0

  begin codes
    0 0xc837
    1 0xf00f
    2 0xf20d
    3 0x728d
    4 0xe01f
    5 0xe21d
    6 0x629d
    7 0xc03f
    8 0xc23d
    9 0x42bd
    A 0xca35
    B 0x4ab5
    E 0x7c83
  end codes

end remote
    • 曲番号を 3825A4 とした場合、以下のようにパディングと終端をつけて送信する
$ irsend SEND_ONCE SIGMA 3 8 2 5 A 0 4 E
    • これを Homebridge のスイッチに登録
{
  "accessories": [
    {
      "accessory": "CMD",
      "name": "ようこそジャパリパークへ",
      "on_cmd": "/usr/bin/irsend SEND_ONCE SIGMA 3 8 2 5 A 0 4 E",
      "off_cmd": "true"
    }
  ]
}
  • iOS 端末の HomeKit を設定

f:id:asannou:20170926013901p:image:w320

f:id:asannou:20170925152800j:image:w320

  • 愛はどこにある…

引用元: LIVE DAM STADIUM / ようこそジャパリパークへ / どうぶつビスケッツ×PPP

2017-08-03 サーバレスに OpenID Connect で AWS にサインイン このエントリーを含むブックマーク

準備

$ git clone https://github.com/asannou/aws-signin-with-oidc
$ git diff
diff --git a/terraform.tfvars b/terraform.tfvars
index 2d3d635..ec5d990 100644
--- a/terraform.tfvars
+++ b/terraform.tfvars
@@ -1,17 +1,17 @@
 aws_region = "us-east-1"

-s3_bucket = "bucketname"
+s3_bucket = "aws-signin-with-oidc"

 client_id = {
-  google = "000000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
+  google = "689439588880-vmco4me6g34au0h43ao9tbkb3slgffht.apps.googleusercontent.com"
 }

 email = {
   dev = [
-    "dev@example.com",
+    "asannou@gmail.com",
   ]
   admin = [
-    "admin@example.com",
+    "asannou@gmail.com",
   ]
 }
  • terraform を適用します
$ terraform apply
...
google = {
  admin = https://aws-signin-with-oidc.s3.amazonaws.com/google?role=arn:aws:iam::123456789012:role/Admin
  dev = https://aws-signin-with-oidc.s3.amazonaws.com/google?role=arn:aws:iam::123456789012:role/Dev
}
origin = https://aws-signin-with-oidc.s3.amazonaws.com

利用

f:id:asannou:20170803214548p:image:w656

    • できない場合は、反映が遅れている可能性があるため、少し待って再度試してください
  • AWS CLI を使用する際は、URL の末尾に &export=1 をつけて認証をおこなうと、ブラウザ環境変数が表示されます
    • 環境変数を設定すると、同等の権限で AWS CLI を実行することが可能です
$ export AWS_ACCESS_KEY_ID=ASIAIOSFODNN7EXAMPLE; export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY; export AWS_SESSION_TOKEN=AQoDYXdzEJr...
$ aws sts get-caller-identity
{
    "Account": "123456789012",
    "UserId": "AROAIOSFODNN7EXAMPLEU:asannou@gmail.com",
    "Arn": "arn:aws:sts::123456789012:assumed-role/Dev/asannou@gmail.com"
}

雑感

  • SAML であれば、ネイティブAWS コンソールへのサインインが提供されているが…
  • そういった事情により OpenID Connect 採用
    • Implicit Grant Flow なんて、バックエンドで検証しなきゃいけないし一生使わねえと思ってたけど、まるでサーバレスのために用意されていたかのよう
    • G Suite 環境では OAuth濫用が懸念だったが、昨日解決した

2017-07-24 Vuls で EC2 をバルスされないために(実践編) このエントリーを含むブックマーク

これは Amazon EC2 Systems Manager Advent Calendar 2017 の -129 日目の記事です。

$ aws iam create-role --role-name EC2RoleSample --assume-role-policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}}'
$ aws iam create-instance-profile --instance-profile-name EC2RoleSample
$ aws iam add-role-to-instance-profile --instance-profile-name EC2RoleSample --role-name EC2RoleSample
$ aws ec2 associate-iam-instance-profile --instance-id i-1234567890abcdef0 --iam-instance-profile Name=EC2RoleSample
    • EC2 インスタンスにアタッチされた IAM ロールに AmazonEC2RoleforSSM ポリシーをアタッチする
$ aws iam attach-role-policy --role-name EC2RoleSample --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
[ec2-user ~]$ sudo yum install amazon-ssm-agent
[ec2-user ~]$ sudo start amazon-ssm-agent
  • 前提を満たしたら、やっていきます
$ terraform apply
var.vuls_roles
  Enter a value: []

var.vuls_users
  Enter a value: ["Bob"]

...
  • 晴れて IAM ユーザ "Bob" は EC2 インスタンス "i-1234567890abcdef0" に Vuls のためのユーザを作成できるようになりました
$ aws sts get-caller-identity --output text --query Arn
arn:aws:iam::123456789012:user/Bob
$ aws ssm send-command --document-name CreateVulsUser --instance-ids i-1234567890abcdef0 --parameters publickey="$(cat $HOME/.ssh/id_rsa.pub)" --output text --query Command.CommandId
854365f0-aefb-48eb-be4a-f2a0c6c2cf9e
  • 同時に実行結果として、インスタンスSSH ホスト公開鍵を取得できるので known_hosts に追加すれば安全に接続できます
$ aws ssm get-command-invocation --command-id 854365f0-aefb-48eb-be4a-f2a0c6c2cf9e --instance-id i-1234567890abcdef0 --output text --query StandardOutputContent | tee -a $HOME/.ssh/known_hosts
203.0.113.1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJKQ5+Aauvvb5iPFfYeQhcIegsich7SJ1Ji97mh3sGx3wvXAV53wuzn4ILSEn9ENtb6bXT/puLiCUi2bza2To24= root@ip-172-31-28-103
$ ssh -t vuls@203.0.113.1 'stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id'
i-1234567890abcdef0Connection to 203.0.113.1 closed.

2017-07-04 Vuls で EC2 をバルスされないために(簡易版) このエントリーを含むブックマーク

$ sudo iptables -I OUTPUT -d 169.254.169.254 -j REJECT --reject-with icmp-admin-prohibited
  • 禁止されていることを確認
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
curl: (7) Failed to connect to 169.254.169.254 port 80: ホストへの経路がありません
  • root ユーザ (UID 0) による通信を許可するルールを追加
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 0 -j ACCEPT
$ sudo curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • ec2-user ユーザ (UID 500) も許可します
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 500 -j ACCEPT
  • sudo なしで取れました
$ id
uid=500(ec2-user) gid=500(ec2-user) groups=500(ec2-user),10(wheel)
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • 最終的なルールです
$ sudo iptables -L OUTPUT
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match ec2-user
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match root
REJECT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  reject-with icmp-admin-prohibited
[Jul  3 16:22:49] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 0
  stdout: i-0bf0d78a268f80335
  stderr:
  err: %!s(<nil>)
[Jul  3 16:22:49]  INFO [localhost] (1/1) amazon is running on aws
[Jul  3 16:14:56] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 7
  stdout: Warning: Transient problem: timeout Will retry in 1 seconds. 3 retries left.
curl: (7) Failed to connect to 169.254.169.254 port 80: No route to host

  stderr:
  err: %!s(<nil>)
[Jul  3 16:14:56]  INFO [localhost] (1/1) amazon is running on other
  • したがって、Vuls に必要なコマンドだけ許可する no title がおすすめです!

2017-06-28 Vuls で EC2 をバルスされないために このエントリーを含むブックマーク

  • Vuls という脆弱性スキャナーがありますが、それを Amazon EC2 に対して実行できるようにすることで、ある条件下で、サービスが滅ぶ可能性について説きます
  • 以下に Vuls の単純化した使用方法を示します
$ ssh-keygen -N '' -f $HOME/.ssh/id_rsa_vuls
$ cat $HOME/.ssh/id_rsa_vuls.pub
ssh-rsa ...
ec2-user$ sudo adduser -m vuls
ec2-user$ sudo su vuls
vuls$ mkdir -m 700 /home/vuls/.ssh
vuls$ echo 'ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
  • Vuls の設定ファイルを作成
$ mkdir vuls
$ cat <<EOD > vuls/config.toml
[servers]

[servers.amazon]
host        = "203.0.113.1"
port        = "22"
user        = "vuls"
keyPath     = "/root/.ssh/id_rsa_vuls"
EOD
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls scan
$ for i in `seq 2002 $(date +"%Y")`; do docker run --rm -it -v $PWD:/vuls -v $PWD/go-cve-dictionary-log:/var/log/vuls vuls/go-cve-dictionary fetchnvd -years $i; done
  • Vuls のレポートを表示
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls report
  • これによって、ユーザが認証情報を管理する必要がなくなるため、アプリケーションサーバとしての EC2 に、様々な権限を持つロールをアタッチするケースが考えられます
ec2-user$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
{
  "Code" : "Success",
  "LastUpdated" : "2017-06-27T09:17:33Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIAJHNLJH7SISLPSOMA",
  "SecretAccessKey" : "...",
  "Token" : "...",
  "Expiration" : "2017-06-27T15:39:38Z"
}Connection to 203.0.113.1 closed.
    • 取得できました
  • したがって、SSH 秘密鍵 "id_rsa_vuls" は、EC2 にアタッチされているロールによっては、高いレベルで保護する必要があります
  • しかし、その認識にギャップがあると思われ、次のように扱われる懸念があります
  • vuls アカウントに設定することで、Vuls の実行のみが許可され、その他のコマンドは無視されます
vuls$ curl https://raw.githubusercontent.com/asannou/vuls-ssh-command/master/amazon-linux.sh > /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ chmod +x /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ echo 'command="/home/vuls/.ssh/vuls-ssh-command.sh" ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
Connection to 203.0.113.1 closed.
  • たぶんバグあり、Amazon LinuxUbuntu しか用意していませんが、よろしければお使いください

2017-06-18 Slack ストーキング このエントリーを含むブックマーク

  • message event | Slack は、いくつかのサブタイプがあり、メッセージの変化を表現しています
$ node index.js | jq --unbuffered 'select(.subtype=="message_deleted")'
  • ユーザ @bsannou がチャンネル #twitter で "Despite the constant negative press covfefe" というメッセージを削除した様子です
{
  "type": "message",
  "deleted_ts": "1497783180.894743",
  "subtype": "message_deleted",
  "hidden": true,
  "channel": {
    "id": "C5VCH7HFV",
    "name": "#twitter"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "Despite the constant negative press covfefe",
    "ts": "1497783180.894743"
  },
  "event_ts": "1497783204.895728",
  "ts": "1497783204.895728"
}
$ node index.js | jq --unbuffered 'select(.subtype=="message_changed")'
  • 様子です
{
  "type": "message",
  "message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもクサレ脳ミソですね",
    "edited": {
      "user": {
        "id": "U5JPRSD6U",
        "name": "@bsannou"
      },
      "ts": "1497188145.000000"
    },
    "ts": "1497188126.496907"
  },
  "subtype": "message_changed",
  "hidden": true,
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもド低能ですね",
    "ts": "1497188126.496907"
  },
  "event_ts": "1497188145.497849",
  "ts": "1497188145.497849"
}
  • 公式クライアントでは、削除や修正が常に適用された状態で表示されますが、一旦送信した情報は取り消せないので、留意していきましょう
$ node index.js | jq --unbuffered 'select(.presence=="active") | .now=now'
  • 様子
{
  "type": "presence_change",
  "presence": "active",
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  },
  "now": 1497786620.436581
}
  • 深夜に active がいるとオッとなる
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.presence=="active") | {"type":"message","text":"進捗どうですか?activeだから居ますよね??","channel":{"name":.user.name}}' > fifo
$ node index.js | jq --unbuffered 'select(.type=="user_typing")'
{
  "type": "user_typing",
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  }
}
  • うざみ
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.type=="user_typing") | {"type":"message","text":("ちょっと待って!今 "+.user.name+" が何か言おうとしてる!"),"channel":{"name":.channel.name}}' > fifo
  • 今日書きたいことはこれくらいです

2017-05-28 Node.js Stream からいつの間にか Slack クライアントができた このエントリーを含むブックマーク

  • Mastodon は観察専門なので、使用頻度の高い Slack を題材とします
let ws;
const url = "https://slack.com/api/rtm.connect?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => {
  res.on("data", chunk => {
    const obj = JSON.parse(chunk);
    ws = new (require("ws"))(obj.url);
    ws.on("message", data => stream.write(data + "\n"));
  });
});
stream.pipe(process.stdout);
$ node index.js
{"type":"hello"}
{"type":"reconnect_url","url":"wss://mpmulti-0a1g.slack-msgs.com/websocket/ZvwORwLc1uFhgMHJAZva-04tZg2Lp5iJEclZ8vnOCctMTnJC64LQvb7MZvgJNifnscAGXvki2UAEcJpnlwFwCY031HGGe-NizIM5cZZKfdMQcO9kbXBhb8LkUKQnhR3bx-Z5hxefvQgsVQVgZwSFO9huhgXo3cBvjCc44YTZklY="}
{"type":"presence_change","presence":"active","user":"U5HCFCV5W"}
...
  • ここに、書き込むと WebSocket にデータを送る Writable Stream のコードを追加して stdin と繋ぎます
const writable = new require("stream").Writable({
  write: (chunk, encoding, callback) => {
    ws.send(chunk.toString());
  }
});
process.stdin.pipe(writable);
  • stdin へチャンネルにメッセージを投稿するための JSON を入力します
$ node index.js
...
{"type":"message","channel":"C5JT9C849","text":"test"}
  • 直後に成功を表す JSON が送られてきたら、ブラウザ等から確認すると反映されていることがわかります
{"ok":true,"reply_to":0,"ts":"1495991511.232093","text":"test"}
  • 送られてくる JSON は channel や user の情報が "C5JT9C849" のような id となっていて扱いづらく、よって name に変換する Transform Stream を作って間にはさみます
const url = "https://slack.com/api/rtm.start?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => { ... });
const transform = new require("stream").Transform({
  transform: function(chunk, encoding, callback) { ... }
});
stream.pipe(transform).pipe(process.stdout);
{"type":"message","user":{"id":"U5HCFCV5W","name":"@asannou"},"text":"test","ts":"1496083043.865560","channel":{"id":"C5JT9C849","name":"#vip"}}
  • 出力が JSON のままでは可読性が低いので、jq を使って整形します
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.","ts":"1495732144.332015","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Pleasure to meet you. Let me show you a couple things about Slack.","ts":"1495732157.337778","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
170525170904.332015 [@slackbot] (@slackbot) Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.
170525170917.337778 [@slackbot] (@slackbot) Pleasure to meet you. Let me show you a couple things about Slack.
  • タイムスタンプ部分 "%y%m%d%H%M%S.%6N" が苦しいですが、生 JSON よりはマシでしょう
    • 出力のすべては JSON であるがゆえに、特定のチャンネルをフィルタリングするなど、jq の能力が発揮されます
$ node index.js | jq -c --unbuffered 'select(.channel.name != "#general")'
  • 入力についても、手打ち JSON は死ぬので jq に頼ります
$ echo "test" | jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}'
{"type":"message","channel":{"name":"@slackbot"},"text":"test"}
  • 入力と出力を jq で挟むことにより、ちょっとだけまともな Slack クライアントになりました
$ jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}' | node index.js | jq --unbuffered -r -f format.jq
help!
170528175842.970929 [@slackbot] (@asannou) help!
170528175842.970933 [@slackbot] (@slackbot) I can help by answering simple questions about how Slack works. I'm just a bot, though! If you need more help, try our <https://get.slack.help/hc/en-us/|Help Center> for loads of useful information about Slack ― it's easy to search! Or simply type */feedback* followed by your question or comment, and a human person will get back to you. :smile:
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq"
  • Readline は、他にもタブキーによる入力補完もサポートしているため、公式クライアントのような、ユーザ名の補完なども実現可能です
  • Slack といえば通知ですが、指定された文字列に色を付けると同時に BEL (\x07) を出力する no title を最後にパイプすると目的を達成できます
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq | ./highlight.js asannou"

170528175842.970929 [@slackbot] (@asannou) help!

  • もちろん、同様のことができるコマンドで、代替してもかまいません
  • その他いろいろ
    • チャンネルをステートフルに指定する
    • スレッドの対応
    • WebSocket で受け付けてもらえないリクエストを HTTPS で送って、結果だけ stream に流す
    • パイプで繋いだ各コマンドでバッファされないよう、無効にするか、ラインバッファオプションを指定する
    • stdout からのデータは JSON として処理されてしまうので、補助的なデータは stderr から出力

f:id:asannou:20170530115133g:image

  • 個人的に Slack の機能はこれで足りているので、しばらく使ってみようと思います

2017-04-25 Mastodon のタイムラインをターミナルに流す このエントリーを含むブックマーク

  • Node.js の Stream の学習を兼ねて、これを様々なターミナルに出力できるようにする
    • 「Stream を制するものは、 Node.js を制す」らしい
$ node -e 'new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public").on("message", (data, flags) => console.log(data))'
{"event":"update","payload":"{\"id\":1920985,\"created_at\":\"2017-04-24T16:10:03.248Z\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"application\":{\"name\":\"Web\",\"website\":null},\"account\":{...
  • これを parse() して Stream を作成し transform して出力
const stream = new require("stream").Transform({
    objectMode: true,
    transform: function(chunk, encoding, callback) { ... }
});

new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public")
    .on("message", (data, flags) => stream.write(JSON.parse(data)));

stream.pipe(process.stdout);

f:id:asannou:20170425023534g:image:w576

  • Stream は TCP Socket に pipe() でつなぐことができる
    • 標準入力の Stream を pipe() して、ポート 12345 に接続すると ping の結果が流れてくるサンプル
$ ping localhost | node -e 'process.stdin.resume();require("net").createServer(socket => process.stdin.pipe(socket)).listen(12345);'
$ telnet localhost 12345
Trying ::1...
Connected to localhost.
Escape character is '^]'.
64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.065 ms
64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.056 ms
64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.095 ms
64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.049 ms
64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.054 ms
^]
telnet> Connection closed.
  • pipe() によって、何回でも Stream の分岐や合流が可能
    • クライアントが複数接続しても、それぞれに同じデータを送ることができる
    • resume() しておくことで pipe() されているものが何もない時のデータは捨てる
  • まとめたものを no title として公開
$ telnet tootcat.0j0.jp
$ nc tootcat.0j0.jp 7007
$ telnet tootcat.0j0.jp 7008
$ telnet tootcat.0j0.jp 7009
$ telnet tootcat.0j0.jp 7010

  • 未加工の JSON を受信できるようにした
$ nc tootcat.0j0.jp 7006 | head -c 1000
{"id":6021698,"created_at":"2017-05-06T07:18:52.994Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"Web","website":null},"account":{"id":0,"username":"username","acct":"acct","display_name":"display_name","locked":false,"created_at":"2017-04-21T12:07:59.324Z","followers_count":0,"following_count":0,"statuses_count":1,"note":"note","url":"https://friends.nico/url","avatar":"avatar","avatar_static":"avatar_static","header":"header","header_static":"header_static","nico_url":"nico_url"},"media_attachments":[],"mentions":[],"tags":[],"uri":"tag:friends.nico,2017-05-06:objectId=6021698:objectType=Status","content":"content1\r\ncontent2","url":"url","reblogs_count":0,"favourites_count":0,"reblog":null}{...
$ nc tootcat.0j0.jp 7006 | docker run -i --rm -e "TZ=Asia/Tokyo" asannou/jq --unbuffered -r '.prefix = (.created_at | sub(".[0-9]*Z";"Z") | fromdate | strflocaltime("%H:%M")) | .prefix += " (" + .account.username + ") " | .prefix as $prefix | select(.uri | test("^tag:friends.nico,")) | .content | split("\r\n") | join("\r\n" + $prefix) | $prefix + .'
16:18 (username) content1
16:18 (username) content2
    • IRC
    • strflocaltime がまだ development version のみの機能だったので asannou/jq を作った
    • --unbuffered で滞りなく出力される
 
Connection: close