Skip to content

Commit

Permalink
Add HTTP(S) dualstack support (incl. option to generate self-signed c…
Browse files Browse the repository at this point in the history
…erts). roblillack#6
  • Loading branch information
roblillack committed Mar 2, 2017
1 parent c2fd5ed commit 1271cba
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 66 deletions.
7 changes: 1 addition & 6 deletions README.md
Expand Up @@ -71,7 +71,6 @@ The major changes since forking away from Revel are these:
)
func main() {
port := flag.Int("p", -1, "Port to listen on (default: use mars config)")
mode := flag.String("m", "prod", "Runtime mode to select (default: prod)")
flag.Parse()
Expand All @@ -91,11 +90,7 @@ The major changes since forking away from Revel are these:
// Reads the config, sets up template loader, creates router
mars.InitDefaults(mode, ".")
if *port == -1 {
*port = mars.HttpPort
}
mars.Run(*port)
mars.Run()
}
```
7. Run `go generate && go build && ./myapp` and be happy.
Expand Down
64 changes: 64 additions & 0 deletions cert.go
@@ -0,0 +1,64 @@
package mars

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"strings"
"time"
)

func createCertificate(organization, domainNames string) (tls.Certificate, error) {
INFO.Printf("Creating self-signed TLS certificate for %s\n", organization)
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
ERROR.Fatalf("Failed to generate private key: %s", err)
}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
ERROR.Fatalf("failed to generate serial number: %s", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{organization},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),

IsCA: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

hosts := strings.Split(strings.TrimSpace(strings.Replace(domainNames, ",", " ", -1)), " ")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
ERROR.Fatalf("Failed to create certificate: %s", err)
}
cert := &bytes.Buffer{}
pem.Encode(cert, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})

key := &bytes.Buffer{}
pem.Encode(key, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})

return tls.X509KeyPair(cert.Bytes(), key.Bytes())
}
7 changes: 1 addition & 6 deletions docs/migration.md
Expand Up @@ -34,7 +34,6 @@
)

func main() {
port := flag.Int("p", -1, "Port to listen on (default: use mars config)")
mode := flag.String("m", "prod", "Runtime mode to select (default: prod)")
flag.Parse()

Expand All @@ -54,10 +53,6 @@
// Reads the config, sets up template loader, creates router
mars.InitDefaults(mode, ".")

if *port == -1 {
*port = mars.HttpPort
}

mars.Run(*port)
mars.Run()
}
7. Run `go generate && go build && ./myapp` and be happy.
2 changes: 1 addition & 1 deletion docs/testing.md
Expand Up @@ -33,7 +33,7 @@ which can be used like this:
mars.InitDefaults("dev", filepath.Join(filepath.Dir(filename), "..", ".."))
mars.DevMode = true

go mars.Run(0)
go mars.Run()

time.Sleep(1 * time.Second)
}
Expand Down
54 changes: 42 additions & 12 deletions revel.go
@@ -1,6 +1,7 @@
package mars

import (
"fmt"
"io"
"io/ioutil"
"log"
Expand Down Expand Up @@ -55,11 +56,16 @@ var (
// the current process reality. For example, if the app is configured for
// port 9000, HttpPort will always be 9000, even though in dev mode it is
// run on a random port and proxied.
HttpPort = 9000
HttpAddr = "" // e.g. "", "127.0.0.1"
HttpSsl = false // e.g. true if using ssl
HttpSslCert = "" // e.g. "/path/to/cert.pem"
HttpSslKey = "" // e.g. "/path/to/key.pem"
HttpAddr = ":9000" // e.g. "", "127.0.0.1"
HttpSsl = false // e.g. true if using ssl
HttpSslCert = "" // e.g. "/path/to/cert.pem"
HttpSslKey = "" // e.g. "/path/to/key.pem"

DualStackHTTP = false
SSLAddr = ":https"
SelfSignedCert = false
SelfSignedOrganization = "ACME Inc."
SelfSignedDomains = "127.0.0.1"

// All cookies dropped by the framework begin with this prefix.
CookiePrefix = "MARS"
Expand Down Expand Up @@ -144,18 +150,42 @@ func InitDefaults(mode, basePath string) {

// Configure properties from app.conf
DevMode = Config.BoolDefault("mode.dev", DevMode)
HttpPort = Config.IntDefault("http.port", HttpPort)
HttpAddr = Config.StringDefault("http.addr", HttpAddr)
HttpSsl = Config.BoolDefault("http.ssl", HttpSsl)
HttpSslCert = Config.StringDefault("http.sslcert", HttpSslCert)
HttpSslKey = Config.StringDefault("http.sslkey", HttpSslKey)
HttpSsl = Config.BoolDefault("https.enabled", Config.BoolDefault("http.ssl", HttpSsl))
HttpSslCert = Config.StringDefault("https.certfile", Config.StringDefault("http.sslcert", HttpSslCert))
HttpSslKey = Config.StringDefault("https.keyfile", Config.StringDefault("http.sslkey", HttpSslKey))

DualStackHTTP = Config.BoolDefault("http.dualstack", DualStackHTTP)
SSLAddr = Config.StringDefault("https.addr", "")
SelfSignedCert = Config.BoolDefault("https.selfsign", SelfSignedCert)
SelfSignedOrganization = Config.StringDefault("https.organization", SelfSignedOrganization)
SelfSignedDomains = Config.StringDefault("https.domains", SelfSignedDomains)

if HttpSsl {
if (DualStackHTTP || HttpSsl) && !SelfSignedCert {
if HttpSslCert == "" {
log.Fatalln("No http.sslcert provided.")
log.Fatalln("No https.certfile provided and https.selfsign not true.")
}
if HttpSslKey == "" {
log.Fatalln("No http.sslkey provided.")
log.Fatalln("No https.keyfile provided and https.selfsign not true.")
}
}

tryAddingSSLPort := false
// Support legacy way of specifying HTTPS addr
if SSLAddr == "" {
if HttpSsl && !DualStackHTTP {
SSLAddr = HttpAddr
tryAddingSSLPort = true
} else {
SSLAddr = ":https"
}
}

// Support legacy way of specifying port number as config setting http.port
if p := Config.IntDefault("http.port", -1); p != -1 {
HttpAddr = fmt.Sprintf("%s:%d", HttpAddr, p)
if tryAddingSSLPort {
SSLAddr = fmt.Sprintf("%s:%d", SSLAddr, p)
}
}

Expand Down
96 changes: 55 additions & 41 deletions server.go
@@ -1,12 +1,10 @@
package mars

import (
"fmt"
"crypto/tls"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"

"golang.org/x/net/websocket"
Expand All @@ -17,6 +15,7 @@ var (
MainTemplateLoader *TemplateLoader
MainWatcher *Watcher
Server *http.Server
SecureServer *http.Server
)

// Handler is a http.HandlerFunc which exposes Mars' filtering, routing, and
Expand Down Expand Up @@ -63,54 +62,69 @@ func handleInternal(w http.ResponseWriter, r *http.Request, ws *websocket.Conn)
}
}

// Run the server.
// This is called from the generated main file.
// If port is non-zero, use that. Else, read the port from app.conf.
func Run(port int) {
address := HttpAddr
if port == 0 {
port = HttpPort
func makeServer(addr string) *http.Server {
return &http.Server{
Addr: addr,
Handler: Handler,
ReadTimeout: time.Duration(Config.IntDefault("timeout.read", 0)) * time.Second,
WriteTimeout: time.Duration(Config.IntDefault("timeout.write", 0)) * time.Second,
}
}

var network = "tcp"
var localAddress string
func Run() {
wg := sync.WaitGroup{}

// If the port is zero, treat the address as a fully qualified local address.
// This address must be prefixed with the network type followed by a colon,
// e.g. unix:/tmp/app.socket or tcp6:::1 (equivalent to tcp6:0:0:0:0:0:0:0:1)
if port == 0 {
parts := strings.SplitN(address, ":", 2)
network = parts[0]
localAddress = parts[1]
} else {
localAddress = address + ":" + strconv.Itoa(port)
if !HttpSsl || DualStackHTTP {
go func() {
time.Sleep(100 * time.Millisecond)
INFO.Printf("Listening on %s (HTTP) ...\n", HttpAddr)
}()

wg.Add(1)
go func() {
defer wg.Done()

Server = makeServer(HttpAddr)
ERROR.Fatalln("Failed to serve:", Server.ListenAndServe())
}()
}

Server = &http.Server{
Addr: localAddress,
Handler: Handler,
ReadTimeout: time.Duration(Config.IntDefault("timeout.read", 0)) * time.Second,
WriteTimeout: time.Duration(Config.IntDefault("timeout.write", 0)) * time.Second,
if HttpSsl || DualStackHTTP {
go func() {
time.Sleep(100 * time.Millisecond)
INFO.Printf("Listening on %s (HTTPS) ...\n", SSLAddr)
}()

wg.Add(1)
go func() {
defer wg.Done()

serveTLS(SSLAddr)
}()
}

go func() {
time.Sleep(100 * time.Millisecond)
fmt.Printf("Listening on %s...\n", localAddress)
}()
wg.Wait()
}

func serveTLS(addr string) {
SecureServer = makeServer(addr)

if HttpSsl {
if network != "tcp" {
// This limitation is just to reduce complexity, since it is standard
// to terminate SSL upstream when using unix domain sockets.
ERROR.Fatalln("SSL is only supported for TCP sockets. Specify a port to listen on.")
SecureServer.TLSConfig = &tls.Config{
Certificates: make([]tls.Certificate, 1),
}
if SelfSignedCert {
keypair, err := createCertificate(SelfSignedOrganization, SelfSignedDomains)
if err != nil {
ERROR.Fatalln("Unable to create key pair:", err)
}
ERROR.Fatalln("Failed to listen:",
Server.ListenAndServeTLS(HttpSslCert, HttpSslKey))
SecureServer.TLSConfig.Certificates[0] = keypair
} else {
listener, err := net.Listen(network, localAddress)
keypair, err := tls.LoadX509KeyPair(HttpSslCert, HttpSslKey)
if err != nil {
ERROR.Fatalln("Failed to listen:", err)
ERROR.Fatalln("Unable to load key pair:", err)
}
ERROR.Fatalln("Failed to serve:", Server.Serve(listener))
SecureServer.TLSConfig.Certificates[0] = keypair
}

ERROR.Fatalln("Failed to serve:", SecureServer.ListenAndServeTLS("", ""))
}

0 comments on commit 1271cba

Please sign in to comment.