MySQL Protocolに入門する

ふと気になった。

ローカル開発環境でMySQLに接続するときにはTCPunix domain socketを使用していた。ここで、例えばAPIにアクセスするときにはさらにHTTPなどのプロトコルを使用することになるが、MySQLでは一体何のプロトコルを使用しているんだろうということが気になった。HTTPではなさそうだし。

というわけで調べていくとMySQL ProtocolというMySQL独自のプロトコルを使用していることがわかった。

MySQL :: MySQL Internals Manual :: 14 MySQL Client/Server Protocol

では具体的に見ていこうということで、ここではまずMySQL接続時にサーバから送られるInitial Handshake Packetから解析する。次に、Handshake Response Packetを構築してクライアントからサーバに送信し、OK_packetが返ってくるところまで確認する。

コードとその出力結果は以下の通り(usernamepasswordはブログを書くときに変更している)。

package main

import (
    "bufio"
    "bytes"
    "crypto/sha1"
    "encoding/binary"
    "encoding/hex"
    "fmt"
    "io"
    "net"
)

const (
    CLIENT_PLUGIN_AUTH       = 0x00080000
    CLIENT_SECURE_CONNECTION = 0x00008000
    CLIENT_PROTOCOL_41       = 0x00000200
    CLIENT_LONG_PASSWORD     = 0x00000001
    CLIENT_TRANSACTIONS      = 0x00002000
    CLIENT_LONG_FLAG         = 0x00000004
)

