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