Implement CSRF protection
[cacert-boardvoting.git] / boardvoting.go
index 1fe031e..e8a83e4 100644 (file)
@@ -1,37 +1,72 @@
 package main
 
 import (
+       "bytes"
        "context"
        "crypto/tls"
        "crypto/x509"
+       "database/sql"
+       "encoding/base64"
+       "encoding/pem"
+       "flag"
        "fmt"
-       "github.com/Masterminds/sprig"
-       "github.com/jmoiron/sqlx"
-       _ "github.com/mattn/go-sqlite3"
-       "gopkg.in/yaml.v2"
        "html/template"
        "io/ioutil"
-       "log"
        "net/http"
        "os"
+       "sort"
        "strconv"
        "strings"
+       "time"
+
+       "github.com/Masterminds/sprig"
+       "github.com/gorilla/csrf"
+       "github.com/gorilla/sessions"
+       _ "github.com/mattn/go-sqlite3"
+       "github.com/op/go-logging"
+       "gopkg.in/yaml.v2"
+
+       "git.cacert.org/cacert-boardvoting/boardvoting"
 )
 
-var logger *log.Logger
+var configFile string
 var config *Config
+var store *sessions.CookieStore
+var csrfKey []byte
+var version = "undefined"
+var build = "undefined"
+var log *logging.Logger
 
-func getTemplateFilenames(tmpl []string) (result []string) {
-       result = make([]string, len(tmpl))
-       for i := range tmpl {
-               result[i] = fmt.Sprintf("templates/%s", tmpl[i])
+const sessionCookieName = "votesession"
+
+func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
+       funcMaps := sprig.FuncMap()
+       funcMaps["nl2br"] = func(text string) template.HTML {
+               return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
+       }
+       funcMaps[csrf.TemplateTag] = func() template.HTML {
+               return csrf.TemplateField(r)
+       }
+
+       var baseTemplate *template.Template
+
+       for count, t := range templates {
+               if assetBytes, err := boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+               } else {
+                       if count == 0 {
+                               if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+                                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                               }
+                       } else {
+                               if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+                                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                               }
+                       }
+               }
        }
-       return result
-}
 
-func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
-       t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...))
-       if err := t.Execute(w, context); err != nil {
+       if err := baseTemplate.Execute(w, context); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
        }
 }
@@ -39,23 +74,30 @@ func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
 type contextKey int
 
 const (
-       ctxNeedsAuth contextKey = iota
-       ctxVoter     contextKey = iota
-       ctxDecision  contextKey = iota
+       ctxNeedsAuth         contextKey = iota
+       ctxVoter
+       ctxDecision
+       ctxVote
+       ctxAuthenticatedCert
 )
 
 func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
