diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18311bd..910f2f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: test: strategy: matrix: - go-version: [1.17.x] + go-version: [1.18.x, 1.19.*] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -25,7 +25,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.16.x + go-version: 1.19.x - name: Checkout code uses: actions/checkout@v2 - name: Test diff --git a/cmd/gencert/main.go b/cmd/gencert/main.go new file mode 100644 index 0000000..9d8ecce --- /dev/null +++ b/cmd/gencert/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "log" + "math/big" + "os" + "strings" + "time" +) + +const ( + keySize = 4096 + dateFormat = "2006-01-02" +) + +func main() { + keyOut := "key.pem" + flag.StringVar(&keyOut, "key", keyOut, "dst file to write private key") + + certOut := "cert.pem" + flag.StringVar(&certOut, "cert.pem", certOut, "dst file to write certificate") + + var dnsNames []string + flag.Func("dns", "DNS records for cert", func(name string) error { + if strings.TrimSpace(name) == "" { + return nil + } + dnsNames = append(dnsNames, name) + return nil + }) + + organization := "dev" + flag.StringVar(&organization, "org", organization, "organization which generates the certificate") + + country := "OO" + flag.StringVar(&country, "country", country, "country of certificate emitter") + + locality := "ether" + flag.StringVar(&locality, "loc", locality, "locality of certificate emitter") + + expiration := time.Now().AddDate(32, 0, 0) + flag.Func("exp", + "certificate expiration date. Format: "+dateFormat+". Default: "+expiration.Format(dateFormat), + func(value string) error { + t, errParse := time.Parse(value, dateFormat) + if errParse != nil { + return errParse + } + expiration = t + return nil + }) + flag.Parse() + + log.Print("generating private key") + privKey, errKey := rsa.GenerateKey(rand.Reader, keySize) + if errKey != nil { + panic(errKey) + } + + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{organization}, + Country: []string{country}, + Locality: []string{locality}, + }, + DNSNames: dnsNames, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: false, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + log.Print("generating certificate") + certEncoded, errCert := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privKey.PublicKey, privKey) + if errCert != nil { + panic(errCert) + } + keyEncoded := x509.MarshalPKCS1PrivateKey(privKey) + + log.Print("writing key and certificate data") + if err := writePEM(keyOut, "RSA PRIVATE KEY", keyEncoded); err != nil { + panic(err) + } + if err := writePEM(certOut, "CERTIFICATE", certEncoded); err != nil { + panic(err) + } +} + +func writePEM(file, name string, data []byte) error { + // #nosec G304 // hardcoded in code + f, errCreate := os.Create(file) + if errCreate != nil { + return fmt.Errorf("creating file: %w", errCreate) + } + defer func() { _ = f.Close() }() + + errEncode := pem.Encode(f, &pem.Block{ + Type: name, + Bytes: data, + }) + if errEncode != nil { + return fmt.Errorf("encoding PEM data: %w", errEncode) + } + return nil +} diff --git a/gemax/mime.go b/gemax/mime.go index 73d363b..5b9c672 100644 --- a/gemax/mime.go +++ b/gemax/mime.go @@ -24,11 +24,11 @@ package gemax // // Valid values for the "lang" parameter are comma-separated lists of one or // more language tags as defined in RFC4646. For example: -// * "text/gemini; lang=en" Denotes a text/gemini document written in English -// * "text/gemini; lang=fr" Denotes a text/gemini document written in French -// * "text/gemini; lang=en,fr" Denotes a text/gemini document written in a mixture of English and French -// * "text/gemini; lang=de-CH" Denotes a text/gemini document written in Swiss German -// * "text/gemini; lang=sr-Cyrl" Denotes a text/gemini document written in Serbian using the Cyrllic script -// * "text/gemini; lang=zh-Hans-CN" Denotes a text/gemini document written in Chinese using -// the Simplified script as used in mainland China +// - "text/gemini; lang=en" Denotes a text/gemini document written in English +// - "text/gemini; lang=fr" Denotes a text/gemini document written in French +// - "text/gemini; lang=en,fr" Denotes a text/gemini document written in a mixture of English and French +// - "text/gemini; lang=de-CH" Denotes a text/gemini document written in Swiss German +// - "text/gemini; lang=sr-Cyrl" Denotes a text/gemini document written in Serbian using the Cyrllic script +// - "text/gemini; lang=zh-Hans-CN" Denotes a text/gemini document written in Chinese using +// the Simplified script as used in mainland China const MIMEGemtext = "text/gemini" diff --git a/gemax/server_test.go b/gemax/server_test.go index b6f2a29..2e52f8b 100644 --- a/gemax/server_test.go +++ b/gemax/server_test.go @@ -73,7 +73,7 @@ func TestServerCancelListen(test *testing.T) { var server = &gemax.Server{ Addr: testaddr.Addr(), Logf: test.Logf, - Handler: func(ctx context.Context, rw gemax.ResponseWriter, req gemax.IncomingRequest) { + Handler: func(_ context.Context, rw gemax.ResponseWriter, _ gemax.IncomingRequest) { _, _ = io.WriteString(rw, "example text") }, } @@ -102,7 +102,7 @@ func TestListenAndServe(test *testing.T) { var server = &gemax.Server{ Addr: "localhost:40423", Logf: test.Logf, - Handler: func(ctx context.Context, rw gemax.ResponseWriter, req gemax.IncomingRequest) { + Handler: func(_ context.Context, rw gemax.ResponseWriter, _ gemax.IncomingRequest) { _, _ = io.WriteString(rw, "example text") }, } diff --git a/gemax/server_utils.go b/gemax/server_utils.go index 332cf5a..18df3db 100644 --- a/gemax/server_utils.go +++ b/gemax/server_utils.go @@ -17,9 +17,10 @@ import ( // This mechanism can be used as redirect on other protocol pages. // // Examples: -// Redirect(rw, req, "gemini://other.server.com/page", status.Redirect) -// Redirect(rw, req, "../root/page", status.PermanentRedirect) -// Redirect(rw, req, "https://wikipedia.org", status.Success) +// +// Redirect(rw, req, "gemini://other.server.com/page", status.Redirect) +// Redirect(rw, req, "../root/page", status.PermanentRedirect) +// Redirect(rw, req, "https://wikipedia.org", status.Success) func Redirect(rw ResponseWriter, req IncomingRequest, target string, code status.Code) { if code == status.Success { rw.WriteStatus(code, MIMEGemtext) @@ -67,8 +68,9 @@ func ServeContent(contentType string, content []byte) Handler { // Query extracts canonical gemini query values // from url query part. Values are sorted in ascending order. // Expected values: -// ?query&key=value => [query] -// ?a&b=&key=value => [a, b] +// +// ?query&key=value => [query] +// ?a&b=&key=value => [a, b] func Query(query urlpkg.Values) []string { var keys = make([]string, 0, len(query)) for key, values := range query { diff --git a/gemax/status/status.go b/gemax/status/status.go index 8ffa264..b84e581 100644 --- a/gemax/status/status.go +++ b/gemax/status/status.go @@ -17,6 +17,7 @@ func Text(code Code) string { } // Comments copy-pasted from official gemini specification. +// //go:generate stringer -type Code -linecomment -output status_string.go const ( // Undefined is a default empty status code value.