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