Skip to content

Commit

Permalink
Add support for HTTPS
Browse files Browse the repository at this point in the history
  • Loading branch information
samherrmann committed Sep 18, 2020
1 parent 38ce199 commit 0a3e607
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 15 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
dist
serveit
serveit
*.key
*.crt
*.csr
*.srl
*.pem
*.ext
2 changes: 2 additions & 0 deletions flag/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ type Config struct {
Port int
// The path of the file to serve when the requested resource cannot be found.
NotFoundFile string
// When true the app will use HTTPS instead of HTTP.
TLS bool
}
6 changes: 6 additions & 0 deletions flag/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ func Parse(args []string) (*Config, error) {
"The path of the file to serve when the requested resource cannot be found. "+
"For single-page applications, this flag is typically set to index.html.",
)
tls := flagSet.Bool(
"tls",
false,
"When true, the app is accessible over HTTPS instead of HTTP.",
)
if err := flagSet.Parse(args); err != nil {
return nil, err
}
return &Config{
Port: *port,
NotFoundFile: *notFoundFile,
TLS: *tls,
}, nil
}
42 changes: 34 additions & 8 deletions flag/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ func TestParse(t *testing.T) {
{
args: []string{},
want: &Want{
config: &flag.Config{Port: 8080, NotFoundFile: ""},
config: &flag.Config{Port: 8080, NotFoundFile: "", TLS: false},
err: nil,
},
}, {
args: []string{"-port", "3000"},
want: &Want{
config: &flag.Config{Port: 3000, NotFoundFile: ""},
config: &flag.Config{Port: 3000, NotFoundFile: "", TLS: false},
err: nil,
},
}, {
Expand All @@ -47,19 +47,39 @@ func TestParse(t *testing.T) {
}, {
args: []string{"-not-found-file", "404.html"},
want: &Want{
config: &flag.Config{Port: 8080, NotFoundFile: "404.html"},
config: &flag.Config{Port: 8080, NotFoundFile: "404.html", TLS: false},
err: nil,
},
}, {
args: []string{"-not-found-file", "foo"},
want: &Want{
config: &flag.Config{Port: 8080, NotFoundFile: "foo"},
config: &flag.Config{Port: 8080, NotFoundFile: "foo", TLS: false},
err: nil,
},
}, {
args: []string{"-port", "3000", "-not-found-file", "index.html"},
args: []string{"-port", "3000", "-not-found-file", "index.html", "-tls"},
want: &Want{
config: &flag.Config{Port: 3000, NotFoundFile: "index.html"},
config: &flag.Config{Port: 3000, NotFoundFile: "index.html", TLS: true},
err: nil,
},
}, {
args: []string{"-tls"},
want: &Want{
config: &flag.Config{Port: 8080, NotFoundFile: "", TLS: true},
err: nil,
},
}, {
args: []string{"-tls=false"},
want: &Want{
config: &flag.Config{Port: 8080, NotFoundFile: "", TLS: false},
err: nil,
},
}, {
args: []string{"-tls", "false"},
want: &Want{
// Note that setting boolean flag values explicitly needs to be done in
// the form of "-flag=value", the form "-flag value" does not work.
config: &flag.Config{Port: 8080, NotFoundFile: "", TLS: true},
err: nil,
},
},
Expand All @@ -81,8 +101,8 @@ func TestParse(t *testing.T) {
if err != nil && tc.want.err != nil {
continue
}
// Check values against Expected values
if got.Port != tc.want.config.Port || got.NotFoundFile != tc.want.config.NotFoundFile {
// Check values against expected values
if doValuesMatch(got, tc.want.config) {
t.Errorf(
"For arguments %+v, got %+v, but want %+v.",
tc.args,
Expand All @@ -92,3 +112,9 @@ func TestParse(t *testing.T) {
}
}
}

func doValuesMatch(got *flag.Config, want *flag.Config) bool {
return got.Port != want.Port ||
got.NotFoundFile != want.NotFoundFile ||
got.TLS != want.TLS
}
28 changes: 22 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import (

"github.com/samherrmann/serveit/flag"
"github.com/samherrmann/serveit/handlers"
"github.com/samherrmann/serveit/security"
)

func main() {
// Parse command-line flags into a configuration object
// Parse command-line flags into a configuration object.
config := parseFlags()
// Register file handler
// Register file handler.
http.HandleFunc("/", handlers.FileHandler(config.NotFoundFile))
// Start HTTP server
listenAndServe(config.Port)
// Start HTTP server.
listenAndServe(config.Port, config.TLS)
}

func parseFlags() *flag.Config {
Expand All @@ -28,8 +29,23 @@ func parseFlags() *flag.Config {
return config
}

func listenAndServe(port int) {
func ensureSecrets() {
if err := security.EnsureKeyPairs(); err != nil {
log.Fatalln(err)
}
}

func listenAndServe(port int, tls bool) {
addr := ":" + strconv.Itoa(port)
log.Println("Serving current directory on port " + addr)
log.Fatalln(http.ListenAndServe(addr, nil))
var err error
if tls {
ensureSecrets()
err = http.ListenAndServeTLS(addr, security.CertFilename, security.KeyFilename, nil)
} else {
err = http.ListenAndServe(addr, nil)
}
if err != nil {
log.Fatalln(err)
}
}
77 changes: 77 additions & 0 deletions security/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package security

import (
"io/ioutil"
"os"
"os/exec"
)

// CertFilename is the application certificate filename.
var CertFilename = "serveit.crt"

// CSRFilename is the application certificate signing request filename.
var CSRFilename = "serveit.csr"

// ExtFilename is the application certificate extensions filename.
var ExtFilename = "serveit.ext"

// EnsureCert creates an application X.509 certificate if it doesn't already
// exist.
func EnsureCert() error {
_, err := os.Stat(CertFilename)
if os.IsNotExist(err) {
if err := createCSR(); err != nil {
return err
}
if err = createExtFile(); err != nil {
return err
}
return CreateCert()
}
return err
}

// CreateCert creates an application X.509 certificate.
func CreateCert() error {
cmd := exec.Command(
"openssl", "x509",
"-req",
"-in", CSRFilename,
"-CA", RootCACertFilename,
"-CAkey", RootCAKeyFilename,
"-passin", "pass:serveit",
"-CAcreateserial",
"-out", CertFilename,
"-days", "3650",
"-sha256",
"-extfile", ExtFilename,
)
_, err := cmd.CombinedOutput()
return err
}

// createCSR creates a certificate signing request.
func createCSR() error {
cmd := exec.Command(
"openssl", "req",
"-new",
"-key", KeyFilename,
"-subj", "/C=CA/ST=Ontario/L=Ottawa/O=samherrmann/CN=serveit",
"-out", CSRFilename,
)
_, err := cmd.CombinedOutput()
return err
}

// createExtFile creates a certificate extensions file.
func createExtFile() error {
content := []byte(
"authorityKeyIdentifier=keyid,issuer\n" +
"basicConstraints=CA:FALSE\n" +
"keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\n" +
"subjectAltName = @alt_names\n" +
"[alt_names]\n" +
"DNS.1 = localhost\n",
)
return ioutil.WriteFile(ExtFilename, content, 0644)
}
29 changes: 29 additions & 0 deletions security/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package security

import (
"os"
"os/exec"
)

// KeyFilename is the application RSA key filename.
var KeyFilename = "serveit.key"

// EnsureKey creates an RSA key if it doesn't already exist.
func EnsureKey() error {
_, err := os.Stat(KeyFilename)
if os.IsNotExist(err) {
return CreateKey()
}
return err
}

// CreateKey creates an RSA key.
func CreateKey() error {
cmd := exec.Command(
"openssl", "genrsa",
"-out", KeyFilename,
"2048",
)
_, err := cmd.CombinedOutput()
return err
}
27 changes: 27 additions & 0 deletions security/keypairs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package security

import (
"fmt"
)

// EnsureKeyPairs creates an RSA key and a X.509 certificate for both the
// certificate authority (CA) and the application if they don't already exist.
func EnsureKeyPairs() error {
err := EnsureRootCAKey()
if err != nil {
return fmt.Errorf("Error creating %v: %w", RootCAKeyFilename, err)
}
err = EnsureRootCACert()
if err != nil {
return fmt.Errorf("Error creating %v: %w", RootCACertFilename, err)
}
err = EnsureKey()
if err != nil {
return fmt.Errorf("Error creating %v: %w", KeyFilename, err)
}
err = EnsureCert()
if err != nil {
return fmt.Errorf("Error creating %v: %w", CertFilename, err)
}
return nil
}
95 changes: 95 additions & 0 deletions security/keypairs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package security_test

import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/samherrmann/serveit/security"
)

func TestEnsureKeyPairs(t *testing.T) {
// Start with a clean slate.
if err := removeAllFiles(); err != nil {
t.Error(err)
}

if err := security.EnsureKeyPairs(); err != nil {
t.Error(err)
}

if err := verifyKey(security.RootCAKeyFilename); err != nil {
t.Error(err)
}

if err := verifyCert(security.RootCACertFilename); err != nil {
t.Error(err)
}

if err := verifyKey(security.KeyFilename); err != nil {
t.Error(err)
}

if err := verifyCert(security.CertFilename); err != nil {
t.Error(err)
}

// Clean up.
if err := removeAllFiles(); err != nil {
t.Error(err)
}
}

func verifyKey(filename string) error {
return verifyFileContent(
filename,
"-----BEGIN RSA PRIVATE KEY-----",
"-----END RSA PRIVATE KEY-----",
)
}

func verifyCert(filename string) error {
return verifyFileContent(
filename,
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----",
)
}

func verifyFileContent(filename string, prefix string, suffix string) error {
// Read content from file.
content, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("Error reading %v: %v", filename, err)
}

// Verify file content.
contentStr := string(content)
if !strings.HasPrefix(contentStr, prefix) &&
!strings.HasSuffix(contentStr, suffix) {
return fmt.Errorf("%v does not have expected content, got %v", filename, contentStr)
}
return nil
}

func removeAllFiles() error {
files := []string{
security.RootCAKeyFilename,
security.RootCACertFilename,
security.RootCACertSerialFilename,
security.KeyFilename,
security.CSRFilename,
security.ExtFilename,
security.CertFilename,
}

for _, file := range files {
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("Error removing %v: %w", file, err)
}
}
return nil
}
Loading

0 comments on commit 0a3e607

Please sign in to comment.