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