カスタムドメインのApp EngineアプリでもEchoでTLSがしたーい

とあるプロジェクトで、App Engine アプリとして [Echo](https://github.com/labstack/echo) 製の API サーバをホストしているんですが、 TLS 対応させようとしたら、意外とすんなりいかなかったので、その話を書きます。

## TL;DR

これでいけます。 `cert` と `key` は [statik](https://github.com/rakyll/statik) とかで埋め込むなり、GCSから引っ張ってくるなり、良い感じにしてください。

```go
func startTLS(e *echo.Echo, address string, cert []byte, key []byte) error {
	s := e.TLSServer
	s.TLSConfig = new(tls.Config)
	s.TLSConfig.Certificates = make([]tls.Certificate, 1)
	s.TLSConfig.Certificates[0], err = tls.X509KeyPair(cert, key)
	if err != nil {
		return err
	}
	s.Addr = address
	if !e.DisableHTTP2 {
		s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "h2")
	}
	return e.StartServer(e.TLSServer)}
```

## Echo と TLS

まず、 GAE 関係なく、 Echo で TLS を有効にするには↓のような書き方をします。

```go
func main(){
	e := echo.New()
	e.GET("/", func(context echo.Context) error {return nil})
	err := e.StartTLS(":443","hoge.crt","key.pem")
	if err != nil{
		log.Fatal(err)
	}
}
```

`(*Echo)StartTLS()` に、証明書と秘密鍵のファイルパスを渡すだけです。楽チンですね。

しかし、GAEではファイルシステムを使えないため、ファイルパスを渡していては使えません。

## (*Echo)StartTLS() を読む

`(*Echo)StartTLS()` の中で、証明書と秘密鍵を読み込んでいる箇所だけ書き換えてしまえばなんとかなりそうですね。
というわけで `(*Echo)StartTLS()` の中を読んで行きましょう。

```go
// StartTLS starts an HTTPS server.
// If `certFile` or `keyFile` is `string` the values are treated as file paths.
// If `certFile` or `keyFile` is `[]byte` the values are treated as the certificate or key as-is.
func (e *Echo) StartTLS(address string, certFile, keyFile interface{}) (err error) {
	var cert []byte
	if cert, err = filepathOrContent(certFile); err != nil {
		return
	}

	var key []byte
	if key, err = filepathOrContent(keyFile); err != nil {
		return
	}

	s := e.TLSServer
	s.TLSConfig = new(tls.Config)
	s.TLSConfig.Certificates = make([]tls.Certificate, 1)
	if s.TLSConfig.Certificates[0], err = tls.X509KeyPair(cert, key); err != nil {
		return
	}

	return e.startTLS(address)
}
```
https://github.com/labstack/echo/blob/v4.1.17/echo.go#L647-L669

どうやら `tls.X509KeyPair()` を使って証明書と公開鍵を読み込んでいることが分かります。

あとは非公開メソッドになっている `(*Echo).startTLS()` を追えばよさそうです。

```go
func (e *Echo) startTLS(address string) error {
	s := e.TLSServer
	s.Addr = address
	if !e.DisableHTTP2 {
		s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "h2")
	}
	return e.StartServer(e.TLSServer)
}
```
https://github.com/labstack/echo/blob/v4.1.17/echo.go#L691-L698

追い切れましたね。
あとはこの部分だけメソッドに切り出してしまえばよさそうです。

## 関数に切り出す

ここまでのコードを1つの関数にして切り出してみます。

```go
func startTLS(e *echo.Echo, address string, cert []byte, key []byte) error {
	s := e.TLSServer
	s.TLSConfig = new(tls.Config)
	s.TLSConfig.Certificates = make([]tls.Certificate, 1)
	s.TLSConfig.Certificates[0], err = tls.X509KeyPair(cert, key)
	if err != nil {
		return err
	}
	s.Addr = address
	if !e.DisableHTTP2 {
		s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "h2")
	}
	return e.StartServer(e.TLSServer)}
```

あとはこれを呼び出せば良いですね。

```go
func main() {
	e := echo.New()
	e.GET("/", func(context echo.Context) error { return nil })
	
	var (
		cert []byte
		key []byte
	)
	
	/* 証明書と秘密鍵を、statikで埋め込んだファイルを読み込んだり、GCSから読み込んだりする */
	
	err := startTLS(e, ":443", cert, key)
	if err != nil {
		log.Fatal(err)
	}
}
```

## まとめ
ここまで書いておいてあれですが、App Engine はマネージドSSL証明書を提供しているので、基本はそっちを利用すると楽です。
ただ、カスタムドメインで運用したい場合はそっちを使うわけにはいかないので、今回のような方法を使う必要があります。

Echo の内部実装を、フレームワークの外に引っ張り出しているので、今後バージョンが更新されたときに意図せず壊れる可能性があるのでは？という問題がありますが、この差分は後日 Echo に PR を出すつもりなので、そっちがマージされたらまた追記します。