+       emailsTried := make(map[string]bool)
        for _, cert := range r.TLS.PeerCertificates {
                for _, extKeyUsage := range cert.ExtKeyUsage {
                        if extKeyUsage == x509.ExtKeyUsageClientAuth {
                                for _, emailAddress := range cert.EmailAddresses {
-                                       voter, err := FindVoterByAddress(emailAddress)
+                                       emailLower := strings.ToLower(emailAddress)
+                                       emailsTried[emailLower] = true
+                                       voter, err := FindVoterByAddress(emailLower)
                                        if err != nil {
                                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                                                return
                                        }
                                        if voter != nil {
-                                               handler(w, r.WithContext(context.WithValue(r.Context(), ctxVoter, voter)))
+                                               requestContext := context.WithValue(r.Context(), ctxVoter, voter)
+                                               requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
+                                               handler(w, r.WithContext(requestContext))
                                                return
                                        }
                                }
@@ -64,8 +106,18 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
        }
        needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
        if ok && needsAuth {
+               var templateContext struct {
+                       PageTitle string
+                       Voter     *Voter
+                       Flashes   interface{}
+                       Emails    []string
+               }
+               for k := range emailsTried {
+                       templateContext.Emails = append(templateContext.Emails, k)
+               }
+               sort.Strings(templateContext.Emails)
                w.WriteHeader(http.StatusForbidden)
-               renderTemplate(w, []string{"denied.html"}, nil)
+               renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
                return
        }
        handler(w, r)
@@ -76,7 +128,7 @@ type motionParameters struct {
 }
 
 type motionListParameters struct {
-       Page  int64
+       Page int64
        Flags struct {
                Confirmed, Withdraw, Unvoted bool
        }
@@ -85,7 +137,6 @@ type motionListParameters struct {
 func parseMotionParameters(r *http.Request) motionParameters {
        var m = motionParameters{}
        m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
-       logger.Printf("parsed parameters: %+v\n", m)
        return m
 }
 
@@ -102,12 +153,16 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
        if r.Method == http.MethodPost {
                m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
        }
-       logger.Printf("parsed parameters: %+v\n", m)
        return m
 }
 
 func motionListHandler(w http.ResponseWriter, r *http.Request) {
        params := parseMotionListParameters(r)
+       session, err := store.Get(r, sessionCookieName)
+       if err != nil {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
 
        var templateContext struct {
                Decisions          []*DecisionForDisplay
@@ -115,28 +170,24 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
                Params             *motionListParameters
                PrevPage, NextPage int64
                PageTitle          string
+               Flashes            interface{}
        }
        if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
                templateContext.Voter = voter
        }
+       if flashes := session.Flashes(); len(flashes) > 0 {
+               templateContext.Flashes = flashes
+       }
+       session.Save(r, w)
        templateContext.Params = &params
-       var err error
 
-       if params.Flags.Unvoted && templateContext.Voter != nil {
-               if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(
-                       params.Page, templateContext.Voter); err != nil {
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-       } else {
-               if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
+       if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
        }
 
        if len(templateContext.Decisions) > 0 {
-               olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists()
+               olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
                if err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
@@ -150,7 +201,7 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
                templateContext.PrevPage = params.Page - 1
        }
 
-       renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
+       renderTemplate(w, r, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
 func motionHandler(w http.ResponseWriter, r *http.Request) {
@@ -168,6 +219,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
                Params             *motionParameters
                PrevPage, NextPage int64
                PageTitle          string
+               Flashes            interface{}
        }
        voter, ok := getVoterFromRequest(r)
        if ok {
@@ -182,7 +234,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
        }
        templateContext.Decision = decision
        templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
-       renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
+       renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
 func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
@@ -209,10 +261,6 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
        return true
 }
 
-type withDrawMotionAction struct {
-       authenticationRequiredHandler
-}
-
 func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
        voter, ok = r.Context().Value(ctxVoter).(*Voter)
        return
@@ -223,67 +271,216 @@ func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok b
        return
 }
 
-func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
-       voter, voter_ok := getVoterFromRequest(r)
-       decision, decision_ok := getDecisionFromRequest(r)
+func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
+       vote, ok = r.Context().Value(ctxVote).(VoteChoice)
+       return
+}
+
+type FlashMessageAction struct{}
+
+func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) {
+       session, err := store.Get(r, sessionCookieName)
+       if err != nil {
+               log.Errorf("getting session failed: %v", err)
+               return
+       }
+       session.AddFlash(message, tags...)
+       session.Save(r, w)
+       if err != nil {
+               log.Errorf("saving session failed: %v", err)
+               return
+       }
+       return
+}
+
+type withDrawMotionAction struct {
+       FlashMessageAction
+       authenticationRequiredHandler
+}
 
-       if !voter_ok || !decision_ok || decision.Status != voteStatusPending {
+func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok || decision.Status != voteStatusPending {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+               return
+       }
+       voter, ok := getVoterFromRequest(r)
+       if !ok {
                http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
                return
        }
+       templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html"}
+       var templateContext struct {
+               PageTitle string
+               Decision  *DecisionForDisplay
+               Flashes   interface{}
+               Voter     *Voter
+       }
 
        switch r.Method {
        case http.MethodPost:
-               if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
-                       log.Println("could not parse confirm parameter:", err)
-                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               } else if confirm {
-                       WithdrawMotion(&decision.Decision, voter)
-               } else {
-                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               decision.Status = voteStatusWithdrawn
+               decision.Modified = time.Now().UTC()
+               if err := decision.UpdateStatus(); err != nil {
+                       log.Errorf("withdrawing motion failed: %v", err)
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
                }
+
+               NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
+
+               a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag))
+
                http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
+       default:
+               templateContext.Decision = decision
+               templateContext.Voter = voter
+               renderTemplate(w, r, templates, templateContext)
+       }
+}
+
+type newMotionHandler struct {
+       FlashMessageAction
+}
+
+func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
+       voter, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+       }
+
+       templates := []string{"create_motion_form.html", "header.html", "footer.html"}
+       var templateContext struct {
+               Form      NewDecisionForm
+               PageTitle string
+               Voter     *Voter
+               Flashes   interface{}
+       }
+       switch r.Method {
+       case http.MethodPost:
+               form := NewDecisionForm{
+                       Title:    r.FormValue("Title"),
+                       Content:  r.FormValue("Content"),
+                       VoteType: r.FormValue("VoteType"),
+                       Due:      r.FormValue("Due"),
+               }
+
+               if valid, data := form.Validate(); !valid {
+                       templateContext.Voter = voter
+                       templateContext.Form = form
+                       renderTemplate(w, r, templates, templateContext)
+               } else {
+                       data.Proposed = time.Now().UTC()
+                       data.ProponentId = voter.Id
+                       if err := data.Create(); err != nil {
+                               log.Errorf("saving motion failed: %v", err)
+                               http.Error(w, "Saving motion failed", http.StatusInternalServerError)
+                               return
+                       }
+
+                       NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
+
+                       h.AddFlash(w, r, "The motion has been proposed!")
+
+                       http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
+               }
+
                return
        default:
-               fmt.Fprintln(w, "Withdraw motion", decision.Tag)
+               templateContext.Voter = voter
+               templateContext.Form = NewDecisionForm{
+                       VoteType: strconv.FormatInt(voteTypeMotion, 10),
+               }
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
 type editMotionAction struct {
+       FlashMessageAction
        authenticationRequiredHandler
 }
 
-func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
+func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
        decision, ok := getDecisionFromRequest(r)
        if !ok || decision.Status != voteStatusPending {
                http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
                return
        }
-       fmt.Fprintln(w, "Edit motion", decision.Tag)
-       // TODO: implement
+       voter, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+               return
+       }
+       templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
+       var templateContext struct {
+               Form      EditDecisionForm
+               PageTitle string
+               Voter     *Voter
+               Flashes   interface{}
+       }
+       switch r.Method {
+       case http.MethodPost:
+               form := EditDecisionForm{
+                       Title:    r.FormValue("Title"),
+                       Content:  r.FormValue("Content"),
+                       VoteType: r.FormValue("VoteType"),
+                       Due:      r.FormValue("Due"),
+                       Decision: &decision.Decision,
+               }
+
+               if valid, data := form.Validate(); !valid {
+                       templateContext.Voter = voter
+                       templateContext.Form = form
+                       renderTemplate(w, r, templates, templateContext)
+               } else {
+                       data.Modified = time.Now().UTC()
+                       if err := data.Update(); err != nil {
+                               log.Errorf("updating motion failed: %v", err)
+                               http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
+                               return
+                       }
+
+                       NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
+
+                       a.AddFlash(w, r, "The motion has been modified!")
+
+                       http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
+               }
+               return
+       default:
+               templateContext.Voter = voter
+               templateContext.Form = EditDecisionForm{
+                       Title:    decision.Title,
+                       Content:  decision.Content,
+                       VoteType: fmt.Sprintf("%d", decision.VoteType),
+                       Decision: &decision.Decision,
+               }
+               renderTemplate(w, r, templates, templateContext)
+       }
 }
 
 type motionsHandler struct{}
 
 func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       if err := db.Ping(); err != nil {
-               logger.Fatal(err)
-       }
-
        subURL := r.URL.Path
 
        var motionActionMap = map[string]motionActionHandler{
-               "withdraw": withDrawMotionAction{},
-               "edit":     editMotionAction{},
+               "withdraw": &withDrawMotionAction{},
+               "edit":     &editMotionAction{},
        }
 
        switch {
        case subURL == "":
                authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
                return
+       case subURL == "/newmotion/":
+               handler := &newMotionHandler{}
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
+                       handler.Handle)
+               return
        case strings.Count(subURL, "/") == 1:
                parts := strings.Split(subURL, "/")
