Implement proxy voting
[cacert-boardvoting.git] / boardvoting.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "crypto/x509"
8 "encoding/base64"
9 "encoding/pem"
10 "fmt"
11 "github.com/Masterminds/sprig"
12 "github.com/gorilla/sessions"
13 "github.com/jmoiron/sqlx"
14 _ "github.com/mattn/go-sqlite3"
15 "gopkg.in/yaml.v2"
16 "html/template"
17 "io/ioutil"
18 "log"
19 "net/http"
20 "os"
21 "strconv"
22 "strings"
23 "time"
24 )
25
26 var logger *log.Logger
27 var config *Config
28 var store *sessions.CookieStore
29 var version = "undefined"
30 var build = "undefined"
31
32 const sessionCookieName = "votesession"
33
34 func getTemplateFilenames(templates []string) (result []string) {
35 result = make([]string, len(templates))
36 for i := range templates {
37 result[i] = fmt.Sprintf("templates/%s", templates[i])
38 }
39 return result
40 }
41
42 func renderTemplate(w http.ResponseWriter, templates []string, context interface{}) {
43 t := template.Must(template.New(templates[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(templates)...))
44 if err := t.Execute(w, context); err != nil {
45 http.Error(w, err.Error(), http.StatusInternalServerError)
46 }
47 }
48
49 type contextKey int
50
51 const (
52 ctxNeedsAuth contextKey = iota
53 ctxVoter
54 ctxDecision
55 ctxVote
56 )
57
58 func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
59 for _, cert := range r.TLS.PeerCertificates {
60 for _, extKeyUsage := range cert.ExtKeyUsage {
61 if extKeyUsage == x509.ExtKeyUsageClientAuth {
62 for _, emailAddress := range cert.EmailAddresses {
63 voter, err := FindVoterByAddress(emailAddress)
64 if err != nil {
65 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
66 return
67 }
68 if voter != nil {
69 handler(w, r.WithContext(context.WithValue(r.Context(), ctxVoter, voter)))
70 return
71 }
72 }
73 }
74 }
75 }
76 needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
77 if ok && needsAuth {
78 w.WriteHeader(http.StatusForbidden)
79 renderTemplate(w, []string{"denied.html"}, nil)
80 return
81 }
82 handler(w, r)
83 }
84
85 type motionParameters struct {
86 ShowVotes bool
87 }
88
89 type motionListParameters struct {
90 Page int64
91 Flags struct {
92 Confirmed, Withdraw, Unvoted bool
93 }
94 }
95
96 func parseMotionParameters(r *http.Request) motionParameters {
97 var m = motionParameters{}
98 m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
99 return m
100 }
101
102 func parseMotionListParameters(r *http.Request) motionListParameters {
103 var m = motionListParameters{}
104 if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); err != nil {
105 m.Page = 1
106 } else {
107 m.Page = page
108 }
109 m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
110 m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
111
112 if r.Method == http.MethodPost {
113 m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
114 }
115 return m
116 }
117
118 func motionListHandler(w http.ResponseWriter, r *http.Request) {
119 params := parseMotionListParameters(r)
120 session, err := store.Get(r, sessionCookieName)
121 if err != nil {
122 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
123 return
124 }
125
126 var templateContext struct {
127 Decisions []*DecisionForDisplay
128 Voter *Voter
129 Params *motionListParameters
130 PrevPage, NextPage int64
131 PageTitle string
132 Flashes interface{}
133 }
134 if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
135 templateContext.Voter = voter
136 }
137 if flashes := session.Flashes(); len(flashes) > 0 {
138 templateContext.Flashes = flashes
139 }
140 session.Save(r, w)
141 templateContext.Params = &params
142
143 if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
144 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
145 return
146 }
147
148 if len(templateContext.Decisions) > 0 {
149 olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
150 if err != nil {
151 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
152 return
153 }
154 if olderExists {
155 templateContext.NextPage = params.Page + 1
156 }
157 }
158
159 if params.Page > 1 {
160 templateContext.PrevPage = params.Page - 1
161 }
162
163 renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
164 }
165
166 func motionHandler(w http.ResponseWriter, r *http.Request) {
167 params := parseMotionParameters(r)
168
169 decision, ok := getDecisionFromRequest(r)
170 if !ok {
171 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
172 return
173 }
174
175 var templateContext struct {
176 Decision *DecisionForDisplay
177 Voter *Voter
178 Params *motionParameters
179 PrevPage, NextPage int64
180 PageTitle string
181 Flashes interface{}
182 }
183 voter, ok := getVoterFromRequest(r)
184 if ok {
185 templateContext.Voter = voter
186 }
187 templateContext.Params = &params
188 if params.ShowVotes {
189 if err := decision.LoadVotes(); err != nil {
190 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
191 return
192 }
193 }
194 templateContext.Decision = decision
195 templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
196 renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
197 }
198
199 func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
200 decision, err := FindDecisionForDisplayByTag(tag)
201 if err != nil {
202 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
203 return
204 }
205 if decision == nil {
206 http.NotFound(w, r)
207 return
208 }
209 handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
210 }
211
212 type motionActionHandler interface {
213 Handle(w http.ResponseWriter, r *http.Request)
214 NeedsAuth() bool
215 }
216
217 type authenticationRequiredHandler struct{}
218
219 func (authenticationRequiredHandler) NeedsAuth() bool {
220 return true
221 }
222
223 func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
224 voter, ok = r.Context().Value(ctxVoter).(*Voter)
225 return
226 }
227
228 func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
229 decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
230 return
231 }
232
233 func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
234 vote, ok = r.Context().Value(ctxVote).(VoteChoice)
235 return
236 }
237
238 type FlashMessageAction struct{}
239
240 func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) (err error) {
241 session, err := store.Get(r, sessionCookieName)
242 if err != nil {
243 logger.Println("ERROR getting session:", err)
244 return
245 }
246 session.AddFlash(message, tags...)
247 session.Save(r, w)
248 if err != nil {
249 logger.Println("ERROR saving session:", err)
250 return
251 }
252 return
253 }
254
255 type withDrawMotionAction struct {
256 FlashMessageAction
257 authenticationRequiredHandler
258 }
259
260 func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
261 decision, ok := getDecisionFromRequest(r)
262 if !ok || decision.Status != voteStatusPending {
263 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
264 return
265 }
266 voter, ok := getVoterFromRequest(r)
267 if !ok {
268 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
269 return
270 }
271 templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html"}
272 var templateContext struct {
273 PageTitle string
274 Decision *DecisionForDisplay
275 Flashes interface{}
276 }
277
278 switch r.Method {
279 case http.MethodPost:
280 decision.Status = voteStatusWithdrawn
281 decision.Modified = time.Now().UTC()
282 if err := decision.UpdateStatus(); err != nil {
283 logger.Println("Error withdrawing motion:", err)
284 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
285 return
286 }
287
288 NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
289
290 if err := a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag)); err != nil {
291 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
292 return
293 }
294
295 http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
296 default:
297 templateContext.Decision = decision
298 renderTemplate(w, templates, templateContext)
299 }
300 }
301
302 type newMotionHandler struct {
303 FlashMessageAction
304 }
305
306 func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
307 voter, ok := getVoterFromRequest(r)
308 if !ok {
309 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
310 }
311
312 templates := []string{"create_motion_form.html", "header.html", "footer.html"}
313 var templateContext struct {
314 Form NewDecisionForm
315 PageTitle string
316 Voter *Voter
317 Flashes interface{}
318 }
319 switch r.Method {
320 case http.MethodPost:
321 form := NewDecisionForm{
322 Title: r.FormValue("Title"),
323 Content: r.FormValue("Content"),
324 VoteType: r.FormValue("VoteType"),
325 Due: r.FormValue("Due"),
326 }
327
328 if valid, data := form.Validate(); !valid {
329 templateContext.Voter = voter
330 templateContext.Form = form
331 renderTemplate(w, templates, templateContext)
332 } else {
333 data.Proposed = time.Now().UTC()
334 data.ProponentId = voter.Id
335 if err := data.Create(); err != nil {
336 logger.Println("Error saving motion:", err)
337 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
338 return
339 }
340
341 NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
342
343 if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
344 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
345 return
346 }
347
348 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
349 }
350
351 return
352 default:
353 templateContext.Voter = voter
354 templateContext.Form = NewDecisionForm{
355 VoteType: strconv.FormatInt(voteTypeMotion, 10),
356 }
357 renderTemplate(w, templates, templateContext)
358 }
359 }
360
361 type editMotionAction struct {
362 FlashMessageAction
363 authenticationRequiredHandler
364 }
365
366 func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
367 decision, ok := getDecisionFromRequest(r)
368 if !ok || decision.Status != voteStatusPending {
369 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
370 return
371 }
372 voter, ok := getVoterFromRequest(r)
373 if !ok {
374 http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
375 return
376 }
377 templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
378 var templateContext struct {
379 Form EditDecisionForm
380 PageTitle string
381 Voter *Voter
382 Flashes interface{}
383 }
384 switch r.Method {
385 case http.MethodPost:
386 form := EditDecisionForm{
387 Title: r.FormValue("Title"),
388 Content: r.FormValue("Content"),
389 VoteType: r.FormValue("VoteType"),
390 Due: r.FormValue("Due"),
391 Decision: &decision.Decision,
392 }
393
394 if valid, data := form.Validate(); !valid {
395 templateContext.Voter = voter
396 templateContext.Form = form
397 renderTemplate(w, templates, templateContext)
398 } else {
399 data.Modified = time.Now().UTC()
400 if err := data.Update(); err != nil {
401 logger.Println("Error updating motion:", err)
402 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
403 return
404 }
405
406 NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
407
408 if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
409 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
410 return
411 }
412
413 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
414 }
415 return
416 default:
417 templateContext.Voter = voter
418 templateContext.Form = EditDecisionForm{
419 Title: decision.Title,
420 Content: decision.Content,
421 VoteType: fmt.Sprintf("%d", decision.VoteType),
422 Decision: &decision.Decision,
423 }
424 renderTemplate(w, templates, templateContext)
425 }
426 }
427
428 type motionsHandler struct{}
429
430 func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
431 if err := db.Ping(); err != nil {
432 logger.Fatal(err)
433 }
434
435 subURL := r.URL.Path
436
437 var motionActionMap = map[string]motionActionHandler{
438 "withdraw": &withDrawMotionAction{},
439 "edit": &editMotionAction{},
440 }
441
442 switch {
443 case subURL == "":
444 authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
445 return
446 case subURL == "/newmotion/":
447 handler := &newMotionHandler{}
448 authenticateRequest(
449 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
450 handler.Handle)
451 return
452 case strings.Count(subURL, "/") == 1:
453 parts := strings.Split(subURL, "/")
454 motionTag := parts[0]
455 action, ok := motionActionMap[parts[1]]
456 if !ok {
457 http.NotFound(w, r)
458 return
459 }
460 authenticateRequest(
461 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
462 func(w http.ResponseWriter, r *http.Request) {
463 singleDecisionHandler(w, r, motionTag, action.Handle)
464 })
465 return
466 case strings.Count(subURL, "/") == 0:
467 authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
468 func(w http.ResponseWriter, r *http.Request) {
469 singleDecisionHandler(w, r, subURL, motionHandler)
470 })
471 return
472 default:
473 http.NotFound(w, r)
474 return
475 }
476 }
477
478 type voteHandler struct {
479 FlashMessageAction
480 authenticationRequiredHandler
481 }
482
483 func (h *voteHandler) Handle(w http.ResponseWriter, r *http.Request) {
484 decision, ok := getDecisionFromRequest(r)
485 if !ok {
486 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
487 return
488 }
489 voter, ok := getVoterFromRequest(r)
490 if !ok {
491 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
492 return
493 }
494 vote, ok := getVoteFromRequest(r)
495 if !ok {
496 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
497 return
498 }
499 fmt.Fprintln(w, "to be implemented")
500 fmt.Fprintln(w, "Decision:", decision)
501 fmt.Fprintln(w, "Voter:", voter)
502 fmt.Fprintln(w, "Vote:", vote)
503 }
504
505 type proxyVoteHandler struct {
506 FlashMessageAction
507 authenticationRequiredHandler
508 }
509
510 func getPEMClientCert(r *http.Request) string {
511 clientCertPEM := bytes.NewBufferString("")
512 pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: r.TLS.PeerCertificates[0].Raw})
513 return clientCertPEM.String()
514 }
515
516 func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
517 decision, ok := getDecisionFromRequest(r)
518 if !ok {
519 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
520 return
521 }
522 proxy, ok := getVoterFromRequest(r)
523 if !ok {
524 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
525 return
526 }
527 templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
528 var templateContext struct {
529 Form ProxyVoteForm
530 Decision *DecisionForDisplay
531 Voters *[]Voter
532 PageTitle string
533 Flashes interface{}
534 }
535 switch r.Method {
536 case http.MethodPost:
537 form := ProxyVoteForm{
538 Voter: r.FormValue("Voter"),
539 Vote: r.FormValue("Vote"),
540 Justification: r.FormValue("Justification"),
541 }
542
543 if valid, voter, data, justification := form.Validate(); !valid {
544 templateContext.Form = form
545 templateContext.Decision = decision
546 if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
547 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
548 return
549 } else {
550 templateContext.Voters = voters
551 }
552 renderTemplate(w, templates, templateContext)
553 } else {
554 data.DecisionId = decision.Id
555 data.Voted = time.Now().UTC()
556 data.Notes = fmt.Sprintf(
557 "Proxy-Vote by %s\n\n%s\n\n%s",
558 proxy.Name, justification, getPEMClientCert(r))
559
560 if err := data.Save(); err != nil {
561 logger.Println("Error saving vote:", err)
562 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
563 return
564 }
565
566 NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
567
568 if err := h.AddFlash(w, r, "The vote has been registered."); err != nil {
569 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
570 return
571 }
572
573 http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
574 }
575 return
576 default:
577 templateContext.Form = ProxyVoteForm{}
578 templateContext.Decision = decision
579 if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
580 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
581 return
582 } else {
583 templateContext.Voters = voters
584 }
585 renderTemplate(w, templates, templateContext)
586 }
587 }
588
589 type decisionVoteHandler struct{}
590
591 func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
592 if err := db.Ping(); err != nil {
593 logger.Fatal(err)
594 }
595
596 switch {
597 case strings.HasPrefix(r.URL.Path, "/proxy/"):
598 motionTag := r.URL.Path[len("/proxy/"):]
599 handler := &proxyVoteHandler{}
600 authenticateRequest(
601 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
602 func(w http.ResponseWriter, r *http.Request) {
603 singleDecisionHandler(w, r, motionTag, handler.Handle)
604 })
605 case strings.HasPrefix(r.URL.Path, "/vote/"):
606 parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
607 motionTag := parts[0]
608 voteValue, ok := VoteValues[parts[1]]
609 if !ok {
610 http.NotFound(w, r)
611 return
612 }
613 handler := &voteHandler{}
614 authenticateRequest(
615 w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
616 func(w http.ResponseWriter, r *http.Request) {
617 singleDecisionHandler(
618 w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
619 motionTag, handler.Handle)
620 })
621 return
622 }
623 }
624
625 type Config struct {
626 BoardMailAddress string `yaml:"board_mail_address"`
627 VoteNoticeAddress string `yaml:"notice_sender_address"`
628 NotificationSenderAddress string `yaml:"reminder_sender_address"`
629 DatabaseFile string `yaml:"database_file"`
630 ClientCACertificates string `yaml:"client_ca_certificates"`
631 ServerCert string `yaml:"server_certificate"`
632 ServerKey string `yaml:"server_key"`
633 CookieSecret string `yaml:"cookie_secret"`
634 BaseURL string `yaml:"base_url"`
635 MailServer struct {
636 Host string `yaml:"host"`
637 Port int `yaml:"port"`
638 } `yaml:"mail_server"`
639 }
640
641 func init() {
642 logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
643
644 source, err := ioutil.ReadFile("config.yaml")
645 if err != nil {
646 logger.Fatal(err)
647 }
648 if err := yaml.Unmarshal(source, &config); err != nil {
649 logger.Fatal(err)
650 }
651
652 cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
653 if err != nil {
654 logger.Fatal(err)
655 }
656 if len(cookieSecret) < 32 {
657 logger.Fatalln("Cookie secret is less than 32 bytes long")
658 }
659 store = sessions.NewCookieStore(cookieSecret)
660 logger.Println("read configuration")
661
662 db, err = sqlx.Open("sqlite3", config.DatabaseFile)
663 if err != nil {
664 logger.Fatal(err)
665 }
666 logger.Println("opened database connection")
667 }
668
669 func main() {
670 logger.Printf("CAcert Board Voting version %s, build %s\n", version, build)
671
672 defer db.Close()
673
674 quitMailChannel := make(chan int)
675 go MailNotifier(quitMailChannel)
676 defer func() { quitMailChannel <- 1 }()
677
678 quitChannel := make(chan int)
679 go JobScheduler(quitChannel)
680 defer func() { quitChannel <- 1 }()
681
682 http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
683 http.Handle("/newmotion/", motionsHandler{})
684 http.Handle("/proxy/", &decisionVoteHandler{})
685 http.Handle("/vote/", &decisionVoteHandler{})
686 http.Handle("/static/", http.FileServer(http.Dir(".")))
687 http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
688
689 // load CA certificates for client authentication
690 caCert, err := ioutil.ReadFile(config.ClientCACertificates)
691 if err != nil {
692 logger.Fatal(err)
693 }
694 caCertPool := x509.NewCertPool()
695 if !caCertPool.AppendCertsFromPEM(caCert) {
696 logger.Fatal("could not initialize client CA certificate pool")
697 }
698
699 // setup HTTPS server
700 tlsConfig := &tls.Config{
701 ClientCAs: caCertPool,
702 ClientAuth: tls.VerifyClientCertIfGiven,
703 }
704 tlsConfig.BuildNameToCertificate()
705
706 server := &http.Server{
707 Addr: ":8443",
708 TLSConfig: tlsConfig,
709 }
710
711 logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
712
713 errs := make(chan error, 1)
714 go func() {
715 if err := http.ListenAndServe(":8080", http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
716 errs <- err
717 }
718 close(errs)
719 }()
720
721 if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
722 logger.Fatal("ListenAndServerTLS: ", err)
723 }
724 if err := <-errs; err != nil {
725 logger.Fatal("ListenAndServe: ", err)
726 }
727 }