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