Start refactoring to packages

main
Jan Dittberner 2 years ago
parent a5c1a64a3c
commit ec7d2fe324

@ -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
}

@ -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)
}

@ -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"
)
type application struct {
errorLog, infoLog *log.Logger
}
func main() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
log.Infof("CAcert Board Voting version %s, commit %s built at %s", version, commit, date)
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)
}

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

@ -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

@ -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 }}

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

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