/* Copyright 2017-2022 CAcert Inc. SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // The CAcert board voting software. package main import ( "bytes" "context" "crypto/tls" "crypto/x509" "database/sql" "embed" "encoding/base64" "encoding/pem" "errors" "flag" "fmt" "html/template" "io/ioutil" "net/http" "net/url" "os" "sort" "strconv" "strings" "time" "github.com/Masterminds/sprig/v3" "github.com/gorilla/csrf" "github.com/gorilla/sessions" _ "github.com/mattn/go-sqlite3" log "github.com/sirupsen/logrus" "github.com/vearutop/statigz" "github.com/vearutop/statigz/brotli" "gopkg.in/yaml.v2" ) var configFile string var config *Config var store *sessions.CookieStore var csrfKey []byte var version = "undefined" var commit = "undefined" var date = "undefined" const ( cookieSecretMinLen = 32 csrfKeyLength = 32 httpIdleTimeout = 5 httpReadHeaderTimeout = 10 httpReadTimeout = 10 httpWriteTimeout = 60 sessionCookieName = "votesession" ) //go:embed boardvoting/templates var fsTemplates embed.FS func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) { funcMaps := sprig.FuncMap() funcMaps["nl2br"] = func(text string) template.HTML { // #nosec G203 input is sanitized return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "
")) } funcMaps[csrf.TemplateTag] = func() template.HTML { return csrf.TemplateField(r) } var baseTemplate *template.Template for count, t := range templates { var ( err error assetBytes []byte ) if assetBytes, err = fsTemplates.ReadFile(fmt.Sprintf("boardvoting/templates/%s", t)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if count == 0 { if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } else { if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } } if err := baseTemplate.Execute(w, context); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } type contextKey int const ( ctxNeedsAuth contextKey = iota ctxVoter ctxDecision ctxVote ctxAuthenticatedCert ) func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) { emailsTried := make(map[string]bool) for _, cert := range r.TLS.PeerCertificates { for _, extKeyUsage := range cert.ExtKeyUsage { if extKeyUsage != x509.ExtKeyUsageClientAuth { continue } log.Infof( "got a client certificate for the following email addresses: %s", strings.Join(cert.EmailAddresses, ", "), ) for _, emailAddress := range cert.EmailAddresses { emailLower := strings.ToLower(emailAddress) emailsTried[emailLower] = true voter, err := FindVoterByAddress(emailLower) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if voter != nil { requestContext := context.WithValue(r.Context(), ctxVoter, voter) requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert) log.Infof("authenticated as %s", voter.Name) handler(w, r.WithContext(requestContext)) return } } } } needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool) if ok && needsAuth { var templateContext struct { PageTitle string Voter *Voter Flashes interface{} Emails []string } for k := range emailsTried { templateContext.Emails = append(templateContext.Emails, k) } sort.Strings(templateContext.Emails) w.WriteHeader(http.StatusForbidden) renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext) return } handler(w, r) } type motionParameters struct { ShowVotes bool } type motionListParameters struct { Page int Flags struct { Confirmed, Withdraw, Unvoted bool } } func parseMotionParameters(r *http.Request) motionParameters { var m = motionParameters{} m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes")) return m } func parseMotionListParameters(r *http.Request) motionListParameters { var m = motionListParameters{} if page, err := strconv.Atoi(r.URL.Query().Get("page")); err != nil { m.Page = 1 } else { m.Page = page } m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw")) m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted")) if r.Method == http.MethodPost { m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm")) } return m } func motionListHandler(w http.ResponseWriter, r *http.Request) { params := parseMotionListParameters(r) session, err := store.Get(r, sessionCookieName) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } var templateContext struct { Decisions []*DecisionForDisplay Voter *Voter Params *motionListParameters PrevPage, NextPage int PageTitle string Flashes interface{} } if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok { templateContext.Voter = voter } if flashes := session.Flashes(); len(flashes) > 0 { templateContext.Flashes = flashes } err = session.Save(r, w) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } templateContext.Params = ¶ms if templateContext.Decisions, err = FindDecisionsForDisplayOnPage( params.Page, params.Flags.Unvoted, templateContext.Voter, ); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if len(templateContext.Decisions) > 0 { olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists( params.Flags.Unvoted, templateContext.Voter, ) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if olderExists { templateContext.NextPage = params.Page + 1 } } if params.Page > 1 { templateContext.PrevPage = params.Page - 1 } renderTemplate(w, r, []string{ "motions.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html", }, templateContext) } func motionHandler(w http.ResponseWriter, r *http.Request) { params := parseMotionParameters(r) decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } var templateContext struct { Decision *DecisionForDisplay Voter *Voter Params *motionParameters PrevPage, NextPage int64 PageTitle string Flashes interface{} } voter, ok := getVoterFromRequest(r) if ok { templateContext.Voter = voter } templateContext.Params = ¶ms if params.ShowVotes { if err := decision.LoadVotes(); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } templateContext.Decision = decision templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) renderTemplate(w, r, []string{ "motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html", }, templateContext) } func singleDecisionHandler( w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request), ) { decision, err := FindDecisionForDisplayByTag(tag) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if decision == nil { http.NotFound(w, r) return } handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision))) } type motionActionHandler interface { Handle(w http.ResponseWriter, r *http.Request) NeedsAuth() bool } type authenticationRequiredHandler struct{} func (authenticationRequiredHandler) NeedsAuth() bool { return true } func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) { voter, ok = r.Context().Value(ctxVoter).(*Voter) return } func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) { decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay) return } func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) { vote, ok = r.Context().Value(ctxVote).(VoteChoice) return } type FlashMessageAction struct{} func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) { session, err := store.Get(r, sessionCookieName) if err != nil { log.Warnf("could not get session cookie: %v", err) return } session.AddFlash(message, tags...) err = session.Save(r, w) if err != nil { log.Warnf("could not save flash message: %v", err) return } } type withDrawMotionAction struct { FlashMessageAction authenticationRequiredHandler } func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok || decision.Status != voteStatusPending { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } templates := []string{ "withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html", } var templateContext struct { PageTitle string Decision *DecisionForDisplay Flashes interface{} Voter *Voter } switch r.Method { case http.MethodPost: decision.Status = voteStatusWithdrawn decision.Modified = time.Now().UTC() if err := decision.UpdateStatus(); err != nil { log.Errorf("withdrawing motion failed: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter) a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag)) http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect) default: templateContext.Decision = decision templateContext.Voter = voter renderTemplate(w, r, templates, templateContext) } } type newMotionHandler struct { FlashMessageAction } func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) { voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) } templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"} var templateContext struct { Form NewDecisionForm PageTitle string Voter *Voter Flashes interface{} } switch r.Method { case http.MethodPost: form := NewDecisionForm{ Title: r.FormValue("Title"), Content: r.FormValue("Content"), VoteType: r.FormValue("VoteType"), Due: r.FormValue("Due"), } if valid, data := form.Validate(); !valid { templateContext.Voter = voter templateContext.Form = form renderTemplate(w, r, templates, templateContext) } else { data.Proposed = time.Now().UTC() data.ProponentID = voter.ID if err := data.Create(); err != nil { log.Errorf("saving motion failed: %v", err) http.Error(w, "Saving motion failed", http.StatusInternalServerError) return } NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter} h.AddFlash(w, r, "The motion has been proposed!") http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) } return default: templateContext.Voter = voter templateContext.Form = NewDecisionForm{ VoteType: strconv.Itoa(voteTypeMotion), } renderTemplate(w, r, templates, templateContext) } } type editMotionAction struct { FlashMessageAction authenticationRequiredHandler } func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok || decision.Status != voteStatusPending { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) return } templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"} var templateContext struct { Form EditDecisionForm PageTitle string Voter *Voter Flashes interface{} } switch r.Method { case http.MethodPost: form := EditDecisionForm{ Title: r.FormValue("Title"), Content: r.FormValue("Content"), VoteType: r.FormValue("VoteType"), Due: r.FormValue("Due"), Decision: &decision.Decision, } if valid, data := form.Validate(); !valid { templateContext.Voter = voter templateContext.Form = form renderTemplate(w, r, templates, templateContext) } else { data.Modified = time.Now().UTC() if err := data.Update(); err != nil { log.Errorf("updating motion failed: %v", err) http.Error(w, "Updating the motion failed.", http.StatusInternalServerError) return } NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter) a.AddFlash(w, r, "The motion has been modified!") http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) } return default: templateContext.Voter = voter templateContext.Form = EditDecisionForm{ Title: decision.Title, Content: decision.Content, VoteType: fmt.Sprintf("%d", decision.VoteType), Decision: &decision.Decision, } renderTemplate(w, r, templates, templateContext) } } type motionsHandler struct{} func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { subURL := r.URL.Path var motionActionMap = map[string]motionActionHandler{ "withdraw": &withDrawMotionAction{}, "edit": &editMotionAction{}, } switch { case subURL == "": authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler) return case subURL == "/newmotion/": handler := &newMotionHandler{} authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), handler.Handle) return case strings.Count(subURL, "/") == 1: parts := strings.Split(subURL, "/") motionTag := parts[0] action, ok := motionActionMap[parts[1]] if !ok { http.NotFound(w, r) return } authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, motionTag, action.Handle) }) return case strings.Count(subURL, "/") == 0: authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, subURL, motionHandler) }) return default: http.NotFound(w, r) return } } type directVoteHandler struct { FlashMessageAction authenticationRequiredHandler } func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } vote, ok := getVoteFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } switch r.Method { case http.MethodPost: clientCert, err := getPEMClientCert(r) if err != nil { log.Errorf("could not get client certificate from request: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } voteResult := &Vote{ VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(), Notes: fmt.Sprintf("Direct Vote\n\n%s", clientCert)} if err := voteResult.Save(); err != nil { log.Errorf("Problem saving vote: %v", err) http.Error(w, "Problem saving vote", http.StatusInternalServerError) return } NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult) h.AddFlash(w, r, "Your vote has been registered.") http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) default: templates := []string{ "direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html", } var templateContext struct { Decision *DecisionForDisplay VoteChoice VoteChoice PageTitle string Flashes interface{} Voter *Voter } templateContext.Decision = decision templateContext.VoteChoice = vote templateContext.Voter = voter renderTemplate(w, r, templates, templateContext) } } type proxyVoteHandler struct { FlashMessageAction authenticationRequiredHandler } func getPEMClientCert(r *http.Request) (string, error) { cert := r.Context().Value(ctxAuthenticatedCert) authenticatedCertificate, ok := cert.(*x509.Certificate) if !ok { return "", errors.New("could not handle certificate as x509.Certificate") } clientCertPEM := bytes.NewBuffer(make([]byte, 0)) err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw}) if err != nil { return "", fmt.Errorf("error encoding client certificate: %w", err) } return clientCertPEM.String(), nil } func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } proxy, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } templates := []string{ "proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html", } var templateContext struct { Form ProxyVoteForm Decision *DecisionForDisplay Voters *[]Voter PageTitle string Flashes interface{} Voter *Voter } templateContext.Voter = proxy switch r.Method { case http.MethodPost: form := ProxyVoteForm{ Voter: r.FormValue("Voter"), Vote: r.FormValue("Vote"), Justification: r.FormValue("Justification"), } if valid, voter, data, justification := form.Validate(); !valid { templateContext.Form = form templateContext.Decision = decision voters, err := GetVotersForProxy(proxy) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } templateContext.Voters = voters renderTemplate(w, r, templates, templateContext) } else { clientCert, err := getPEMClientCert(r) if err != nil { log.Errorf("could not get client certificate information: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } data.DecisionID = decision.ID data.Voted = time.Now().UTC() data.Notes = fmt.Sprintf("Proxy-Vote by %s\n\n%s\n\n%s", proxy.Name, justification, clientCert) if err := data.Save(); err != nil { log.Errorf("Error saving vote: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification) h.AddFlash(w, r, "The vote has been registered.") http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) } return default: templateContext.Form = ProxyVoteForm{} templateContext.Decision = decision voters, err := GetVotersForProxy(proxy) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } templateContext.Voters = voters renderTemplate(w, r, templates, templateContext) } } type decisionVoteHandler struct{} func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.HasPrefix(r.URL.Path, "/proxy/"): motionTag := r.URL.Path[len("/proxy/"):] handler := &proxyVoteHandler{} authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, motionTag, handler.Handle) }) case strings.HasPrefix(r.URL.Path, "/vote/"): const expectedParts = 2 parts := strings.Split(r.URL.Path[len("/vote/"):], "/") if len(parts) != expectedParts { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } motionTag := parts[0] voteValue, ok := VoteValues[parts[1]] if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } handler := &directVoteHandler{} authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler( w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)), motionTag, handler.Handle) }) return } } type Config struct { NoticeMailAddress string `yaml:"notice_mail_address"` VoteNoticeMailAddress string `yaml:"vote_notice_mail_address"` NotificationSenderAddress string `yaml:"notification_sender_address"` DatabaseFile string `yaml:"database_file"` ClientCACertificates string `yaml:"client_ca_certificates"` ServerCert string `yaml:"server_certificate"` ServerKey string `yaml:"server_key"` CookieSecret string `yaml:"cookie_secret"` CsrfKey string `yaml:"csrf_key"` BaseURL string `yaml:"base_url"` HTTPAddress string `yaml:"http_address"` HTTPSAddress string `yaml:"https_address"` MailServer struct { Host string `yaml:"host"` Port int `yaml:"port"` } `yaml:"mail_server"` } func readConfig() { source, err := ioutil.ReadFile(configFile) if err != nil { log.Panicf("Opening configuration file failed: %v", err) } if err := yaml.Unmarshal(source, &config); err != nil { log.Panicf("Loading configuration failed: %v", err) } if config.HTTPSAddress == "" { config.HTTPSAddress = "127.0.0.1:8443" } if config.HTTPAddress == "" { config.HTTPAddress = "127.0.0.1:8080" } cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret) if err != nil { log.Panicf("Decoding cookie secret failed: %v", err) panic(err) } if len(cookieSecret) < cookieSecretMinLen { log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen) } csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey) if err != nil { log.Panicf("Decoding csrf key failed: %v", err) } if len(csrfKey) != csrfKeyLength { log.Panicf( "CSRF key must be exactly %d bytes long but is %d bytes long", csrfKeyLength, len(csrfKey), ) } store = sessions.NewCookieStore(cookieSecret) store.Options.Secure = true log.Info("Read configuration") } func setupDbConfig(ctx context.Context) { database, err := sql.Open("sqlite3", config.DatabaseFile) if err != nil { log.Panicf("Opening database failed: %v", err) } db = NewDB(database) go func() { for range ctx.Done() { if err := db.Close(); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Problem closing the database: %v", err) } } }() log.Infof("opened database connection") } func setupNotifications(ctx context.Context) { quitMailChannel := make(chan int) go MailNotifier(quitMailChannel) go func() { for range ctx.Done() { quitMailChannel <- 1 } }() } func setupJobs(ctx context.Context) { quitChannel := make(chan int) go JobScheduler(quitChannel) go func() { for range ctx.Done() { quitChannel <- 1 } }() } //go:embed ui/static var uiStatic embed.FS func setupHandlers() { http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{})) http.Handle("/newmotion/", motionsHandler{}) http.Handle("/proxy/", &decisionVoteHandler{}) http.Handle("/vote/", &decisionVoteHandler{}) http.Handle("/static/", addPrefix("/ui", statigz.FileServer(uiStatic, brotli.AddEncoding, statigz.EncodeOnInit))) http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently)) } func addPrefix(prefix string, h http.Handler) http.Handler { if prefix == "" { return h } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { newPath := prefix + r.URL.Path newRawPath := prefix + r.URL.RawPath r2 := new(http.Request) *r2 = *r r2.URL = new(url.URL) *r2.URL = *r.URL r2.URL.Path = newPath r2.URL.RawPath = newRawPath h.ServeHTTP(w, r2) }) } func setupTLSConfig() (tlsConfig *tls.Config) { // load CA certificates for client authentication caCert, err := ioutil.ReadFile(config.ClientCACertificates) if err != nil { log.Panicf("Error reading client certificate CAs %v", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { log.Panic("could not initialize client CA certificate pool") } // setup HTTPS server tlsConfig = &tls.Config{ MinVersion: tls.VersionTLS12, ClientCAs: caCertPool, ClientAuth: tls.VerifyClientCertIfGiven, } return } func main() { log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) log.Infof("CAcert Board Voting version %s, commit %s built at %s", version, commit, date) flag.StringVar( &configFile, "config", "config.yaml", "Configuration file name") flag.Parse() var stopAll func() executionContext, stopAll := context.WithCancel(context.Background()) readConfig() setupDbConfig(executionContext) setupNotifications(executionContext) setupJobs(executionContext) setupHandlers() tlsConfig := setupTLSConfig() defer stopAll() server := &http.Server{ Addr: config.HTTPSAddress, TLSConfig: tlsConfig, IdleTimeout: time.Second * httpIdleTimeout, ReadHeaderTimeout: time.Second * httpReadHeaderTimeout, ReadTimeout: time.Second * httpReadTimeout, WriteTimeout: time.Second * httpWriteTimeout, } server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux) log.Infof("Launching application on https://%s/", server.Addr) errs := make(chan error, 1) go func() { httpRedirector := &http.Server{ Addr: config.HTTPAddress, Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently), IdleTimeout: time.Second * httpIdleTimeout, ReadHeaderTimeout: time.Second * httpReadHeaderTimeout, ReadTimeout: time.Second * httpReadTimeout, WriteTimeout: time.Second * httpWriteTimeout, } if err := httpRedirector.ListenAndServe(); err != nil { errs <- err } close(errs) }() if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil { log.Panicf("ListenAndServerTLS failed: %v", err) } if err := <-errs; err != nil { log.Panicf("ListenAndServe failed: %v", err) } }