-               logger.Printf("handle %v\n", parts)
                motionTag := parts[0]
                action, ok := motionActionMap[parts[1]]
                if !ok {
@@ -295,7 +492,6 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                        func(w http.ResponseWriter, r *http.Request) {
                                singleDecisionHandler(w, r, motionTag, action.Handle)
                        })
-               logger.Printf("motion: %s, action: %s\n", motionTag, action)
                return
        case strings.Count(subURL, "/") == 0:
                authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
@@ -304,87 +500,381 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                        })
                return
        default:
-               fmt.Fprintf(w, "No handler for '%s'", subURL)
+               http.NotFound(w, r)
+               return
+       }
+}
+
+type directVoteHandler struct {
+       FlashMessageAction
+       authenticationRequiredHandler
+}
+
+func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       voter, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       vote, ok := getVoteFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       switch r.Method {
+       case http.MethodPost:
+               voteResult := &Vote{
+                       VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
+                       Notes:   fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
+               if err := voteResult.Save(); err != nil {
+                       log.Errorf("Problem saving vote: %v", err)
+                       http.Error(w, "Problem saving vote", http.StatusInternalServerError)
+                       return
+               }
+
+               NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult)
+
+               h.AddFlash(w, r, "Your vote has been registered.")
+
+               http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
+       default:
+               templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
+               var templateContext struct {
+                       Decision   *DecisionForDisplay
+                       VoteChoice VoteChoice
+                       PageTitle  string
+                       Flashes    interface{}
+                       Voter      *Voter
+               }
+               templateContext.Decision = decision
+               templateContext.VoteChoice = vote
+               templateContext.Voter = voter
+               renderTemplate(w, r, templates, templateContext)
+       }
+}
+
+type proxyVoteHandler struct {
+       FlashMessageAction
+       authenticationRequiredHandler
+}
+
+func getPEMClientCert(r *http.Request) string {
+       clientCertPEM := bytes.NewBufferString("")
+       authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
+       pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
+       return clientCertPEM.String()
+}
+
+func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       proxy, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
+       var templateContext struct {
+               Form      ProxyVoteForm
+               Decision  *DecisionForDisplay
+               Voters    *[]Voter
+               PageTitle string
+               Flashes   interface{}
+               Voter     *Voter
+       }
+       templateContext.Voter = proxy
+       switch r.Method {
+       case http.MethodPost:
+               form := ProxyVoteForm{
+                       Voter:         r.FormValue("Voter"),
+                       Vote:          r.FormValue("Vote"),
+                       Justification: r.FormValue("Justification"),
+               }
+
+               if valid, voter, data, justification := form.Validate(); !valid {
+                       templateContext.Form = form
+                       templateContext.Decision = decision
+                       if voters, err := GetVotersForProxy(proxy); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       } else {
+                               templateContext.Voters = voters
+                       }
+                       renderTemplate(w, r, templates, templateContext)
+               } else {
+                       data.DecisionId = decision.Id
+                       data.Voted = time.Now().UTC()
+                       data.Notes = fmt.Sprintf(
+                               "Proxy-Vote by %s\n\n%s\n\n%s",
+                               proxy.Name, justification, getPEMClientCert(r))
+
+                       if err := data.Save(); err != nil {
+                               log.Errorf("Error saving vote: %s", err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+
+                       NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
+
+                       h.AddFlash(w, r, "The vote has been registered.")
+
+                       http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
+               }
                return
+       default:
+               templateContext.Form = ProxyVoteForm{}
+               templateContext.Decision = decision
+               if voters, err := GetVotersForProxy(proxy); err != nil {
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               } else {
+                       templateContext.Voters = voters
+               }
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
-func newMotionHandler(w http.ResponseWriter, r *http.Request) {
-       fmt.Fprintln(w, "New motion")
-       voter, _ := getVoterFromRequest(r)
-       fmt.Fprintf(w, "%+v\n", voter)
-       // TODO: implement
+type decisionVoteHandler struct{}
+
+func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       switch {
+       case strings.HasPrefix(r.URL.Path, "/proxy/"):
+               motionTag := r.URL.Path[len("/proxy/"):]
+               handler := &proxyVoteHandler{}
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(w, r, motionTag, handler.Handle)
+                       })
+       case strings.HasPrefix(r.URL.Path, "/vote/"):
+               parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
+               if len(parts) != 2 {
+                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+                       return
+               }
+               motionTag := parts[0]
+               voteValue, ok := VoteValues[parts[1]]
+               if !ok {
+                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+                       return
+               }
+               handler := &directVoteHandler{}
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(
+                                       w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
+                                       motionTag, handler.Handle)
+                       })
+               return
+       }
 }
 
 type Config struct {
-       BoardMailAddress     string `yaml:"board_mail_address"`
-       NoticeSenderAddress  string `yaml:"notice_sender_address"`
-       DatabaseFile         string `yaml:"database_file"`
-       ClientCACertificates string `yaml:"client_ca_certificates"`
-       ServerCert           string `yaml:"server_certificate"`
-       ServerKey            string `yaml:"server_key"`
+       NoticeMailAddress         string `yaml:"notice_mail_address"`
+       VoteNoticeMailAddress     string `yaml:"vote_notice_mail_address"`
+       NotificationSenderAddress string `yaml:"notification_sender_address"`
+       DatabaseFile              string `yaml:"database_file"`
+       ClientCACertificates      string `yaml:"client_ca_certificates"`
+       ServerCert                string `yaml:"server_certificate"`
+       ServerKey                 string `yaml:"server_key"`
+       CookieSecret              string `yaml:"cookie_secret"`
+       CsrfKey                   string `yaml:"csrf_key"`
+       BaseURL                   string `yaml:"base_url"`
+       MigrationsPath            string `yaml:"migrations_path"`
+       HttpAddress               string `yaml:"http_address"`
+       HttpsAddress              string `yaml:"https_address"`
+       MailServer struct {
+               Host string `yaml:"host"`
+               Port int    `yaml:"port"`
+       } `yaml:"mail_server"`
 }
 
