cacert-boardvoting/boardvoting.go

1077 lines
27 KiB
Go
Raw Permalink Normal View History

2019-07-31 15:30:58 +00:00
/*
Copyright 2017-2021 Jan Dittberner
2019-07-31 15:30:58 +00:00
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
2019-07-31 15:30:58 +00:00
http://www.apache.org/licenses/LICENSE-2.0
2019-07-31 15:30:58 +00:00
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.
2019-07-31 15:30:58 +00:00
*/
// The CAcert board voting software.
2017-04-15 17:23:40 +00:00
package main
import (
2017-04-21 00:25:49 +00:00
"bytes"
2017-04-17 20:56:20 +00:00
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"embed"
2017-04-18 00:34:21 +00:00
"encoding/base64"
2017-04-21 00:25:49 +00:00
"encoding/pem"
"flag"
2017-04-15 17:23:40 +00:00
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"sort"
"strconv"
"strings"
2017-04-19 19:35:08 +00:00
"time"
2018-03-31 08:50:06 +00:00
"github.com/Masterminds/sprig/v3"
2018-03-31 08:50:06 +00:00
"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"
2018-03-31 08:50:06 +00:00
"gopkg.in/yaml.v2"
2017-04-15 17:23:40 +00:00
)
var configFile string
var config *Config
2017-04-18 00:34:21 +00:00
var store *sessions.CookieStore
2018-03-31 08:50:06 +00:00
var csrfKey []byte
2017-04-18 14:07:54 +00:00
var version = "undefined"
var build = "undefined"
2017-04-15 17:23:40 +00:00
const (
cookieSecretMinLen = 32
csrfKeyLength = 32
httpIdleTimeout = 5
httpReadHeaderTimeout = 10
httpReadTimeout = 10
httpWriteTimeout = 60
sessionCookieName = "votesession"
)
2017-04-18 00:34:21 +00:00
//go:embed boardvoting/templates
var fsTemplates embed.FS
2018-03-31 08:50:06 +00:00
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", "<br>"))
}
2018-03-31 08:50:06 +00:00
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 {
2017-04-15 17:23:40 +00:00
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
2017-04-17 20:56:20 +00:00
type contextKey int
const (
ctxNeedsAuth contextKey = iota
2017-04-18 22:05:42 +00:00
ctxVoter
ctxDecision
2017-04-21 00:25:49 +00:00
ctxVote
2017-04-21 09:31:32 +00:00
ctxAuthenticatedCert
2017-04-17 20:56:20 +00:00
)
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 {
2017-04-15 17:23:40 +00:00
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
}
2017-04-15 17:23:40 +00:00
}
}
}
2017-04-17 20:56:20 +00:00
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)
2017-04-15 17:23:40 +00:00
w.WriteHeader(http.StatusForbidden)
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
2017-04-15 17:23:40 +00:00
return
}
2017-04-17 20:56:20 +00:00
handler(w, r)
2017-04-15 17:23:40 +00:00
}
type motionParameters struct {
ShowVotes bool
}
type motionListParameters struct {
Page int64
Flags struct {
Confirmed, Withdraw, Unvoted bool
2017-04-15 17:23:40 +00:00
}
}
2017-04-15 17:23:40 +00:00
func parseMotionParameters(r *http.Request) motionParameters {
var m = motionParameters{}
m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
return m
}
2017-04-15 17:23:40 +00:00
func parseMotionListParameters(r *http.Request) motionListParameters {
var m = motionListParameters{}
if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); 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
}
2017-04-17 20:56:20 +00:00
func motionListHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionListParameters(r)
2017-04-18 00:34:21 +00:00
session, err := store.Get(r, sessionCookieName)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-18 00:34:21 +00:00
return
}
2017-04-17 20:56:20 +00:00
var templateContext struct {
Decisions []*DecisionForDisplay
Voter *Voter
Params *motionListParameters
PrevPage, NextPage int64
PageTitle string
2017-04-18 00:34:21 +00:00
Flashes interface{}
2017-04-15 17:23:40 +00:00
}
2017-04-17 20:56:20 +00:00
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter
}
2017-04-18 00:34:21 +00:00
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
}
2017-04-17 20:56:20 +00:00
templateContext.Params = &params
2017-04-15 17:23:40 +00:00
if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(
params.Page, params.Flags.Unvoted, templateContext.Voter,
); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
2017-04-15 17:23:40 +00:00
}
2017-04-17 20:56:20 +00:00
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 {
2017-04-17 20:56:20 +00:00
templateContext.NextPage = params.Page + 1
}
}
if params.Page > 1 {
2017-04-17 20:56:20 +00:00
templateContext.PrevPage = params.Page - 1
}
2017-04-15 17:23:40 +00:00
renderTemplate(w, r, []string{
"motions.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html",
}, templateContext)
}
2017-04-17 20:56:20 +00:00
func motionHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionParameters(r)
2017-04-17 20:56:20 +00:00
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
2017-04-17 20:56:20 +00:00
return
}
var templateContext struct {
Decision *DecisionForDisplay
Voter *Voter
Params *motionParameters
PrevPage, NextPage int64
PageTitle string
2017-04-18 00:34:21 +00:00
Flashes interface{}
}
2017-04-17 20:56:20 +00:00
voter, ok := getVoterFromRequest(r)
if ok {
templateContext.Voter = voter
}
2017-04-17 20:56:20 +00:00
templateContext.Params = &params
if params.ShowVotes {
if err := decision.LoadVotes(); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
2017-04-17 20:56:20 +00:00
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
}
2017-04-17 20:56:20 +00:00
handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
}
type motionActionHandler interface {
2017-04-17 20:56:20 +00:00
Handle(w http.ResponseWriter, r *http.Request)
NeedsAuth() bool
}
2017-04-17 20:56:20 +00:00
type authenticationRequiredHandler struct{}
2017-04-17 20:56:20 +00:00
func (authenticationRequiredHandler) NeedsAuth() bool {
return true
}
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
voter, ok = r.Context().Value(ctxVoter).(*Voter)
2017-04-17 20:56:20 +00:00
return
}
func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
2017-04-17 20:56:20 +00:00
return
}
2017-04-21 00:25:49 +00:00
func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
vote, ok = r.Context().Value(ctxVote).(VoteChoice)
2017-04-21 00:25:49 +00:00
return
}
2017-04-19 19:35:08 +00:00
type FlashMessageAction struct{}
2017-04-17 20:56:20 +00:00
func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) {
2017-04-19 19:35:08 +00:00
session, err := store.Get(r, sessionCookieName)
if err != nil {
log.Warnf("could not get session cookie: %v", err)
2017-04-19 19:35:08 +00:00
return
}
2017-04-19 19:35:08 +00:00
session.AddFlash(message, tags...)
err = session.Save(r, w)
2017-04-19 19:35:08 +00:00
if err != nil {
log.Warnf("could not save flash message: %v", err)
2017-04-19 19:35:08 +00:00
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)
2017-04-19 19:35:08 +00:00
return
}
2017-04-19 19:35:08 +00:00
voter, ok := getVoterFromRequest(r)
if !ok {
2017-04-17 20:56:20 +00:00
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
2017-04-17 20:56:20 +00:00
return
}
templates := []string{
"withdraw_motion_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
2017-04-19 19:35:08 +00:00
var templateContext struct {
PageTitle string
Decision *DecisionForDisplay
Flashes interface{}
2018-03-31 08:50:06 +00:00
Voter *Voter
2017-04-19 19:35:08 +00:00
}
2017-04-17 20:56:20 +00:00
switch r.Method {
case http.MethodPost:
2017-04-19 19:35:08 +00:00
decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC()
if err := decision.UpdateStatus(); err != nil {
log.Errorf("withdrawing motion failed: %v", err)
2017-04-19 19:35:08 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-19 19:35:08 +00:00
return
}
2017-04-21 00:25:49 +00:00
NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag))
2017-04-17 20:56:20 +00:00
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
default:
2017-04-19 19:35:08 +00:00
templateContext.Decision = decision
2018-03-31 08:50:06 +00:00
templateContext.Voter = voter
renderTemplate(w, r, templates, templateContext)
}
}
2017-04-19 19:35:08 +00:00
type newMotionHandler struct {
FlashMessageAction
}
2017-04-19 19:35:08 +00:00
func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
2017-04-18 22:05:42 +00:00
voter, ok := getVoterFromRequest(r)
if !ok {
2017-04-17 20:56:20 +00:00
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
}
templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
2017-04-18 22:05:42 +00:00
var templateContext struct {
Form NewDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
2017-04-18 22:05:42 +00:00
switch r.Method {
case http.MethodPost:
form := NewDecisionForm{
Title: r.FormValue("Title"),
Content: r.FormValue("Content"),
VoteType: r.FormValue("VoteType"),
Due: r.FormValue("Due"),
}
2017-04-18 22:05:42 +00:00
if valid, data := form.Validate(); !valid {
templateContext.Voter = voter
templateContext.Form = form
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-18 22:05:42 +00:00
} else {
2017-04-19 19:35:08 +00:00
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)
2017-04-18 22:05:42 +00:00
return
}
2017-04-21 00:25:49 +00:00
NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
h.AddFlash(w, r, "The motion has been proposed!")
2017-04-18 22:05:42 +00:00
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
2017-04-18 22:05:42 +00:00
return
default:
2017-04-18 22:05:42 +00:00
templateContext.Voter = voter
templateContext.Form = NewDecisionForm{
VoteType: strconv.FormatInt(voteTypeMotion, 10),
}
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
}
2017-04-15 17:23:40 +00:00
}
2017-04-19 19:35:08 +00:00
type editMotionAction struct {
FlashMessageAction
authenticationRequiredHandler
}
func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
2017-04-18 22:05:42 +00:00
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
2017-04-18 22:05:42 +00:00
return
}
2017-04-18 00:34:21 +00:00
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
2017-04-19 19:35:08 +00:00
return
2017-04-18 00:34:21 +00:00
}
templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
2017-04-18 00:34:21 +00:00
var templateContext struct {
2017-04-18 22:05:42 +00:00
Form EditDecisionForm
2017-04-18 00:34:21 +00:00
PageTitle string
Voter *Voter
Flashes interface{}
}
2017-04-18 00:34:21 +00:00
switch r.Method {
case http.MethodPost:
2017-04-18 22:05:42 +00:00
form := EditDecisionForm{
2017-04-18 00:34:21 +00:00
Title: r.FormValue("Title"),
Content: r.FormValue("Content"),
VoteType: r.FormValue("VoteType"),
Due: r.FormValue("Due"),
2017-04-18 22:05:42 +00:00
Decision: &decision.Decision,
2017-04-18 00:34:21 +00:00
}
if valid, data := form.Validate(); !valid {
templateContext.Voter = voter
templateContext.Form = form
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-18 00:34:21 +00:00
} else {
2017-04-19 19:35:08 +00:00
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)
2017-04-18 00:34:21 +00:00
return
}
2017-04-21 00:25:49 +00:00
NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
a.AddFlash(w, r, "The motion has been modified!")
2017-04-18 00:34:21 +00:00
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
2017-04-18 00:34:21 +00:00
}
2017-04-18 00:34:21 +00:00
return
default:
templateContext.Voter = voter
2017-04-18 22:05:42 +00:00
templateContext.Form = EditDecisionForm{
Title: decision.Title,
Content: decision.Content,
VoteType: fmt.Sprintf("%d", decision.VoteType),
Decision: &decision.Decision,
2017-04-18 00:34:21 +00:00
}
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-18 00:34:21 +00:00
}
2017-04-15 17:23:40 +00:00
}
2017-04-18 22:05:42 +00:00
type motionsHandler struct{}
func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
subURL := r.URL.Path
var motionActionMap = map[string]motionActionHandler{
2017-04-19 19:35:08 +00:00
"withdraw": &withDrawMotionAction{},
"edit": &editMotionAction{},
2017-04-18 22:05:42 +00:00
}
switch {
case subURL == "":
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
2017-04-18 22:05:42 +00:00
return
2017-04-19 19:35:08 +00:00
case subURL == "/newmotion/":
handler := &newMotionHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
handler.Handle)
2017-04-19 19:35:08 +00:00
return
2017-04-18 22:05:42 +00:00
case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/")
motionTag := parts[0]
2017-04-18 22:05:42 +00:00
action, ok := motionActionMap[parts[1]]
if !ok {
http.NotFound(w, r)
2017-04-18 22:05:42 +00:00
return
}
2017-04-18 22:05:42 +00:00
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, action.Handle)
})
2017-04-18 22:05:42 +00:00
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)
})
2017-04-18 22:05:42 +00:00
return
default:
http.NotFound(w, r)
2017-04-18 22:05:42 +00:00
return
}
}
2017-04-21 09:31:32 +00:00
type directVoteHandler struct {
2017-04-21 00:25:49 +00:00
FlashMessageAction
authenticationRequiredHandler
}
2017-04-21 09:31:32 +00:00
func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
2017-04-21 00:25:49 +00:00
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
2017-04-21 00:25:49 +00:00
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
2017-04-21 00:25:49 +00:00
vote, ok := getVoteFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
2017-04-21 09:31:32 +00:00
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))}
2017-04-21 09:31:32 +00:00
if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError)
2017-04-21 09:31:32 +00:00
return
}
NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult)
h.AddFlash(w, r, "Your vote has been registered.")
2017-04-21 09:31:32 +00:00
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
default:
templates := []string{
"direct_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
2017-04-21 09:31:32 +00:00
var templateContext struct {
Decision *DecisionForDisplay
VoteChoice VoteChoice
PageTitle string
Flashes interface{}
2018-03-31 08:50:06 +00:00
Voter *Voter
2017-04-21 09:31:32 +00:00
}
2017-04-21 09:31:32 +00:00
templateContext.Decision = decision
templateContext.VoteChoice = vote
2018-03-31 08:50:06 +00:00
templateContext.Voter = voter
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-21 09:31:32 +00:00
}
2017-04-21 00:25:49 +00:00
}
type proxyVoteHandler struct {
FlashMessageAction
authenticationRequiredHandler
}
func getPEMClientCert(r *http.Request) string {
clientCertPEM := bytes.NewBufferString("")
2017-04-21 09:31:32 +00:00
authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
if err != nil {
log.Errorf("error encoding client certificate: %v", err)
}
2017-04-21 00:25:49 +00:00
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)
2017-04-21 00:25:49 +00:00
return
}
2017-04-21 00:25:49 +00:00
proxy, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
templates := []string{
"proxy_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
2017-04-21 00:25:49 +00:00
var templateContext struct {
Form ProxyVoteForm
Decision *DecisionForDisplay
Voters *[]Voter
PageTitle string
Flashes interface{}
2018-03-31 08:50:06 +00:00
Voter *Voter
2017-04-21 00:25:49 +00:00
}
2018-03-31 08:50:06 +00:00
templateContext.Voter = proxy
2017-04-21 00:25:49 +00:00
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 {
2017-04-21 00:25:49 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
templateContext.Voters = voters
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-21 00:25:49 +00:00
} else {
data.DecisionID = decision.ID
2017-04-21 00:25:49 +00:00
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)
2017-04-21 00:25:49 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
h.AddFlash(w, r, "The vote has been registered.")
2017-04-21 00:25:49 +00:00
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
2017-04-21 00:25:49 +00:00
return
default:
templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision
voters, err := GetVotersForProxy(proxy)
if err != nil {
2017-04-21 00:25:49 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2017-04-21 00:25:49 +00:00
return
}
templateContext.Voters = voters
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, templates, templateContext)
2017-04-21 00:25:49 +00:00
}
}
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{}
2017-04-21 00:25:49 +00:00
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
2017-04-21 00:25:49 +00:00
parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
if len(parts) != expectedParts {
2017-04-21 09:31:32 +00:00
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
2017-04-21 09:31:32 +00:00
return
}
2017-04-21 00:25:49 +00:00
motionTag := parts[0]
2017-04-21 00:25:49 +00:00
voteValue, ok := VoteValues[parts[1]]
if !ok {
2017-04-21 09:31:32 +00:00
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
2017-04-21 00:25:49 +00:00
return
}
2017-04-21 09:31:32 +00:00
handler := &directVoteHandler{}
2017-04-21 00:25:49 +00:00
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)
})
2017-04-21 00:25:49 +00:00
return
}
}
2017-04-15 17:23:40 +00:00
type Config struct {
NoticeMailAddress string `yaml:"notice_mail_address"`
VoteNoticeMailAddress string `yaml:"vote_notice_mail_address"`
NotificationSenderAddress string `yaml:"notification_sender_address"`
2017-04-21 00:25:49 +00:00
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"`
2018-03-31 08:50:06 +00:00
CsrfKey string `yaml:"csrf_key"`
2017-04-21 00:25:49 +00:00
BaseURL string `yaml:"base_url"`
HTTPAddress string `yaml:"http_address"`
HTTPSAddress string `yaml:"https_address"`
MailServer struct {
2017-04-18 22:05:42 +00:00
Host string `yaml:"host"`
2017-04-19 19:35:08 +00:00
Port int `yaml:"port"`
2017-04-18 22:05:42 +00:00
} `yaml:"mail_server"`
2017-04-15 17:23:40 +00:00
}
func readConfig() {
source, err := ioutil.ReadFile(configFile)
2017-04-15 17:23:40 +00:00
if err != nil {
log.Panicf("Opening configuration file failed: %v", err)
2017-04-15 17:23:40 +00:00
}
2017-04-18 00:34:21 +00:00
if err := yaml.Unmarshal(source, &config); err != nil {
log.Panicf("Loading configuration failed: %v", err)
2017-04-18 00:34:21 +00:00
}
if config.HTTPSAddress == "" {
config.HTTPSAddress = "127.0.0.1:8443"
}
if config.HTTPAddress == "" {
config.HTTPAddress = "127.0.0.1:8080"
}
2017-04-18 00:34:21 +00:00
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
2017-04-15 17:23:40 +00:00
if err != nil {
log.Panicf("Decoding cookie secret failed: %v", err)
2017-04-21 22:06:16 +00:00
panic(err)
2017-04-15 17:23:40 +00:00
}
if len(cookieSecret) < cookieSecretMinLen {
log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen)
2017-04-18 00:34:21 +00:00
}
2018-03-31 08:50:06 +00:00
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),
)
2018-03-31 08:50:06 +00:00
}
2017-04-18 00:34:21 +00:00
store = sessions.NewCookieStore(cookieSecret)
store.Options.Secure = true
log.Info("Read configuration")
}
2017-04-15 17:23:40 +00:00
func setupDbConfig(ctx context.Context) {
database, err := sql.Open("sqlite3", config.DatabaseFile)
2017-04-15 17:23:40 +00:00
if err != nil {
log.Panicf("Opening database failed: %v", err)
2017-04-15 17:23:40 +00:00
}
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")
}
2017-04-15 17:23:40 +00:00
func setupNotifications(ctx context.Context) {
2017-04-21 00:25:49 +00:00
quitMailChannel := make(chan int)
go MailNotifier(quitMailChannel)
go func() {
for range ctx.Done() {
quitMailChannel <- 1
}
}()
}
func setupJobs(ctx context.Context) {
2017-04-20 09:35:33 +00:00
quitChannel := make(chan int)
go JobScheduler(quitChannel)
go func() {
for range ctx.Done() {
quitChannel <- 1
}
}()
}
//go:embed boardvoting/static
var staticAssets embed.FS
func setupHandlers() {
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
2017-04-19 19:35:08 +00:00
http.Handle("/newmotion/", motionsHandler{})
2017-04-21 00:25:49 +00:00
http.Handle("/proxy/", &decisionVoteHandler{})
http.Handle("/vote/", &decisionVoteHandler{})
http.Handle("/boardvoting/static/", statigz.FileServer(staticAssets, brotli.AddEncoding, statigz.EncodeOnInit))
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
}
2017-04-15 17:23:40 +00:00
func setupTLSConfig() (tlsConfig *tls.Config) {
2017-04-15 17:23:40 +00:00
// load CA certificates for client authentication
caCert, err := ioutil.ReadFile(config.ClientCACertificates)
if err != nil {
log.Panicf("Error reading client certificate CAs %v", err)
2017-04-15 17:23:40 +00:00
}
2017-04-15 17:23:40 +00:00
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Panic("could not initialize client CA certificate pool")
2017-04-15 17:23:40 +00:00
}
// setup HTTPS server
tlsConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool,
2017-04-17 20:56:20 +00:00
ClientAuth: tls.VerifyClientCertIfGiven,
2017-04-15 17:23:40 +00:00
}
return
}
func main() {
2019-07-31 15:45:05 +00:00
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
log.Infof("CAcert Board Voting version %s, build %s", version, build)
2019-07-31 15:45:05 +00:00
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()
2017-04-15 17:23:40 +00:00
server := &http.Server{
Addr: config.HTTPSAddress,
TLSConfig: tlsConfig,
IdleTimeout: time.Second * httpIdleTimeout,
ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
ReadTimeout: time.Second * httpReadTimeout,
WriteTimeout: time.Second * httpWriteTimeout,
2017-04-15 17:23:40 +00:00
}
2018-03-31 08:50:06 +00:00
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
log.Infof("Launching application on https://%s/", server.Addr)
2017-04-19 19:35:08 +00:00
errs := make(chan error, 1)
2017-04-19 19:35:08 +00:00
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 {
2017-04-19 19:35:08 +00:00
errs <- err
}
2017-04-19 19:35:08 +00:00
close(errs)
}()
if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
log.Panicf("ListenAndServerTLS failed: %v", err)
2017-04-15 17:23:40 +00:00
}
2017-04-19 19:35:08 +00:00
if err := <-errs; err != nil {
log.Panicf("ListenAndServe failed: %v", err)
2017-04-19 19:35:08 +00:00
}
2017-04-15 17:23:40 +00:00
}