M Dockerfile => Dockerfile +1 -1
@@ 4,7 4,7 @@ RUN apk add git binutils
COPY go.* ./
RUN go mod download
COPY . ./
-RUN go build . && strip tls-redirector
+RUN go generate ./... && go build . && strip tls-redirector
FROM alpine:3.12.0
COPY --from=builder /go/src/git.sr.ht/~ancarda/tls-redirector/tls-redirector .
M README.md => README.md +8 -0
@@ 38,6 38,14 @@ up once and forget about it.
## How To Build
+The first time you checkout this project, you'll need to run this command:
+
+ go generate ./...
+
+Re-run that anytime you edit files inside `fancy/html`.
+
+----
+
If you want systemd socket activation, you need to compile this way:
go build -tags systemd
M cli_test.go => cli_test.go +1 -1
@@ 13,7 13,7 @@ func TestCLIVersionFlag(t *testing.T) {
args := []string{"./test", "--version"}
assert.Equal(t, 0, handleCliArgs(&buf, args, false))
- assert.Equal(t, "tls redirector 2.3\n", buf.String())
+ assert.Equal(t, "tls redirector 2.4\n", buf.String())
}
func TestCLIUnknownFlag(t *testing.T) {
A fancy/fancy.go => fancy/fancy.go +32 -0
@@ 0,0 1,32 @@
+//go:generate go run generate/cmd.go
+
+package fancy
+
+import (
+ "bytes"
+ "text/template"
+)
+
+var pageT *template.Template
+
+func init() { pageT = template.Must(template.New("").Parse(pageTemplate)) }
+
+// ErrorPage produces a nice looking HTML error page.
+func ErrorPage(title, message, ti, ver string) []byte {
+ footer := "<footer>Powered by tls-redirector/" + ver + "</footer>"
+
+ dat := struct {
+ Title string
+ Message string
+ TI string
+ Footer string
+ }{title, message, ti, footer}
+
+ var buf bytes.Buffer
+ err := pageT.Execute(&buf, dat)
+ if err != nil {
+ panic(err)
+ }
+
+ return buf.Bytes()
+}
A fancy/fancy_test.go => fancy/fancy_test.go +37 -0
@@ 0,0 1,37 @@
+package fancy
+
+import (
+ "math/rand"
+ "reflect"
+ "testing"
+ "testing/quick"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func randomString() string {
+ v, ok := quick.Value(reflect.TypeOf(""),
+ rand.New(rand.NewSource(time.Now().Unix())))
+
+ if !ok {
+ panic("wasn't able to generate a string")
+ }
+
+ return v.String()
+}
+
+func TestErrorPage(t *testing.T) {
+ title := randomString()
+ message := randomString()
+ techInfo := randomString()
+ version := randomString()
+
+ page := string(ErrorPage(title, message, techInfo, version))
+
+ assert.Contains(t, page, "<title>"+title+"</title>")
+ assert.Contains(t, page, "<h1>"+title+"</h1>")
+ assert.Contains(t, page, message)
+ assert.Contains(t, page, techInfo)
+ assert.Contains(t, page, "tls-redirector/"+version)
+}
A fancy/generate/cmd.go => fancy/generate/cmd.go +44 -0
@@ 0,0 1,44 @@
+package main
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "strings"
+)
+
+func main() {
+ files, err := ioutil.ReadDir("html")
+ if err != nil {
+ panic(err)
+ }
+
+ var buf bytes.Buffer
+ buf.WriteString("package fancy\n\n")
+ buf.WriteString("// Autogenerated -- DO NOT EDIT BY HAND.\n\n")
+ buf.WriteString("const (\n")
+
+ for _, file := range files {
+ fileName := file.Name()
+
+ if strings.HasPrefix(fileName, ".") {
+ continue
+ }
+
+ fileName = strings.Split(fileName, ".")[0]
+
+ src, err := ioutil.ReadFile("html" + string(os.PathSeparator) + fileName + ".html")
+ if err != nil {
+ panic(err)
+ }
+
+ buf.WriteString("\t// " + fileName + " is a copy of html/" + fileName + ".html\n")
+ buf.WriteString("\t" + fileName + " = `")
+ buf.Write(src)
+ buf.WriteString("`\n")
+ }
+
+ buf.WriteString(")\n")
+
+ ioutil.WriteFile("templates.go", buf.Bytes(), 0755)
+}
A fancy/html/Acme404Message.html => fancy/html/Acme404Message.html +1 -0
@@ 0,0 1,1 @@
+<p>You have requested an ACME HTTP challenge that doesn't exist.</p>
A fancy/html/Acme404TI.html => fancy/html/Acme404TI.html +1 -0
@@ 0,0 1,1 @@
+Requested path: <code>%s</code>
A => +2 -0
@@ 0,0 1,2 @@
<p>HTTP request <code>Host</code> header is empty or wasn't sent.</p>
<p>Expected a domain name such as <code>example.com</code>.</p>
A fancy/html/GenericMessage.html => fancy/html/GenericMessage.html +3 -0
@@ 0,0 1,3 @@
+<p>You've reached this webserver in an insecure way.</p>
+<p>Normally, you would be taken to the secure version of the website.</p>
+<p>Unfortunately, that isn't possible right now. Either you visited this webserver directly — using its IP address rather than a domain name — or your webbrowser is broken somehow.</p>
A => +2 -0
@@ 0,0 1,2 @@
<p>HTTP request <code>Host</code> header has value <code>%s</code>, which looks like an IP address.</p>
<p>Expected a domain name such as <code>example.com</code>.</p>
A fancy/html/pageTemplate.html => fancy/html/pageTemplate.html +61 -0
@@ 0,0 1,61 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8" />
+ <title>{{.Title}}</title>
+ <style>
+ html { box-sizing: border-box }
+ body {
+ background-color: lightgray;
+ margin: 2em;
+ font-family: sans-serif;
+ font-size: 18px;
+ line-height: 1.4em;
+ }
+ main {
+ background-color: white;
+ padding: 2em;
+ border: 4px solid gray;
+ }
+ h1 { margin-top: 0 }
+ hr { border-top: 4px solid gray; border-bottom: none }
+ a { color: blue }
+ code { background-color: peachpuff }
+ footer {
+ padding: 1em;
+ text-align: center;
+ font-size: 0.8em;
+ }
+ .block {
+ border-left: 4px solid skyblue;
+ background: azure;
+ }
+ .block > header {
+ background-color: skyblue;
+ padding: 0 0.5em;
+ text-transform: uppercase;
+ font-size: 0.8em;
+ color: white;
+ font-weight: bold;
+ }
+ .block > article { padding: 1em }
+ .block > article p:first-child { margin-top: 0 }
+ .block > article p:last-child { margin-bottom: 0 }
+ </style>
+ </head>
+
+ <body>
+ <main>
+ <h1>{{.Title}}</h1>
+ {{.Message}}
+ <div class="block">
+ <header>Technical Information</header>
+ <article>
+ {{.TI}}
+ <p><small>If this looks like a bug, please go to <a href="https://sr.ht/~ancarda/tls-redirector/">https://sr.ht/~ancarda/tls-redirector</a> to find out how to file a bug report.</small></p>
+ </article>
+ </div>
+ </main>
+ {{.Footer}}
+ </body>
+</html>
M http.go => http.go +30 -6
@@ 1,11 1,15 @@
package main
import (
+ "fmt"
+ "html"
"io/ioutil"
"net/http"
"os"
"strings"
+ "git.sr.ht/~ancarda/tls-redirector/fancy"
+
"github.com/spf13/afero"
)
@@ 20,13 24,18 @@ func newApp(acd string) app {
func (a app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "tls-redirector/"+version)
- w.Header().Set("Content-Type", "text/plain")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
// If we haven't been given a host, just abort.
if r.Host == "" {
w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("tls-redirector cannot handle this request because no `host` header was sent by your browser.\n"))
+ w.Write(fancy.ErrorPage(
+ "Bad Request",
+ fancy.GenericMessage,
+ fancy.EmptyHostHeader,
+ version,
+ ))
return
}
@@ 37,22 46,38 @@ func (a app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// real web server some effort by dropping it now.
if isIPAddress(r.Host) {
w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("tls-redirector cannot redirect IP addresses.\n"))
+ w.Write(fancy.ErrorPage(
+ "400 Bad Request",
+ fancy.GenericMessage,
+ fmt.Sprintf(
+ fancy.HostHeaderIsIPTechInfo,
+ html.EscapeString(r.Host),
+ ),
+ version,
+ ))
return
}
// If we are serving the ACME HTTP challenges, handle that here.
if a.acmeChallengeDir != "" {
if strings.HasPrefix(r.URL.Path, acmeChallengeURLPrefix) {
- id := strings.TrimPrefix(r.URL.Path, acmeChallengeURLPrefix)
+ id := strings.TrimPrefix(r.URL.Path,
+ acmeChallengeURLPrefix)
b, err := readFile(a.fs,
a.acmeChallengeDir+string(os.PathSeparator)+id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
- w.Write([]byte("File Not Found\n"))
+ w.Write(fancy.ErrorPage(
+ "404 File Not Found",
+ fancy.Acme404Message,
+ fmt.Sprintf(fancy.Acme404TI,
+ html.EscapeString(r.URL.Path)),
+ version,
+ ))
return
}
+ w.Header().Set("Content-Type", "text/plain")
w.Write(b)
return
}
@@ 62,7 87,6 @@ func (a app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Change host as well as in r.URL, it's empty.
r.URL.Host = r.Host
r.URL.Scheme = "https"
- w.Header().Set("Content-Type", "text/html")
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
}
M http_test.go => http_test.go +8 -9
@@ 17,7 17,7 @@ import (
const (
TextPlain = "text/plain"
- TextHTML = "text/html"
+ TextHTML = "text/html; charset=utf-8"
)
func randomString() string {
@@ 33,7 33,7 @@ func randomString() string {
func assertionsCommonToAllResponses(t *testing.T, res *http.Response) {
assert.Equal(t, "nosniff", res.Header.Get("X-Content-Type-Options"))
- assert.Equal(t, "tls-redirector/2.3", res.Header.Get("Server"))
+ assert.Equal(t, "tls-redirector/2.4", res.Header.Get("Server"))
}
func TestNewApp_UsesRealFileSystem(t *testing.T) {
@@ 58,12 58,11 @@ func TestServer_ServeACME_404(t *testing.T) {
res := rr.Result()
assert.Equal(t, http.StatusNotFound, res.StatusCode)
- assert.Equal(t, TextPlain, res.Header.Get("Content-Type"))
+ assert.Equal(t, TextHTML, res.Header.Get("Content-Type"))
assertionsCommonToAllResponses(t, res)
body, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "File Not Found\n", string(body))
-
+ assert.Contains(t, string(body), "File Not Found")
}
func TestServer_ServeACME_HappyPath(t *testing.T) {
@@ 91,11 90,11 @@ func TestServer_NoHostHeader_WillError(t *testing.T) {
res := rr.Result()
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
- assert.Equal(t, TextPlain, res.Header.Get("Content-Type"))
+ assert.Equal(t, TextHTML, res.Header.Get("Content-Type"))
assertionsCommonToAllResponses(t, res)
body, _ := ioutil.ReadAll(res.Body)
- assert.Contains(t, string(body), "no `host` header was sent")
+ assert.Contains(t, string(body), "header is empty or wasn't sent")
}
func TestServer_IPAddressHostHeader_IsRejected(t *testing.T) {
@@ 105,11 104,11 @@ func TestServer_IPAddressHostHeader_IsRejected(t *testing.T) {
res := rr.Result()
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
- assert.Equal(t, TextPlain, res.Header.Get("Content-Type"))
+ assert.Equal(t, TextHTML, res.Header.Get("Content-Type"))
assertionsCommonToAllResponses(t, res)
body, _ := ioutil.ReadAll(res.Body)
- assert.Contains(t, string(body), "cannot redirect IP addresses")
+ assert.Contains(t, string(body), "looks like an IP address")
}
func TestServer_HappyPath(t *testing.T) {
M main.go => main.go +1 -1
@@ 10,7 10,7 @@ import (
const (
acmeChallengeURLPrefix = "/.well-known/acme-challenge/"
- version = "2.3"
+ version = "2.4"
defaultPort = "80"
)