var (
    username = "xxx"
    password = "xxx"
)

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:3306")
    if err != nil {
        panic(err)
    }
    defer conn.Close()
    reader := bufio.NewReader(conn)

    // Parse connection phase packets
    // See https://dev.mysql.com/doc/internals/en/connection-phase-packets.html
    header := make([]byte, 4)
    reader.Read(header)
    fmt.Println("[header]")
    fmt.Printf("%s\n", hex.Dump(header))

    payloadSize := int(uint32(header[0]) | uint32(header[1])<<8 | uint32(header[2])<<16)
    payload := make([]byte, payloadSize)
    reader.Read(payload)
    fmt.Println("[payload]")
    fmt.Printf("%s\n", hex.Dump(payload))

    offset := 0
    protocolVersion := int(payload[offset])
    fmt.Printf("  [protocol_version] %d\n", protocolVersion)
    offset++

    idx := bytes.IndexByte(payload[offset:], 0x00)
    serverVersion := payload[offset : offset+idx]
    fmt.Printf("  [server_version] %s\n", serverVersion)
    offset += idx + 1

    connectionID := binary.LittleEndian.Uint32(payload[offset : offset+4])
    fmt.Printf("  [connection ID] %d\n", connectionID)
    offset += 4

    authPluginDataPart1 := payload[offset : offset+8]
    fmt.Print("  [auth-plugin-data-part1]\n")
    fmt.Printf("    %s", hex.Dump(authPluginDataPart1))
    offset += 8

    filter := int(payload[offset])
    fmt.Printf("  [filter] %d\n", filter)
    offset++

    capabilityLower := payload[offset : offset+2]
    offset += 2

    characterSet := int(payload[offset])
    fmt.Printf("  [character set] %d\n", characterSet)
    offset++

    statusFlags := binary.LittleEndian.Uint16(payload[offset : offset+2])
    fmt.Printf("  [status flags] %d\n", statusFlags)
    offset += 2

    capabilityUpper := payload[offset : offset+2]
    offset += 2

    capabilities := binary.LittleEndian.Uint32(append(capabilityLower, capabilityUpper...))

    var authPluginDataPart2 []byte
    var authPluginName []byte
    if capabilities&CLIENT_PLUGIN_AUTH > 0 {
        lengthOfAuthPluginData := int(payload[offset])
        fmt.Printf("  [length of auth-plugin-data] %d\n", lengthOfAuthPluginData)
        offset++

        // skipped reserved 10 bytes
        offset += 10

        if capabilities&CLIENT_SECURE_CONNECTION > 0 {
            lengthAuthPluginDataPart2 := lengthOfAuthPluginData - 8
            if lengthAuthPluginDataPart2 < 13 {
                lengthAuthPluginDataPart2 = 13
            }
            authPluginDataPart2 = payload[offset : offset+lengthAuthPluginDataPart2]
            fmt.Print("  [auth-plugin-data-part2]\n")
            fmt.Printf("    %s", hex.Dump(authPluginDataPart2))
            offset += lengthAuthPluginDataPart2

            idx = bytes.IndexByte(payload[offset:], 0x00)
            authPluginName = payload[offset : offset+idx]
            fmt.Printf("  [auth-plugin name] %s\n", authPluginName)
            offset += idx + 1
        }
    } else {
        panic("not supported")
    }

    // Handshake response

    // Secure Password Authentication
    // See https://dev.mysql.com/doc/internals/en/secure-password-authentication.html
    authResponse := sha1Sum([]byte(password))
    tmp := sha1Sum(append(append(authPluginDataPart1, authPluginDataPart2[0:12]...), sha1Sum([]byte(authResponse))...))
    for i := 0; i < 20; i++ {
        authResponse[i] ^= tmp[i]
    }

    bufferSize := 4                       // header
    bufferSize += 4                       // capability flags
    bufferSize += 4                       // max-packet size
    bufferSize++                          // character set
    bufferSize += 23                      // reserved
    bufferSize += len(username) + 1       // username
    bufferSize++                          // length of auth-response
    bufferSize += len(authResponse)       // auth-response
    bufferSize += len(authPluginName) + 1 // auth plugin name + NUL
    buffer := make([]byte, bufferSize)

    offset = 0
    size := bufferSize - 4
    buffer[0] = byte(size)
    buffer[1] = byte(size >> 8)
    buffer[2] = byte(size >> 16)
    buffer[3] = 0x01
    offset += 4

    // Capability flags
    // See https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
    capabilityFlags := uint32(CLIENT_PROTOCOL_41 |
        CLIENT_SECURE_CONNECTION | CLIENT_LONG_PASSWORD |
        CLIENT_TRANSACTIONS | CLIENT_LONG_FLAG)
    buffer[offset] = byte(capabilityFlags)
    buffer[offset+1] = byte(capabilityFlags >> 8)
    buffer[offset+2] = byte(capabilityFlags >> 16)
    buffer[offset+3] = byte(capabilityFlags >> 24)
    offset += 4

    offset += 4 // Skip max-packet size

    charSet := 0x21
    buffer[offset] = byte(charSet)
    offset++

    offset += 23 // Skip reserved

    copy(buffer[offset:], username)
    offset += len(username) + 1

    buffer[offset] = byte(len(authResponse))
    offset++

    copy(buffer[offset:], authResponse)
    offset += len(authResponse) + 1

    copy(buffer[offset:], authPluginName)

    fmt.Printf("\n[write packets: %d bytes]: \n%s\n", len(buffer), hex.Dump(buffer))
    conn.Write(buffer)

    header = make([]byte, 4)
    reader.Read(header)
    fmt.Println("[header]")
    fmt.Printf("%s\n", hex.Dump(header))

    payloadSize = int(uint32(header[0]) | uint32(header[1])>>8 | uint32(header[2])>>16)
    payload = make([]byte, payloadSize)
    reader.Read(payload)
    fmt.Println("[payload]")
    fmt.Printf("%s\n", hex.Dump(payload))
}

func sha1Sum(data []byte) []byte {
    h := sha1.New()
    io.Copy(h, bytes.NewReader(data))
    return h.Sum(nil)
}
[header]
00000000  4a 00 00 00                                       |J...|

