Configure golangci-lint and apply suggestions

main
Jan Dittberner 3 years ago
parent 594df29dc1
commit 03827874cf

@ -0,0 +1,63 @@
---
run:
skip-files:
- boardvoting/assets.go
output:
sort-results: true
linter-settings:
goheader:
values:
const:
ORGANIZATION: CAcert Inc.
template: |
Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }}
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.
goimports:
local-prefixes: git.cacert.org/cacert-boardvoting
misspell:
locale: US
ignore-words:
- CAcert
linters:
disable-all: false
enable:
- bodyclose
- errorlint
- gocognit
- goconst
- gocritic
- gofmt
- goheader
- goimports
- golint
- gomnd
- gosec
- interfacer
- lll
- makezero
- misspell
- nakedret
- nestif
- nlreturn
- nolintlint
- predeclared
- rowserrcheck
- scopelint
- sqlclosecheck
- wrapcheck
- wsl

@ -1,18 +1,20 @@
/*
Copyright 2017-2020 Jan Dittberner
Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
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.
*/
// The CAcert board voting software.
package main
import (
@ -51,12 +53,21 @@ var csrfKey []byte
var version = "undefined"
var build = "undefined"
const sessionCookieName = "votesession"
const (
cookieSecretMinLen = 32
csrfKeyLength = 32
httpIdleTimeout = 5
httpReadHeaderTimeout = 10
httpReadTimeout = 10
httpWriteTimeout = 60
sessionCookieName = "votesession"
)
func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
funcMaps := sprig.FuncMap()
funcMaps["nl2br"] = func(text string) template.HTML {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
// #nosec G203 input is sanitized
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
}
funcMaps[csrf.TemplateTag] = func() template.HTML {
return csrf.TemplateField(r)
@ -65,20 +76,24 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string,
var baseTemplate *template.Template
for count, t := range templates {
var err error
var assetBytes []byte
if //noinspection GoUnresolvedReference
assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
var (
err error
assetBytes []byte
)
if assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if count == 0 {
if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if count == 0 {
if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
@ -100,27 +115,35 @@ const (
func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
emailsTried := make(map[string]bool)
for _, cert := range r.TLS.PeerCertificates {
for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageClientAuth {
for _, emailAddress := range cert.EmailAddresses {
emailLower := strings.ToLower(emailAddress)
emailsTried[emailLower] = true
voter, err := FindVoterByAddress(emailLower)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if voter != nil {
requestContext := context.WithValue(r.Context(), ctxVoter, voter)
requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
handler(w, r.WithContext(requestContext))
return
}
if extKeyUsage != x509.ExtKeyUsageClientAuth {
continue
}
for _, emailAddress := range cert.EmailAddresses {
emailLower := strings.ToLower(emailAddress)
emailsTried[emailLower] = true
voter, err := FindVoterByAddress(emailLower)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if voter != nil {
requestContext := context.WithValue(r.Context(), ctxVoter, voter)
requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
handler(w, r.WithContext(requestContext))
return
}
}
}
}
needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
if ok && needsAuth {
var templateContext struct {
@ -129,14 +152,18 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
Flashes interface{}
Emails []string
}
for k := range emailsTried {
templateContext.Emails = append(templateContext.Emails, k)
}
sort.Strings(templateContext.Emails)
w.WriteHeader(http.StatusForbidden)
renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
return
}
handler(w, r)
}
@ -154,6 +181,7 @@ type motionListParameters struct {
func parseMotionParameters(r *http.Request) motionParameters {
var m = motionParameters{}
m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
return m
}
@ -164,20 +192,24 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
} 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"))
}
return m
}
func motionListHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionListParameters(r)
session, err := store.Get(r, sessionCookieName)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -189,30 +221,42 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string
Flashes interface{}
}
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter
}
if flashes := session.Flashes(); len(flashes) > 0 {
templateContext.Flashes = flashes
}
err = session.Save(r, w)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templateContext.Params = &params
if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(
params.Page, params.Flags.Unvoted, templateContext.Voter,
); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if len(templateContext.Decisions) > 0 {
olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(
params.Flags.Unvoted, templateContext.Voter,
)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if olderExists {
templateContext.NextPage = params.Page + 1
}
@ -233,6 +277,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
@ -244,32 +289,53 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string
Flashes interface{}
}
voter, ok := getVoterFromRequest(r)
if ok {
templateContext.Voter = voter
}
templateContext.Params = &params
if params.ShowVotes {
if err := decision.LoadVotes(); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
templateContext.Decision = decision
templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html"}, templateContext)
renderTemplate(w, r, []string{
"motion.html",
"motion_fragments.html",
"page_fragments.html",
"header.html",
"footer.html",
}, templateContext)
}
func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
func singleDecisionHandler(
w http.ResponseWriter,
r *http.Request,
tag string,
handler func(http.ResponseWriter, *http.Request),
) {
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.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
}
@ -286,16 +352,19 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
voter, ok = r.Context().Value(ctxVoter).(*Voter)
return
}
func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
return
}
func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
vote, ok = r.Context().Value(ctxVote).(VoteChoice)
return
}
@ -305,12 +374,16 @@ func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, me
session, err := store.Get(r, sessionCookieName)
if err != nil {
log.Warnf("could not get session cookie: %v", err)
return
}
session.AddFlash(message, tags...)
err = session.Save(r, w)
if err != nil {
log.Warnf("could not save flash message: %v", err)
return
}
}
@ -324,14 +397,25 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
templates := []string{
"withdraw_motion_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct {
PageTitle string
Decision *DecisionForDisplay
@ -343,9 +427,11 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC()
if err := decision.UpdateStatus(); err != nil {
log.Errorf("withdrawing motion failed: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -372,12 +458,14 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
}
templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
var templateContext struct {
Form NewDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
switch r.Method {
case http.MethodPost:
form := NewDecisionForm{
@ -393,10 +481,11 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, templates, templateContext)
} else {
data.Proposed = time.Now().UTC()
data.ProponentId = voter.Id
data.ProponentID = voter.ID
if err := data.Create(); err != nil {
log.Errorf("saving motion failed: %v", err)
http.Error(w, "Saving motion failed", http.StatusInternalServerError)
return
}
@ -426,20 +515,26 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
var templateContext struct {
Form EditDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
switch r.Method {
case http.MethodPost:
form := EditDecisionForm{
@ -459,6 +554,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Update(); err != nil {
log.Errorf("updating motion failed: %v", err)
http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
return
}
@ -468,6 +564,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
return
default:
templateContext.Voter = voter
@ -494,35 +591,43 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case subURL == "":
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
return
case subURL == "/newmotion/":
handler := &newMotionHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
handler.Handle)
return
case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/")
motionTag := parts[0]
action, ok := motionActionMap[parts[1]]
if !ok {
http.NotFound(w, r)
return
}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, action.Handle)
})
return
case strings.Count(subURL, "/") == 0:
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, subURL, motionHandler)
})
return
default:
http.NotFound(w, r)
return
}
}
@ -536,26 +641,33 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
vote, ok := getVoteFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodPost:
voteResult := &Vote{
VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(),
Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError)
return
}
@ -565,7 +677,14 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
default:
templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
templates := []string{
"direct_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct {
Decision *DecisionForDisplay
VoteChoice VoteChoice
@ -573,9 +692,11 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{}
Voter *Voter
}
templateContext.Decision = decision
templateContext.VoteChoice = vote
templateContext.Voter = voter
renderTemplate(w, r, templates, templateContext)
}
}
@ -588,10 +709,12 @@ type proxyVoteHandler struct {
func getPEMClientCert(r *http.Request) string {
clientCertPEM := bytes.NewBufferString("")
authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
if err != nil {
log.Errorf("error encoding client certificate: %v", err)
}
return clientCertPEM.String()
}
@ -599,14 +722,25 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
proxy, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
templates := []string{
"proxy_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct {
Form ProxyVoteForm
Decision *DecisionForDisplay
@ -615,7 +749,9 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{}
Voter *Voter
}
templateContext.Voter = proxy
switch r.Method {
case http.MethodPost:
form := ProxyVoteForm{
@ -627,15 +763,19 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if valid, voter, data, justification := form.Validate(); !valid {
templateContext.Form = form
templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
voters, err := GetVotersForProxy(proxy)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
templateContext.Voters = voters
}
templateContext.Voters = voters
renderTemplate(w, r, templates, templateContext)
} else {
data.DecisionId = decision.Id
data.DecisionID = decision.ID
data.Voted = time.Now().UTC()
data.Notes = fmt.Sprintf(
"Proxy-Vote by %s\n\n%s\n\n%s",
@ -644,6 +784,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Save(); err != nil {
log.Errorf("Error saving vote: %s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -653,16 +794,21 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
return
default:
templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
voters, err := GetVotersForProxy(proxy)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} else {
templateContext.Voters = voters
}
templateContext.Voters = voters
renderTemplate(w, r, templates, templateContext)
}
}
@ -674,24 +820,33 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
case strings.HasPrefix(r.URL.Path, "/proxy/"):
motionTag := r.URL.Path[len("/proxy/"):]
handler := &proxyVoteHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, handler.Handle)
})
case strings.HasPrefix(r.URL.Path, "/vote/"):
const expectedParts = 2
parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
if len(parts) != 2 {
if len(parts) != expectedParts {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
motionTag := parts[0]
voteValue, ok := VoteValues[parts[1]]
if !ok {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
handler := &directVoteHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
@ -699,6 +854,7 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
motionTag, handler.Handle)
})
return
}
}
@ -714,8 +870,8 @@ type Config struct {
CookieSecret string `yaml:"cookie_secret"`
CsrfKey string `yaml:"csrf_key"`
BaseURL string `yaml:"base_url"`
HttpAddress string `yaml:"http_address"`
HttpsAddress string `yaml:"https_address"`
HTTPAddress string `yaml:"http_address"`
HTTPSAddress string `yaml:"https_address"`
MailServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
@ -727,15 +883,17 @@ func readConfig() {
if err != nil {
log.Panicf("Opening configuration file failed: %v", err)
}
if err := yaml.Unmarshal(source, &config); err != nil {
log.Panicf("Loading configuration failed: %v", err)
}
if config.HttpsAddress == "" {
config.HttpsAddress = "127.0.0.1:8443"
if config.HTTPSAddress == "" {
config.HTTPSAddress = "127.0.0.1:8443"
}
if config.HttpAddress == "" {
config.HttpAddress = "127.0.0.1:8080"
if config.HTTPAddress == "" {
config.HTTPAddress = "127.0.0.1:8080"
}
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
@ -743,17 +901,26 @@ func readConfig() {
log.Panicf("Decoding cookie secret failed: %v", err)
panic(err)
}
if len(cookieSecret) < 32 {
log.Panic("Cookie secret is less than 32 bytes long")
if len(cookieSecret) < cookieSecretMinLen {
log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen)
}
csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
if err != nil {
log.Panicf("Decoding csrf key failed: %v", err)
}
if len(csrfKey) != 32 {
log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
if len(csrfKey) != csrfKeyLength {
log.Panicf(
"CSRF key must be exactly %d bytes long but is %d bytes long",
csrfKeyLength,
len(csrfKey),
)
}
store = sessions.NewCookieStore(cookieSecret)
log.Info("Read configuration")
}
@ -762,6 +929,7 @@ func setupDbConfig(ctx context.Context) {
if err != nil {
log.Panicf("Opening database failed: %v", err)
}
db = NewDB(database)
go func() {
@ -812,6 +980,7 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
if err != nil {
log.Panicf("Error reading client certificate CAs %v", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Panic("could not initialize client CA certificate pool")
@ -819,9 +988,11 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
// setup HTTPS server
tlsConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool,
ClientAuth: tls.VerifyClientCertIfGiven,
}
return
}
@ -835,23 +1006,26 @@ func main() {
flag.Parse()
var stopAll func()
executionContext, stopAll := context.WithCancel(context.Background())
readConfig()
setupDbConfig(executionContext)
setupNotifications(executionContext)
setupJobs(executionContext)
setupHandlers()
tlsConfig := setupTLSConfig()
defer stopAll()
server := &http.Server{
Addr: config.HttpsAddress,
Addr: config.HTTPSAddress,
TLSConfig: tlsConfig,
IdleTimeout: time.Second * 120,
ReadHeaderTimeout: time.Second * 10,
ReadTimeout: time.Second * 20,
WriteTimeout: time.Second * 60,
IdleTimeout: time.Second * httpIdleTimeout,
ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
ReadTimeout: time.Second * httpReadTimeout,
WriteTimeout: time.Second * httpWriteTimeout,
}
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
@ -859,24 +1033,27 @@ func main() {
log.Infof("Launching application on https://%s/", server.Addr)
errs := make(chan error, 1)
go func() {
httpRedirector := &http.Server{
Addr: config.HttpAddress,
Addr: config.HTTPAddress,
Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently),
IdleTimeout: time.Second * 5,
ReadHeaderTimeout: time.Second * 10,
ReadTimeout: time.Second * 10,
WriteTimeout: time.Second * 60,
IdleTimeout: time.Second * httpIdleTimeout,
ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
ReadTimeout: time.Second * httpReadTimeout,
WriteTimeout: time.Second * httpWriteTimeout,
}
if err := httpRedirector.ListenAndServe(); err != nil {
errs <- err
}
close(errs)
}()
if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
log.Panicf("ListenAndServerTLS failed: %v", err)
}
if err := <-errs; err != nil {
log.Panicf("ListenAndServe failed: %v", err)
}

@ -1,18 +1,20 @@
/*
Copyright 2017-2019 Jan Dittberner
Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
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.
*/
// Assets for the CAcert board voting software.
package boardvoting
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/...
@ -29,8 +31,11 @@ import (
"time"
)
const defaultFileMode = 0400
var defaultFileTimestamp = time.Now()
// Simulated asset file system.
type AssetFS struct {
Asset func(path string) ([]byte, error)
AssetDir func(path string) ([]string, error)
@ -46,16 +51,19 @@ type FakeFile struct {
func (f *FakeFile) Name() string {
_, name := filepath.Split(f.Path)
return name
}
func (f *FakeFile) Size() int64 { return f.Len }
func (f *FakeFile) Mode() os.FileMode {
mode := os.FileMode(0644)
mode := os.FileMode(defaultFileMode)
if f.Dir {
return mode | os.ModeDir
}
return mode
}
@ -73,6 +81,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
if timestamp.IsZero() {
timestamp = defaultFileTimestamp
}
return &AssetFile{
bytes.NewReader(content),
ioutil.NopCloser(nil),
@ -80,7 +89,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
}
}
func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
func (f *AssetFile) Readdir(int) ([]os.FileInfo, error) {
return nil, errors.New("not a directory")
}
@ -102,24 +111,31 @@ func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
if count <= 0 {
return f.Children, nil
}
if f.ChildrenRead+count > len(f.Children) {
count = len(f.Children) - f.ChildrenRead
}
rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
f.ChildrenRead += count
return rv, nil
}
// Simulate os.Stat for a simulated asset directory.
func (f *AssetDirectory) Stat() (os.FileInfo, error) {
return f, nil
}
// Return a new asset directory.
func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
fileInfos := make([]os.FileInfo, 0, len(children))
for _, child := range children {
_, err := fs.AssetDir(filepath.Join(name, child))
fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}})
}
return &AssetDirectory{
AssetFile{
bytes.NewReader(nil),
@ -130,29 +146,37 @@ func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirect
fileInfos}
}
// Open named simulated file.
func (f *AssetFS) Open(name string) (http.File, error) {
if len(name) > 0 && name[0] == '/' {
name = name[1:]
}
if b, err := f.Asset(name); err == nil {
timestamp := defaultFileTimestamp
if f.AssetInfo != nil {
if info, err := f.AssetInfo(name); err == nil {
timestamp = info.ModTime()
}
}
return NewAssetFile(name, b, timestamp), nil
}
if children, err := f.AssetDir(name); err == nil {
children, err := f.AssetDir(name)
if err == nil {
return NewAssetDirectory(name, children, f), nil
} else {
if strings.Contains(err.Error(), "not found") {
return nil, os.ErrNotExist
}
return nil, err
}
if strings.Contains(err.Error(), "not found") {
return nil, os.ErrNotExist
}
return nil, err
}
// Get the simulated asset filesystem initialized with generated values.
func GetAssetFS() *AssetFS {
return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}
}

@ -1,23 +1,24 @@
/*
Copyright 2017-2019 Jan Dittberner
Copyright 2017-2020 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
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 db
import (
migrate "github.com/rubenv/sql-migrate"
"git.cacert.org/cacert-boardvoting/boardvoting"
"github.com/rubenv/sql-migrate"
)
func Migrations() migrate.MigrationSource {

@ -1,18 +1,19 @@
/*
Copyright 2017-2019 Jan Dittberner
Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
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 (
@ -22,11 +23,24 @@ import (
"time"
)
const (
minimumContentLen = 3
minimumTitleLen = 3
)
const (
hoursInADay = 24
dueThreeDays = 3
dueOneWeek = 7
dueTwoWeeks = 14
dueFourWeeks = 28
)
var validDueDurations = map[string]time.Duration{
"+3 days": time.Hour * 24 * 3,
"+7 days": time.Hour * 24 * 7,
"+14 days": time.Hour * 24 * 14,
"+28 days": time.Hour * 24 * 28,
"+3 days": time.Hour * hoursInADay * dueThreeDays,
"+7 days": time.Hour * hoursInADay * dueOneWeek,
"+14 days": time.Hour * hoursInADay * dueTwoWeeks,
"+28 days": time.Hour * hoursInADay * dueFourWeeks,
}
type NewDecisionForm struct {
@ -43,13 +57,13 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
data := &Decision{}
data.Title = strings.TrimSpace(f.Title)
if len(data.Title) < 3 {
f.Errors["Title"] = "Please enter at least 3 characters for Title."
if len(data.Title) < minimumTitleLen {
f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
}
data.Content = strings.TrimSpace(f.Content)
if len(strings.Fields(data.Content)) < 3 {
f.Errors["Content"] = "Please enter at least 3 words as Text."
if len(strings.Fields(data.Content)) < minimumContentLen {
f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
}
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@ -83,13 +97,13 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
data := f.Decision
data.Title = strings.TrimSpace(f.Title)
if len(data.Title) < 3 {
f.Errors["Title"] = "Please enter at least 3 characters for Title."
if len(data.Title) < minimumTitleLen {
f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
}
data.Content = strings.TrimSpace(f.Content)
if len(strings.Fields(data.Content)) < 3 {
f.Errors["Content"] = "Please enter at least 3 words as Text."
if len(strings.Fields(data.Content)) < minimumContentLen {
f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
}
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@ -118,27 +132,34 @@ type ProxyVoteForm struct {
func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) {
f.Errors = make(map[string]string)
const minimumJustificationLen = 3
var (
voter *Voter
err error
voterID, vote int64
)
data := &Vote{}
var voter *Voter
if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil {
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err)
} else if voter, err = GetVoterById(voterId); err != nil {
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err)
if voterID, err = strconv.ParseInt(f.Voter, 10, 64); err != nil {
f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
} else if voter, err = GetVoterByID(voterID); err != nil {
f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
} else {
data.VoterId = voter.Id
data.VoterID = voter.ID
}
if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil {
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.", err)
if vote, err = strconv.ParseInt(f.Vote, 10, 8); err != nil {
f.Errors["Vote"] = fmt.Sprintf("Please choose a valid vote: %v.", err)
} else if voteChoice, ok := VoteChoices[vote]; !ok {
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.")
f.Errors["Vote"] = "Please choose a valid vote."
} else {
data.Vote = voteChoice
}
justification := strings.TrimSpace(f.Justification)
if len(justification) < 3 {
if len(justification) < minimumJustificationLen {
f.Errors["Justification"] = "Please enter at least 3 characters for justification."
}

@ -4,23 +4,26 @@ go 1.14
require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.20.0+incompatible
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/csrf v1.6.0
github.com/gorilla/sessions v1.2.0
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/google/uuid v1.1.4 // indirect
github.com/gorilla/csrf v1.7.0
github.com/gorilla/sessions v1.2.1
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jmoiron/sqlx v1.2.0
github.com/mattn/go-sqlite3 v1.11.0
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079
github.com/sirupsen/logrus v1.4.2
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/mattn/go-sqlite3 v1.14.6
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
github.com/sirupsen/logrus v1.7.0
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
google.golang.org/appengine v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/gorp.v1 v1.7.2 // indirect
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.4.0
)

369
go.sum

@ -1,52 +1,191 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8=
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=