77499a2a9c8bd6f41e133abab4c0415d189e1a34
[cacert-boardvoting.git] / boardvoting.go
1 package main
2
3 import (
4 "crypto/tls"
5 "crypto/x509"
6 "fmt"
7 "github.com/Masterminds/sprig"
8 "github.com/jmoiron/sqlx"
9 _ "github.com/mattn/go-sqlite3"
10 "gopkg.in/yaml.v2"
11 "html/template"
12 "io/ioutil"
13 "log"
14 "net/http"
15 "os"
16 "strconv"
17 "strings"
18 )
19
20 var logger *log.Logger
21 var config *Config
22
23 func getTemplateFilenames(tmpl []string) (result []string) {
24 result = make([]string, len(tmpl))
25 for i := range tmpl {
26 result[i] = fmt.Sprintf("templates/%s", tmpl[i])
27 }
28 return result
29 }
30
31 func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
32 t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...))
33 if err := t.Execute(w, context); err != nil {
34 http.Error(w, err.Error(), http.StatusInternalServerError)
35 }
36 }
37
38 func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bool, handler func(http.ResponseWriter, *http.Request, *Voter)) {
39 for _, cert := range r.TLS.PeerCertificates {
40 for _, extKeyUsage := range cert.ExtKeyUsage {
41 if extKeyUsage == x509.ExtKeyUsageClientAuth {
42 for _, emailAddress := range cert.EmailAddresses {
43 voter, err := FindVoterByAddress(emailAddress)
44 if err != nil {
45 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
46 return
47 }
48 if voter != nil {
49 handler(w, r, voter)
50 return
51 }
52 }
53 }
54 }
55 }
56 if authRequired {
57 w.WriteHeader(http.StatusForbidden)
58 renderTemplate(w, []string{"denied.html"}, nil)
59 return
60 }
61 handler(w, r, nil)
62 }
63
64 type motionParameters struct {
65 ShowVotes bool
66 }
67
68 type motionListParameters struct {
69 Page int64
70 Flags struct {
71 Confirmed, Withdraw, Unvoted bool
72 }
73 }
74
75 func parseMotionParameters(r *http.Request) motionParameters {
76 var m = motionParameters{}
77 m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
78 logger.Printf("parsed parameters: %+v\n", m)
79 return m
80 }
81
82 func parseMotionListParameters(r *http.Request) motionListParameters {
83 var m = motionListParameters{}
84 if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); err != nil {
85 m.Page = 1
86 } else {
87 m.Page = page
88 }
89 m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
90 m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
91
92 if r.Method == http.MethodPost {
93 m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
94 }
95 logger.Printf("parsed parameters: %+v\n", m)
96 return m
97 }
98
99 func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) {
100 params := parseMotionListParameters(r)
101
102 var context struct {
103 Decisions []*DecisionForDisplay
104 Voter *Voter
105 Params *motionListParameters
106 PrevPage, NextPage int64
107 PageTitle string
108 }
109 context.Voter = voter
110 context.Params = &params
111 var err error
112
113 if params.Flags.Unvoted {
114 if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil {
115 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
116 return
117 }
118 } else {
119 if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
120 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
121 return
122 }
123 }
124
125 if len(context.Decisions) > 0 {
126 olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists()
127 if err != nil {
128 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
129 return
130 }
131 if olderExists {
132 context.NextPage = params.Page + 1
133 }
134 }
135
136 if params.Page > 1 {
137 context.PrevPage = params.Page - 1
138 }
139
140 renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, context)
141 }
142
143 func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
144 params := parseMotionParameters(r)
145
146 var context struct {
147 Decision *DecisionForDisplay
148 Voter *Voter
149 Params *motionParameters
150 PrevPage, NextPage int64
151 PageTitle string
152 }
153 context.Voter = voter
154 context.Params = &params
155 if params.ShowVotes {
156 if err := decision.LoadVotes(); err != nil {
157 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
158 return
159 }
160 }
161 context.Decision = decision
162 context.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
163 renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, context)
164 }
165
166 func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) {
167 decision, err := FindDecisionForDisplayByTag(tag)
168 if err != nil {
169 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
170 return
171 }
172 if decision == nil {
173 http.NotFound(w, r)
174 return
175 }
176 handler(w, r, v, decision)
177 }
178
179 type motionsHandler struct{}
180
181 type motionActionHandler interface {
182 Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay)
183 NeedsAuth() bool
184 }
185
186 type withDrawMotionAction struct{}
187
188 func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
189 fmt.Fprintln(w, "Withdraw motion", decision.Tag)
190 // TODO: implement
191 if r.Method == http.MethodPost {
192 if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
193 log.Println("could not parse confirm parameter:", err)
194 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
195 } else if confirm {
196 WithdrawMotion(&decision.Decision, voter)
197 } else {
198 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
199 }
200 }
201 }
202
203 func (withDrawMotionAction) NeedsAuth() bool {
204 return true
205 }
206
207 type editMotionAction struct{}
208
209 func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
210 fmt.Fprintln(w, "Edit motion", decision.Tag)
211 // TODO: implement
212 }
213
214 func (editMotionAction) NeedsAuth() bool {
215 return true
216 }
217
218 func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
219 if err := db.Ping(); err != nil {
220 logger.Fatal(err)
221 }
222
223 subURL := r.URL.Path
224
225 var motionActionMap = map[string]motionActionHandler{
226 "withdraw": withDrawMotionAction{},
227 "edit": editMotionAction{},
228 }
229
230 switch {
231 case subURL == "":
232 authenticateRequest(w, r, false, motionListHandler)
233 return
234 case strings.Count(subURL, "/") == 1:
235 parts := strings.Split(subURL, "/")
236 logger.Printf("handle %v\n", parts)
237 motionTag := parts[0]
238 action, ok := motionActionMap[parts[1]]
239 if !ok {
240 http.NotFound(w, r)
241 return
242 }
243 authenticateRequest(w, r, action.NeedsAuth(), func(w http.ResponseWriter, r *http.Request, v *Voter) {
244 singleDecisionHandler(w, r, v, motionTag, action.Handle)
245 })
246
247 logger.Printf("motion: %s, action: %s\n", motionTag, action)
248 return
249 case strings.Count(subURL, "/") == 0:
250 authenticateRequest(w, r, false, func(w http.ResponseWriter, r *http.Request, v *Voter) {
251 singleDecisionHandler(w, r, v, subURL, motionHandler)
252 })
253 return
254 default:
255 fmt.Fprintf(w, "No handler for '%s'", subURL)
256 return
257 }
258 }
259
260 func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) {
261 fmt.Fprintln(w,"New motion")
262 // TODO: implement
263 }
264
265 type Config struct {
266 BoardMailAddress string `yaml:"board_mail_address"`
267 NoticeSenderAddress string `yaml:"notice_sender_address"`
268 DatabaseFile string `yaml:"database_file"`
269 ClientCACertificates string `yaml:"client_ca_certificates"`
270 ServerCert string `yaml:"server_certificate"`
271 ServerKey string `yaml:"server_key"`
272 }
273
274 func main() {
275 logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
276
277 var filename = "config.yaml"
278 if len(os.Args) == 2 {
279 filename = os.Args[1]
280 }
281
282 var err error
283
284 var source []byte
285
286 source, err = ioutil.ReadFile(filename)
287 if err != nil {
288 logger.Fatal(err)
289 }
290 err = yaml.Unmarshal(source, &config)
291 if err != nil {
292 logger.Fatal(err)
293 }
294 logger.Printf("read configuration %v", config)
295
296 db, err = sqlx.Open("sqlite3", config.DatabaseFile)
297 if err != nil {
298 logger.Fatal(err)
299 }
300 defer db.Close()
301
302 http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
303 http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
304 authenticateRequest(w, r, true, newMotionHandler)
305 })
306 http.Handle("/static/", http.FileServer(http.Dir(".")))
307 http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
308
309 // load CA certificates for client authentication
310 caCert, err := ioutil.ReadFile(config.ClientCACertificates)
311 if err != nil {
312 logger.Fatal(err)
313 }
314 caCertPool := x509.NewCertPool()
315 if !caCertPool.AppendCertsFromPEM(caCert) {
316 logger.Fatal("could not initialize client CA certificate pool")
317 }
318
319 // setup HTTPS server
320 tlsConfig := &tls.Config{
321 ClientCAs: caCertPool,
322 ClientAuth: tls.RequireAndVerifyClientCert,
323 }
324 tlsConfig.BuildNameToCertificate()
325
326 server := &http.Server{
327 Addr: ":8443",
328 TLSConfig: tlsConfig,
329 }
330
331 logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
332
333 if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
334 logger.Fatal("ListenAndServerTLS: ", err)
335 }
336 }