Browse Source

Initial version

tags/v1.0.0
Chris Smith 5 years ago
commit
a0186456a6
9 changed files with 387 additions and 0 deletions
  1. 15
    0
      Dockerfile
  2. 21
    0
      LICENCE.adoc
  3. 53
    0
      README.adoc
  4. 34
    0
      failure.html
  5. 78
    0
      form.html
  6. 8
    0
      go.mod
  7. 9
    0
      go.sum
  8. 136
    0
      main.go
  9. 33
    0
      success.html

+ 15
- 0
Dockerfile View File

@@ -0,0 +1,15 @@
1
+FROM golang:1.12 AS build
2
+
3
+WORKDIR /go/src/app
4
+
5
+COPY . .
6
+RUN CGO_ENABLED=0 GO111MODULE=on go install .
7
+
8
+FROM scratch
9
+COPY --from=build /go/bin/contact-form /contact-form
10
+COPY --from=build /go/src/app/*.html /templates/
11
+COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
12
+
13
+WORKDIR /templates
14
+ENTRYPOINT ["/contact-form"]
15
+EXPOSE 8080

+ 21
- 0
LICENCE.adoc View File

@@ -0,0 +1,21 @@
1
+= MIT License
2
+
3
+Copyright © 2019 Chris Smith
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 53
- 0
README.adoc View File

@@ -0,0 +1,53 @@
1
+= Go contact form
2
+
3
+== About
4
+
5
+Provides a simple, tiny webservice that serves a contact form and sends responses
6
+via e-mail.
7
+
8
+== Usage
9
+
10
+The simplest way to use this is via docker:
11
+
12
+    docker run -d csmith/contact-form -from form@server.com -to me@email.com .....
13
+
14
+You should place this service behind an TLS-terminating proxy, and ensure it
15
+is requested over a secure connection.
16
+
17
+== Command line flags
18
+
19
+----
20
+  -crsf-key string
21
+        CRSF key to use
22
+  -from string
23
+        address to send e-mail from
24
+  -port int
25
+        port to listen on for connections (default 8080)
26
+  -smtp-host string
27
+        SMTP server to connect to
28
+  -smtp-pass string
29
+        password to supply to the SMTP server
30
+  -smtp-port int
31
+        port to use when connecting to the SMTP server (default 25)
32
+  -smtp-user string
33
+        username to supply to the SMTP server
34
+  -subject string
35
+        e-mail subject (default "Contact form submission")
36
+  -to string
37
+        address to send e-mail to
38
+----
39
+
40
+_from_, _to_, _smtp-host_, _smtp-user_, and _smtp-pass_ are required; other options have vaguely sensible fallbacks.
41
+
42
+== Templates
43
+
44
+The form itself is loaded from `form.html` in the working directory; success and failure pages from `success.html`
45
+and `failure.html` respectively. Each is loaded as a https://golang.org/pkg/html/template/[go html.template] and
46
+can use the templating syntax described there.
47
+
48
+The form must contain the `{{ .csrfField }}` template field, which will automatically insert the CSRF token for
49
+the request.
50
+
51
+== Licence
52
+
53
+This software is licensed under the MIT licence. See the LICENCE.adoc file for the full text.

+ 34
- 0
failure.html View File

@@ -0,0 +1,34 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta http-equiv="Content-Type" content="text/html" charset="UTF-8"/>
5
+    <title>Contact me</title>
6
+    <style type="text/css">
7
+        .page {
8
+            width: 480px;
9
+            padding: 8% 0 0;
10
+            margin: auto;
11
+        }
12
+
13
+        .form {
14
+            position: relative;
15
+            z-index: 1;
16
+            background: #FFFFFF;
17
+            max-width: 360px;
18
+            margin: 0 auto 100px;
19
+            padding: 45px;
20
+            text-align: center;
21
+            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
22
+            font-family: sans-serif;
23
+        }
24
+    </style>
25
+</head>
26
+<body>
27
+<div class="page">
28
+    <div class="form">
29
+        Your message could not be sent at this time.
30
+        Please go back and try again.
31
+    </div>
32
+</div>
33
+</body>
34
+</html>

+ 78
- 0
form.html View File

@@ -0,0 +1,78 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta http-equiv="Content-Type" content="text/html" charset="UTF-8"/>
5
+    <title>Contact me</title>
6
+    <style type="text/css">
7
+        .page {
8
+            width: 640px;
9
+            padding: 8% 0 0;
10
+            margin: auto;
11
+        }
12
+
13
+        .form {
14
+            position: relative;
15
+            z-index: 1;
16
+            background: #FFFFFF;
17
+            max-width: 640px;
18
+            margin: 0 auto 100px;
19
+            padding: 25px 45px 35px 45px;
20
+            text-align: center;
21
+            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
22
+        }
23
+
24
+        .form input, .form textarea {
25
+            outline: 0;
26
+            background: #f2f2f2;
27
+            width: 100%;
28
+            border: 0;
29
+            margin: 0 0 15px;
30
+            padding: 15px;
31
+            box-sizing: border-box;
32
+            font-size: 14px;
33
+        }
34
+
35
+        .form textarea {
36
+            resize: vertical;
37
+        }
38
+
39
+        .form button {
40
+            text-transform: uppercase;
41
+            outline: 0;
42
+            background: #4CAF50;
43
+            width: 100%;
44
+            border: 0;
45
+            padding: 15px;
46
+            color: #FFFFFF;
47
+            font-size: 14px;
48
+            transition: all 0.3 ease;
49
+            cursor: pointer;
50
+        }
51
+
52
+        .form button:hover, .form button:active, .form button:focus {
53
+            background: #43A047;
54
+        }
55
+
56
+        h1 {
57
+            font-family: sans-serif;
58
+            font-variant: all-small-caps;
59
+            font-size: 1.4em;
60
+            margin: 0 0 0.9em 0;
61
+        }
62
+    </style>
63
+</head>
64
+<body>
65
+<div class="page">
66
+    <div class="form">
67
+        <form action="submit" method="post">
68
+            <h1>Contact me</h1>
69
+            {{ .csrfField }}
70
+            <input type="text" name="name" placeholder="Your name">
71
+            <input type="email" name="from" placeholder="Your e-mail address">
72
+            <textarea name="message" placeholder="Your message"></textarea>
73
+            <button>SEND</button>
74
+        </form>
75
+    </div>
76
+</div>
77
+</body>
78
+</html>

+ 8
- 0
go.mod View File

@@ -0,0 +1,8 @@
1
+module contact-form
2
+
3
+go 1.12
4
+
5
+require (
6
+	github.com/gorilla/csrf v1.5.1 // indirect
7
+	github.com/gorilla/mux v1.7.1 // indirect
8
+)

+ 9
- 0
go.sum View File

@@ -0,0 +1,9 @@
1
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
2
+github.com/gorilla/csrf v1.5.1 h1:UASc2+EB0T51tvl6/2ls2ciA8/qC7KdTO7DsOEKbttQ=
3
+github.com/gorilla/csrf v1.5.1/go.mod h1:HTDW7xFOO1aHddQUmghe9/2zTvg7AYCnRCs7MxTGu/0=
4
+github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU=
5
+github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
6
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
7
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
8
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
9
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

+ 136
- 0
main.go View File

@@ -0,0 +1,136 @@
1
+package main
2
+
3
+import (
4
+	"flag"
5
+	"fmt"
6
+	"github.com/gorilla/csrf"
7
+	"github.com/gorilla/mux"
8
+	"html/template"
9
+	"log"
10
+	"math/rand"
11
+	"net/http"
12
+	"net/smtp"
13
+	"os"
14
+	"strings"
15
+)
16
+
17
+const (
18
+	csrfFieldName = "csrf.Token"
19
+)
20
+
21
+var (
22
+	fromAddress, toAddress, subject, smtpServer, smtpUsername, smtpPassword, csrfKey *string
23
+	smtpPort, port                                                                   *int
24
+	formTemplate, successTemplate, failureTemplate                                   *template.Template
25
+)
26
+
27
+func sendMail(replyTo, message string) bool {
28
+	auth := smtp.PlainAuth("", *smtpUsername, *smtpPassword, *smtpServer)
29
+	body := fmt.Sprintf("To: %s\r\nSubject: %s\r\nReply-to: %s\r\nFrom: Online contact form <%s>\r\n\r\n%s\r\n", *toAddress, *subject, replyTo, *fromAddress, message)
30
+	err := smtp.SendMail(fmt.Sprintf("%s:%d", *smtpServer, *smtpPort), auth, *fromAddress, []string{*toAddress}, []byte(body))
31
+	if err != nil {
32
+		log.Printf("Unable to send mail: %s", err)
33
+		return false
34
+	}
35
+	return true
36
+}
37
+
38
+func handleForm(rw http.ResponseWriter, req *http.Request) {
39
+	body := ""
40
+	for k, v := range req.Form {
41
+		if k != csrfFieldName {
42
+			body += fmt.Sprintf("%s:\r\n%s\r\n\r\n", strings.ToUpper(k), v[0])
43
+		}
44
+	}
45
+	if sendMail(req.Form.Get("from"), body) {
46
+		rw.Header().Add("Location", "success")
47
+	} else {
48
+		rw.Header().Add("Location", "failure")
49
+	}
50
+	rw.WriteHeader(http.StatusTemporaryRedirect)
51
+}
52
+
53
+func showForm(rw http.ResponseWriter, req *http.Request) {
54
+	_ = formTemplate.ExecuteTemplate(rw, "form.html", map[string]interface{}{
55
+		csrf.TemplateTag: csrf.TemplateField(req),
56
+	})
57
+}
58
+
59
+func showSuccess(rw http.ResponseWriter, req *http.Request) {
60
+	_ = successTemplate.ExecuteTemplate(rw, "success.html", map[string]interface{}{
61
+		csrf.TemplateTag: csrf.TemplateField(req),
62
+	})
63
+}
64
+
65
+func showFailure(rw http.ResponseWriter, req *http.Request) {
66
+	_ = failureTemplate.ExecuteTemplate(rw, "failure.html", map[string]interface{}{
67
+		csrf.TemplateTag: csrf.TemplateField(req),
68
+	})
69
+}
70
+
71
+func randomKey() string {
72
+	var runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
73
+	b := make([]rune, 32)
74
+	for i := range b {
75
+		b[i] = runes[rand.Intn(len(runes))]
76
+	}
77
+	return string(b)
78
+}
79
+
80
+func checkFlag(value string, name string) {
81
+	if len(value) == 0 {
82
+		_, _ = fmt.Fprintf(os.Stderr, "No %s specified\n", name)
83
+		flag.Usage()
84
+		os.Exit(1)
85
+	}
86
+}
87
+
88
+func loadTemplate(file string) (result *template.Template) {
89
+	var err error
90
+	result, err = template.ParseFiles(file)
91
+	if err != nil {
92
+		_, _ = fmt.Fprintf(os.Stderr, "Unable to load %s: %s\n", file, err.Error())
93
+		os.Exit(1)
94
+	}
95
+	return
96
+}
97
+
98
+func main() {
99
+	fromAddress = flag.String("from", "", "address to send e-mail from")
100
+	toAddress = flag.String("to", "", "address to send e-mail to")
101
+	subject = flag.String("subject", "Contact form submission", "e-mail subject")
102
+	smtpServer = flag.String("smtp-host", "", "SMTP server to connect to")
103
+	smtpPort = flag.Int("smtp-port", 25, "port to use when connecting to the SMTP server")
104
+	smtpUsername = flag.String("smtp-user", "", "username to supply to the SMTP server")
105
+	smtpPassword = flag.String("smtp-pass", "", "password to supply to the SMTP server")
106
+	csrfKey = flag.String("crsf-key", "", "CRSF key to use")
107
+	port = flag.Int("port", 8080, "port to listen on for connections")
108
+	flag.Parse()
109
+
110
+	checkFlag(*fromAddress, "from address")
111
+	checkFlag(*toAddress, "to address")
112
+	checkFlag(*smtpServer, "SMTP server")
113
+	checkFlag(*smtpUsername, "SMTP username")
114
+	checkFlag(*smtpPassword, "SMTP password")
115
+
116
+	if len(*csrfKey) != 32 {
117
+		newKey := randomKey()
118
+		csrfKey = &newKey
119
+	}
120
+
121
+	formTemplate = loadTemplate("form.html")
122
+	successTemplate = loadTemplate("success.html")
123
+	failureTemplate = loadTemplate("failure.html")
124
+
125
+	r := mux.NewRouter()
126
+	r.HandleFunc("/", showForm).Methods("GET")
127
+	r.HandleFunc("/success", showSuccess).Methods("GET")
128
+	r.HandleFunc("/failure", showFailure).Methods("GET")
129
+	r.HandleFunc("/submit", handleForm).Methods("POST")
130
+
131
+	CSRF := csrf.Protect([]byte(*csrfKey), csrf.FieldName(csrfFieldName))
132
+	err := http.ListenAndServe(fmt.Sprintf(":%d", *port), CSRF(r))
133
+	if err != nil {
134
+		_, _ = fmt.Fprintf(os.Stderr, "Unable to listen on port %d: %s\n", *port, err.Error())
135
+	}
136
+}

+ 33
- 0
success.html View File

@@ -0,0 +1,33 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta http-equiv="Content-Type" content="text/html" charset="UTF-8"/>
5
+    <title>Contact me</title>
6
+    <style type="text/css">
7
+        .page {
8
+            width: 480px;
9
+            padding: 8% 0 0;
10
+            margin: auto;
11
+        }
12
+
13
+        .form {
14
+            position: relative;
15
+            z-index: 1;
16
+            background: #FFFFFF;
17
+            max-width: 360px;
18
+            margin: 0 auto 100px;
19
+            padding: 45px;
20
+            text-align: center;
21
+            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
22
+            font-family: sans-serif;
23
+        }
24
+    </style>
25
+</head>
26
+<body>
27
+<div class="page">
28
+    <div class="form">
29
+        Your message has been sent. Thank you!
30
+    </div>
31
+</div>
32
+</body>
33
+</html>

Loading…
Cancel
Save