cacert-boardvoting/boardvoting.go

867 lines
25 KiB
Go

/*
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.
*/
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/pem"
"flag"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/Masterminds/sprig"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
_ "github.com/mattn/go-sqlite3"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"git.cacert.org/cacert-boardvoting/boardvoting"
)
var configFile string
var config *Config
var store *sessions.CookieStore
var csrfKey []byte
var version = "undefined"
var build = "undefined"
const sessionCookieName = "votesession"
func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
funcMaps := sprig.FuncMap()
funcMaps["nl2br"] = func(text string) template.HTML {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
}
funcMaps[csrf.TemplateTag] = func() template.HTML {
return csrf.TemplateField(r)
}
var baseTemplate *template.Template
for count, t := range templates {
if assetBytes, err := boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
if count == 0 {
if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
}
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 {
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)
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 int64
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.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
}
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 int64
PageTitle string
Flashes interface{}
}
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter
}
if flashes := session.Flashes(); len(flashes) > 0 {
templateContext.Flashes = flashes
}
err = session.Save(r, w)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templateContext.Params = &params
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", "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 = &params
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", "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"}
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", "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.FormatInt(voteTypeMotion, 10),
}
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", "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:
voteResult := &Vote{
VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError)
return
}
NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult)
h.AddFlash(w, r, "Your vote has been registered.")
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
default:
templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
var templateContext struct {
Decision *DecisionForDisplay
VoteChoice VoteChoice
PageTitle string
Flashes interface{}
Voter *Voter
}
templateContext.Decision = decision
templateContext.VoteChoice = vote
templateContext.Voter = voter
renderTemplate(w, r, templates, templateContext)
}
}
type proxyVoteHandler struct {
FlashMessageAction
authenticationRequiredHandler
}
func getPEMClientCert(r *http.Request) string {
clientCertPEM := bytes.NewBufferString("")
authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
if err != nil {
log.Errorf("error encoding client certificate: %v", err)
}
return clientCertPEM.String()
}
func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
proxy, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
var templateContext struct {
Form ProxyVoteForm
Decision *DecisionForDisplay
Voters *[]Voter
PageTitle string
Flashes interface{}
Voter *Voter
}
templateContext.Voter = proxy
switch r.Method {
case http.MethodPost:
form := ProxyVoteForm{
Voter: r.FormValue("Voter"),
Vote: r.FormValue("Vote"),
Justification: r.FormValue("Justification"),
}
if valid, voter, data, justification := form.Validate(); !valid {
templateContext.Form = form
templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
templateContext.Voters = voters
}
renderTemplate(w, r, templates, templateContext)
} else {
data.DecisionId = decision.Id
data.Voted = time.Now().UTC()
data.Notes = fmt.Sprintf(
"Proxy-Vote by %s\n\n%s\n\n%s",
proxy.Name, justification, getPEMClientCert(r))
if err := data.Save(); err != nil {
log.Errorf("Error saving vote: %s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
h.AddFlash(w, r, "The vote has been registered.")
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
return
default:
templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
templateContext.Voters = voters
}
renderTemplate(w, r, templates, templateContext)
}
}
type decisionVoteHandler struct{}
func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/proxy/"):
motionTag := r.URL.Path[len("/proxy/"):]
handler := &proxyVoteHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, handler.Handle)
})
case strings.HasPrefix(r.URL.Path, "/vote/"):
parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
if len(parts) != 2 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
motionTag := parts[0]
voteValue, ok := VoteValues[parts[1]]
if !ok {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
handler := &directVoteHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(
w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
motionTag, handler.Handle)
})
return
}
}
type Config struct {
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) < 32 {
log.Panic("Cookie secret is less than 32 bytes long")
}
csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
if err != nil {
log.Panicf("Decoding csrf key failed: %v", err)
}
if len(csrfKey) != 32 {
log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
}
store = sessions.NewCookieStore(cookieSecret)
log.Info("Read configuration")
}
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
}
}()
}
func setupHandlers() {
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
http.Handle("/newmotion/", motionsHandler{})
http.Handle("/proxy/", &decisionVoteHandler{})
http.Handle("/vote/", &decisionVoteHandler{})
http.Handle("/static/", http.FileServer(boardvoting.GetAssetFS()))
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
}
func setupTLSConfig() (tlsConfig *tls.Config) {
// load CA certificates for client authentication
caCert, err := ioutil.ReadFile(config.ClientCACertificates)
if err != nil {
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{
ClientCAs: caCertPool,
ClientAuth: tls.VerifyClientCertIfGiven,
}
tlsConfig.BuildNameToCertificate()
return
}
func main() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
log.Infof("CAcert Board Voting version %s, build %s", version, build)
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,
}
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
log.Infof("Launching application on https://%s/", server.Addr)
errs := make(chan error, 1)
go func() {
if err := http.ListenAndServe(config.HttpsAddress, http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
errs <- err
}
close(errs)
}()
if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
log.Panicf("ListenAndServerTLS failed: %v", err)
}
if err := <-errs; err != nil {
log.Panicf("ListenAndServe failed: %v", err)
}
}