2018年振り返り

1月

2017年末あたりから書いてたGoのAPIを引き続き書いてた。あと、デプロイまわりをそれなりにまわせるように環境を整えていた。

5月

GoのAPIも自分なしで運用できるようになったのと、諸々の事情で社内で自分が積極的に関われるお仕事が尽きてきたので、上長にヒアリングしながら社内での仕事探しを始めた。この結果として、iOSでアプリのX対応をやることになった。ちょっと触っただけなので、仕事でiOSアプリ開発やれと言われたらできないが、多少iOSエンジニアとお話しできるようにはなった。

Lambda(Go)やDynamoDBを利用した簡単なアプリを引き継いで書いていった。

7月

GoでOAuth認証サーバ開発に参加した。メインどころは他のメンバーが書いてくれていたので、レビューとOracleとの接続まわりを中心にやった。

9月

転職した。RailsでWebアプリケーションを書くことになった。

10月

Heroku上のアプリをHeroku Enterpriseで使用できるPrivate Spaceへ移設する作業をやった。Heroku自体まともに触るのは初めてだったので既存アプリの仕様把握や検証含めて1ヶ月くらいかかったが、楽しかった。

2019年

2019年のゆるい目標を書いていく。

  • クラウドネイティブ
    • 個人的にサーバサイドからインフラ領域に興味があって、マネージャーやシニアエンジニアとの1on1でもそのことを都度伝えたりしている。また、現職で全社的にクラウドネイティブやっていくという流れができている。このため、うまいことこの流れにのっていって、現在担当しているサービスの基盤整備をやっていきつつ、その流れでk8sなどに手を出せたらなと考えている。
  • 英語のリスニング
    • 2018年は技術書は英語で読んでいく、というのをやっていてだいぶ習慣化した。これに伴って普段の情報収拾も英語を読んでいくのが当たり前になってきたが、次は動画でつまづくようになってきた。特に、海外カンファレンスの資料を漁ると動画しかないものもよくあって聞いても全然頭に入ってこなくて困った。なので、2019年は英語のリスニングをやっていきたい。
  • ジム
    • 子育てに重点的にリソースを割くためジム通いが完全になくなってしまった1年だった。しかし、1年行かなくなると熱意も薄れるかと思っていたがそんなことはなく、むしろ再開したい気持ちが強くなった。直近で引越しする予定がありジムに近くなるので今年は再開したい。

クライアント証明書はオプションだったのか

GoでHTTPSサーバをシュッと書いて、curl --cacertで動作確認したら普通に動いたんだが、そういえばcurlはクライアント証明書送ってないんじゃないかって疑問が出てきた。

結論から言うと、サーバからクライアントへの証明書要求はオプションであって、ListenAndServeTLSではデフォルトで要求しないになってるからクライアントの証明書は不要だった。RFCで確認した。

https://tools.ietf.org/html/rfc5246#section-7.4.4

ちなみに、Goでサーバからクライアントへの証明書要求を行う場合、tlsパッケージのClientAuthTypeをサーバに設定することで要求できる。

最後に、クライアント証明書なしで自己署名した証明書を使ったシンプルなHTTPSクライアント・サーバをGoで書いた。

server.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/index", index)

    err := http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
    if err != nil {
        log.Fatalln(err)
    }
}

func index(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, https!")
    w.Write([]byte("Hello, https!"))
}

client.go

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    caCert, err := ioutil.ReadFile("../server/server.crt")
    if err != nil {
        panic(err)
    }
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs: certPool,
        },
    }
    client := &http.Client{
        Transport: tr,
    }
    res, err := client.Get("https://localhost:8443/index")
    if err != nil {
        panic(err)
    }
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}

Heroku Loginについてメモ

heroku loginで何が起きるか、herokuのgitにgit pushするときにどうやって認証しているか、について気になって調べたのでメモ。

本番データの修正をためらうきもち

例えば、本来 not null であるべきカラムが null可 になってしまっていて、データにnullと空文字が混在していたとする。このとき、既存のデータを null => 空文字 に修正して、null可 => not nullに修正するのがあるべき姿で正しい修正だと思う。

しかし、このとき、自分はアプリケーション側でそれらを吸収するような修正を行ってしまうという判断をやりがちである。もちろん、DBのデータ修正が複雑なのであればそれは特に間違いではないと思うが、それほど複雑でない場合もこういう判断をしがちな記憶がある。

自分でもなぜだろうと考えて見たが、不正なデータを生んでしまったなどどうしてもやらざるを得ない状況以外では、本番データを極力直接さわりたくないという思いがあるのではないかと考えた。本番データを直接さわりたくないというのは、それはそれで悪くはないと思うが、なんというか容易な修正でもできれば避けたいというきもちになってしまっていて、ちょっと極端過ぎたなと思う。

グダグダ書きすぎて文章がわかりにくくなった。 要するに、DB直接修正するのは別に名前を言ってはいけないあの人みたいな触れちゃいけないものではなくて、いくつかある手段の一つとして平等に評価すべきだ、不必要に恐れるなということを言いたかった。

技術的な判断をする上で今後も起こり得そうだと思ったのでここでメモしておく。

bundle installでmysql2のgemがインストールできずに詰まった

あるあるネタらしくググると、xcode-select --installやそもそもmysqlをインストールしてないからbrew install mysqlで解決するという記事がいろいろ出てきた。

qiita.com

しかし、自分の場合、xcode-select --installしてもbrew install mysqlしても解決しなかった。

結論としては、mysqlのバージョンが高い + mysql2のバージョンが低いためだったようだ。参考にしたリンクを以下に記載する。

github.com

herokuにmasterでないブランチをpushする

herokuにmasterでないブランチをpushすると怒られた。

git push heroku_staging foo

こうやる。

git push heroku_staging foo:master

参考 devcenter.heroku.com

: は何?

上記のgit pushで出てきた:がわからなかったので調べた。結論は以下になる。

git push origin {ローカルのbranch}:{リモートのbranch}

ローカルとリモートが同じ名前だと、いつも書いてる:なしで書ける。

参考 shoma2da.hatenablog.com

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