ブログをGitHub Pages + Hugoへ移行
ブログをGitHub Pages + Hugoへ移行した
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年のゆるい目標を書いていく。
- クラウドネイティブ
- 英語のリスニング
- 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するときにどうやって認証しているか、について気になって調べたのでメモ。
heroku loginで認証情報を~/.netrcに作成- herokuに
git pushするときには~/.netrcの認証情報を見に行く- これはherokuというよりgitの機能な気がする(ソースは確認できていないが
- netrc初見。元々はftp のためのユーザー設定ファイルらしい(https://linuxjm.osdn.jp/html/netkit/man5/netrc.5.html)
本番データの修正をためらうきもち
例えば、本来 not null であるべきカラムが null可 になってしまっていて、データにnullと空文字が混在していたとする。このとき、既存のデータを null => 空文字 に修正して、null可 => not nullに修正するのがあるべき姿で正しい修正だと思う。
しかし、このとき、自分はアプリケーション側でそれらを吸収するような修正を行ってしまうという判断をやりがちである。もちろん、DBのデータ修正が複雑なのであればそれは特に間違いではないと思うが、それほど複雑でない場合もこういう判断をしがちな記憶がある。
自分でもなぜだろうと考えて見たが、不正なデータを生んでしまったなどどうしてもやらざるを得ない状況以外では、本番データを極力直接さわりたくないという思いがあるのではないかと考えた。本番データを直接さわりたくないというのは、それはそれで悪くはないと思うが、なんというか容易な修正でもできれば避けたいというきもちになってしまっていて、ちょっと極端過ぎたなと思う。
グダグダ書きすぎて文章がわかりにくくなった。 要するに、DB直接修正するのは別に名前を言ってはいけないあの人みたいな触れちゃいけないものではなくて、いくつかある手段の一つとして平等に評価すべきだ、不必要に恐れるなということを言いたかった。
技術的な判断をする上で今後も起こり得そうだと思ったのでここでメモしておく。
herokuにmasterでないブランチをpushする
herokuにmasterでないブランチをpushすると怒られた。
git push heroku_staging foo
こうやる。
git push heroku_staging foo:master
: は何?
上記のgit pushで出てきた:がわからなかったので調べた。結論は以下になる。
git push origin {ローカルのbranch}:{リモートのbranch}
ローカルとリモートが同じ名前だと、いつも書いてる:なしで書ける。
MySQL Protocolに入門する
ふと気になった。
ローカル開発環境でMySQLに接続するときにはTCPかunix 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が返ってくるところまで確認する。
コードとその出力結果は以下の通り(usernameとpasswordはブログを書くときに変更している)。
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表記だとこうなるとのこと。
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はこちらを参照するといい。
OK_packet
OK_packetはこちらを参照するといい。
https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
参考
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のメタ循環評価機を作った。
しかし、SchemeでSchemeのインタープリタを書く、というのはどうにも自分の中で腹落ちしなかった。自分はインタープリタを書けた、また理解できた、という実感が得られなかった(これは単に写経しただけであったというのもあると思うが)。
このため、SchemeのインタープリタをScheme以外の言語で書くことにした。ここでは最近手に馴染んでいるGoで書くことに決めた。
goscheme
最低限の機能しかなく、またREPLのみしか対応していないが、書いたコードはGitHubに上げている。
メタ循環評価機を書いたときとの大きな違いは、LexerとParserを自分で書かなければならなかったことである。
Schemeのメタ循環評価機ではLexerとParserは必要ないためSICPにサンプルコードはなかったが、言語実装パターンに簡単なLL(k)パーサ書いて感覚を掴んでいたので、比較的スムーズにSchemeに対するLexerとParserを書くことができた。
また、既にSchemeインタープリタのGo実装は何人もの方によって書かれているが、特にsuzuken/gigueの実装が参考になった。感謝。