[payload]
00000000  0a 35 2e 37 2e 32 32 00  03 00 00 00 58 51 61 25  |.5.7.22.....XQa%|
00000010  52 49 5b 63 00 ff ff 21  02 00 ff c1 15 00 00 00  |RI[c...!........|
00000020  00 00 00 00 00 00 00 12  3d 25 6e 23 01 66 6c 40  |........=%n#.fl@|
00000030  1f 63 36 00 6d 79 73 71  6c 5f 6e 61 74 69 76 65  |.c6.mysql_native|
00000040  5f 70 61 73 73 77 6f 72  64 00                    |_password.|

  [protocol_version] 10
  [server_version] 5.7.22
  [connection ID] 3
  [auth-plugin-data-part1]
    00000000  58 51 61 25 52 49 5b 63                           |XQa%RI[c|
  [filter] 0
  [character set] 33
  [status flags] 2
  [length of auth-plugin-data] 21
  [auth-plugin-data-part2]
    00000000  12 3d 25 6e 23 01 66 6c  40 1f 63 36 00           |.=%n#.fl@.c6.|
  [auth-plugin name] mysql_native_password

[write packets: 90 bytes]: 
00000000  56 00 00 01 05 a2 00 00  00 00 00 00 21 00 00 00  |V...........!...|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 6f 61 75 74  68 5f 75 73 65 72 00 14  |....oauth_user..|
00000030  ff 59 45 be 61 d8 e2 0b  b3 8a 30 fb ea 5a 3f 05  |.YE.a.....0..Z?.|
00000040  2b dd fe 97 00 6d 79 73  71 6c 5f 6e 61 74 69 76  |+....mysql_nativ|
00000050  65 5f 70 61 73 73 77 6f  72 64                    |e_password|

[header]
00000000  07 00 00 02                                       |....|

[payload]
00000000  00 00 00 02 00 00 00                              |.......|

以下、各やりとりの補足。

MySQL Packet

MySQL Client/Serverで通信するときのパケットの構成は、

  • 先頭3バイトはpayload_length
  • 次の1バイトはsequence_id
  • 残りがpayload

となっている(コード上では先頭4バイトをheaderと呼んでいる)。

MySQL :: MySQL Internals Manual :: 14.1.2 MySQL Packets

Initial Handshake Packetでの先頭4バイトが4a 00 00 00となっており、ここではpayload_lengthが4a 00 00である。最初、なんで最後じゃなくて先頭が4aになっているんだと思っていたが、little endian表記だとこうなるとのこと。

エンディアン - Wikipedia

Initial Handshake Packet

ここではplain-handshakeに従っている。

MySQL :: MySQL Internals Manual :: 14.2.1.1 Plain Handshake

Initial Handshake Packetはこちらを参照するといい。

https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake

Handshake Response Packet

Handshake Response Packetはこちらを参照するといい。

https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse

OK_packet

OK_packetはこちらを参照するといい。

https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html

参考

MySQLユーザーのためのMySQLプロトコル入門 | GREE Engineers' Blog

go vetでprintfuncsオプションを使った

go vetコマンドはPrintfのようなメソッドに対してフォーマットで表示する変数の数とその引数の数が一致しない場合(以下参照)にチェックしてくれる。

fmt.Printf("%v, %v\n", "test")

しかし、例えば標準logパッケージをラップするパッケージを作った際に、同様にフォーマットを引数で受け取るメソッド(以下、Debugfメソッド)を定義しても上記と同様のチェックをgo vetコマンドはデフォルトでは行ってくれない。

package testlog

...

func Debugf(format string, v ...interface{}) {
    l.Output(2, fmt.Sprint("[DEBUG] ", fmt.Sprintf(format, v...)))
}

ここで、Debugfメソッドに対しても同様のチェックを行うために-printfuncsオプションを使用する。具体的には以下のようにオプションを設定すればDebugfメソッドに対してもチェックをしてくれる。

go vet -printfuncs Debugf ./...

なお、複数メソッドをオプションに追加したい場合は,で繋ぐといい。

SchemeインタープリタをGoで書いた

背景

Structure and Interpretation of Computer Programs(SICP)を読んでいた。第4章でメタ循環評価機という話題が上がる。これはSchemeインタープリタSchemeで書くという、一見矛盾しているようなそんなものである。一旦、そのまま本を読み進めSchemeのコードを写経していき、Schemeのメタ循環評価機を作った。

しかし、SchemeSchemeインタープリタを書く、というのはどうにも自分の中で腹落ちしなかった。自分はインタープリタを書けた、また理解できた、という実感が得られなかった(これは単に写経しただけであったというのもあると思うが)。

このため、SchemeインタープリタScheme以外の言語で書くことにした。ここでは最近手に馴染んでいるGoで書くことに決めた。

goscheme

最低限の機能しかなく、またREPLのみしか対応していないが、書いたコードはGitHubに上げている。

github.com

メタ循環評価機を書いたときとの大きな違いは、LexerとParserを自分で書かなければならなかったことである。

Schemeのメタ循環評価機ではLexerとParserは必要ないためSICPにサンプルコードはなかったが、言語実装パターンに簡単なLL(k)パーサ書いて感覚を掴んでいたので、比較的スムーズにSchemeに対するLexerとParserを書くことができた。

また、既にSchemeインタープリタのGo実装は何人もの方によって書かれているが、特にsuzuken/gigueの実装が参考になった。感謝。

github.com

プロセスが環境変数をもつ

勘違いしていたのだがプロセスが環境変数を保持している。ユーザが持っているわけではない。

supervisodをrootユーザで実行し、その子プロセスが実行するアプリケーションは別ユーザで実行するようにsupervisor.confを設定( http://supervisord.org/configuration.html#program-x-section-valuesuserを参照)するようにした。そして、アプリケーションで使用する環境変数を実行ユーザの.bash_profileexport XXX=xxxのように記載して設定するようにしていた。しかし、アプリケーションのログを見ると、設定した環境変数が読み込まれていないようであった。最終的には/proc/{pid}/environを確認し、所望の環境変数が設定されていないことがわかった。

なぜ環境変数が設定されなかったかというと、冒頭に戻るが、そもそもユーザではなくプロセスが環境変数を保持しているためである。この場合だと、supervisordを実行したrootユーザ(のbashプロセス)の環境変数が子プロセスであるアプリケーションのプロセスにそのまま引き継がれる。このため、supervisorのドキュメントにも記載されている(以下、引用)が、実行ユーザを変更するときにはsetuidしているだけで実行ユーザの.bash_profileなどは実行されない。このため、実行ユーザの.bash_profile環境変数を設定してもアプリケーションのプロセスが保持する環境変数に設定が反映されない。

The user will be changed using setuid only. This does not start a login shell and does not change environment variables like USER or HOME. See Subprocess Environment for details.

いつも環境変数を設定するときはそのユーザのホームディレクトリにある.bash_profile.bashrcに設定を記載しており、なんとなくユーザが環境変数をもっていると誤解したため、このような間違いをやってしまったのではないかと思う。

ここで正しく環境変数を設定する方法とは、rootユーザに環境変数を設定してsupervisordを実行し直すか、supervisor.confのenvironment環境変数を設定するか、のどちらかである。

なお、Advanced Programming in the UNIX Environmentを確認したが、環境変数はstackやheapと同様にプロセスのメモリに格納される。

the environment strings are typically stored at the top of a process’s memory space, above the stack.

GoのバイナリをデプロイするためにGitHub ReleaseからDLするツールをGoで書いた

GoのバイナリをデプロイするためにGitHub Enterprise(以下、GHE)のReleaseにアップロードしておいたバイナリをダウンロードするツールをGoで書いた話をする。

背景

複数の開発が並行していてGitHubのブランチと本番・ステージ環境が対応付けできなかったり、一方はオンプレで一方はAWSでといった環境の違いによるネットワーク的な問題から、CIでテスト・ビルドまでは自動化できた一方でデプロイまでは自動化できなかった。このため、CIでのビルドで作成したGoのバイナリをGHEのReleaseにアップロードした後に、手動でデプロイ(本番環境からGHEのReleaseに登録されたバイナリをダウンロード)することにした。

ここで、GHEのReleaseからバイナリをダウンロードするツールが必要になったので最初はシェルスクリプトで書こうとしたが、GitHub APIを叩く必要があるためJSONの取り扱いをシェルスクリプトで行うのが面倒だと感じた。jqコマンドを使用すればいいことはわかっていたが、全てのサーバにjqコマンドをインストールしているわけではなかった。jqコマンドをインストールすればいいのだが、この時点でサーバのConfigurationが自動化されていなかったため、jqコマンドがインストールされていることを保証できなかった。

このため、単にGHEのReleaseからバイナリをダウンロードするだけなのだが、このためのツールをGoで書くことにした。一つのバイナリだけで動作するのが使い勝手がいいと思ったので。

比較

自分で作らずとも既にあるツールを使えばいいとも思ったが、いろいろと探してはみたがタグを目印にGitHub Releaseからバイナリをダウンロードするツールがなかった(見つけられなかった)。そもそもこのようなデプロイをする人はあまりいないだろうから誰も作っていないのかな。このため、自分でツールを作ることにした。

話は逸れるが、逆に、バイナリを作成してGitHubのReleaseにアップロードするためのツールはいくつか見つけた。

github.com

github.com

ghrd

作成したツールはこれ。実際には仕事で書いたものを一般的に使えるように一から書き直したので、背景で書いたツールと中身としては別物であるが。

github.com

使い方は以下の通り、タグを目印にGitHub Releaseから対象をダウンロードする。

ghrd -u [OWNER] -r [REPOSITORY] [TAG]

また、環境変数を指定することでGitHub Enterpriseでも使用できる。

作った後に考えたこと

自分はサーバサイドの開発をメインにしている。だが、今回デプロイ用のツールを書いたようにリリース・インフラにも手を出すことで、普段この辺りを運用しているインフラチームとの仕事が進めやすくなったように感じた。

以前読んだ記事( Full-stack developers ) で、フルスタックエンジニアは現実的ではないがフロントからインフラまでの各チームを横断的に動いて全体の生産性をあげる人が必要とされている(かなり意訳)、と書かれてあった。

まだまだスタート地点に過ぎないが、この記事に書かれてあるようなことを自分としてはやっていきたいと思うようになった。 今後は今回と反対側であるフロントサイド(モバイルを含む)にもチャレンジしていきたいとぼんやりだが考えている。

AWSでアラームの閾値を設定する

GoでLambdaを含むいわゆる一般的なサーバレスな環境(API Gateway <=> Lambda <=> DynamoDB + CloudWatch)を構築した。監視のためCloudWatchでアラームを設定する必要があったのだが、そのアラームでの閾値の考え方がよくわからなかったのでメモする。アラームの設定画面は以下の通りである。

f:id:takayukinakata:20180330194828p:plain

こちらの記事が理解する上で大変参考になった。

AWS CloudWatch Alarmの判定方法

上記の記事でのPeriodが画面の間隔にあたる。また、Evaluation Periodsが画面の期間にあたる。

CloudWatch アラームは、1分毎にその時点から Period に指定された期間を遡り、範囲内のデータポイントから Maximum 等の値を算出するとのこと。各データポイントの最大値が画面の次の時" "がのところで評価された値に少なくとも一つ当てはまればアラームが実行される。

UIの更新はmain threadから行う

iOSからAPIにリクエストを送って返ってきたjsonを画面に表示させたい、というシンプルな実装をしようとしたら詰まったのでメモする。まず、以下のように実装したがエラーが発生した。

 @IBOutlet weak var myTextView: UITextView!
    func makeData() {
        let url = URL(string: "https://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jpn&APPID=xxxxx")!
        let req = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: req) { (data, res, error) in
            guard let data = data else {
                return
            }
            do {
                let obj = try JSONSerialization.jsonObject(with: data, options: [])
                self.myTextView.text = "\(obj)"
            } catch let e {
                print(e)
            }
        }
        task.resume()
    }

デバッガに表示されたエラーは以下の通り。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Only run on the main thread!'

main thread

iOSではmain threadという考え方があり、UIの描画や更新などはこのmain threadで行なっている。また、UIの更新はmain threadで行う必要がある(ここではmainではないthreadをbackground threadと呼ぶことにする)。 この辺りに関しては、GCDも含めてIntro to iOS threading.での説明がわかりやすかった。日本語だとmixiの研修資料がよかった。

上記のコードでいうと、URLSessionを使用した通信はbackground threadで行われており、background threadでself.myTextView.text = "\(obj)"のようにUIを更新しようとしたためエラーが発生していた。

では上記のコードでどのようにmain threadでUIを更新するかというと、DispatchQueue.main.syncを利用する。DispatchQueue.main.syncのブロック内に記述されたコードはmain threadで実行される。修正後のコードは以下の通り。

 @IBOutlet weak var myTextView: UITextView!
    func makeData() {
        let url = URL(string: "https://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jpn&APPID=xxxxx")!
        let req = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: req) { (data, res, error) in
            guard let data = data else {
                return
            }
            do {
                let obj = try JSONSerialization.jsonObject(with: data, options: [])
                DispatchQueue.main.sync {
                    str = "\(obj)"
                }
            } catch let e {
                print(e)
            }
        }
        task.resume()
    }

GitHub Enterpriseからreviewdogを使用できないときに見るメモ

GitHub Enterpriseのバージョンが古い場合、新しいreviewdog( > 0.9.7)が動作しないので0.9.6のreviewdogを試してみるとよい。

GitHub & GitHub Enterprise Support: As of version 0.9.7, reviewdog completely switched to use new Pull Reqeust Review Comments API including GitHub Enterprise. If your GitHub Enterprise version is too low to use this new API, please use reviewdog version < 0.9.7

https://github.com/haya14busa/reviewdog/releases より引用

参考

github.com

RFC2822のフォーマットでテキストを作成するパッケージを書いた

前にgmail APIを叩いた時にRFC2822について調べたが、絶対にフォーマットを忘れる自信があったので簡単なパッケージとしてまとめた。 特にメールのヘッダーに日本語を使いたい時のフォーマットなんて覚えて置ける自信がまるでなかったので。

ブログで記事として残すのもいいが、こんな感じでパッケージとして残しておいた方が後々使いたくなった時に便利かなと思った。今回くらい小さなパッケージであれば、コード書くのも文章書くのもそんなに労力は変わらないし。

github.com

参考

takayukinakata.hatenablog.com

Serverless Frameworkで複数AWSアカウントを切り替えられるようにする

serverless.com

AWSのアクセスキーを登録すると~/.aws/credentialsに情報が登録される。登録方法は以下を参照。

https://serverless.com/framework/docs/providers/aws/guide/credentials#setup-with-serverless-config-credentials-command

通常だと上記リンク先のコマンドで登録した一つのアクセスキーのみ管理することになるが、複数のAWSアカウントに対するアカウントを管理したい場合は以下のように~/.aws/credentialsを書き換える。

https://serverless.com/framework/docs/providers/aws/guide/credentials#use-an-existing-aws-profile

[default]
aws_access_key_id=***************
aws_secret_access_key=***************

[production]
aws_access_key_id=***************
aws_secret_access_key=***************

デプロイ時にproductionのアクセスキーを使用したい場合は以下のように--aws-profileオプションを設定する。

https://serverless.com/framework/docs/providers/aws/guide/credentials#using-the-aws-profile-option

$ serverless deploy --aws-profile production

--aws-profileオプションを指定しなければdefaultのアクセスキーが使用される。