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