If you think this is in error, please contact the administrator.
-If you don't know who that is, it is definitely not an error ;)
- {{ if .Emails }} -The following addresses were present in your certificate:
-
diff --git a/.gitignore b/.gitignore
index bfc178e..ca0c831 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,11 @@
*.pem
*.req.conf
*.sqlite
+*.sqlite-journal
.*.swp
.idea/
+/dist/
+/ui/semantic/dist/
cacert-boardvoting
config.yaml
node_modules/
-/dist/
-/ui/semantic/dist/
diff --git a/boardvoting.go b/boardvoting.go
deleted file mode 100644
index 00c33c1..0000000
--- a/boardvoting.go
+++ /dev/null
@@ -1,1120 +0,0 @@
-/*
-Copyright 2017-2022 CAcert Inc.
-SPDX-License-Identifier: Apache-2.0
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// The CAcert board voting software.
-package main
-
-import (
- "bytes"
- "context"
- "crypto/tls"
- "crypto/x509"
- "database/sql"
- "embed"
- "encoding/base64"
- "encoding/pem"
- "errors"
- "flag"
- "fmt"
- "html/template"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "sort"
- "strconv"
- "strings"
- "time"
-
- "github.com/Masterminds/sprig/v3"
- "github.com/gorilla/csrf"
- "github.com/gorilla/sessions"
- _ "github.com/mattn/go-sqlite3"
- log "github.com/sirupsen/logrus"
- "github.com/vearutop/statigz"
- "github.com/vearutop/statigz/brotli"
- "gopkg.in/yaml.v2"
-)
-
-var configFile string
-var config *Config
-var store *sessions.CookieStore
-var csrfKey []byte
-var version = "undefined"
-var commit = "undefined"
-var date = "undefined"
-
-const (
- cookieSecretMinLen = 32
- csrfKeyLength = 32
- httpIdleTimeout = 5
- httpReadHeaderTimeout = 10
- httpReadTimeout = 10
- httpWriteTimeout = 60
- sessionCookieName = "votesession"
-)
-
-//go:embed boardvoting/templates
-var fsTemplates embed.FS
-
-func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
- funcMaps := sprig.FuncMap()
- funcMaps["nl2br"] = func(text string) template.HTML {
- // #nosec G203 input is sanitized
- return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "
"))
- }
- funcMaps[csrf.TemplateTag] = func() template.HTML {
- return csrf.TemplateField(r)
- }
-
- var baseTemplate *template.Template
-
- for count, t := range templates {
- var (
- err error
- assetBytes []byte
- )
-
- if assetBytes, err = fsTemplates.ReadFile(fmt.Sprintf("boardvoting/templates/%s", t)); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
-
- return
- }
-
- 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)
- }
- }
- }
-
- if err := baseTemplate.Execute(w, context); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
-}
-
-type contextKey int
-
-const (
- 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 {
- continue
- }
-
- log.Infof(
- "got a client certificate for the following email addresses: %s",
- strings.Join(cert.EmailAddresses, ", "),
- )
-
- for _, emailAddress := range cert.EmailAddresses {
- 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 {
- requestContext := context.WithValue(r.Context(), ctxVoter, voter)
- requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
-
- log.Infof("authenticated as %s", voter.Name)
-
- handler(w, r.WithContext(requestContext))
-
- return
- }
- }
- }
- }
-
- 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, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
-
- return
- }
-
- handler(w, r)
-}
-
-type motionParameters struct {
- ShowVotes bool
-}
-
-type motionListParameters struct {
- Page int
- Flags struct {
- Confirmed, Withdraw, Unvoted bool
- }
-}
-
-func parseMotionParameters(r *http.Request) motionParameters {
- var m = motionParameters{}
- m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
-
- return m
-}
-
-func parseMotionListParameters(r *http.Request) motionListParameters {
- var m = motionListParameters{}
- if page, err := strconv.Atoi(r.URL.Query().Get("page")); err != nil {
- m.Page = 1
- } else {
- m.Page = page
- }
-
- m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
- m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
-
- if r.Method == http.MethodPost {
- m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
- }
-
- 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
- Voter *Voter
- Params *motionListParameters
- PrevPage, NextPage int
- 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
- }
-
- err = session.Save(r, w)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- templateContext.Params = ¶ms
-
- 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(
- params.Flags.Unvoted, templateContext.Voter,
- )
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- if olderExists {
- templateContext.NextPage = params.Page + 1
- }
- }
-
- if params.Page > 1 {
- templateContext.PrevPage = params.Page - 1
- }
-
- renderTemplate(w, r, []string{
- "motions.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html",
- }, templateContext)
-}
-
-func motionHandler(w http.ResponseWriter, r *http.Request) {
- params := parseMotionParameters(r)
-
- decision, ok := getDecisionFromRequest(r)
- if !ok {
- http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
-
- return
- }
-
- var templateContext struct {
- Decision *DecisionForDisplay
- Voter *Voter
- Params *motionParameters
- PrevPage, NextPage int64
- PageTitle string
- Flashes interface{}
- }
-
- voter, ok := getVoterFromRequest(r)
- if ok {
- templateContext.Voter = voter
- }
-
- templateContext.Params = ¶ms
-
- if params.ShowVotes {
- if err := decision.LoadVotes(); err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
- }
-
- templateContext.Decision = decision
- templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
-
- renderTemplate(w, r, []string{
- "motion.html",
- "motion_fragments.html",
- "page_fragments.html",
- "header.html",
- "footer.html",
- }, templateContext)
-}
-
-func singleDecisionHandler(
- w http.ResponseWriter,
- r *http.Request,
- tag string,
- handler func(http.ResponseWriter, *http.Request),
-) {
- decision, err := FindDecisionForDisplayByTag(tag)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- if decision == nil {
- http.NotFound(w, r)
-
- return
- }
-
- handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
-}
-
-type motionActionHandler interface {
- Handle(w http.ResponseWriter, r *http.Request)
- NeedsAuth() bool
-}
-
-type authenticationRequiredHandler struct{}
-
-func (authenticationRequiredHandler) NeedsAuth() bool {
- return true
-}
-
-func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
- voter, ok = r.Context().Value(ctxVoter).(*Voter)
-
- return
-}
-
-func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
- decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
-
- return
-}
-
-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.Warnf("could not get session cookie: %v", err)
-
- return
- }
-
- session.AddFlash(message, tags...)
-
- err = session.Save(r, w)
- if err != nil {
- log.Warnf("could not save flash message: %v", err)
-
- return
- }
-}
-
-type withDrawMotionAction struct {
- FlashMessageAction
- authenticationRequiredHandler
-}
-
-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",
- "page_fragments.html",
- }
-
- var templateContext struct {
- PageTitle string
- Decision *DecisionForDisplay
- Flashes interface{}
- Voter *Voter
- }
-
- switch r.Method {
- case http.MethodPost:
- 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", "page_fragments.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:
- templateContext.Voter = voter
- templateContext.Form = NewDecisionForm{
- VoteType: strconv.Itoa(voteTypeMotion),
- }
- renderTemplate(w, r, templates, templateContext)
- }
-}
-
-type editMotionAction struct {
- FlashMessageAction
- authenticationRequiredHandler
-}
-
-func (a 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
- }
-
- voter, ok := getVoterFromRequest(r)
- if !ok {
- http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
-
- return
- }
-
- templates := []string{"edit_motion_form.html", "page_fragments.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) {
- subURL := r.URL.Path
-
- var motionActionMap = map[string]motionActionHandler{
- "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, "/")
- motionTag := parts[0]
-
- action, ok := motionActionMap[parts[1]]
- if !ok {
- http.NotFound(w, r)
-
- return
- }
-
- authenticateRequest(
- w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
- func(w http.ResponseWriter, r *http.Request) {
- singleDecisionHandler(w, r, motionTag, action.Handle)
- })
-
- return
- case strings.Count(subURL, "/") == 0:
- authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
- func(w http.ResponseWriter, r *http.Request) {
- singleDecisionHandler(w, r, subURL, motionHandler)
- })
-
- return
- default:
- 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:
- clientCert, err := getPEMClientCert(r)
- if err != nil {
- log.Errorf("could not get client certificate from request: %v", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- voteResult := &Vote{
- VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(),
- Notes: fmt.Sprintf("Direct Vote\n\n%s", clientCert)}
- 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",
- "page_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, error) {
- cert := r.Context().Value(ctxAuthenticatedCert)
-
- authenticatedCertificate, ok := cert.(*x509.Certificate)
- if !ok {
- return "", errors.New("could not handle certificate as x509.Certificate")
- }
-
- clientCertPEM := bytes.NewBuffer(make([]byte, 0))
-
- err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
- if err != nil {
- return "", fmt.Errorf("error encoding client certificate: %w", err)
- }
-
- return clientCertPEM.String(), nil
-}
-
-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",
- "page_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
-
- voters, err := GetVotersForProxy(proxy)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- templateContext.Voters = voters
-
- renderTemplate(w, r, templates, templateContext)
- } else {
- clientCert, err := getPEMClientCert(r)
- if err != nil {
- log.Errorf("could not get client certificate information: %v", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- 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, clientCert)
-
- 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
-
- voters, err := GetVotersForProxy(proxy)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-
- return
- }
-
- templateContext.Voters = voters
-
- renderTemplate(w, r, templates, templateContext)
- }
-}
-
-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/"):
- const expectedParts = 2
-
- parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
- if len(parts) != expectedParts {
- 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 {
- 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"`
- HTTPAddress string `yaml:"http_address"`
- HTTPSAddress string `yaml:"https_address"`
- MailServer struct {
- Host string `yaml:"host"`
- Port int `yaml:"port"`
- } `yaml:"mail_server"`
-}
-
-func readConfig() {
- source, err := ioutil.ReadFile(configFile)
- if err != nil {
- 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"
- }
-
- cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
- if err != nil {
- log.Panicf("Decoding cookie secret failed: %v", err)
- panic(err)
- }
-
- if len(cookieSecret) < cookieSecretMinLen {
- log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen)
- }
-
- csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
- if err != nil {
- log.Panicf("Decoding csrf key failed: %v", err)
- }
-
- if len(csrfKey) != csrfKeyLength {
- log.Panicf(
- "CSRF key must be exactly %d bytes long but is %d bytes long",
- csrfKeyLength,
- len(csrfKey),
- )
- }
-
- store = sessions.NewCookieStore(cookieSecret)
- store.Options.Secure = true
-
- log.Info("Read configuration")
-}
-
-func setupDbConfig(ctx context.Context) {
- database, err := sql.Open("sqlite3", config.DatabaseFile)
- if err != nil {
- log.Panicf("Opening database failed: %v", err)
- }
-
- 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
- }
- }()
-}
-
-//go:embed ui/static
-var uiStatic embed.FS
-
-func setupHandlers() {
- http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
- http.Handle("/newmotion/", motionsHandler{})
- http.Handle("/proxy/", &decisionVoteHandler{})
- http.Handle("/vote/", &decisionVoteHandler{})
- http.Handle("/static/", addPrefix("/ui", statigz.FileServer(uiStatic, brotli.AddEncoding, statigz.EncodeOnInit)))
- http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
-}
-
-func addPrefix(prefix string, h http.Handler) http.Handler {
- if prefix == "" {
- return h
- }
-
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- newPath := prefix + r.URL.Path
- newRawPath := prefix + r.URL.RawPath
-
- r2 := new(http.Request)
- *r2 = *r
- r2.URL = new(url.URL)
- *r2.URL = *r.URL
- r2.URL.Path = newPath
- r2.URL.RawPath = newRawPath
-
- h.ServeHTTP(w, r2)
- })
-}
-
-func setupTLSConfig() (tlsConfig *tls.Config) {
- // load CA certificates for client authentication
- caCert, err := ioutil.ReadFile(config.ClientCACertificates)
- if err != nil {
- log.Panicf("Error reading client certificate CAs %v", err)
- }
-
- caCertPool := x509.NewCertPool()
- if !caCertPool.AppendCertsFromPEM(caCert) {
- log.Panic("could not initialize client CA certificate pool")
- }
-
- // setup HTTPS server
- tlsConfig = &tls.Config{
- MinVersion: tls.VersionTLS12,
- ClientCAs: caCertPool,
- ClientAuth: tls.VerifyClientCertIfGiven,
- }
-
- return
-}
-
-func main() {
- log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
- log.Infof("CAcert Board Voting version %s, commit %s built at %s", version, commit, date)
-
- flag.StringVar(
- &configFile, "config", "config.yaml", "Configuration file name")
-
- flag.Parse()
-
- var stopAll func()
-
- executionContext, stopAll := context.WithCancel(context.Background())
-
- readConfig()
- setupDbConfig(executionContext)
- setupNotifications(executionContext)
- setupJobs(executionContext)
- setupHandlers()
-
- tlsConfig := setupTLSConfig()
-
- defer stopAll()
-
- server := &http.Server{
- Addr: config.HTTPSAddress,
- TLSConfig: tlsConfig,
- IdleTimeout: time.Second * httpIdleTimeout,
- ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
- ReadTimeout: time.Second * httpReadTimeout,
- WriteTimeout: time.Second * httpWriteTimeout,
- }
-
- server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
-
- log.Infof("Launching application on https://%s/", server.Addr)
-
- errs := make(chan error, 1)
-
- go func() {
- httpRedirector := &http.Server{
- Addr: config.HTTPAddress,
- Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently),
- IdleTimeout: time.Second * httpIdleTimeout,
- ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
- ReadTimeout: time.Second * httpReadTimeout,
- WriteTimeout: time.Second * httpWriteTimeout,
- }
- if err := httpRedirector.ListenAndServe(); 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)
- }
-}
diff --git a/boardvoting/templates/create_motion_form.html b/boardvoting/templates/create_motion_form.html
deleted file mode 100644
index 2d8db12..0000000
--- a/boardvoting/templates/create_motion_form.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{{ template "header.html" . }}
-{{ template "return_header" . }}
-