336 lines
9.2 KiB
Go
336 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"github.com/Masterminds/sprig"
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"gopkg.in/yaml.v2"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var logger *log.Logger
|
|
var config *Config
|
|
|
|
func getTemplateFilenames(tmpl []string) (result []string) {
|
|
result = make([]string, len(tmpl))
|
|
for i := range tmpl {
|
|
result[i] = fmt.Sprintf("templates/%s", tmpl[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
|
|
t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...))
|
|
if err := t.Execute(w, context); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bool, handler func(http.ResponseWriter, *http.Request, *Voter)) {
|
|
for _, cert := range r.TLS.PeerCertificates {
|
|
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 {
|
|
handler(w, r, voter)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if authRequired {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
renderTemplate(w, []string{"denied.html"}, nil)
|
|
return
|
|
}
|
|
handler(w, r, nil)
|
|
}
|
|
|
|
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"))
|
|
logger.Printf("parsed parameters: %+v\n", m)
|
|
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"))
|
|
}
|
|
logger.Printf("parsed parameters: %+v\n", m)
|
|
return m
|
|
}
|
|
|
|
func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) {
|
|
params := parseMotionListParameters(r)
|
|
|
|
var context struct {
|
|
Decisions []*DecisionForDisplay
|
|
Voter *Voter
|
|
Params *motionListParameters
|
|
PrevPage, NextPage int64
|
|
PageTitle string
|
|
}
|
|
context.Voter = voter
|
|
context.Params = ¶ms
|
|
var err error
|
|
|
|
if params.Flags.Unvoted {
|
|
if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
if len(context.Decisions) > 0 {
|
|
olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists()
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if olderExists {
|
|
context.NextPage = params.Page + 1
|
|
}
|
|
}
|
|
|
|
if params.Page > 1 {
|
|
context.PrevPage = params.Page - 1
|
|
}
|
|
|
|
renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, context)
|
|
}
|
|
|
|
func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
|
|
params := parseMotionParameters(r)
|
|
|
|
var context struct {
|
|
Decision *DecisionForDisplay
|
|
Voter *Voter
|
|
Params *motionParameters
|
|
PrevPage, NextPage int64
|
|
PageTitle string
|
|
}
|
|
context.Voter = voter
|
|
context.Params = ¶ms
|
|
if params.ShowVotes {
|
|
if err := decision.LoadVotes(); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
context.Decision = decision
|
|
context.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
|
|
renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, context)
|
|
}
|
|
|
|
func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) {
|
|
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, v, decision)
|
|
}
|
|
|
|
type motionsHandler struct{}
|
|
|
|
type motionActionHandler interface {
|
|
Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay)
|
|
NeedsAuth() bool
|
|
}
|
|
|
|
type withDrawMotionAction struct{}
|
|
|
|
func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
|
|
fmt.Fprintln(w, "Withdraw motion", decision.Tag)
|
|
// TODO: implement
|
|
if r.Method == http.MethodPost {
|
|
if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
|
|
log.Println("could not parse confirm parameter:", err)
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
} else if confirm {
|
|
WithdrawMotion(&decision.Decision, voter)
|
|
} else {
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (withDrawMotionAction) NeedsAuth() bool {
|
|
return true
|
|
}
|
|
|
|
type editMotionAction struct{}
|
|
|
|
func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
|
|
fmt.Fprintln(w, "Edit motion", decision.Tag)
|
|
// TODO: implement
|
|
}
|
|
|
|
func (editMotionAction) NeedsAuth() bool {
|
|
return true
|
|
}
|
|
|
|
func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if err := db.Ping(); err != nil {
|
|
logger.Fatal(err)
|
|
}
|
|
|
|
subURL := r.URL.Path
|
|
|
|
var motionActionMap = map[string]motionActionHandler{
|
|
"withdraw": withDrawMotionAction{},
|
|
"edit": editMotionAction{},
|
|
}
|
|
|
|
switch {
|
|
case subURL == "":
|
|
authenticateRequest(w, r, false, motionListHandler)
|
|
return
|
|
case strings.Count(subURL, "/") == 1:
|
|
parts := strings.Split(subURL, "/")
|
|
logger.Printf("handle %v\n", parts)
|
|
motionTag := parts[0]
|
|
action, ok := motionActionMap[parts[1]]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
authenticateRequest(w, r, action.NeedsAuth(), func(w http.ResponseWriter, r *http.Request, v *Voter) {
|
|
singleDecisionHandler(w, r, v, motionTag, action.Handle)
|
|
})
|
|
|
|
logger.Printf("motion: %s, action: %s\n", motionTag, action)
|
|
return
|
|
case strings.Count(subURL, "/") == 0:
|
|
authenticateRequest(w, r, false, func(w http.ResponseWriter, r *http.Request, v *Voter) {
|
|
singleDecisionHandler(w, r, v, subURL, motionHandler)
|
|
})
|
|
return
|
|
default:
|
|
fmt.Fprintf(w, "No handler for '%s'", subURL)
|
|
return
|
|
}
|
|
}
|
|
|
|
func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) {
|
|
fmt.Fprintln(w,"New motion")
|
|
// TODO: implement
|
|
}
|
|
|
|
type Config struct {
|
|
BoardMailAddress string `yaml:"board_mail_address"`
|
|
NoticeSenderAddress string `yaml:"notice_sender_address"`
|
|
DatabaseFile string `yaml:"database_file"`
|
|
ClientCACertificates string `yaml:"client_ca_certificates"`
|
|
ServerCert string `yaml:"server_certificate"`
|
|
ServerKey string `yaml:"server_key"`
|
|
}
|
|
|
|
func main() {
|
|
logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
|
|
|
|
var filename = "config.yaml"
|
|
if len(os.Args) == 2 {
|
|
filename = os.Args[1]
|
|
}
|
|
|
|
var err error
|
|
|
|
var source []byte
|
|
|
|
source, err = ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
logger.Fatal(err)
|
|
}
|
|
err = yaml.Unmarshal(source, &config)
|
|
if err != nil {
|
|
logger.Fatal(err)
|
|
}
|
|
logger.Printf("read configuration %v", config)
|
|
|
|
db, err = sqlx.Open("sqlite3", config.DatabaseFile)
|
|
if err != nil {
|
|
logger.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
|
|
http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
|
|
authenticateRequest(w, r, true, newMotionHandler)
|
|
})
|
|
http.Handle("/static/", http.FileServer(http.Dir(".")))
|
|
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
|
|
|
|
// load CA certificates for client authentication
|
|
caCert, err := ioutil.ReadFile(config.ClientCACertificates)
|
|
if err != nil {
|
|
logger.Fatal(err)
|
|
}
|
|
caCertPool := x509.NewCertPool()
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
logger.Fatal("could not initialize client CA certificate pool")
|
|
}
|
|
|
|
// setup HTTPS server
|
|
tlsConfig := &tls.Config{
|
|
ClientCAs: caCertPool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
}
|
|
tlsConfig.BuildNameToCertificate()
|
|
|
|
server := &http.Server{
|
|
Addr: ":8443",
|
|
TLSConfig: tlsConfig,
|
|
}
|
|
|
|
logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
|
|
|
|
if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
|
|
logger.Fatal("ListenAndServerTLS: ", err)
|
|
}
|
|
}
|