-func main() {
-       logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
+func setupLogging(ctx context.Context) {
+       log = logging.MustGetLogger("boardvoting")
+       consoleLogFormat := logging.MustStringFormatter(`%{color}%{time:20060102 15:04:05.000-0700} %{longfile} ▶ %{level:s} %{id:05d}%{color:reset} %{message}`)
+       fileLogFormat := logging.MustStringFormatter(`%{time:20060102 15:04:05.000-0700} %{level:s} %{id:05d} %{message}`)
+
+       consoleBackend := logging.NewLogBackend(os.Stderr, "", 0)
 
-       var filename = "config.yaml"
-       if len(os.Args) == 2 {
-               filename = os.Args[1]
+       logfile, err := os.OpenFile("boardvoting.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.FileMode(0640))
+       if err != nil {
+               panic("Could not open logfile")
        }
 
-       var err error
+       fileBackend := logging.NewLogBackend(logfile, "", 0)
+       fileBackendLeveled := logging.AddModuleLevel(logging.NewBackendFormatter(fileBackend, fileLogFormat))
+       fileBackendLeveled.SetLevel(logging.INFO, "")
+
+       logging.SetBackend(fileBackendLeveled,
+               logging.NewBackendFormatter(consoleBackend, consoleLogFormat))
+
+       go func() {
+               for range ctx.Done() {
+                       if err = logfile.Close(); err != nil {
+                               fmt.Fprintf(os.Stderr, "Problem closing the log file: %v", err)
+                       }
+               }
+       }()
 
-       var source []byte
+       log.Info("Setup logging")
+}
 
-       source, err = ioutil.ReadFile(filename)
+func readConfig() {
+       source, err := ioutil.ReadFile(configFile)
        if err != nil {
-               logger.Fatal(err)
+               log.Panicf("Opening configuration file failed: %v", err)
+       }
+       if err := yaml.Unmarshal(source, &config); err != nil {
+               log.Panicf("Loading configuration failed: %v", err)
+       }
+
+       if config.HttpsAddress == "" {
+               config.HttpsAddress = "127.0.0.1:8443"
+       }
+       if config.HttpAddress == "" {
+               config.HttpAddress = "127.0.0.1:8080"
        }
-       err = yaml.Unmarshal(source, &config)
+
+       cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
        if err != nil {
-               logger.Fatal(err)
+               log.Panicf("Decoding cookie secret failed: %v", err)
+               panic(err)
+       }
+       if len(cookieSecret) < 32 {
+               log.Panic("Cookie secret is less than 32 bytes long")
        }
-       logger.Printf("read configuration %v", config)
+       csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
+       if err != nil {
+               log.Panicf("Decoding csrf key failed: %v", err)
+       }
+       if len(csrfKey) != 32 {
+               log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
+       }
+       store = sessions.NewCookieStore(cookieSecret)
+       log.Info("Read configuration")
+}
 
-       db, err = sqlx.Open("sqlite3", config.DatabaseFile)
+func setupDbConfig(ctx context.Context) {
+       database, err := sql.Open("sqlite3", config.DatabaseFile)
        if err != nil {
-               logger.Fatal(err)
+               log.Panicf("Opening database failed: %v", err)
        }
-       defer db.Close()
+       db = NewDB(database)
+
+       go func() {
+               for range ctx.Done() {
+                       if err := db.Close(); err != nil {
+                               fmt.Fprintf(os.Stderr, "Problem closing the database: %v", err)
+                       }
+               }
+       }()
+
+       log.Infof("opened database connection")
+}
+
+func setupNotifications(ctx context.Context) {
+       quitMailChannel := make(chan int)
+       go MailNotifier(quitMailChannel)
+
+       go func() {
+               for range ctx.Done() {
+                       quitMailChannel <- 1
+               }
+       }()
+}
+
+func setupJobs(ctx context.Context) {
+       quitChannel := make(chan int)
+       go JobScheduler(quitChannel)
 
+       go func() {
+               for range ctx.Done() {
+                       quitChannel <- 1
+               }
+       }()
+}
+
+func setupHandlers() {
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
-       http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
-               authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), newMotionHandler)
-       })
-       http.Handle("/static/", http.FileServer(http.Dir(".")))
+       http.Handle("/newmotion/", motionsHandler{})
+       http.Handle("/proxy/", &decisionVoteHandler{})
+       http.Handle("/vote/", &decisionVoteHandler{})
+       http.Handle("/static/", http.FileServer(boardvoting.GetAssetFS()))
        http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
