cacert-boardvoting/boardvoting.go

815 lines
23 KiB
Go
Raw Normal View History

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"
2017-04-18 00:34:21 +00:00
"encoding/base64"
2017-04-21 00:25:49 +00:00
"encoding/pem"
2017-04-15 17:23:40 +00:00
"fmt"
"github.com/Masterminds/sprig"
2017-04-18 00:34:21 +00:00
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
2017-04-15 17:23:40 +00:00
_ "github.com/mattn/go-sqlite3"
"github.com/op/go-logging"
2017-04-15 17:23:40 +00:00
"gopkg.in/yaml.v2"
"html/template"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
2017-04-19 19:35:08 +00:00
"time"
2017-04-15 17:23:40 +00:00
)
var config *Config
2017-04-18 00:34:21 +00:00
var store *sessions.CookieStore
2017-04-18 14:07:54 +00:00
var version = "undefined"
var build = "undefined"
var log *logging.Logger
2017-04-15 17:23:40 +00:00
2017-04-18 00:34:21 +00:00
const sessionCookieName = "votesession"
func getTemplateFilenames(templates []string) (result []string) {
result = make([]string, len(templates))
for i := range templates {
result[i] = fmt.Sprintf("templates/%s", templates[i])
}
return result
}
2017-04-18 00:34:21 +00:00
func renderTemplate(w http.ResponseWriter, 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))
}
t := template.Must(template.New(templates[0]).Funcs(funcMaps).ParseFiles(getTemplateFilenames(templates)...))
if err := t.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)) {
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 {
voter, err := FindVoterByAddress(emailAddress)
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 {
2017-04-15 17:23:40 +00:00
w.WriteHeader(http.StatusForbidden)
2017-04-30 00:37:29 +00:00
renderTemplate(w, []string{"denied.html", "header.html", "footer.html"}, nil)
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
}
session.Save(r, w)
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
2017-04-17 20:56:20 +00:00
renderTemplate(w, []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)
renderTemplate(w, []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.Errorf("getting session failed: %v", err)
2017-04-19 19:35:08 +00:00
return
}
session.AddFlash(message, tags...)
session.Save(r, w)
if err != nil {
log.Errorf("saving session failed: %v", err)
2017-04-19 19:35:08 +00:00
return
}
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{}
}
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
renderTemplate(w, 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
renderTemplate(w, templates, templateContext)
} 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),
}
renderTemplate(w, 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
renderTemplate(w, templates, templateContext)
} 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
}
renderTemplate(w, templates, templateContext)
}
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))}
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{}
}
templateContext.Decision = decision
templateContext.VoteChoice = vote
renderTemplate(w, templates, templateContext)
}
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)
pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
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{}
}
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
}
renderTemplate(w, 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)
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
}
renderTemplate(w, 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/"):
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"`
BaseURL string `yaml:"base_url"`
MigrationsPath string `yaml:"migrations_path"`
2017-04-21 00:25:49 +00:00
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 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)
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")
}
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)
}
}
}()
log.Info("Setup logging")
}
2017-04-15 17:23:40 +00:00
func readConfig() {
2017-04-18 00:34:21 +00:00
source, err := ioutil.ReadFile("config.yaml")
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
}
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
}
store = sessions.NewCookieStore(cookieSecret)
log.Info("Read configuration")
}
2017-04-15 17:23:40 +00:00
func setupDbConfig(ctx context.Context) {
database, err := sqlx.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(http.Dir(".")))
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 main() {
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)
2017-04-15 17:23:40 +00:00
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
2017-04-15 17:23:40 +00:00
}
log.Infof("Launching application on https://localhost%s/", server.Addr)
2017-04-19 19:35:08 +00:00
errs := make(chan error, 1)
go func() {
if err := http.ListenAndServe(":8080", 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)
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
}