cacert-boardvoting/boardvoting.go

869 lines
25 KiB
Go
Raw Normal View History

2019-07-31 15:30:58 +00:00
/*
Copyright 2017-2019 Jan Dittberner
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
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.
*/
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"
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"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
_ "github.com/mattn/go-sqlite3"
log "github.com/sirupsen/logrus"
2018-03-31 08:50:06 +00:00
"gopkg.in/yaml.v2"
"git.cacert.org/cacert-boardvoting/boardvoting"
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
2017-04-18 00:34:21 +00:00
const sessionCookieName = "votesession"
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 {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
}
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 {
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)
}
}
}
}
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 {
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 {
2017-04-21 09:31:32 +00:00
requestContext := context.WithValue(r.Context(), ctxVoter, voter)
requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
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)
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
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, []string{"motions.html", "motion_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)
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
}
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)
2018-03-31 08:50:06 +00:00
renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
}
2017-04-17 20:56:20 +00:00
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)
return
}
func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
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)
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
}
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)
return
}
voter, ok := getVoterFromRequest(r)
if !ok {
2017-04-17 20:56:20 +00:00
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
2017-04-19 19:35:08 +00:00
templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html"}
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)
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)
}
2017-04-18 22:05:42 +00:00
templates := []string{"create_motion_form.html", "header.html", "footer.html"}
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)
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
}
2017-04-18 22:05:42 +00:00
templates := []string{"edit_motion_form.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{}
}
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
}
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)
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)
return
2017-04-18 22:05:42 +00:00
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
}
}
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)
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
}
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"}
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
}
templateContext.Decision = decision
templateContext.VoteChoice = vote
2018-03-31 08:50:06 +00:00
templateContext.Voter = voter
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)
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{}
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
if voters, err := GetVotersForProxy(proxy); err != nil {
2017-04-21 00:25:49 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
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
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)
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)
}
return
default:
templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
2017-04-21 00:25:49 +00:00
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
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{}
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/"):], "/")
2017-04-21 09:31:32 +00:00
if len(parts) != 2 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
2017-04-21 00:25:49 +00:00
motionTag := parts[0]
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)
})
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 == "" {
2018-03-31 08:50:06 +00:00
config.HttpsAddress = "127.0.0.1:8443"
}
if config.HttpAddress == "" {
2018-03-31 08:50:06 +00:00
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
}
2017-04-18 00:34:21 +00:00
if len(cookieSecret) < 32 {
log.Panic("Cookie secret is less than 32 bytes long")
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) != 32 {
log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
}
2017-04-18 00:34:21 +00:00
store = sessions.NewCookieStore(cookieSecret)
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
}
}()
}
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("/static/", http.FileServer(boardvoting.GetAssetFS()))
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
}
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{
ClientCAs: caCertPool,
2017-04-17 20:56:20 +00:00
ClientAuth: tls.VerifyClientCertIfGiven,
2017-04-15 17:23:40 +00:00
}
tlsConfig.BuildNameToCertificate()
return
}
func init() {
flag.StringVar(
&configFile, "config", "config.yaml", "Configuration file name")
}
func main() {
log.Infof("CAcert Board Voting version %s, build %s", version, build)
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,
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)
go func() {
if err := http.ListenAndServe(config.HttpsAddress, http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
2017-04-19 19:35:08 +00:00
errs <- err
}
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
}