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

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

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

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

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 ) で、フルスタックエンジニアは現実的ではないがフロントからインフラまでの各チームを横断的に動いて全体の生産性をあげる人が必要とされている(かなり意訳)、と書かれてあった。

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