Implement CSRF protection
[cacert-boardvoting.git] / boardvoting.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "crypto/x509"
8 "database/sql"
9 "encoding/base64"
10 "encoding/pem"
11 "flag"
12 "fmt"
13 "html/template"
14 "io/ioutil"
15 "net/http"
16 "os"
17 "sort"
18 "strconv"
19 "strings"
20 "time"
21
22 "github.com/Masterminds/sprig"
23 "github.com/gorilla/csrf"
24 "github.com/gorilla/sessions"
25 _ "github.com/mattn/go-sqlite3"
26 "github.com/op/go-logging"
27 "gopkg.in/yaml.v2"
28
29 "git.cacert.org/cacert-boardvoting/boardvoting"
30 )
31
32 var configFile string
33 var config *Config
34 var store *sessions.CookieStore
35 var csrfKey []byte
36 var version = "undefined"
37 var build = "undefined"
38 var log *logging.Logger
39
40 const sessionCookieName = "votesession"
41
42 func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
43 funcMaps := sprig.FuncMap()
44 funcMaps["nl2br"] = func(text string) template.HTML {
45 return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
46 }
47 funcMaps[csrf.TemplateTag] = func() template.HTML {
48 return csrf.TemplateField(r)
49 }
50
51 var baseTemplate *template.Template
52
53 for count, t := range templates {
54 if assetBytes, err := boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
55 http.Error(w, err.Error(), http.StatusInternalServerError)
56 } else {
57 if count == 0 {
58 if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
59 http.Error(w, err.Error(), http.StatusInternalServerError)
60 }
61 } else {
62 if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
63 http.Error(w, err.Error(), http.StatusInternalServerError)
64 }
65 }
66 }
67 }
68
69 if err := baseTemplate.Execute(w, context); err != nil {
70 http.Error(w, err.Error(), http.StatusInternalServerError)
71 }
72 }
73
74 type contextKey int
75
76 const (
77 ctxNeedsAuth contextKey = iota
78 ctxVoter
79 ctxDecision
80 ctxVote
81 ctxAuthenticatedCert
82 )
83
84 func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
85 emailsTried := make(map[string]bool)
86 for _, cert := range r.TLS.PeerCertificates {
87 for _, extKeyUsage := range cert.ExtKeyUsage {
88 if extKeyUsage == x509.ExtKeyUsageClientAuth {
89 for _, emailAddress := range cert.EmailAddresses {
90 emailLower := strings.ToLower(emailAddress)
91 emailsTried[emailLower] = true
92 voter, err := FindVoterByAddress(emailLower)
93 if err != nil {
94 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
95 return
96 }
97 if voter != nil {
98 requestContext := context.WithValue(r.Context(), ctxVoter, voter)
99 requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
100 handler(w, r.WithContext(requestContext))
101 return
102 }
103 }
104 }
105 }
106 }
107 needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
108 if ok && needsAuth {
109 var templateContext struct {
110 PageTitle string
111 Voter *Voter
112 Flashes interface{}
113 Emails []string
114 }
115 for k := range emailsTried {
116 templateContext.Emails = append(templateContext.Emails, k)
117 }
118 sort.Strings(templateContext.Emails)
119 w.WriteHeader(http.StatusForbidden)
120 renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
121 return
122 }
123 handler(w, r)
124 }
125
126 type motionParameters struct {
127 ShowVotes bool
128 }
129
130 type motionListParameters struct {
131 Page int64
132 Flags struct {
133 Confirmed, Withdraw, Unvoted bool
134 }
135 }
136
137 func parseMotionParameters(r *http.Request) motionParameters {
138 var m = motionParameters{}
139 m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
140 return m
141 }
142
143 func parseMotionListParameters(r *http.Request) motionListParameters {
144 var m = motionListParameters{}
145 if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); err != nil {
146 m.Page = 1
147 } else {
148 m.Page = page
149 }
150 m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
151 m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
152
153 if r.Method == http.MethodPost {
154 m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
155 }
156 return m
157 }
158
159 func motionListHandler(w http.ResponseWriter, r *http.Request) {
160 params := parseMotionListParameters(r)
161 session, err := store.Get(r, sessionCookieName)
162 if err != nil {
163 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
164 return
165 }
166
167 var templateContext struct {
168 Decisions []*DecisionForDisplay
169 Voter *Voter
170 Params *motionListParameters
171 PrevPage, NextPage int64
172 PageTitle string
173 Flashes interface{}
174 }
175 if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
176 templateContext.Voter = voter
177 }
178 if flashes := session.Flashes(); len(flashes) > 0 {
179 templateContext.Flashes = flashes
180 }
181 session.Save(r, w)
182 templateContext.Params = &params
183
184 if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
185 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
186 return
187 }
188
189 if len(templateContext.Decisions) > 0 {
190 olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
191 if err != nil {
192 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
193 return
194 }
195 if olderExists {
196 templateContext.NextPage = params.Page + 1
197 }
198 }
199
200 if params.Page > 1 {
201 templateContext.PrevPage = params.Page - 1
202 }
203
204 renderTemplate(w, r, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
205 }
206
207 func motionHandler(w http.ResponseWriter, r *http.Request) {
208 params := parseMotionParameters(r)
209
210 decision, ok := getDecisionFromRequest(r)
211 if !ok {
212 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
213 return
214 }
215
216 var templateContext struct {
217 Decision *DecisionForDisplay
218 Voter *Voter
219 Params *motionParameters
220 PrevPage, NextPage int64
221 PageTitle string
222 Flashes interface{}
223 }
224 voter, ok := getVoterFromRequest(r)
225 if ok {
226 templateContext.Voter = voter
227 }
228 templateContext.Params = &params
229 if params.ShowVotes {
230 if err := decision.LoadVotes(); err != nil {
231 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
232 return
233 }
234 }
235 templateContext.Decision = decision
236 templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
237 renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
238 }
239
240 func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
241 decision, err := FindDecisionForDisplayByTag(tag)
242 if err != nil {
243 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
244 return
245 }
246 if decision == nil {
247 http.NotFound(w, r)
248 return
249 }
250 handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
251 }
252
253 type motionActionHandler interface {
254 Handle(w http.ResponseWriter, r *http.Request)
255 NeedsAuth() bool
256 }
257
258 type authenticationRequiredHandler struct{}
259
260 func (authenticationRequiredHandler) NeedsAuth() bool {
261 return true
262 }
263
264 func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
265 voter, ok = r.Context().Value(ctxVoter).(*Voter)
266 return
267 }
268
269 func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
270 decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
271 return
272 }
273
274 func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
275 vote, ok = r.Context().Value(ctxVote).(VoteChoice)
276 return
277 }
278
279 type FlashMessageAction struct{}
280
281 func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) {
282 session, err := store.Get(r, sessionCookieName)
283 if err != nil {
284 log.Errorf("getting session failed: %v", err)
285 return
286 }
287 session.AddFlash(message, tags...)
288 session.Save(r, w)
289 if err != nil {
290 log.Errorf("saving session failed: %v", err)
291 return
292 }
293 return
294 }
295
296 type withDrawMotionAction struct {
297 FlashMessageAction
298 authenticationRequiredHandler
299 }
300
301 func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
302 decision, ok := getDecisionFromRequest(r)
303 if !ok || decision.Status != voteStatusPending {
304 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
305 return
306 }
307 voter, ok := getVoterFromRequest(r)
308 if !ok {
309 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
310 return
311 }
312 templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html"}
313 var templateContext struct {
314 PageTitle string
315 Decision *DecisionForDisplay
316 Flashes interface{}
317 Voter *Voter
318 }
319
320 switch r.Method {
321 case http.MethodPost:
322 decision.Status = voteStatusWithdrawn
323 decision.Modified = time.Now().UTC()
324 if err := decision.UpdateStatus(); err != nil {
325 log.Errorf("withdrawing motion failed: %v", err)
326 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
327 return
328 }
329
330 NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
331
332 a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag))
333
334 http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
335 default:
336 templateContext.Decision = decision
337 templateContext.Voter = voter
338 renderTemplate(w, r, templates, templateContext)
339 }
340 }
341
342 type newMotionHandler struct {
343 FlashMessageAction
344 }
345
346 func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
347 voter, ok := getVoterFromRequest(r)
348 if !ok {
349 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
350 }
351
352 templates := []string{"create_motion_form.html", "header.html", "footer.html"}
353 var templateContext struct {
354 Form NewDecisionForm
355 PageTitle string
356 Voter *Voter
357 Flashes interface{}
358 }
359 switch r.Method {
360 case http.MethodPost:
361 form := NewDecisionForm{
362 Title: r.FormValue("Title"),
363 Content: r.FormValue("Content"),
364 VoteType: r.FormValue("VoteType"),
365 Due: r.FormValue("Due"),
366 }
367
368 if valid, data := form.Validate(); !valid {
369 templateContext.Voter = voter
370 templateContext.Form = form
371 renderTemplate(w, r, templates, templateContext)
372 } else {
373 data.Proposed = time.Now().UTC()
374 data.ProponentId = voter.Id
375 if err := data.Create(); err != nil {
376 log.Errorf("saving motion failed: %v", err)
377 http.Error(w, "Saving motion failed", http.StatusInternalServerError)
378 return
379 }
380
381 NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
382
383 h.AddFlash(w, r, "The motion has been proposed!")
384
385 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
386 }
387
388 return
389 default:
390 templateContext.Voter = voter
391 templateContext.Form = NewDecisionForm{
392 VoteType: strconv.FormatInt(voteTypeMotion, 10),
393 }
394 renderTemplate(w, r, templates, templateContext)
395 }
396 }
397
398 type editMotionAction struct {
399 FlashMessageAction
400 authenticationRequiredHandler
401 }
402
403 func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
404 decision, ok := getDecisionFromRequest(r)
405 if !ok || decision.Status != voteStatusPending {
406 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
407 return
408 }
409 voter, ok := getVoterFromRequest(r)
410 if !ok {
411 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
412 return
413 }
414 templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
415 var templateContext struct {
416 Form EditDecisionForm
417 PageTitle string
418 Voter *Voter
419 Flashes interface{}
420 }
421 switch r.Method {
422 case http.MethodPost:
423 form := EditDecisionForm{
424 Title: r.FormValue("Title"),
425 Content: r.FormValue("Content"),
426 VoteType: r.FormValue("VoteType"),
427 Due: r.FormValue("Due"),
428 Decision: &decision.Decision,
429 }
430
431 if valid, data := form.Validate(); !valid {
432 templateContext.Voter = voter
433 templateContext.Form = form
434 renderTemplate(w, r, templates, templateContext)
435 } else {
436 data.Modified = time.Now().UTC()
437 if err := data.Update(); err != nil {
438 log.Errorf("updating motion failed: %v", err)
439 http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
440 return
441 }
442
443 NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
444
445 a.AddFlash(w, r, "The motion has been modified!")
446
447 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
448 }
449 return
450 default:
451 templateContext.Voter = voter
452 templateContext.Form = EditDecisionForm{
453 Title: decision.Title,
454 Content: decision.Content,
455 VoteType: fmt.Sprintf("%d", decision.VoteType),
456 Decision: &decision.Decision,
457 }
458 renderTemplate(w, r, templates, templateContext)
459 }
460 }
461
462 type motionsHandler struct{}
463
464 func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
465 subURL := r.URL.Path
466
467 var motionActionMap = map[string]motionActionHandler{
468 "withdraw": &withDrawMotionAction{},
469 "edit": &editMotionAction{},
470 }
471
472 switch {
473 case subURL == "":
474 authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
475 return
476 case subURL == "/newmotion/":
477 handler := &newMotionHandler{}
478 authenticateRequest(
479 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
480 handler.Handle)
481 return
482 case strings.Count(subURL, "/") == 1:
483 parts := strings.Split(subURL, "/")
484 motionTag := parts[0]
485 action, ok := motionActionMap[parts[1]]
486 if !ok {
487 http.NotFound(w, r)
488 return
489 }
490 authenticateRequest(
491 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
492 func(w http.ResponseWriter, r *http.Request) {
493 singleDecisionHandler(w, r, motionTag, action.Handle)
494 })
495 return
496 case strings.Count(subURL, "/") == 0:
497 authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
498 func(w http.ResponseWriter, r *http.Request) {
499 singleDecisionHandler(w, r, subURL, motionHandler)
500 })
501 return
502 default:
503 http.NotFound(w, r)
504 return
505 }
506 }
507
508 type directVoteHandler struct {
509 FlashMessageAction
510 authenticationRequiredHandler
511 }
512
513 func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
514 decision, ok := getDecisionFromRequest(r)
515 if !ok {
516 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
517 return
518 }
519 voter, ok := getVoterFromRequest(r)
520 if !ok {
521 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
522 return
523 }
524 vote, ok := getVoteFromRequest(r)
525 if !ok {
526 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
527 return
528 }
529 switch r.Method {
530 case http.MethodPost:
531 voteResult := &Vote{
532 VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
533 Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
534 if err := voteResult.Save(); err != nil {
535 log.Errorf("Problem saving vote: %v", err)
536 http.Error(w, "Problem saving vote", http.StatusInternalServerError)
537 return
538 }
539
540 NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult)
541
542 h.AddFlash(w, r, "Your vote has been registered.")
543
544 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
545 default:
546 templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
547 var templateContext struct {
548 Decision *DecisionForDisplay
549 VoteChoice VoteChoice
550 PageTitle string
551 Flashes interface{}
552 Voter *Voter
553 }
554 templateContext.Decision = decision
555 templateContext.VoteChoice = vote
556 templateContext.Voter = voter
557 renderTemplate(w, r, templates, templateContext)
558 }
559 }
560
561 type proxyVoteHandler struct {
562 FlashMessageAction
563 authenticationRequiredHandler
564 }
565
566 func getPEMClientCert(r *http.Request) string {
567 clientCertPEM := bytes.NewBufferString("")
568 authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
569 pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
570 return clientCertPEM.String()
571 }
572
573 func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
574 decision, ok := getDecisionFromRequest(r)
575 if !ok {
576 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
577 return
578 }
579 proxy, ok := getVoterFromRequest(r)
580 if !ok {
581 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
582 return
583 }
584 templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
585 var templateContext struct {
586 Form ProxyVoteForm
587 Decision *DecisionForDisplay
588 Voters *[]Voter
589 PageTitle string
590 Flashes interface{}
591 Voter *Voter
592 }
593 templateContext.Voter = proxy
594 switch r.Method {
595 case http.MethodPost:
596 form := ProxyVoteForm{
597 Voter: r.FormValue("Voter"),
598 Vote: r.FormValue("Vote"),
599 Justification: r.FormValue("Justification"),
600 }
601
602 if valid, voter, data, justification := form.Validate(); !valid {
603 templateContext.Form = form
604 templateContext.Decision = decision
605 if voters, err := GetVotersForProxy(proxy); err != nil {
606 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
607 return
608 } else {
609 templateContext.Voters = voters
610 }
611 renderTemplate(w, r, templates, templateContext)
612 } else {
613 data.DecisionId = decision.Id
614 data.Voted = time.Now().UTC()
615 data.Notes = fmt.Sprintf(
616 "Proxy-Vote by %s\n\n%s\n\n%s",
617 proxy.Name, justification, getPEMClientCert(r))
618
619 if err := data.Save(); err != nil {
620 log.Errorf("Error saving vote: %s", err)
621 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
622 return
623 }
624
625 NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
626
627 h.AddFlash(w, r, "The vote has been registered.")
628
629 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
630 }
631 return
632 default:
633 templateContext.Form = ProxyVoteForm{}
634 templateContext.Decision = decision
635 if voters, err := GetVotersForProxy(proxy); err != nil {
636 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
637 return
638 } else {
639 templateContext.Voters = voters
640 }
641 renderTemplate(w, r, templates, templateContext)
642 }
643 }
644
645 type decisionVoteHandler struct{}
646
647 func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
648 switch {
649 case strings.HasPrefix(r.URL.Path, "/proxy/"):
650 motionTag := r.URL.Path[len("/proxy/"):]
651 handler := &proxyVoteHandler{}
652 authenticateRequest(
653 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
654 func(w http.ResponseWriter, r *http.Request) {
655 singleDecisionHandler(w, r, motionTag, handler.Handle)
656 })
657 case strings.HasPrefix(r.URL.Path, "/vote/"):
658 parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
659 if len(parts) != 2 {
660 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
661 return
662 }
663 motionTag := parts[0]
664 voteValue, ok := VoteValues[parts[1]]
665 if !ok {
666 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
667 return
668 }
669 handler := &directVoteHandler{}
670 authenticateRequest(
671 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
672 func(w http.ResponseWriter, r *http.Request) {
673 singleDecisionHandler(
674 w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
675 motionTag, handler.Handle)
676 })
677 return
678 }
679 }
680
681 type Config struct {
682 NoticeMailAddress string `yaml:"notice_mail_address"`
683 VoteNoticeMailAddress string `yaml:"vote_notice_mail_address"`
684 NotificationSenderAddress string `yaml:"notification_sender_address"`
685 DatabaseFile string `yaml:"database_file"`
686 ClientCACertificates string `yaml:"client_ca_certificates"`
687 ServerCert string `yaml:"server_certificate"`
688 ServerKey string `yaml:"server_key"`
689 CookieSecret string `yaml:"cookie_secret"`
690 CsrfKey string `yaml:"csrf_key"`
691 BaseURL string `yaml:"base_url"`
692 MigrationsPath string `yaml:"migrations_path"`
693 HttpAddress string `yaml:"http_address"`
694 HttpsAddress string `yaml:"https_address"`
695 MailServer struct {
696 Host string `yaml:"host"`
697 Port int `yaml:"port"`
698 } `yaml:"mail_server"`
699 }
700
701 func setupLogging(ctx context.Context) {
702 log = logging.MustGetLogger("boardvoting")
703 consoleLogFormat := logging.MustStringFormatter(`%{color}%{time:20060102 15:04:05.000-0700} %{longfile} ▶ %{level:s} %{id:05d}%{color:reset} %{message}`)
704 fileLogFormat := logging.MustStringFormatter(`%{time:20060102 15:04:05.000-0700} %{level:s} %{id:05d} %{message}`)
705
706 consoleBackend := logging.NewLogBackend(os.Stderr, "", 0)
707
708 logfile, err := os.OpenFile("boardvoting.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.FileMode(0640))
709 if err != nil {
710 panic("Could not open logfile")
711 }
712
713 fileBackend := logging.NewLogBackend(logfile, "", 0)
714 fileBackendLeveled := logging.AddModuleLevel(logging.NewBackendFormatter(fileBackend, fileLogFormat))
715 fileBackendLeveled.SetLevel(logging.INFO, "")
716
717 logging.SetBackend(fileBackendLeveled,
718 logging.NewBackendFormatter(consoleBackend, consoleLogFormat))
719
720 go func() {
721 for range ctx.Done() {
722 if err = logfile.Close(); err != nil {
723 fmt.Fprintf(os.Stderr, "Problem closing the log file: %v", err)
724 }
725 }
726 }()
727
728 log.Info("Setup logging")
729 }
730
731 func readConfig() {
732 source, err := ioutil.ReadFile(configFile)
733 if err != nil {
734 log.Panicf("Opening configuration file failed: %v", err)
735 }
736 if err := yaml.Unmarshal(source, &config); err != nil {
737 log.Panicf("Loading configuration failed: %v", err)
738 }
739
740 if config.HttpsAddress == "" {
741 config.HttpsAddress = "127.0.0.1:8443"
742 }
743 if config.HttpAddress == "" {
744 config.HttpAddress = "127.0.0.1:8080"
745 }
746
747 cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
748 if err != nil {
749 log.Panicf("Decoding cookie secret failed: %v", err)
750 panic(err)
751 }
752 if len(cookieSecret) < 32 {
753 log.Panic("Cookie secret is less than 32 bytes long")
754 }
755 csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
756 if err != nil {
757 log.Panicf("Decoding csrf key failed: %v", err)
758 }
759 if len(csrfKey) != 32 {
760 log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
761 }
762 store = sessions.NewCookieStore(cookieSecret)
763 log.Info("Read configuration")
764 }
765
766 func setupDbConfig(ctx context.Context) {
767 database, err := sql.Open("sqlite3", config.DatabaseFile)
768 if err != nil {
769 log.Panicf("Opening database failed: %v", err)
770 }
771 db = NewDB(database)
772
773 go func() {
774 for range ctx.Done() {
775 if err := db.Close(); err != nil {
776 fmt.Fprintf(os.Stderr, "Problem closing the database: %v", err)
777 }
778 }
779 }()
780
781 log.Infof("opened database connection")
782 }
783
784 func setupNotifications(ctx context.Context) {
785 quitMailChannel := make(chan int)
786 go MailNotifier(quitMailChannel)
787
788 go func() {
789 for range ctx.Done() {
790 quitMailChannel <- 1
791 }
792 }()
793 }
794
795 func setupJobs(ctx context.Context) {
796 quitChannel := make(chan int)
797 go JobScheduler(quitChannel)
798
799 go func() {
800 for range ctx.Done() {
801 quitChannel <- 1
802 }
803 }()
804 }
805
806 func setupHandlers() {
807 http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
808 http.Handle("/newmotion/", motionsHandler{})
809 http.Handle("/proxy/", &decisionVoteHandler{})
810 http.Handle("/vote/", &decisionVoteHandler{})
811 http.Handle("/static/", http.FileServer(boardvoting.GetAssetFS()))
812 http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
813 }
814
815 func setupTLSConfig() (tlsConfig *tls.Config) {
816 // load CA certificates for client authentication
817 caCert, err := ioutil.ReadFile(config.ClientCACertificates)
818 if err != nil {
819 log.Panicf("Error reading client certificate CAs %v", err)
820 }
821 caCertPool := x509.NewCertPool()
822 if !caCertPool.AppendCertsFromPEM(caCert) {
823 log.Panic("could not initialize client CA certificate pool")
824 }
825
826 // setup HTTPS server
827 tlsConfig = &tls.Config{
828 ClientCAs: caCertPool,
829 ClientAuth: tls.VerifyClientCertIfGiven,
830 }
831 tlsConfig.BuildNameToCertificate()
832 return
833 }
834
835 func init() {
836 flag.StringVar(
837 &configFile, "config", "config.yaml", "Configuration file name")
838 }
839
840 func main() {
841 flag.Parse()
842
843 var stopAll func()
844 executionContext, stopAll := context.WithCancel(context.Background())
845 setupLogging(executionContext)
846 readConfig()
847 setupDbConfig(executionContext)
848 setupNotifications(executionContext)
849 setupJobs(executionContext)
850 setupHandlers()
851 tlsConfig := setupTLSConfig()
852
853 defer stopAll()
854
855 log.Infof("CAcert Board Voting version %s, build %s", version, build)
856
857 server := &http.Server{
858 Addr: config.HttpsAddress,
859 TLSConfig: tlsConfig,
860 }
861
862 server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
863
864 log.Infof("Launching application on https://%s/", server.Addr)
865
866 errs := make(chan error, 1)
867 go func() {
868 if err := http.ListenAndServe(config.HttpsAddress, http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
869 errs <- err
870 }
871 close(errs)
872 }()
873
874 if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
875 log.Panicf("ListenAndServerTLS failed: %v", err)
876 }
877 if err := <-errs; err != nil {
878 log.Panicf("ListenAndServe failed: %v", err)
879 }
880 }