-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
245 lines (192 loc) · 7.04 KB
/
Copy pathmain.go
File metadata and controls
245 lines (192 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// aws lambda function handler for web page contact forms with binary attachments
// copyright (c) 2019 Dennis Furey
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"io"
"fmt"
"mime"
"bytes"
"errors"
"strings"
"net/textproto"
"mime/multipart"
"encoding/base64"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
)
const (
success_page = "http://www.example.com/thankyou.html" // redirect here when the contact form submission succeeds
failure_page = "http://www.example.com/problem.html" // redirect here when the contact form submission fails
sender = "my_contact_form@example.com" // form mailed from here; must be validated in advance with SES
recipient = "my_personal_email@my_provider.com" // sent to here; must be validated in advance with SES
subject = "contact me"
charset = "UTF-8"
region = "us-west-2"
)
func confirmation(url string) (events.APIGatewayProxyResponse, error) {
// Make the user's browser load the page at the given url and exit
// the handler.
res := events.APIGatewayProxyResponse{
StatusCode: 301, // not 200 or else
Headers: map[string]string{"Location": url},
}
return res, nil
}
func form_fields (req events.APIGatewayProxyRequest) (*multipart.Form, error) {
// Return the contact form fields from the base64 encoded request
// body. Maintainers of this code should check header field
// identifier capitalizations if it stops working after an API
// update.
decoded, err := base64.StdEncoding.DecodeString(req.Body)
if err != nil {
return nil, err
}
mediatype, parts, err := mime.ParseMediaType(req.Headers["content-type"]) // must be lower case content-type or else
if err != nil {
return nil, err
}
if ! strings.HasPrefix(mediatype, "multipart/") { // probably better be lower case here too
return nil, err
}
mr := multipart.NewReader(bytes.NewReader (decoded), parts["boundary"]) // lower case here too
return mr.ReadForm (16777216) // 16 mb of memory, but 10 are enough
}
func email_body (form *multipart.Form, message *bytes.Buffer) (*multipart.Writer, error) {
// Initialize and return a multipart message writer associated with
// the given buffer after writing the first part of it, which will
// be made to contain the text portion of the email triggered by
// the contact form submission. The contact form depends on the web
// front end and is assumed to have a honeypot field and three
// useful fields named as shown.
mw := multipart.NewWriter (message)
h := textproto.MIMEHeader{
"Content-Type": {"text/plain; charset=utf-8"},
"Content-Transfer-Encoding": {"quoted-printable"},
}
body, err := mw.CreatePart (h)
if err != nil {
return mw, err
}
field := strings.Join(form.Value["office"],"\n") // hidden field that should always be empty unless filled by a bot,
if field != "" { // plausibly named with display:none buried in the CSS
err = errors.New ("spambot attack suspected")
return mw, err
}
field = strings.Join(form.Value["name"],"\n")
if field == "" {
field = "(withheld)"
}
_, err = fmt.Fprintf (body, "name: %s\n\n", field)
if err != nil {
return mw, err
}
field = strings.Join(form.Value["email"],"\n")
if field == "" {
field = "(withheld)"
}
_, err = fmt.Fprintf (body, "email: %s\n\n", field)
if err != nil {
return mw, err
}
field = strings.Join(form.Value["message"],"\n")
if field == "" {
field = "(withheld)"
}
_, err = fmt.Fprintf (body, "message:\n\n%s", field)
return mw, err
}
func unattachable (mw *multipart.Writer, fileheader *multipart.FileHeader) error {
// Load the content of an attachable file from the fileheader into
// the next part of the multipart message context mw in base64
// encoded format.
mimetype := ""
if strings.Contains (fileheader.Filename, ".") {
fields := strings.Split (fileheader.Filename, ".")
mimetype = mime.TypeByExtension("." + fields[len(fields) - 1])
}
if mimetype == "" {
mimetype = "application/octet-stream"
}
h := textproto.MIMEHeader{
"Content-Type": {mimetype},
"Content-Transfer-Encoding": {"base64"},
"Content-Disposition": {"attachment; filename=\"" + fileheader.Filename + "\""},
}
attachment_src, err := fileheader.Open ()
if err != nil {
return err
}
attachment_dest, err := mw.CreatePart (h)
if err != nil {
return err
}
encoder := base64.NewEncoder (base64.StdEncoding, attachment_dest)
_, err = io.Copy (encoder, attachment_src)
if err != nil {
return err
}
return encoder.Close ()
}
func header_of (boundary string) []byte {
// Return the email header, which depends on the multipart message
// boundary. The subject has to come last or else.
var header bytes.Buffer
header.WriteString("MIME-Version: 1.0\n")
header.WriteString("Content-Disposition: inline\n")
header.WriteString("Content-Type: multipart/mixed; boundary=\"" + boundary + "\"\n")
header.WriteString("From: " + sender + "\n")
header.WriteString("To: " + recipient + "\n")
header.WriteString("Subject: " + subject +"\n\n")
return header.Bytes()
}
func unsendable (header []byte, message []byte) error {
// Put the header and the message together and mail them with the
// SES raw message API.
params := &ses.SendRawEmailInput{RawMessage: &ses.RawMessage{ Data: bytes.Join ([][]byte{header,message}, []byte{})}}
sess, err := session.NewSession(&aws.Config{ Region:aws.String(region)} )
if err == nil {
svc := ses.New(sess)
_, err = svc.SendRawEmail(params)
}
return err
}
func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Get the form fields from the request, email them, and show a
// confirmation.
var message bytes.Buffer
form, err := form_fields (req)
if err != nil {
return confirmation (failure_page)
}
mw, err := email_body (form, &message)
for _, fileheader := range form.File["attachment"] {
if (err == nil) && (fileheader.Filename != "") {
err = unattachable (mw, fileheader)
}
}
if (err != nil) || (mw.Close () != nil) {
return confirmation (failure_page)
}
if unsendable (header_of (mw.Boundary ()), message.Bytes ()) != nil {
return confirmation (failure_page)
}
return confirmation (success_page)
}
func main() {
lambda.Start(handler)
}