Start refactoring to packages

This commit is contained in:
Jan Dittberner 2022-05-09 21:09:24 +02:00
parent a5c1a64a3c
commit ec7d2fe324
8 changed files with 350 additions and 5 deletions

125
cmd/boardvoting/config.go Normal file
View file

@ -0,0 +1,125 @@
/*
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.
*/
package main
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"github.com/gorilla/sessions"
"gopkg.in/yaml.v2"
)
const (
cookieSecretMinLen = 32
csrfKeyLength = 32
httpIdleTimeout = 5
httpReadHeaderTimeout = 10
httpReadTimeout = 10
httpWriteTimeout = 60
sessionCookieName = "votesession"
)
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,omitempty"`
HTTPSAddress string `yaml:"https_address,omitempty"`
MailServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"mail_server"`
}
type configKey int
const (
ctxConfig configKey = iota
ctxCookieStore
)
func parseConfig(ctx context.Context, configFile string) (context.Context, error) {
source, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("could not read configuration file %s: %w", configFile, err)
}
config := &Config{
HTTPAddress: "127.0.0.1:8000",
HTTPSAddress: "127.0.0.1:8433",
}
if err := yaml.Unmarshal(source, config); err != nil {
return nil, fmt.Errorf("could not parse configuration: %w", err)
}
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
if err != nil {
return nil, fmt.Errorf("could not decode cookie secret: %w", err)
}
if len(cookieSecret) < cookieSecretMinLen {
return nil, fmt.Errorf("cookie secret is less than the minimum require %d bytes long", cookieSecretMinLen)
}
csrfKey, err := base64.StdEncoding.DecodeString(config.CsrfKey)
if err != nil {
return nil, fmt.Errorf("could not decode CSRF key: %w", err)
}
if len(csrfKey) != csrfKeyLength {
return nil, fmt.Errorf("CSRF key must be exactly %d bytes long but is %d bytes long", csrfKeyLength, len(csrfKey))
}
cookieStore := sessions.NewCookieStore(cookieSecret)
cookieStore.Options.Secure = true
ctx = context.WithValue(ctx, ctxConfig, config)
ctx = context.WithValue(ctx, ctxCookieStore, cookieStore)
return ctx, nil
}
func GetConfig(ctx context.Context) (*Config, error) {
config, ok := ctx.Value(ctxConfig).(*Config)
if !ok {
return nil, errors.New("invalid value type for config in context")
}
return config, nil
}
func GetCookieStore(ctx context.Context) (*sessions.CookieStore, error) {
cookieStore, ok := ctx.Value(ctxConfig).(*sessions.CookieStore)
if !ok {
return nil, errors.New("invalid value type for cookie store in context")
}
return cookieStore, nil
}

View file

@ -0,0 +1,68 @@
/*
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.
*/
package main
import (
"html/template"
"net/http"
)
func (app *application) motions(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/motions/" {
http.NotFound(w, r)
return
}
files := []string{
"./ui/html/base.html",
"./ui/html/partials/nav.html",
"./ui/html/pages/motions.html",
}
ts, err := template.ParseFiles(files...)
if err != nil {
app.errorLog.Print(err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = ts.ExecuteTemplate(w, "base", nil)
if err != nil {
app.errorLog.Print(err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
if r.Method != "GET" && r.Method != "HEAD" {
w.Header().Set("Allow", "GET,HEAD")
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}

View file

@ -18,7 +18,19 @@ limitations under the License.
// The CAcert board voting software.
package main
import log "github.com/sirupsen/logrus"
import (
"context"
"flag"
"io/fs"
"log"
"net/http"
"os"
"github.com/vearutop/statigz"
"github.com/vearutop/statigz/brotli"
"git.cacert.org/cacert-boardvoting/ui"
)
var (
version = "undefined"
@ -26,7 +38,58 @@ var (
date = "undefined"
)
func main() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
log.Infof("CAcert Board Voting version %s, commit %s built at %s", version, commit, date)
type application struct {
errorLog, infoLog *log.Logger
}
func main() {
configFile := flag.String("config", "config.yaml", "Configuration file name")
flag.Parse()
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime)
infoLog.Printf("CAcert Board Voting version %s, commit %s built at %s", version, commit, date)
ctx, err := parseConfig(context.Background(), *configFile)
if err != nil {
errorLog.Fatal(err)
}
mux := http.NewServeMux()
staticDir, _ := fs.Sub(ui.Files, "static")
staticData, ok := staticDir.(fs.ReadDirFS)
if !ok {
errorLog.Fatal("could not use uiStaticDir as fs.ReadDirFS")
}
fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit)
mux.Handle("/static/", http.StripPrefix("/static", fileServer))
app := &application{
errorLog: errorLog,
infoLog: infoLog,
}
mux.HandleFunc("/", app.home)
mux.HandleFunc("/motions/", app.motions)
config, err := GetConfig(ctx)
if err != nil {
errorLog.Fatal(err)
}
srv := &http.Server{
Addr: config.HTTPAddress,
ErrorLog: errorLog,
Handler: mux,
}
infoLog.Printf("Starting server on %s", config.HTTPAddress)
err = srv.ListenAndServe()
errorLog.Fatal(err)
}

View file

@ -330,7 +330,6 @@ func NewDB(database *sql.DB) *DbHandler {
failedStatements = append(failedStatements, sqlStatement)
}
// nolint:sqlclosecheck
_ = stmt.Close()
}

24
ui/efs.go Normal file
View file

@ -0,0 +1,24 @@
/*
Copyright 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.
*/
// Package ui provides files for the CAcert board voting system user interface.
package ui
import "embed"
//go:embed "html" "static"
var Files embed.FS

55
ui/html/base.html Normal file
View file

@ -0,0 +1,55 @@
{{ define "base" }}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ template "title" . }} - CAcert Board Voting System</title>
<link rel="stylesheet" type="text/css" href="/static/semantic.min.css">
<link rel="icon" href="/static/images/favicon.ico">
</head>
<body id="cacert-board-voting">
<header class="pusher">
<div class="ui vertical masthead center aligned segment">
<div class="ui left secondary container">
<img src="/static/images/CAcert-logo-colour.svg" alt="CAcert" height="40rem"/>
</div>
<div class="ui text container">
<h1 class="ui header">
{{ template "title" . }}
{{ if .Voter }}
<span class="ui left pointing label">Authenticated as {{ .Voter.Name }} &lt;{{ .Voter.Reminder }}&gt;
</span>
{{ end }}
</h1>
</div>
</div>
</header>
{{ template "nav" . }}
<main class="ui container">
{{ with .Flashes }}
<div class="basic segment">
<div class="ui info message">
<i class="close icon"></i>
<div class="ui list">
{{ range . }}
<div class="ui item">{{ . }}</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ template "main" . }}
</main>
<footer class="ui container"><span class="ui small text">© 2017-2022 CAcert Inc.</span></footer>
</body>
<script src="/static/jquery.min.js"></script>
<script src="/static/semantic.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$('.message .close').on('click', function () {
$(this).closest('.message').transition('fade');
});
});
</script>
</html>
{{ end }}

View file

@ -0,0 +1,6 @@
{{ define "title" }}Motions{{ end }}
{{ define "main" }}
<h2>All the motions</h2>
<p>Nothing to see yet.</p>
{{ end }}

View file

@ -0,0 +1,5 @@
{{ define "nav" }}
<nav class="ui container">
<a href="/motions/">Motions</a>
</nav>
{{ end }}