http.ListenAndServeTLSでサーバが起動できずハマったが、エラー出力を自分で書いたらすぐに解決した

# 概要

Go初学者(私)が『[Go プログラミング実践入門](https://www.amazon.co.jp/Go%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80-%E6%A8%99%E6%BA%96%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%A7%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89Web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E3%82%8B-impress-gear%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-Sheong-Chang-ebook/dp/B06XKPNVWV/ref=sr_1_6?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=go&qid=1635589724&sr=8-6)』の3.4 HTTP2の使用 でハマりました。
以下は解決した時のログと、得た教訓を記したものです。

結論、`http.ListenAndServeTLS`の引数として渡すために生成した証明書・鍵ファイルの権限が、何らかの原因で足りていなかったことが原因でした。
このハマりで得たもの・教訓は以下の通りです。

- Goの標準ライブラリで動作テスト用の証明書が作成できるということ
- ハマったらファイルの権限も確認すること
- 怪しい箇所のエラー出力をちゃんと自分で書くこと

# 詳細

## 何が起こったか

`http.ListenAndServeTLS(certFile, keyFile string)` でGoサーバーが起動できない。
正確には、以下のファイルをgo run した後、別ターミナルでcurlコマンドを叩いても `curl: (7) Failed to connect to localhost port 8080: Connection refused`が返ってきてしまう。

そもそも実行した後にすぐターミナルがコマンド待ち状態になってしまうのでおそらくサーバが起動できていない。

```sever.go
package main

import (
	"fmt"
	"golang.org/x/net/http2"
	"net/http"
)

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World!")
}

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}
	http2.ConfigureServer(&server, &http2.Server{})

	server.ListenAndServeTLS("cert.pem", "key.pem")
}

// サーバがHTTP/2で動作しているかどうかはcURLで確認する
// curl -I --http2 --insecure https://localhost:8080/
```

実行してGoサーバを起動しようとした時の様子です。

```bash
% go run server.go
%
```

## 状況

以下のようにディレクトリを切っていた。(他にも章・リストごとにディレクトリがある)
`cert.pem` と`key.pem` は[こちら](https://github.com/suganoo/gowebprog/tree/master/ch03/13http2) から中身をコピーしている。

```bash
http2 % tree
.
├── cert.pem
├── server.go
└── key.pem
```

## 試したこと

### /crypto/tls/generate_cert.go で証明書を新たに作成

「自分の環境で作成した証明書でないといけないのかも」と思い、このパッケージの存在を知りました。
参考: [https://hodalog.com/generate-self-signed-certificate-using-by-golang/](https://hodalog.com/generate-self-signed-certificate-using-by-golang/)

一旦元あった`cert.pem` と`key.pem` を削除し、下記のコマンドを実行するとカレントディレクトリに`cert.pem` と`key.pem` が生成されます。

```bash

% go run /usr/local/go/src/crypto/tls/generate_cert.go -rsa-bits 2048 -host localhost
2021/10/30 17:41:11 wrote cert.pem
2021/10/30 17:41:11 wrote key.pem
```

もう一度先ほどのファイルでGoサーバを起動し、同様のcurlコマンドを実行してみるものの、これだけでは解決しませんでした。
(結果論ですが、これはほとんど関係なかったと言えるでしょう。人様から拝借した証明書・鍵でも組み合わせが合っているなら動作するはずです)

### httpなら動作するのか確認

そもそもTLSの設定を適用しない場合にGoサーバとして正常に動作するのでしょうか。

```server.go
	http2.ConfigureServer(&server, &http2.Server{})

	// server.ListenAndServeTLS("cert.pem", "key.pem")
	server.ListenAndServe()
}
```

```bash
% go run server.go

```

ブラウザでlocalhost:8080 にアクセスすると、
動作しています。
![スクリーンショット 2021-10-30 18.59.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493849/a4fdf7a7-b437-2b1b-3e8f-6351627929f4.png)

つまり、怪しいのは`server.ListenAndServeTLS("cert.pem", "key.pem")` の行だということになりました。

### エラー出力で調査

ここで公式ドキュメントを確認。
[https://pkg.go.dev/net/http#Server.ListenAndServeTLS](https://pkg.go.dev/net/http#Server.ListenAndServeTLS)

`Sever.ListenAndServeTLS` はエラーを返してくれる、ということに気付きます。

```server.go
	http2.ConfigureServer(&server, &http2.Server{})

	// エラーが返ってきたら`err`に格納、そして標準出力
	err := server.ListenAndServeTLS("cert.pem", "key.pem")
	if err != nil {
		fmt.Printf("ERROR: %s", err)
	}
}
```

これで実行。

```bash
% go run server.go
ERROR: open cert.pem: no such file or directory%
```

強制終了した。しかし正常にエラーが出力できています。
ん？`cert.pem` ならさっき作成したのに読み込めていない…もしかして権限がおかしい？と思い`ls -l` で確認。

```bash
% ls -l
total 24
-rw-r--r--  1 daikideal  staff  1090 10 30 17:41 cert.pem
-rw-r--r--  1 daikideal  staff   610 10 30 18:13 server.go
-rw-------  1 daikideal  staff  1704 10 30 17:41 key.pem
```

…そんなことある？と思いましたがそんなことになっているのだから仕方ないと思い、chmodで読み込み権限を付与。
(というか`cert.pem` には権限があるのに`cert.pem` が読めていないとは一体…？)

```bash
% chmod 644 key.pem
```

これでもう一度Goサーバを起動。

```bash
% go run server.go

```

エラーは出ていないし強制終了もしていません。
curlコマンドで確認してみます。

```bash
% curl -I --http2 --insecure -v https://localhost:8080
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: O=Acme Co
*  start date: Oct 30 08:41:11 2021 GMT
*  expire date: Oct 30 08:41:11 2022 GMT
*  issuer: O=Acme Co
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f8f3900dc00)
> HEAD / HTTP/2
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
HTTP/2 200
< content-type: text/plain; charset=utf-8
content-type: text/plain; charset=utf-8
< content-length: 13
content-length: 13
< date: Sat, 30 Oct 2021 09:47:19 GMT
date: Sat, 30 Oct 2021 09:47:19 GMT

<
* Connection #0 to host localhost left intact
* Closing connection 0
```

HTTP/2のヘッダが返ってきました！
ブラウザでもサーバが正常に動作していることを確認できます。
![スクリーンショット 2021-10-30 18.48.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493849/96a08792-68f1-8b57-f605-b0d21318a6bd.png)

解決。

## 結論

当然といえば当然かもしれませんが、`Sever.ListenAndServeTLS` は証明書と鍵のファイルを読めないと動きません。
そして何より、解決の糸口になったのはエラーを標準出力したことだったので、今後もGoのコードが思うように動作しなくなったら「まずエラーを可視化」してみようと思います。
`err` にエラーを格納するのを癖にすることでこのような事故は減らせそうです。

# 参考

- [https://github.com/suganoo/gowebprog/tree/master/ch03/13http2](https://github.com/suganoo/gowebprog/tree/master/ch03/13http2)
- [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
- [テスト用にGoでオレオレ証明書を発行](https://hodalog.com/generate-self-signed-certificate-using-by-golang/)