+}
 
+func setupTLSConfig() (tlsConfig *tls.Config) {
        // load CA certificates for client authentication
        caCert, err := ioutil.ReadFile(config.ClientCACertificates)
        if err != nil {
-               logger.Fatal(err)
+               log.Panicf("Error reading client certificate CAs %v", err)
        }
        caCertPool := x509.NewCertPool()
        if !caCertPool.AppendCertsFromPEM(caCert) {
-               logger.Fatal("could not initialize client CA certificate pool")
+               log.Panic("could not initialize client CA certificate pool")
        }
 
        // setup HTTPS server
-       tlsConfig := &tls.Config{
+       tlsConfig = &tls.Config{
                ClientCAs:  caCertPool,
                ClientAuth: tls.VerifyClientCertIfGiven,
        }
        tlsConfig.BuildNameToCertificate()
+       return
+}
+
+func init() {
+       flag.StringVar(
+               &configFile, "config", "config.yaml", "Configuration file name")
+}
+
+func main() {
+       flag.Parse()
+
+       var stopAll func()
+       executionContext, stopAll := context.WithCancel(context.Background())
+       setupLogging(executionContext)
+       readConfig()
+       setupDbConfig(executionContext)
+       setupNotifications(executionContext)
+       setupJobs(executionContext)
+       setupHandlers()
+       tlsConfig := setupTLSConfig()
+
+       defer stopAll()
+
+       log.Infof("CAcert Board Voting version %s, build %s", version, build)
 
        server := &http.Server{
-               Addr:      ":8443",
+               Addr:      config.HttpsAddress,
                TLSConfig: tlsConfig,
        }
 
-       logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
+       server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
+
+       log.Infof("Launching application on https://%s/", server.Addr)
 
-       if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
-               logger.Fatal("ListenAndServerTLS: ", err)
+       errs := make(chan error, 1)
+       go func() {
+               if err := http.ListenAndServe(config.HttpsAddress, http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
+                       errs <- err
+               }
+               close(errs)
+       }()
+
+       if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
+               log.Panicf("ListenAndServerTLS failed: %v", err)
+       }
+       if err := <-errs; err != nil {
+               log.Panicf("ListenAndServe failed: %v", err)
        }
 }