Rubyでcaching_sha2_password認証を使ってMySQLに接続する

ローカル環境で、RubyMySQLレプリケーションプロトコルを扱う処理を作りたいのだけど、いつも使っている mysql2 gemレプリケーションプロトコルに対応していないので自分で作ることにした。

普段はナイーブに TCP ソケットを使った処理を書くことはなかったので、よい機会ではある。
そのうち RubyMySQL に接続するプログラムを作る人の役に立つかもしれないし。

まずは Ruby から MySQL に接続しないと話にならないので、プログラムで接続するやり方を調べた。

MySQL はバージョン8 以降からデフォルトの認証方式が従来の mysql_native_password から caching_sha2_password に変わっている。
主な違いとしてはこういう感じらしい。

  • ハッシュアルゴリズムに SHA256 を採用(mysql_native_password は SHA-1
  • 認証成功後、サーバー側でクレデンシャル情報をキャッシュすることで再接続時の認証処理を高速化する
    • これを高速認証と呼ぶそう
  • 初回認証時は SSL/TLS 接続または RSA 暗号化を使ったUnix ソケット接続が必要で、平文でのパスワード送信を防止する

ローカル環境なので SSL/TLS 接続ではなく、RSA 暗号化を使ったUnix ソケット接続を採用する。

caching_sha2_password 認証の仕様については公式ドキュメントを見ながら実装していくことになる

8.4.1.2 Caching SHA-2 Pluggable Authentication 6.1.4 Caching SHA-2 Pluggable Authentication

ざっくり認証時の処理の流れはこんな感じになる。

  • TCP ソケットで MySQL サーバに接続し、Handshake パケットを解析する
    • Handshake に含まれるサーバー情報(バージョン、認証プラグイン等)を以降の処理で使う
  • caching_sha2_password 認証を行う
    • サーバ起動後初回認証時は、RSA公開鍵をサーバーから取得し、パスワードをRSA暗号化して送信する
      • 2回目以降の認証はこの処理はスキップする

MySQLとのパケット送受信

通信パケットの仕様は公式に載っているのでそれを見ればOK
MySQL Packets

送受信共に同じ構造。

サイズ フィールド名
3 bytes ペイロード payloadのバイト数(リトルエンディアン)
1 byte シーケンス番号 パケットのシーケンス番号(0始まり)
n bytes ペイロード 実際のデータ

payloadを取り出してもろもろの処理を行う、payloadを作ってもろもろの命令を送信する、というのが基本になる。
送信時に受信時に受け取ったsequence_idをインクリメントして使う必要があるのに注意。

Rubyだとこんな感じになる

socket = TCPSocket.new(host, port)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

sequence_id = 0

# 受信
def read_packet
  header = socket.read(4)

  packet_length = header[0].unpack1('C') | (header[1].unpack1('C') << 8) | (header[2].unpack1('C') << 16)
  sequence_id = header[3]unpack1('C')
  payload = socket.read(packet_length)
end

# 送信
def send_packet(payload)
  packet_length = payload.length
  header = [packet_length].pack('V')[0..2] + [sequence_id + 1].pack('C')
  socket.write(header + payload)
end

Handshakeパケットについて

TCP ソケットで接続したらすぐに Handshake パケットが送られてくるので解析して後続の処理に使う。

Handshake パケットの仕様は公式に載っている。
Protocol::HandshakeV10

サイズ フィールド名
1 byte プロトコルバージョン
変動 サーバーバージョン NULL終端。8.0.4 みたいな文字列が入る
4 bytes スレッドID
8bytes 認証データ(前半)
1 byte フィラー
2 bytes 機能フラグ(下位2バイト)
1 byte 文字セット
2 bytes ステータスフラグ
2 bytes 機能フラグ(上位2バイト)
1 byte 認証データ長 or 0x00
10 bytes 予約済み すべて0x00
13+ bytes 認証データ(後半)
変動 認証プラグイン NULL終端。caching_sha2_password が入る

こんな感じで愚直に仕様通りにパースしていけばいい。

offset = 0

# Protocol version (1 byte)
protocol_version = MysqlReplicator::StringUtil.read_uint8(payload[offset])
offset += 1

# Server version is null-terminated string
server_version_end = payload.index("\0", offset) || 0
server_version = MysqlReplicator::StringUtil.read_str(payload[offset...server_version_end])
offset = server_version_end + 1

# ConnectionID is 4bytes and little endian
connection_id = MysqlReplicator::StringUtil.read_uint32(payload[offset..(offset + 3)])
offset += 4

# Authentication plugin data (first 8 bytes)
auth_plugin_data_part1 = MysqlReplicator::StringUtil.read_str(payload[offset..(offset + 7)])
offset += 8

# Reserved (1 byte, always 0x00)
offset += 1

# Server capability flags (lower 2 bytes)
capability_flags_lower = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
offset += 2

# Character set (1 byte)
charset = MysqlReplicator::StringUtil.read_uint8(payload[offset])
offset += 1

# Status flags (2 bytes)
status_flags = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
offset += 2

# Server capability flags (upper 2 bytes)
capability_flags_upper = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
offset += 2

# Feature flags
capability_flags = capability_flags_lower | (capability_flags_upper << 16)

# Authentication plugin data length (1 byte)
auth_plugin_data_len = MysqlReplicator::StringUtil.read_uint8(payload[offset])
offset += 1

# Reserved (10 bytes)
offset += 10

# Authentication plugin data (part 2)
remaining_auth_data_len = [auth_plugin_data_len - 8, 13].max
auth_plugin_data_part2 = MysqlReplicator::StringUtil.read_str(payload[offset..(offset + remaining_auth_data_len - 1)])
offset += remaining_auth_data_len

# Authentication plugin name (null-terminated string)
plugin_name_end = payload.index("\0", offset)
auth_plugin_name = MysqlReplicator::StringUtil.read_str(payload[offset...plugin_name_end])
auth_plugin_data = auth_plugin_data_part1 + MysqlReplicator::StringUtil.read_str(auth_plugin_data_part2[0..11])
# Adjust 20 bytes
if auth_plugin_data.length > 20
  auth_plugin_data = auth_plugin_data[0..19] || ''
elsif auth_plugin_data.length < 20
  auth_plugin_data += "\x00" * (20 - auth_plugin_data.length)
end

caching_sha2_password 認証について

Handshake パケットを取得できたらいよいよ認証を通せる。

あらためて caching_sha2_password 認証の処理フローはこんな感じになる。

  1. caching_sha2_password 認証用ペイロードを作ってパケット送信
  2. レスポンスを受け取って、高速認証が使われていればここで認証完了
  3. RSA 暗号化のために、公開鍵をリクエストするパケットを送信
  4. 公開鍵を受け取って、パスワードを RSA 暗号化する
  5. RSA 暗号化したパスワードでペイロードを作ってパケット送信
  6. 認証完了

仕様に従って、ペイロードの作成、解析を行っていけばいい。

caching_sha2_password 認証用ペイロードの構造

サイズ フィールド名
4 bytes ケイパビリティフラグ クライアントがサポートする機能のビットマスク
4 bytes 最大パケットサイズ クライアントが受信可能な最大パケット長
1 byte 文字セット 使用する文字エンコーディング。utf8mb4だったら「45」
23 bytes 予約領域 すべて0x00で埋める
ユーザー名 NULL終端
認証データ長 認証データのバイト数を長さエンコード整数にしたもの
32 bytes 認証データ 暗号化したパスワード
データベース名 NULL終端。CLIENT_CONNECT_WITH_DBフラグが立っている場合のみ入れる
認証プラグイン NULL終端。caching_sha2_passwordが入る

暗号化したパスワードは、Handshake パケットで受け取ったスクランブル(auth_plugin_data がそう)とパスワードを使って作る。

# SHA256(password)
hash1 = Digest::SHA256.digest(password.encode('utf-8'))
# SHA256(SHA256(password))
hash2 = Digest::SHA256.digest(hash1)
# SHA256(SHA256(SHA256(password)), salt)
hash3 = Digest::SHA256.digest(hash2 + salt)

# XOR hash1 and hash3
payload = ''
hash1.each_byte.with_index do |byte, i|
   payload += (byte ^ hash3[i].to_s.ord).chr
end

ケイパビリティフラグは基本的な接続であればこれで大丈夫。
データベースを指定して接続する場合はフラグを立てる。

CLIENT_PLUGIN_AUTH = 0x00080000
CLIENT_SECURE_CONNECTION = 0x00008000
CLIENT_PROTOCOL_41 = 0x00000200
CLIENT_CONNECT_WITH_DB = 0x00000008
CLIENT_MULTI_STATEMENTS = 0x00010000
CLIENT_MULTI_RESULTS = 0x00020000

client_flags = CLIENT_PROTOCOL_41 |
                       CLIENT_SECURE_CONNECTION |
                       CLIENT_PLUGIN_AUTH |
                       CLIENT_MULTI_STATEMENTS |
                       CLIENT_MULTI_RESULTS
client_flags |= CLIENT_CONNECT_WITH_DB if database.present?

文字セットは Handshake パケットに入っているものを使えばよいはず。

認証用ペイロードの送信に対するレスポンス

1バイト目が「0x00」だったら高速認証成功なのでそこで認証完了として処理を打ち切る。
「0x01」だったら、次の 1 バイトが「0x03」なら高速認証成功、「0x04」なら初回接続なので RSA暗号化を使った後続の認証処理を行う。

first_byte = payload[0].unpack1('C')
case first_byte
when 0x00
  :success
when 0x01
  command = payload[1].unpack1('C')
  case command
  when 0x03
    :success
  when 0x04
    :challenge
  else
    # エラー
  end
else
  # エラー
end

公開鍵をリクエストするパケット

仕様で「0x02」を8ビット符号なし整数でリクエストしろとあるので、それを送るだけ。

public_key_payload = [0x02].pack('C')
send_packet(public_key_payload)

送ったらパケットを受信すると、ペイロードに公開鍵が入っている(ペイロード = 公開鍵)。

パスワードの RSA 暗号化

パスワードとHandshake パケットのスクランブル(auth_plugin_data がそう)で XOR 演算したものを RSA 暗号化して送信する。
Ruby だと OpenSSL ライブラリを使って暗号化できる。

注意として、パディング方式が MySQL 8.0.5 以降とそれ以前で異なるので、場合分けする必要がある。

MySQLバージョン パディング方式
8.0.4 以下 PKCS#1 v1.5
8.0.5 以上 OAEP (PKCS#1 v2.1)

また、パスワードは NULL 終端の文字列にする必要がある(これにハマった)。

require 'openssl'

rsa_public_key = OpenSSL::PKey::RSA.new(public_key)

password_with_null = password + "\x00"
password_bytes = password_with_null.encode(Encoding::UTF_8).bytes
scramble_bytes = scramble.bytes

xor_result = []
password_bytes.each_with_index do |byte, index|
  scramble_byte = scramble_bytes[index % scramble_bytes.length]
  xor_result << (byte ^ scramble_byte)
end
data_to_encrypt = xor_result.pack('C*')

begin
  # First, try OAEP padding (MySQL 8.0.5+)
  rsa_public_key.public_encrypt(data_to_encrypt, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
rescue OpenSSL::PKey::RSAError
  # If OAEP fails, use PKCS#1 (MySQL 8.0.4 and earlier)
  rsa_public_key.public_encrypt(data_to_encrypt, OpenSSL::PKey::RSA::PKCS1_PADDING)
end

この RSA 暗号化したパスワードを送信すれば認証成功のレスポンスが取れるはず!
認証さえ終われば後は自由に MySQL を扱えるようになる。

別記事で RubyMySQLSQL を発行するやり方を書く。

2024年末

そういえば今年の年初って1/1だよね(あたりまえ)、早すぎ!

コードを書く感覚が戻った

前職を辞める前2~3年くらいはほんとに不具合改修とか障害起きたときに調査するくらいしか開発に触れてなかったのだけど、まあすぐに勘を取り戻すっしょと気楽に考えてたら、全然パフォーマンスが全然出なくて正直本当に焦った。
設計ドキュメントは全然シュッと作るんだけど、いざコード書いて作るぞという算段で全然手が動かなくなる。
知らず知らずに他人にやってもらうのが染みついてしまってたんでしょうね、重すぎる腰を上げて書き出すと早いんだけど、腰が上がるまでが重すぎて結果パフォーマンスが低すぎてやばかった。
なんだかんだ春くらいには数年くらいまでの感じが戻って、コード書き始めるまでの速も出るようになったので本当によかった。
他人に任せるが染みつくと取れるまで半年かかるんだなあと実感しました。

お手伝い

春くらいからは元々顧問として関わっていた会社さんの開発のお手伝いをさせてもらっている。
社員/業務委託さん関係なくガシガシオーナーシップ持って開発ができる希少なやりやすい環境に恵まれたと思う。
業務としてNext.jsやVercelを触れることで新しい知見も溜めることができた。
障害や問題が起きたときは相変わらずという感じで参戦したりしている。

技術顧問も意外と声をかけてもらうことが多く、これまで4社関わらせてもらったうち3社継続している。
以前外で話していたことがきっかけで声をかけてもらったりもあるので、外で話すのも大事なんだなあ。
全部の会社で期待されることは微妙に違っていて、フルスタックに過ごしてきた経験が生きるとはーという感じだ。
なんと技術顧問としてエンジニア面接に出ることも数回あり、いろんな関わり方があるなという学び。

自社

前半は一番基礎になるところを作って、後半はインフラ周りを作っていた。
これまでいかにSREに頼っていたかを実感させられたが、だいぶ理解が進んで何か起きても何とかできそうな自信がついた。
とんでもない円安なのでとにかくケチケチでやることを意識している。
ステージング環境をどうアクセス制限かけるかというところはCloudflare Zero Trustを使うことになった。
これ無料っておかしくね?とどっかで多分有料になりそうな気がするので気持ちの準備だけはしておこう。
ちょっとした技術的な意思決定としては、Next.jsやめてVite + ReactRouterによるSPAに方向転換した。
最近のNext.js、難しすぎない?RSCフルに生かすアーキテクチャに寄せられる未来が見えなかったので使わないことに。

その他

半年に1回くらい体調崩してしまった。
もういい年なので、だんだん1回体調を崩すと完全にリカバリーするまで1か月くらいかかってしまうので、体調管理ちゃんとしていきたい。
来年はちょっとだけ早寝早起きにして毎日ウォーキングくらいはしていくぞという気持ち。

資金的には潤沢な状態で好きなようにやれているのは本当にありがたいこと。
来年もガシガシコードを集中して書いていきたい。
お手伝い先に貢献しつつ、自社サービスの方もステージングでドッグフーディングしながら作れるようになってきたので、機能揃えていくぞ。

10年勤めたfreeeを辞めて零細企業を作った

日記です。 タイトルの通り10年勤めたfreee株式会社を退職して、自分で会社を作ってやっていくことにした。

やってきたことはこの辺のスライドによくまとまっている。 https://speakerdeck.com/waka/da-kinapurodakutofalseyu-tefang

社員5人から1000人になったり、ARRゼロ円からARR200億円になったり、ヤバかった品質をどうにか底上げしたり、開発本部長の立場で上場を経験したり、普通では経験できないことを濃度高く経験できて楽しく過ごせた10年だった。

freee会計という業務系Webサービスを10年間機能面/パフォーマンス/品質面共に育ててきた経験はあるので、もし経験が役立てそうな機会があればお声がけください。

というわけで零細企業を作って、今後はそっちでプロダクト作りをしていくことにした。
スタートアップではなく、零細企業。
資金調達はしない=人は本当に必要になったタイミングでしか増やさない、ということをハッキリさせたく株式発行が起きない合同会社を選択した。
他社さんの開発を手伝いながら、自分たちのサービスを作っていく、多分世の中によくある形式で当面やっていく。
設立間もない会社ではあるが、どう運営していくかの意思決定を自分たちで自由に行えるくらいの余裕を持てているのは、前職でのキャピタルゲインを原資(=給料等)に回せているのが大きいので、がむしゃらに働いてよかったなと思えている。

社名を決めた

社名はもう一人の共同創業者と話して「合同会社 匠のあそび」とした。
ずっと愛着を持てるものでないといけないので、1か月くらいじっくり検討して決めた。
社名が示す通り、コンセプトはものづくりをする人が遊ぶように働ける会社にしていきたい、というもの。
楽に稼ぐ方法は世の中に存在しないので、楽ではないことを楽しくやる。
真面目に開発しつつ、オフィスのQoLを高めたり、気分がリフレッシュできる施策や関連深いOSSやイベントに寄付したり、といった自分たちや世の中に還元することで充実感が得られる施策は積極的に考えて投資していきたい。

オフィスを借りた

自分はリモートだと無意識にサボってしまうことが分かっているのと、遊ぶように働くんだったら遊ぶ場は必要だよなとオフィスを借りた。
シュッと通える範囲でオフィス探しをしていたところ、自由が丘でよい物件が見つかったので大満足。

エンジニアなので作業空間はケチらずに使いたいものを買った結果、自宅環境より快適になったので通勤するモチベも下がらなくてよい。
ちなみにデスク回りの光量が足りなくて買ったBenQ ScreenBarとHueのリボンライト、目に優しい明るさが得られてマジでおすすめです。

office

作業部屋以外にもう1部屋リラックスできる部屋があって、本当に広さとクオリティと家賃がアンバランスなので、次回更新時に家賃上がらないか怖い。
お茶菓子くらいしか用意できませんが、いつでも遊びにきてください。

自分たちが使うサービスを作る

何を作るかは既に決めていてある程度動くものは出来つつあるが、基本的には自分たちが直接使うものを外にも提供できる形で作っていこうと思っている。
2人しかいないのでPM専属は雇えない以上、自分たちが機能企画もやらないといけない。そうした状況で改善し続けるモチベーション的にも、ユーザーテストのコストを減らせる意味でも、自分たちが使うものの方がやりやすい。
これまでグループウェア、会計とやってきているが、次はWebではあるけどまたちょっと特徴が違うものになりそうで、リーズナブル且つ小さくて拡張性のあるアーキテクチャを一から考えて実装していくのは楽しい。
ググってもあまり出てこないような知見がそこそこ発生するので、どこかで公開していくつもり。
何年でARR○○億いくぞ!みたいな大きな野望はなくて、自分たちがサービス売上のみで余裕持って過ごせるくらい稼げればいいので、現実的なスピード感で使うものを公開して、同じように刺さった人たちに長く使ってもらえるものにしていきたい。

業務委託もやる

さすがに外貨ナシでは原資がきついので、他社サービスの開発も手伝っていく。
前職を辞める少し前から2つほどスカウト媒体のプロフィールを公開してみたところ、声をかけてくれる会社があってとてもありがたい限り・・
結果として、1社で業務委託としてサービス開発、1~2社で顧問のようなことをさせてもらって、当面の売上を稼げることになった。
こっちで新しい知見を得たいとかはなく、これまでの経験を還元できるところで価値を感じてもらえればよいと思っているので、これまで経験している技術スタックと技術/組織課題が活きそうで、話していて相性がよさそうだった会社さんに決めた。
その他としては、スクラムを採用していない、開発用マシンが指定されない、という2点を譲れないポイントとして置いていた。
前者はスクラムイベントによる強制拘束時間で時間の使い方の自由度がどうしても減ること、後者は自社開発と合わせてマシンが2台になる不便さが許容できなかったため。
大きめの会社だとスキルの均一化の都合上スクラムを採用しがちだし、内部統制の都合上マシンは支給になりがちですね。
全てを満たしてくれるところと無事に契約に至れてほっとしている。

というわけで心機一転頑張って開発していくぞ。

ブログの実装をWrangler v1からWrangler v3にアップデートした

このブログは[Cloudflare Workersで配信している](https://waka.hatenablog.com/entry/2020/11/08/000000のだけど、ライブラリや実装の見直しをサボりにサボっていたため3年ぶりに更新した・・

開発環境でもあり、CLIツールでもあるWranglerがv3になっていたため、更新。
v3はリリースされている割りにまだexperimentalな機能も多いけど、個人ブログだしどうせ近いうちに上げることを考えるとv3を人柱的に使えばいいかなと。
Wranglerはv2からminiflareを使ったローカルでのエミュレート実行をサポートするようになり、v3でworkerdを使ったローカルでのWASM環境での実行をサポートするようになっていた。
以前は毎回開発環境にデプロイが必要だったので、これは本当に便利。ローカルKVもサポートしてるじゃん・・(ローカルでSQLiteファイルが作られ保存される)

以下にv1 -> v3に上げたことで修正した内容をメモしておく。

カスタムビルドツールがwebpackからesbuildに変わっていたため、webpack.config.jsを廃止。
webpackはDefinePluginでRSSのlastBuildAtやGitHubのアクセストークンを埋め込むために使っていただけだったので、「--var」オプションでビルドコマンド実行時に渡すように変えた。

TypeScriptをデフォルトでサポートしているので、こんな感じで指定すればよい

$ npx wrangler dev src/index.ts --env dev --var BUILD_DATE:$(date -d day '+%Y-%m-%d')

Node.jsのモジュールをコード内でrequireなどして使っている場合は、wrangler.tomlに node_compat = true を指定する必要がある。このブログだと中でpathモジュールを使っていたため指定した。

zone_idの指定が廃止されていたため、routeを使っている場合、zone_nameを指定する必要がある。
このブログでは本番環境で独自ドメインを割り当てて使っているため、指定した。

[env.production]
route = { pattern = "*waka.dev/*", zone_name = "waka.dev" }
kv_namespaces = [
  { ... }
]

kvのバルク実行で渡すJSONファイルのフォーマットが、以前は { "name": "キー名", value: "" } の配列だったのが、キー名の配列のみに変わっていた。
なので、KVのキャッシュをクリアしたい場合はこんな感じで指定すればいい。

$ npx wrangler kv:key list --binding=$YOUR_KV_NAMESPACE --env=production | xargs -0 -i node -pe 'JSON.stringify({}.map(a => { return a.name; }))' > cache.json
$ npx wrangler kv:bulk delete --env=production --binding=$YOUR_KV_NAMESPACE cache.json

実装はほぼ変えずに済んだのだけど、Wrangler v3でESM形式をサポートしていたので、合わせる形にした。
以前はWebWorker形式で、fetchイベントリスナを書く感じだったけど、fetchメソッドを実装してdefault exportすればよい。

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    return somePromise;
  }
}

Envには前述したビルドコマンド実行時に指定した「--var」のKeyValueが入っているので便利に使える。 (Envの型定義は自前で定義しておく必要がある)

また、KV等のbindingを使っている場合、envから呼び出す形になる。

const value: valueの型 | null = await env.YOUR_KV_NAMESPACE.get(key, 'json');

ミラーレス一眼買った

今の情勢だと人が多いところに出かけづらいので、人がいない場所でも楽しめる趣味としてカメラを買った。

レンズ交換式のカメラを買うのは2回目で、大昔デザイナーやってた頃に素材くらい自分で用意できるようになるかーとNikonの一眼レフを持っていた時もあったのだけど、本体だけで1kg強、ちょっと明るいレンズと合わせると3kg近くと重すぎて、自分はすぐ手放してしまったのであった。

というわけで今回は気軽に持ち運べる重さかどうかを最重視して買った。最近のミラーレス一眼は500g切る軽いやつも結構あって選ぶのが大変で楽しかった。
1か月くらい経ったが、週末はシュッとカバンに入れて適当にパシャパシャ撮りまくって楽しめているので、今のところは狙い通りで正解だったと思える。

当たり前だけどスマフォと違うのは圧倒的な解像度の違いで、適当に撮ってもいい感じにボケてくれるし自分が上手くなったと錯覚するようなのが撮れるので楽しいw

下の画像はみなとみらいをちょっと高いところから撮ったやつ。

yokohama

自分はFUJIFILMを買ったのだけど、シャッタースピードとかISOとか絞りを液晶じゃなくて全部ダイアルで操作するので昔のフィルムカメラみたいなマニュアル感があって好き。 とはいえ、スナップメインなのとものぐさなので絞り優先オートで撮りまくってるわけですが・・

びっくりしたのはAPS-Cとはいえ2600万画素!フルサイズと比べるとレンズ小さいので軽くなるにも関わらずこの画素数はすごい。
本体はとにかく軽く、レンズはそこまで大きくなくて好みの写りするやつでというのが自分には合っているようだ。
レンズはFUJIFILM初めてなので無難に標準画角の35mm(フルサイズ換算50mm)単焦点を買ってみたけど、いろいろな操作に慣れてきたら広角とかズーム買ってスナップだけじゃなくて風景や大きな建造物とかも撮りにいきたいなという感じ。