Files
labca/gui/main.go
2022-04-12 21:26:10 +02:00

2500 lines
69 KiB
Go

package main
import (
"bufio"
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"math"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/biz/templates"
"github.com/dustin/go-humanize"
_ "github.com/go-sql-driver/mysql"
"github.com/google/go-github/github"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/gorilla/websocket"
"github.com/nbutton23/zxcvbn-go"
"github.com/spf13/viper"
"golang.org/x/crypto/bcrypt"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
updateInterval = 24 * time.Hour
)
var (
restartSecret string
sessionStore *sessions.CookieStore
tmpls *templates.Templates
version string
dbConn string
dbType string
isDev bool
updateAvailable bool
updateChecked time.Time
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
)
// User struct for storing the admin user account details.
type User struct {
Name string
Email string
Password string
Confirm string
NewPassword string
RequestBase string
Errors map[string]string
}
// ValidatePassword checks that the password of a User is non-empty, matches the confirmation, is not on the blacklist and is sufficiently complex.
func (reg *User) ValidatePassword(isNew bool, isChange bool) {
blacklist := []string{"labca", "acme", reg.Name}
if x := strings.Index(reg.Email, "@"); x > 0 {
blacklist = append(blacklist, reg.Email[:x])
d := strings.Split(reg.Email[x+1:], ".")
for i := 0; i < len(d)-1; i++ {
blacklist = append(blacklist, d[i])
}
}
if strings.TrimSpace(reg.Password) == "" {
reg.Errors["Password"] = "Please enter a password"
} else if isNew {
strength := zxcvbn.PasswordStrength(reg.Password, blacklist).Score
if strength < 1 {
reg.Errors["Password"] = "Please pick a stronger, more secure password"
}
}
if isNew {
if strings.TrimSpace(reg.Confirm) == "" {
reg.Errors["Confirm"] = "Please enter the password again"
} else if strings.TrimSpace(reg.Confirm) != strings.TrimSpace(reg.Password) {
reg.Errors["Confirm"] = "Passwords do not match!"
}
}
if isChange {
if strings.TrimSpace(reg.NewPassword) != "" {
strength := zxcvbn.PasswordStrength(reg.NewPassword, blacklist).Score
if strength < 1 {
reg.Errors["NewPassword"] = "Please pick a stronger, more secure password"
}
if strings.TrimSpace(reg.Confirm) == "" {
reg.Errors["Confirm"] = "Please enter the new password again"
} else if strings.TrimSpace(reg.Confirm) != strings.TrimSpace(reg.NewPassword) {
reg.Errors["Confirm"] = "New passwords do not match!"
}
}
byteStored := []byte(viper.GetString("user.password"))
err := bcrypt.CompareHashAndPassword(byteStored, []byte(reg.Password))
if err != nil {
reg.Errors["Password"] = "Current password is not correct!"
}
}
}
// Validate that User struct contains at least a Name, has a valid email address and password fields.
func (reg *User) Validate(isNew bool, isChange bool) bool {
reg.Errors = make(map[string]string)
if strings.TrimSpace(reg.Name) == "" {
reg.Errors["Name"] = "Please enter a user name"
}
if isNew || isChange {
re := regexp.MustCompile(".+@.+\\..+")
matched := re.Match([]byte(reg.Email))
if matched == false {
reg.Errors["Email"] = "Please enter a valid email address"
}
}
reg.ValidatePassword(isNew, isChange)
return len(reg.Errors) == 0
}
// SetupConfig stores the basic config settings.
type SetupConfig struct {
Fqdn string
Organization string
DNS string
DomainMode string
LockdownDomains string
WhitelistDomains string
ExtendedTimeout bool
RequestBase string
Errors map[string]string
}
// Validate that SetupConfig contains all required data.
func (cfg *SetupConfig) Validate(orgRequired bool) bool {
cfg.Errors = make(map[string]string)
if strings.TrimSpace(cfg.Fqdn) == "" {
cfg.Errors["Fqdn"] = "Please enter the Fully Qualified Domain name for this host"
}
if strings.TrimSpace(cfg.Organization) == "" && orgRequired {
cfg.Errors["Organization"] = "Please enter the organization name to show on the public pages"
}
if strings.TrimSpace(cfg.DNS) == "" {
cfg.Errors["DNS"] = "Please enter the DNS server to be used for validation"
}
if cfg.DomainMode != "lockdown" && cfg.DomainMode != "whitelist" && cfg.DomainMode != "standard" {
cfg.Errors["DomainMode"] = "Please select the domain mode to use"
}
if cfg.DomainMode == "lockdown" && strings.TrimSpace(cfg.LockdownDomains) == "" {
cfg.Errors["LockdownDomains"] = "Please enter one or more domains that this PKI host is locked down to"
}
if cfg.DomainMode == "lockdown" && strings.HasPrefix(cfg.LockdownDomains, ".") {
cfg.Errors["LockdownDomains"] = "Domain should not start with a dot"
}
if cfg.DomainMode == "whitelist" && strings.TrimSpace(cfg.WhitelistDomains) == "" {
cfg.Errors["WhitelistDomains"] = "Please enter one or more domains that are whitelisted for this PKI host"
}
if cfg.DomainMode == "whitelist" && strings.HasPrefix(cfg.WhitelistDomains, ".") {
cfg.Errors["WhitelistDomains"] = "Domain should not start with a dot"
}
return len(cfg.Errors) == 0
}
func errorHandler(w http.ResponseWriter, r *http.Request, err error, status int) {
log.Printf("errorHandler: err=%v\n", err)
w.WriteHeader(status)
pc := make([]uintptr, 15)
n := runtime.Callers(2, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
//fmt.Printf("%s:%d, %s\n", frame.File, frame.Line, frame.Function)
if frame.Function == "main.render" {
fmt.Fprintf(w, "Could not render requested page")
return
}
if status == http.StatusNotFound {
render(w, r, "error", map[string]interface{}{"Message": "That page does not exist"})
} else {
lines := strings.Split(string(debug.Stack()), "\n")
if len(lines) >= 5 {
lines = append(lines[:0], lines[5:]...)
}
fmt.Print(strings.Join(lines, "\n"))
if viper.GetBool("config.complete") {
render(w, r, "error", map[string]interface{}{"Message": "Some unexpected error occurred!"})
} else {
// ONLY in the setup phase to prevent leaking too much details to users
var FileErrors []interface{}
data := getLog(w, r, "cert")
if data != "" {
FileErrors = append(FileErrors, map[string]interface{}{"FileName": "/home/labca/nginx_data/ssl/acme_tiny.log", "Content": data})
}
data = getLog(w, r, "commander")
if data != "" {
FileErrors = append(FileErrors, map[string]interface{}{"FileName": "/home/labca/logs/commander.log", "Content": data})
}
data = getLog(w, r, "labca-notail")
if data != "" {
FileErrors = append(FileErrors, map[string]interface{}{"FileName": "docker-compose logs labca", "Content": data})
}
data = getLog(w, r, "boulder-notail")
if data != "" {
FileErrors = append(FileErrors, map[string]interface{}{"FileName": "docker-compose logs boulder", "Content": data})
}
data = getLog(w, r, "labca-err")
if data != "" {
FileErrors = append(FileErrors, map[string]interface{}{"FileName": "/var/log/labca.err", "Content": data})
}
render(w, r, "error", map[string]interface{}{"Message": "Some unexpected error occurred!", "FileErrors": FileErrors})
}
}
}
func checkUpdates(forced bool) ([]string, []string) {
var versions []string
var descriptions []string
if forced || updateChecked.Add(updateInterval).Before(time.Now()) {
latest := ""
newer := true
client := github.NewClient(nil)
if releases, _, err := client.Repositories.ListReleases(context.Background(), "hakwerk", "labca", nil); err == nil {
for i := 0; i < len(releases); i++ {
release := releases[i]
if !*release.Draft {
if !*release.Prerelease || isDev {
if latest == "" {
latest = *release.Name
}
if *release.Name == version {
newer = false
}
if latest == *release.Name && strings.HasPrefix(version, *release.Name+"-") { // git describe format
newer = false
latest = version
}
if newer {
versions = append(versions, *release.Name)
descriptions = append(descriptions, *release.Body)
}
}
}
}
updateChecked = time.Now()
updateAvailable = (len(releases) > 0) && (latest != version)
}
}
return versions, descriptions
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
dashboardData, err := CollectDashboardData(w, r)
if err == nil {
checkUpdates(false)
dashboardData["UpdateAvailable"] = updateAvailable
dashboardData["UpdateChecked"] = strings.Replace(updateChecked.Format("02-Jan-2006 15:04:05 MST"), "+0000", "GMT", -1)
dashboardData["UpdateCheckedRel"] = humanize.RelTime(updateChecked, time.Now(), "", "")
render(w, r, "dashboard", dashboardData)
}
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
render(w, r, "about", map[string]interface{}{
"Title": "About",
})
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if viper.Get("user.password") == nil {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
session, _ := sessionStore.Get(r, "labca")
var bounceURL string
if session.Values["bounce"] == nil {
bounceURL = "/"
} else {
bounceURL = session.Values["bounce"].(string)
}
if session.Values["user"] != nil {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+bounceURL, http.StatusFound)
return
}
if r.Method == "GET" {
reg := &User{
RequestBase: r.Header.Get("X-Request-Base"),
}
render(w, r, "login", map[string]interface{}{"User": reg, "IsLogin": true})
return
} else if r.Method == "POST" {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
reg := &User{
Name: r.Form.Get("username"),
Password: r.Form.Get("password"),
RequestBase: r.Header.Get("X-Request-Base"),
}
if !reg.Validate(false, false) {
render(w, r, "login", map[string]interface{}{"User": reg, "IsLogin": true})
return
}
if viper.GetString("user.name") != reg.Name {
reg.Errors["Name"] = "Incorrect username or password"
render(w, r, "login", map[string]interface{}{"User": reg, "IsLogin": true})
return
}
byteStored := []byte(viper.GetString("user.password"))
err := bcrypt.CompareHashAndPassword(byteStored, []byte(reg.Password))
if err != nil {
log.Println(err)
reg.Errors["Name"] = "Incorrect username or password"
render(w, r, "login", map[string]interface{}{"User": reg, "IsLogin": true})
return
}
session.Values["user"] = reg.Name
if err = session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
http.Redirect(w, r, r.Header.Get("X-Request-Base")+bounceURL, http.StatusFound)
} else {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/login", http.StatusSeeOther)
return
}
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessionStore.Get(r, "labca")
session.Options.MaxAge = -1
if err := session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/", http.StatusFound)
}
func _sendCmdOutput(w http.ResponseWriter, r *http.Request, cmd string) {
parts := strings.Fields(cmd)
for i := 0; i < len(parts); i++ {
parts[i] = strings.Replace(parts[i], "\\\\", " ", -1)
}
head := parts[0]
parts = parts[1:]
out, err := exec.Command(head, parts...).Output()
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
buf := bytes.NewBuffer(out)
_, err = buf.WriteTo(w)
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
}
func _backupHandler(w http.ResponseWriter, r *http.Request) {
res := struct {
Success bool
Message string
}{Success: true}
action := r.Form.Get("action")
if action == "backup-restore" {
backup := r.Form.Get("backup")
if !_hostCommand(w, r, action, backup) {
res.Success = false
res.Message = "Command failed - see LabCA log for any details"
}
defer _hostCommand(w, r, "server-restart")
} else if action == "backup-delete" {
backup := r.Form.Get("backup")
if !_hostCommand(w, r, action, backup) {
res.Success = false
res.Message = "Command failed - see LabCA log for any details"
}
} else if action == "backup-now" {
res.Message = getLog(w, r, "server-backup")
if res.Message == "" {
res.Success = false
res.Message = "Command failed - see LabCA log for any details"
} else {
res.Message = filepath.Base(res.Message)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _accountUpdateHandler(w http.ResponseWriter, r *http.Request) {
reg := &User{
Name: r.Form.Get("username"),
Email: r.Form.Get("email"),
NewPassword: r.Form.Get("new-password"),
Confirm: r.Form.Get("confirm"),
Password: r.Form.Get("password"),
}
res := struct {
Success bool
Errors map[string]string
}{Success: true}
if reg.Validate(false, true) {
viper.Set("user.name", reg.Name)
viper.Set("user.email", reg.Email)
if reg.NewPassword != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(reg.NewPassword), bcrypt.MinCost)
if err != nil {
res.Success = false
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
viper.Set("user.password", string(hash))
// Forget current session, so user has to login with the new password
session, _ := sessionStore.Get(r, "labca")
session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
}
viper.WriteConfig()
} else {
res.Success = false
res.Errors = reg.Errors
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _configUpdateHandler(w http.ResponseWriter, r *http.Request) {
cfg := &SetupConfig{
Fqdn: r.Form.Get("fqdn"),
Organization: r.Form.Get("organization"),
DNS: r.Form.Get("dns"),
DomainMode: r.Form.Get("domain_mode"),
LockdownDomains: r.Form.Get("lockdown_domains"),
WhitelistDomains: r.Form.Get("whitelist_domains"),
ExtendedTimeout: (r.Form.Get("extended_timeout") == "true"),
}
res := struct {
Success bool
Errors map[string]string
}{Success: true}
if cfg.Validate(true) {
delta := false
if cfg.Fqdn != viper.GetString("labca.fqdn") {
delta = true
viper.Set("labca.fqdn", cfg.Fqdn)
}
if cfg.Organization != viper.GetString("labca.organization") {
delta = true
viper.Set("labca.organization", cfg.Organization)
}
matched, err := regexp.MatchString(":\\d+$", cfg.DNS)
if err == nil && !matched {
cfg.DNS += ":53"
}
if cfg.DNS != viper.GetString("labca.dns") {
delta = true
viper.Set("labca.dns", cfg.DNS)
}
domainMode := cfg.DomainMode
if domainMode != viper.GetString("labca.domain_mode") {
delta = true
viper.Set("labca.domain_mode", cfg.DomainMode)
}
if domainMode == "lockdown" {
if cfg.LockdownDomains != viper.GetString("labca.lockdown") {
delta = true
viper.Set("labca.lockdown", cfg.LockdownDomains)
}
}
if domainMode == "whitelist" {
if cfg.WhitelistDomains != viper.GetString("labca.whitelist") {
delta = true
viper.Set("labca.whitelist", cfg.WhitelistDomains)
}
}
extendedTimeout := cfg.ExtendedTimeout
if extendedTimeout != viper.GetBool("labca.extended_timeout") {
delta = true
viper.Set("labca.extended_timeout", cfg.ExtendedTimeout)
}
if delta {
viper.WriteConfig()
err := _applyConfig()
if err != nil {
res.Success = false
res.Errors = cfg.Errors
res.Errors["ConfigUpdate"] = "Config apply error: '" + err.Error() + "'"
}
} else {
res.Success = false
res.Errors = cfg.Errors
res.Errors["ConfigUpdate"] = "Nothing changed!"
}
} else {
res.Success = false
res.Errors = cfg.Errors
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
// EmailConfig stores configuration used for sending out emails
type EmailConfig struct {
DoEmail bool
Server string
Port string
EmailUser string
EmailPwd []byte
From string
Errors map[string]string
}
// Validate that the email config is valid and complete
func (cfg *EmailConfig) Validate() bool {
cfg.Errors = make(map[string]string)
result, err := _encrypt(cfg.EmailPwd)
if err == nil {
cfg.EmailPwd = []byte(result)
} else {
cfg.Errors["EmailPwd"] = "Could not encrypt this password: " + err.Error()
}
if !cfg.DoEmail {
return len(cfg.Errors) == 0
}
if strings.TrimSpace(cfg.Server) == "" {
cfg.Errors["Server"] = "Please enter the email server address"
}
if strings.TrimSpace(cfg.Port) == "" {
cfg.Errors["Port"] = "Please enter the email server port number"
}
p, err := strconv.Atoi(cfg.Port)
if err != nil {
cfg.Errors["Port"] = "Port number must be numeric"
} else if p <= 0 {
cfg.Errors["Port"] = "Port number must be positive"
} else if p > 65535 {
cfg.Errors["Port"] = "Port number too large"
}
if strings.TrimSpace(cfg.EmailUser) == "" {
cfg.Errors["EmailUser"] = "Please enter the username for authorization to the email server"
}
res, err := _decrypt(string(cfg.EmailPwd))
if err != nil {
cfg.Errors["EmailPwd"] = "Could not decrypt this password: " + err.Error()
}
if strings.TrimSpace(string(res)) == "" {
cfg.Errors["EmailPwd"] = "Please enter the password for authorization to the email server"
}
if strings.TrimSpace(cfg.From) == "" {
cfg.Errors["From"] = "Please enter the from email address"
}
return len(cfg.Errors) == 0
}
func _emailUpdateHandler(w http.ResponseWriter, r *http.Request) {
cfg := &EmailConfig{
DoEmail: (r.Form.Get("do_email") == "true"),
Server: r.Form.Get("server"),
Port: r.Form.Get("port"),
EmailUser: r.Form.Get("email_user"),
EmailPwd: []byte(r.Form.Get("email_pwd")),
From: r.Form.Get("from"),
}
res := struct {
Success bool
Errors map[string]string
}{Success: true}
if cfg.Validate() {
delta := false
if cfg.DoEmail != viper.GetBool("labca.email.enable") {
delta = true
viper.Set("labca.email.enable", cfg.DoEmail)
}
if cfg.Server != viper.GetString("labca.email.server") {
delta = true
viper.Set("labca.email.server", cfg.Server)
}
if cfg.Port != viper.GetString("labca.email.port") {
delta = true
viper.Set("labca.email.port", cfg.Port)
}
if cfg.EmailUser != viper.GetString("labca.email.user") {
delta = true
viper.Set("labca.email.user", cfg.EmailUser)
}
res1, err1 := _decrypt(string(cfg.EmailPwd))
if err1 != nil && cfg.DoEmail {
log.Println("WARNING: could not decrypt given password: " + err1.Error())
}
res2, err2 := _decrypt(viper.GetString("labca.email.pass"))
if err2 != nil && cfg.DoEmail && viper.GetString("labca.email.pass") != "" {
log.Println("WARNING: could not decrypt stored password: " + err2.Error())
}
if string(res1) != string(res2) {
delta = true
viper.Set("labca.email.pass", string(cfg.EmailPwd))
}
if cfg.From != viper.GetString("labca.email.from") {
delta = true
viper.Set("labca.email.from", cfg.From)
}
if delta {
viper.WriteConfig()
err := _applyConfig()
if err != nil {
res.Success = false
res.Errors = cfg.Errors
res.Errors["EmailUpdate"] = "Config apply error: '" + err.Error() + "'"
}
} else {
res.Success = false
res.Errors = cfg.Errors
res.Errors["EmailUpdate"] = "Nothing changed!"
}
} else {
res.Success = false
res.Errors = cfg.Errors
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _emailSendHandler(w http.ResponseWriter, r *http.Request) {
res := struct {
Success bool
Errors map[string]string
}{Success: true, Errors: make(map[string]string)}
recipient := viper.GetString("user.email")
if !_hostCommand(w, r, "test-email", recipient) {
res.Success = false
res.Errors["EmailSend"] = "Failed to send email - see logs"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _exportHandler(w http.ResponseWriter, r *http.Request) {
basename := "certificates"
if r.Form.Get("root") != "true" {
basename = "issuer"
}
if r.Form.Get("issuer") != "true" {
basename = "root"
}
if r.Form.Get("type") == "pfx" {
w.Header().Set("Content-Type", "application/x-pkcs12")
w.Header().Set("Content-Disposition", "attachment; filename=labca_"+basename+".pfx")
var certBase string
if basename == "root" {
certBase = "data/root-ca"
} else {
certBase = "data/issuer/ca-int"
}
cmd := "openssl pkcs12 -export -inkey " + certBase + ".key -in " + certBase + ".pem -passout pass:" + r.Form.Get("export-pwd")
_sendCmdOutput(w, r, cmd)
}
if r.Form.Get("type") == "zip" {
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=labca_"+basename+".zip")
cmd := "zip -j -P " + r.Form.Get("export-pwd") + " - "
var certBase string
if r.Form.Get("root") == "true" {
certBase = "data/root-ca"
cmd = cmd + certBase + ".key " + certBase + ".pem "
}
if r.Form.Get("issuer") == "true" {
certBase = "data/issuer/ca-int"
cmd = cmd + certBase + ".key " + certBase + ".pem "
}
_sendCmdOutput(w, r, cmd)
}
}
func _doCmdOutput(w http.ResponseWriter, r *http.Request, cmd string) string {
parts := strings.Fields(cmd)
for i := 0; i < len(parts); i++ {
parts[i] = strings.Replace(parts[i], "\\\\", " ", -1)
}
head := parts[0]
parts = parts[1:]
out, err := exec.Command(head, parts...).Output()
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return ""
}
return string(out)
}
func _encrypt(plaintext []byte) (string, error) {
key := []byte(viper.GetString("keys.enc"))
block, err := aes.NewCipher(key[:32])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, plaintext, nil)), nil
}
func _decrypt(ciphertext string) ([]byte, error) {
key := []byte(viper.GetString("keys.enc"))
block, err := aes.NewCipher(key[:32])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ct, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}
if len(ct) < gcm.NonceSize() {
return nil, errors.New("malformed ciphertext")
}
return gcm.Open(nil, ct[:gcm.NonceSize()], ct[gcm.NonceSize():], nil)
}
// Result contains data on managed processes
type Result struct {
Success bool
Message string
Timestamp string
TimestampRel string
Class string
}
// ManageComponents sets the additional data to be displayed on the page for the LabCA components
func (res *Result) ManageComponents(w http.ResponseWriter, r *http.Request, action string) {
components := _parseComponents(getLog(w, r, "components"))
for i := 0; i < len(components); i++ {
if (components[i].Name == "NGINX Webserver" && (action == "nginx-reload" || action == "nginx-restart")) ||
(components[i].Name == "Host Service" && action == "svc-restart") ||
(components[i].Name == "Boulder (ACME)" && (action == "boulder-start" || action == "boulder-stop" || action == "boulder-restart")) ||
(components[i].Name == "LabCA Application" && action == "labca-restart") {
res.Timestamp = components[i].Timestamp
res.TimestampRel = components[i].TimestampRel
res.Class = components[i].Class
break
}
}
}
func _checkUpdatesHandler(w http.ResponseWriter, r *http.Request) {
res := struct {
Success bool
UpdateAvailable bool
UpdateChecked string
UpdateCheckedRel string
Versions []string
Descriptions []string
Errors map[string]string
}{Success: true, Errors: make(map[string]string)}
res.Versions, res.Descriptions = checkUpdates(true)
res.UpdateAvailable = updateAvailable
res.UpdateChecked = updateChecked.Format("02-Jan-2006 15:04:05 MST")
res.UpdateChecked = strings.Replace(res.UpdateChecked, "+0000", "GMT", -1)
res.UpdateCheckedRel = humanize.RelTime(updateChecked, time.Now(), "", "")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string) bool {
if action == "backup-restore" || action == "backup-delete" || action == "backup-now" {
_backupHandler(w, r)
return true
}
if action == "cert-export" {
_exportHandler(w, r)
return true
}
if action == "update-account" {
_accountUpdateHandler(w, r)
return true
}
if action == "update-config" {
_configUpdateHandler(w, r)
return true
}
if action == "update-email" {
_emailUpdateHandler(w, r)
return true
}
if action == "send-email" {
_emailSendHandler(w, r)
return true
}
if action == "version-check" {
_checkUpdatesHandler(w, r)
return true
}
return false
}
func _managePost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
action := r.Form.Get("action")
actionKnown := false
for _, a := range []string{
"backup-restore",
"backup-delete",
"backup-now",
"cert-export",
"nginx-reload",
"nginx-restart",
"svc-restart",
"boulder-start",
"boulder-stop",
"boulder-restart",
"labca-restart",
"server-restart",
"server-shutdown",
"update-account",
"update-config",
"update-email",
"send-email",
"version-check",
"version-update",
} {
if a == action {
actionKnown = true
}
}
if !actionKnown {
errorHandler(w, r, fmt.Errorf("unknown manage action '%s'", action), http.StatusBadRequest)
return
}
if _managePostDispatch(w, r, action) {
return
}
res := &Result{Success: true}
if !_hostCommand(w, r, action) {
res.Success = false
res.Message = "Command failed - see LabCA log for any details"
}
if action != "server-restart" && action != "server-shutdown" && action != "version-update" {
res.ManageComponents(w, r, action)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _manageGet(w http.ResponseWriter, r *http.Request) {
manageData := make(map[string]interface{})
manageData["RequestBase"] = r.Header.Get("X-Request-Base")
checkUpdates(false)
manageData["UpdateAvailable"] = updateAvailable
manageData["UpdateChecked"] = strings.Replace(updateChecked.Format("02-Jan-2006 15:04:05 MST"), "+0000", "GMT", -1)
manageData["UpdateCheckedRel"] = humanize.RelTime(updateChecked, time.Now(), "", "")
components := _parseComponents(getLog(w, r, "components"))
for i := 0; i < len(components); i++ {
if components[i].Name == "NGINX Webserver" {
components[i].LogURL = r.Header.Get("X-Request-Base") + "/logs/web"
components[i].LogTitle = "Web Error Log"
btn := make(map[string]interface{})
btn["Class"] = "btn-info"
btn["Id"] = "nginx-reload"
btn["Title"] = "Reload web server configuration with minimal impact to the users"
btn["Label"] = "Reload"
components[i].Buttons = append(components[i].Buttons, btn)
btn = make(map[string]interface{})
btn["Class"] = "btn-warning"
btn["Id"] = "nginx-restart"
btn["Title"] = "Restart the web server with some downtime for the users"
btn["Label"] = "Restart"
components[i].Buttons = append(components[i].Buttons, btn)
}
if components[i].Name == "Host Service" {
components[i].LogURL = ""
components[i].LogTitle = ""
btn := make(map[string]interface{})
btn["Class"] = "btn-warning"
btn["Id"] = "svc-restart"
btn["Title"] = "Restart the host service"
btn["Label"] = "Restart"
components[i].Buttons = append(components[i].Buttons, btn)
}
if components[i].Name == "Boulder (ACME)" {
components[i].LogURL = r.Header.Get("X-Request-Base") + "/logs/boulder"
components[i].LogTitle = "ACME Log"
btn := make(map[string]interface{})
cls := "btn-success"
if components[i].TimestampRel != "stopped" {
cls = cls + " hidden"
}
btn["Class"] = cls
btn["Id"] = "boulder-start"
btn["Title"] = "Start the core ACME application"
btn["Label"] = "Start"
components[i].Buttons = append(components[i].Buttons, btn)
btn = make(map[string]interface{})
cls = "btn-warning"
if components[i].TimestampRel == "stopped" {
cls = cls + " hidden"
}
btn["Class"] = cls
btn["Id"] = "boulder-restart"
btn["Title"] = "Stop and restart the core ACME application"
btn["Label"] = "Restart"
components[i].Buttons = append(components[i].Buttons, btn)
btn = make(map[string]interface{})
cls = "btn-danger"
if components[i].TimestampRel == "stopped" {
cls = cls + " hidden"
}
btn["Class"] = cls
btn["Id"] = "boulder-stop"
btn["Title"] = "Stop the core ACME application; users can no longer use ACME clients to interact with this instance"
btn["Label"] = "Stop"
components[i].Buttons = append(components[i].Buttons, btn)
}
if components[i].Name == "LabCA Application" {
components[i].LogURL = r.Header.Get("X-Request-Base") + "/logs/labca"
components[i].LogTitle = "LabCA Log"
btn := make(map[string]interface{})
btn["Class"] = "btn-warning"
btn["Id"] = "labca-restart"
btn["Title"] = "Stop and restart this LabCA admin application"
btn["Label"] = "Restart"
components[i].Buttons = append(components[i].Buttons, btn)
}
}
manageData["Components"] = components
stats := _parseStats(getLog(w, r, "stats"))
for _, stat := range stats {
if stat.Name == "System Uptime" {
manageData["ServerTimestamp"] = stat.Hint
manageData["ServerTimestampRel"] = stat.Value
break
}
}
backupFiles := strings.Split(getLog(w, r, "backups"), "\n")
backupFiles = backupFiles[:len(backupFiles)-1]
manageData["BackupFiles"] = backupFiles
manageData["RootDetails"] = _doCmdOutput(w, r, "openssl x509 -noout -text -nameopt utf8 -in data/root-ca.pem")
manageData["IssuerDetails"] = _doCmdOutput(w, r, "openssl x509 -noout -text -nameopt utf8 -in data/issuer/ca-int.pem")
manageData["Fqdn"] = viper.GetString("labca.fqdn")
manageData["Organization"] = viper.GetString("labca.organization")
manageData["DNS"] = viper.GetString("labca.dns")
domainMode := viper.GetString("labca.domain_mode")
manageData["DomainMode"] = domainMode
if domainMode == "lockdown" {
manageData["LockdownDomains"] = viper.GetString("labca.lockdown")
}
if domainMode == "whitelist" {
manageData["WhitelistDomains"] = viper.GetString("labca.whitelist")
}
manageData["ExtendedTimeout"] = viper.GetBool("labca.extended_timeout")
manageData["DoEmail"] = viper.GetBool("labca.email.enable")
manageData["Server"] = viper.GetString("labca.email.server")
manageData["Port"] = viper.GetInt("labca.email.port")
manageData["EmailUser"] = viper.GetString("labca.email.user")
manageData["EmailPwd"] = ""
if viper.Get("labca.email.pass") != nil {
pwd := viper.GetString("labca.email.pass")
result, err := _decrypt(pwd)
if err == nil {
manageData["EmailPwd"] = string(result)
} else {
log.Printf("WARNING: could not decrypt email password: %s!\n", err.Error())
}
}
manageData["From"] = viper.GetString("labca.email.from")
manageData["Name"] = viper.GetString("user.name")
manageData["Email"] = viper.GetString("user.email")
render(w, r, "manage", manageData)
}
func manageHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
if r.Method == "POST" {
_managePost(w, r)
} else {
_manageGet(w, r)
}
}
func logsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
logType := vars["type"]
proto := "ws"
if r.Header.Get("X-Forwarded-Proto") == "https" {
proto = "wss"
}
wsurl := proto + "://" + r.Host + r.Header.Get("X-Request-Base") + "/ws?logType=" + logType
var name string
var message string
var data string
switch logType {
case "cert":
name = "Web Certificate Log"
message = "Log file for the certificate renewal for this server."
wsurl = ""
data = getLog(w, r, logType)
case "boulder":
name = "ACME Backend Log"
message = "Live view on the backend ACME application (Boulder) logs."
case "audit":
name = "ACME Audit Log"
message = "Live view on only the audit messages in the backend ACME application (Boulder) logs."
case "labca":
name = "LabCA Log"
message = "Live view on the logs for this LabCA web application."
case "web":
name = "Web Access Log"
message = "Live view on the NGINX web server access log."
default:
errorHandler(w, r, fmt.Errorf("unknown log type '%s'", logType), http.StatusBadRequest)
return
}
render(w, r, "logs", map[string]interface{}{
"Name": name,
"Message": message,
"Data": data,
"WsUrl": wsurl,
})
}
func getLog(w http.ResponseWriter, r *http.Request, logType string) string {
ip, err := _discoverGateway()
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return ""
}
conn, err := net.Dial("tcp", ip.String()+":3030")
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return ""
}
defer conn.Close()
fmt.Fprintf(conn, "log-"+logType+"\n")
reader := bufio.NewReader(conn)
contents, err := ioutil.ReadAll(reader)
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return ""
}
return string(contents)
}
func wsErrorHandler(err error) {
log.Printf("wsErrorHandler: %v", err)
pc := make([]uintptr, 15)
n := runtime.Callers(2, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
fmt.Printf("%s:%d, %s\n", frame.File, frame.Line, frame.Function)
debug.PrintStack()
}
func showLog(ws *websocket.Conn, logType string) {
ip, err := _discoverGateway()
if err != nil {
wsErrorHandler(err)
return
}
conn, err := net.Dial("tcp", ip.String()+":3030")
if err != nil {
wsErrorHandler(err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "log-"+logType+"\n")
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
msg := scanner.Text()
if logType != "audit" || strings.Contains(msg, "[AUDIT]") {
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
// Probably "websocket: close sent"
return
}
}
}
if err := scanner.Err(); err != nil {
wsErrorHandler(err)
return
}
}
func reader(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, _, err := ws.ReadMessage()
if err != nil {
break
}
}
}
func writer(ws *websocket.Conn, logType string) {
pingTicker := time.NewTicker(pingPeriod)
defer func() {
pingTicker.Stop()
ws.Close()
}()
go showLog(ws, logType)
for {
select {
case <-pingTicker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
// Probably "websocket: close sent"
return
}
}
}
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
logType := r.FormValue("logType")
switch logType {
case "boulder":
case "audit":
case "labca":
case "web":
default:
errorHandler(w, r, fmt.Errorf("unknown log type '%s'", logType), http.StatusBadRequest)
return
}
go writer(ws, logType)
reader(ws)
}
func _buildCI(r *http.Request, session *sessions.Session, isRoot bool) *CertificateInfo {
ci := &CertificateInfo{
IsRoot: isRoot,
CreateType: "generate",
CommonName: "Root CA",
RequestBase: r.Header.Get("X-Request-Base"),
}
if !isRoot {
ci.CommonName = "CA"
}
ci.Initialize()
if session.Values["ct"] != nil {
ci.CreateType = session.Values["ct"].(string)
}
if session.Values["kt"] != nil {
ci.KeyType = session.Values["kt"].(string)
}
if session.Values["c"] != nil {
ci.Country = session.Values["c"].(string)
}
if session.Values["o"] != nil {
ci.Organization = session.Values["o"].(string)
}
if session.Values["ou"] != nil {
ci.OrgUnit = session.Values["ou"].(string)
}
if session.Values["cn"] != nil {
ci.CommonName = session.Values["cn"].(string)
ci.CommonName = strings.Replace(ci.CommonName, "Root", "", -1)
ci.CommonName = strings.Replace(ci.CommonName, " ", " ", -1)
}
return ci
}
func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot bool) bool {
path := "data/"
if !isRoot {
path = path + "issuer/"
}
if _, err := os.Stat(path + certBase + ".pem"); os.IsNotExist(err) {
session, _ := sessionStore.Get(r, "labca")
if r.Method == "GET" {
ci := _buildCI(r, session, isRoot)
render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
return false
} else if r.Method == "POST" {
if err := r.ParseMultipartForm(2 * 1024 * 1024); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
ci := &CertificateInfo{
IsRoot: r.Form.Get("cert") == "root",
}
ci.Initialize()
ci.IsRoot = r.Form.Get("cert") == "root"
ci.CreateType = r.Form.Get("createtype")
if r.Form.Get("keytype") != "" {
ci.KeyType = r.Form.Get("keytype")
}
ci.Country = r.Form.Get("c")
ci.Organization = r.Form.Get("o")
ci.OrgUnit = r.Form.Get("ou")
ci.CommonName = r.Form.Get("cn")
if ci.CreateType == "import" {
file, handler, err := r.FormFile("import")
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
defer file.Close()
ci.ImportFile = file
ci.ImportHandler = handler
ci.ImportPwd = r.Form.Get("import-pwd")
}
ci.Key = r.Form.Get("key")
ci.Passphrase = r.Form.Get("passphrase")
ci.Certificate = r.Form.Get("certificate")
ci.RequestBase = r.Header.Get("X-Request-Base")
if !ci.Validate() {
render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
return false
}
if err := ci.Create(path, certBase); err != nil {
ci.Errors[strings.Title(ci.CreateType)] = err.Error()
log.Printf("_certCreate: create failed: %v", err)
render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
return false
}
if viper.Get("labca.organization") == nil {
viper.Set("labca.organization", ci.Organization)
viper.WriteConfig()
}
session.Values["ct"] = ci.CreateType
session.Values["kt"] = ci.KeyType
session.Values["c"] = ci.Country
session.Values["o"] = ci.Organization
session.Values["ou"] = ci.OrgUnit
session.Values["cn"] = ci.CommonName
if err = session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
// Fake the method to GET as we need to continue in the setupHandler() function
r.Method = "GET"
} else {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusSeeOther)
return false
}
}
return true
}
func _parseLinuxIPRouteShow(output []byte) (net.IP, error) {
// Linux '/usr/bin/ip route show' format looks like this:
// default via 192.168.178.1 dev wlp3s0 metric 303
// 192.168.178.0/24 dev wlp3s0 proto kernel scope link src 192.168.178.76 metric 303
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 3 && fields[0] == "default" {
ip := net.ParseIP(fields[2])
if ip != nil {
return ip, nil
}
}
}
return nil, errors.New("no gateway found")
}
func _discoverGateway() (net.IP, error) {
if isDev {
ip := net.ParseIP("127.0.0.1")
if ip != nil {
return ip, nil
}
}
routeCmd := exec.Command("ip", "route", "show")
output, err := routeCmd.CombinedOutput()
if err != nil {
return nil, err
}
return _parseLinuxIPRouteShow(output)
}
func _hostCommand(w http.ResponseWriter, r *http.Request, command string, params ...string) bool {
ip, err := _discoverGateway()
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
conn, err := net.Dial("tcp", ip.String()+":3030")
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
defer conn.Close()
fmt.Fprintf(conn, command+"\n")
for _, param := range params {
fmt.Fprintf(conn, param+"\n")
}
reader := bufio.NewReader(conn)
message, err := ioutil.ReadAll(reader)
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
if strings.Compare(string(message), "ok\n") == 0 {
return true
}
if len(message) >= 4 {
tail := message[len(message)-4:]
if strings.Compare(string(tail), "\nok\n") == 0 {
msg := message[0 : len(message)-4]
log.Printf("Message from server: '%s'", msg)
return true
}
}
log.Printf("ERROR: Message from server: '%s'", message)
errorHandler(w, r, errors.New(string(message)), http.StatusInternalServerError)
return false
}
func randToken() string {
b := make([]byte, 8)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
func _applyConfig() error {
os.Setenv("PKI_ROOT_CERT_BASE", "data/root-ca")
os.Setenv("PKI_INT_CERT_BASE", "data/issuer/ca-int")
os.Setenv("PKI_DEFAULT_O", viper.GetString("labca.organization"))
os.Setenv("PKI_DNS", viper.GetString("labca.dns"))
domain := viper.GetString("labca.fqdn")
os.Setenv("PKI_FQDN", domain)
pos := strings.Index(domain, ".")
if pos > -1 {
pos = pos + 1
domain = domain[pos:]
}
os.Setenv("PKI_DOMAIN", domain)
os.Setenv("PKI_DOMAIN_MODE", viper.GetString("labca.domain_mode"))
os.Setenv("PKI_LOCKDOWN_DOMAINS", viper.GetString("labca.lockdown"))
os.Setenv("PKI_WHITELIST_DOMAINS", viper.GetString("labca.whitelist"))
if viper.GetBool("labca.extended_timeout") {
os.Setenv("PKI_EXTENDED_TIMEOUT", "1")
} else {
os.Setenv("PKI_EXTENDED_TIMEOUT", "0")
}
if viper.GetBool("labca.email.enable") {
os.Setenv("PKI_EMAIL_SERVER", viper.GetString("labca.email.server"))
os.Setenv("PKI_EMAIL_PORT", viper.GetString("labca.email.port"))
os.Setenv("PKI_EMAIL_USER", viper.GetString("labca.email.user"))
res, err := _decrypt(viper.GetString("labca.email.pass"))
if err != nil {
log.Println("WARNING: could not decrypt stored password: " + err.Error())
}
os.Setenv("PKI_EMAIL_PASS", string(res))
os.Setenv("PKI_EMAIL_FROM", viper.GetString("labca.email.from"))
} else {
os.Setenv("PKI_EMAIL_SERVER", "localhost")
os.Setenv("PKI_EMAIL_PORT", "9380")
os.Setenv("PKI_EMAIL_USER", "cert-master@example.com")
os.Setenv("PKI_EMAIL_PASS", "password")
os.Setenv("PKI_EMAIL_FROM", "Expiry bot <test@example.com>")
}
_, err := exeCmd("./apply")
if err != nil {
fmt.Println("")
}
return err
}
func _progress(stage string) int {
max := 20.0 / 100.0
curr := 1.0
if stage == "register" {
return int(math.Round(curr / max))
}
curr += 2.0
if stage == "setup" {
return int(math.Round(curr / max))
}
curr += 3.0
if stage == "root-ca" {
return int(math.Round(curr / max))
}
curr += 4.0
if stage == "ca-int" {
return int(math.Round(curr / max))
}
curr += 3.0
if stage == "polling" {
return int(math.Round(curr / max))
}
curr += 4.0
if stage == "wrapup" {
return int(math.Round(curr / max))
}
curr += 3.0
if stage == "final" {
return int(math.Round(curr / max))
}
return 0
}
func _helptext(stage string) template.HTML {
if stage == "register" {
return template.HTML(fmt.Sprint("<p>You need to create an admin account for managing this instance of\n",
"LabCA. There can only be one admin account, but you can configure all its attributes once the\n",
"initial setup has completed.</p>"))
} else if stage == "setup" {
return template.HTML(fmt.Sprint("<p>The fully qualified domain name (FQDN) is what end users will use\n",
"to connect to this server. It was provided in the initial setup and is shown here for reference.</p>\n",
"<p>Please fill in a DNS server (and optionally port, default is ':53') that will be used to lookup\n",
"the domains for which a certificate is requested.</p>\n",
"<p>LabCA is primarily intended for use inside an organization where all domains end in the same\n",
"domain, e.g. '.localdomain'. In lockdown mode only those domains are allowed. In whitelist mode\n",
"those domains are allowed next to all official, internet accessible domains and in standard\n",
"mode only the official domains are allowed.</p>"))
} else if stage == "root-ca" {
return template.HTML(fmt.Sprint("<p>This is the top level certificate that will sign the issuer\n",
"certificate(s). You can either generate a fresh Root CA (Certificate Authority) or import an\n",
"existing one, e.g. a backup from another LabCA instance.</p>\n",
"<p>If you want to generate a certificate, pick a key type and strength (the higher the number the\n",
"more secure, ECDSA is more modern than RSA), provide at least a country and organization name,\n",
"and the common name. It is recommended that the common name contains the word 'Root' as well\n",
"as your organization name so you can recognize it, and that's why that is automatically filled\n",
"once you leave the organization field.</p>"))
} else if stage == "ca-int" {
return template.HTML(fmt.Sprint("<p>This is what end users will see as the issuing certificate. Again,\n",
"you can either generate a fresh certificate or import an existing one, as long as it is signed by\n",
"the Root CA from the previous step.</p>\n",
"<p>If you want to generate a certificate, by default the same key type and strength is selected as\n",
"was chosen in the previous step when generating the root (except that the issuer certificate cannot\n",
"be ECDSA due to a limitation in the Let's Encrypt implementation), but you may choose a different\n",
"one. By default the common name is the same as the CN for the Root CA, minus the word 'Root'.</p>"))
} else {
return template.HTML("")
}
}
func _setupAdminUser(w http.ResponseWriter, r *http.Request) bool {
if r.Method == "GET" {
reg := &User{
RequestBase: r.Header.Get("X-Request-Base"),
}
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
} else if r.Method == "POST" {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
reg := &User{
Name: r.Form.Get("username"),
Email: r.Form.Get("email"),
Password: r.Form.Get("password"),
Confirm: r.Form.Get("confirm"),
RequestBase: r.Header.Get("X-Request-Base"),
}
if !reg.Validate(true, false) {
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
hash, err := bcrypt.GenerateFromPassword([]byte(reg.Password), bcrypt.MinCost)
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
viper.Set("user.name", reg.Name)
viper.Set("user.email", reg.Email)
viper.Set("user.password", string(hash))
viper.WriteConfig()
session, _ := sessionStore.Get(r, "labca")
session.Values["user"] = reg.Name
if err = session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
// Fake the method to GET as we need to continue in the setupHandler() function
r.Method = "GET"
} else {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusSeeOther)
return false
}
return true
}
func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool {
if r.Method == "GET" {
domain := viper.GetString("labca.fqdn")
pos := strings.Index(domain, ".")
if pos > -1 {
pos = pos + 1
domain = domain[pos:]
}
cfg := &SetupConfig{
Fqdn: viper.GetString("labca.fqdn"),
DomainMode: "lockdown",
LockdownDomains: domain,
WhitelistDomains: domain,
RequestBase: r.Header.Get("X-Request-Base"),
}
render(w, r, "setup:manage", map[string]interface{}{"SetupConfig": cfg, "Progress": _progress("setup"), "HelpText": _helptext("setup")})
return false
} else if r.Method == "POST" {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
cfg := &SetupConfig{
Fqdn: r.Form.Get("fqdn"),
DNS: r.Form.Get("dns"),
DomainMode: r.Form.Get("domain_mode"),
LockdownDomains: r.Form.Get("lockdown_domains"),
WhitelistDomains: r.Form.Get("whitelist_domains"),
RequestBase: r.Header.Get("X-Request-Base"),
}
if !cfg.Validate(false) {
render(w, r, "setup:manage", map[string]interface{}{"SetupConfig": cfg, "Progress": _progress("setup"), "HelpText": _helptext("setup")})
return false
}
matched, err := regexp.MatchString(":\\d+$", cfg.DNS)
if err == nil && !matched {
cfg.DNS += ":53"
}
viper.Set("labca.fqdn", cfg.Fqdn)
viper.Set("labca.dns", cfg.DNS)
viper.Set("labca.domain_mode", cfg.DomainMode)
if cfg.DomainMode == "lockdown" {
viper.Set("labca.lockdown", cfg.LockdownDomains)
}
if cfg.DomainMode == "whitelist" {
viper.Set("labca.whitelist", cfg.WhitelistDomains)
}
viper.WriteConfig()
// Fake the method to GET as we need to continue in the setupHandler() function
r.Method = "GET"
} else {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusSeeOther)
return false
}
return true
}
func setupHandler(w http.ResponseWriter, r *http.Request) {
if viper.GetBool("config.complete") {
render(w, r, "index:manage", map[string]interface{}{"Message": template.HTML("Setup already completed! Go <a href=\"" + r.Header.Get("X-Request-Base") + "/\">home</a>")})
return
}
// 1. Setup admin user
if viper.Get("user.password") == nil {
if !_setupAdminUser(w, r) {
return
}
}
// 2. Setup essential configuration
if viper.Get("labca.dns") == nil {
if !_setupBaseConfig(w, r) {
return
}
}
// 3. Setup root CA certificate
if !_certCreate(w, r, "root-ca", true) {
return
}
// 4. Setup issuer certificate
if !_certCreate(w, r, "ca-int", false) {
return
}
// 5. Apply configuration / populate with certificate info
err := _applyConfig()
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
if !viper.GetBool("config.restarted") {
// 6. Trust the new certs
if !_hostCommand(w, r, "trust-store") {
return
}
// Don't let the retry mechanism generate new restartSecret!
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
render(w, r, "index", map[string]interface{}{"Message": "Retry OK"})
} else {
// 8. Restart application
restartSecret = randToken()
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/wait?restart="+restartSecret, http.StatusFound)
}
return
}
render(w, r, "wrapup:manage", map[string]interface{}{"Progress": _progress("wrapup"), "HelpText": _helptext("wrapup")})
}
func waitHandler(w http.ResponseWriter, r *http.Request) {
if viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/", http.StatusFound)
return
}
render(w, r, "polling:manage", map[string]interface{}{"Progress": _progress("polling"), "HelpText": _helptext("polling")})
}
func restartHandler(w http.ResponseWriter, r *http.Request) {
if viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/", http.StatusFound)
return
}
if strings.Compare(r.URL.Query().Get("token"), restartSecret) != 0 {
log.Println("WARNING: Restart token ('" + r.URL.Query().Get("token") + "') does not match our secret ('" + restartSecret + "')!")
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
viper.Set("config.restarted", true)
viper.WriteConfig()
if !_hostCommand(w, r, "docker-restart") {
viper.Set("config.restarted", false)
viper.WriteConfig()
return
}
}
func finalHandler(w http.ResponseWriter, r *http.Request) {
if viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/", http.StatusFound)
return
}
t := viper.GetTime("config.cert_requested")
if !t.IsZero() && t.After(time.Now().Add(-5*time.Minute)) {
// Too soon
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
w.Header().Set("Content-Type", "application/json")
if viper.GetBool("config.error") {
viper.Set("config.cert_requested", nil)
viper.WriteConfig()
}
json.NewEncoder(w).Encode(map[string]interface{}{"complete": viper.GetBool("config.complete"), "error": viper.GetBool("config.error")})
} else {
render(w, r, "polling:manage", map[string]interface{}{"Progress": _progress("polling"), "HelpText": _helptext("polling")})
}
return
}
viper.Set("config.cert_requested", time.Now())
if viper.GetBool("config.error") {
viper.Set("config.error", false)
}
viper.WriteConfig()
// 9. Setup our own web certificate
if !_hostCommand(w, r, "acme-request") {
viper.Set("config.error", true)
viper.WriteConfig()
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/logs/cert", http.StatusSeeOther)
return
}
// 10. remove the temporary bit from nginx config
if !_hostCommand(w, r, "nginx-remove-redirect") {
return
}
// 11. reload nginx
if !_hostCommand(w, r, "nginx-reload") {
return
}
viper.Set("config.complete", true)
viper.WriteConfig()
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"complete": viper.GetBool("config.complete")})
} else {
render(w, r, "final:manage", map[string]interface{}{"RequestBase": r.Header.Get("X-Request-Base"), "Progress": _progress("final"), "HelpText": _helptext("final")})
}
}
func showErrorHandler(w http.ResponseWriter, r *http.Request) {
errorHandler(w, r, nil, http.StatusInternalServerError)
}
// RangeStructer takes the first argument, which must be a struct, and
// returns the value of each field in a slice. It will return nil
// if there are no arguments or first argument is not a struct
func RangeStructer(args ...interface{}) []interface{} {
if len(args) == 0 {
return nil
}
v := reflect.ValueOf(args[0])
if v.Kind() != reflect.Struct {
return nil
}
out := make([]interface{}, v.NumField())
for i := 0; i < v.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.String:
if v.Field(i).Type().String() == "template.HTML" {
out[i] = template.HTML(v.Field(i).String())
} else {
out[i] = v.Field(i).String()
}
case reflect.Bool:
out[i] = v.Field(i).Bool()
default:
out[i] = v.Field(i)
}
}
return out
}
func accountsHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
Accounts, err := GetAccounts(w, r)
if err == nil {
render(w, r, "list:accounts", map[string]interface{}{"List": Accounts})
}
}
func accountHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
errorHandler(w, r, err, http.StatusBadRequest)
return
}
AccountDetails, err := GetAccount(w, r, id)
if err == nil {
render(w, r, "show:accounts", map[string]interface{}{"Details": AccountDetails})
}
}
func ordersHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
Orders, err := GetOrders(w, r)
if err == nil {
render(w, r, "list:orders", map[string]interface{}{"List": Orders})
}
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
errorHandler(w, r, err, http.StatusBadRequest)
return
}
OrderDetails, err := GetOrder(w, r, id)
if err == nil {
render(w, r, "show:orders", map[string]interface{}{"Details": OrderDetails})
}
}
func authzHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
Authz, err := GetAuthz(w, r)
if err == nil {
render(w, r, "list:authz", map[string]interface{}{"List": Authz})
}
}
func authHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
vars := mux.Vars(r)
id := vars["id"]
AuthDetails, err := GetAuth(w, r, id)
if err == nil {
render(w, r, "show:authz", map[string]interface{}{"Details": AuthDetails})
}
}
func challengesHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
Challenges, err := GetChallenges(w, r)
if err == nil {
render(w, r, "list:challenges", map[string]interface{}{"List": Challenges})
}
}
func challengeHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
errorHandler(w, r, err, http.StatusBadRequest)
return
}
ChallengeDetails, err := GetChallenge(w, r, id)
if err == nil {
render(w, r, "show:challenges", map[string]interface{}{"Details": ChallengeDetails})
}
}
func certificatesHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
Certificates, err := GetCertificates(w, r)
if err == nil {
render(w, r, "list:certificates", map[string]interface{}{"List": Certificates})
}
}
func certificateHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
return
}
var serial string
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
serial = vars["id"]
}
CertificateDetails, err := GetCertificate(w, r, id, serial)
if err == nil {
render(w, r, "show:certificates", map[string]interface{}{"Details": CertificateDetails})
}
}
func certRevokeHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
errorHandler(w, r, errors.New("method not allowed at this point"), http.StatusMethodNotAllowed)
return
}
if r.Method == "POST" {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
serial := r.Form.Get("serial")
reason, err := strconv.Atoi(r.Form.Get("reason"))
if err != nil {
errorHandler(w, r, err, http.StatusBadRequest)
return
}
if !_hostCommand(w, r, "revoke-cert", serial, strconv.Itoa(reason)) {
return
}
}
}
type navItem struct {
Name string
Icon string
Attrs map[template.HTMLAttr]string
IsActive bool
SubMenu []navItem
}
func _matchPrefix(uri string, prefix string) bool {
return (uri == prefix || strings.HasPrefix(uri, prefix+"/"))
}
func _acmeNav(active string, uri string, requestBase string) navItem {
isAcmeActive := _matchPrefix(uri, "/accounts") || _matchPrefix(uri, "/orders") ||
_matchPrefix(uri, "/authz") || _matchPrefix(uri, "/challenges") ||
_matchPrefix(uri, "/certificates") || false
accounts := navItem{
Name: "Accounts",
Icon: "fa-list-alt",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/accounts",
"title": "ACME Accounts",
},
}
orders := navItem{
Name: "Orders",
Icon: "fa-tags",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/orders",
"title": "ACME Orders",
},
}
authz := navItem{
Name: "Authorizations",
Icon: "fa-chain",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/authz",
"title": "ACME Authorizations",
},
}
challenges := navItem{
Name: "Challenges",
Icon: "fa-exchange",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/challenges",
"title": "ACME Challenges",
},
}
certificates := navItem{
Name: "Certificates",
Icon: "fa-lock",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/certificates",
"title": "ACME Certificates",
},
}
acme := navItem{
Name: "ACME",
Icon: "fa-sitemap",
Attrs: map[template.HTMLAttr]string{
"href": "#",
"title": "Automated Certificate Management Environment",
},
IsActive: isAcmeActive,
SubMenu: []navItem{accounts, certificates, orders, authz, challenges},
}
// set active menu class
switch active {
case "accounts":
accounts.Attrs["class"] = "active"
case "certificates":
certificates.Attrs["class"] = "active"
case "orders":
orders.Attrs["class"] = "active"
case "authz":
authz.Attrs["class"] = "active"
case "challenges":
challenges.Attrs["class"] = "active"
}
return acme
}
func activeNav(active string, uri string, requestBase string) []navItem {
// create menu items
home := navItem{
Name: "Dashboard",
Icon: "fa-dashboard",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/",
"title": "Main page with the status of the system",
},
}
acme := _acmeNav(active, uri, requestBase)
cert := navItem{
Name: "Web Certificate",
Icon: "fa-lock",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/logs/cert",
"title": "Log file for the certificate renewal for this server",
},
}
boulder := navItem{
Name: "ACME",
Icon: "fa-search-plus",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/logs/boulder",
"title": "Live view on the backend ACME application logs",
},
}
audit := navItem{
Name: "ACME Audit Log",
Icon: "fa-paw",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/logs/audit",
"title": "Live view on only the audit messages in the backend ACME application logs",
},
}
labca := navItem{
Name: "LabCA",
Icon: "fa-edit",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/logs/labca",
"title": "Live view on the logs for this LabCA web application",
},
}
web := navItem{
Name: "Web Server",
Icon: "fa-globe",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/logs/web",
"title": "Live view on the NGINX web server access log",
},
}
logs := navItem{
Name: "Logs",
Icon: "fa-files-o",
Attrs: map[template.HTMLAttr]string{
"href": "#",
"title": "Log Files",
},
IsActive: strings.HasPrefix(uri, "/logs/"),
SubMenu: []navItem{cert, boulder, audit, labca, web},
}
manage := navItem{
Name: "Manage",
Icon: "fa-wrench",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/manage",
"title": "Manage the system",
},
}
about := navItem{
Name: "About",
Icon: "fa-comments",
Attrs: map[template.HTMLAttr]string{
"href": requestBase + "/about",
"title": "About LabCA",
},
}
public := navItem{
Name: "Public Area",
Icon: "fa-home",
Attrs: map[template.HTMLAttr]string{
"href": "http://" + viper.GetString("labca.fqdn"),
"title": "The non-Admin pages of this LabCA instance",
},
}
// set active menu class
switch active {
case "about":
about.Attrs["class"] = "active"
case "index":
home.Attrs["class"] = "active"
case "manage":
manage.Attrs["class"] = "active"
}
return []navItem{home, acme, logs, manage, about, public}
}
func render(w http.ResponseWriter, r *http.Request, view string, data map[string]interface{}) {
viewSlice := strings.Split(view, ":")
menu := viewSlice[0]
if len(viewSlice) > 1 {
menu = viewSlice[1]
}
data["Menu"] = activeNav(menu, r.RequestURI, r.Header.Get("X-Request-Base"))
if version != "" {
data["Version"] = version
}
b, err := tmpls.Render("base.tmpl", "views/"+viewSlice[0]+".tmpl", data)
if err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
w.Write(b)
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
errorHandler(w, r, fmt.Errorf("NotFoundHandler for: %s %s", r.Method, r.URL), http.StatusNotFound)
}
func authorized(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method + " " + r.RequestURI)
if r.RequestURI == "/login" || strings.Contains(r.RequestURI, "/static/") {
next.ServeHTTP(w, r)
} else {
session, _ := sessionStore.Get(r, "labca")
if session.Values["user"] != nil || (r.RequestURI == "/setup" && viper.Get("user.password") == nil) {
next.ServeHTTP(w, r)
} else {
session.Values["bounce"] = r.RequestURI
if err := session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/login", http.StatusFound)
}
}
})
}
func init() {
if os.Getenv("DEVELOPMENT") != "" {
isDev = true
}
var err error
tmpls, err = templates.New().ParseDir("./templates", "templates/")
if err != nil {
panic(fmt.Errorf("fatal error templates: '%s'", err))
}
tmpls.AddFunc("rangeStruct", RangeStructer)
viper.SetConfigName("config")
viper.AddConfigPath("data")
viper.SetDefault("config.complete", false)
if err := viper.ReadInConfig(); err != nil {
panic(fmt.Errorf("fatal error config file: '%s'", err))
}
if viper.Get("keys.auth") == nil {
key := securecookie.GenerateRandomKey(32)
if key == nil {
panic(fmt.Errorf("fatal error random key"))
}
viper.Set("keys.auth", base64.StdEncoding.EncodeToString(key))
viper.WriteConfig()
}
if viper.Get("keys.enc") == nil {
key := securecookie.GenerateRandomKey(32)
if key == nil {
panic(fmt.Errorf("fatal error random key"))
}
viper.Set("keys.enc", base64.StdEncoding.EncodeToString(key))
viper.WriteConfig()
}
if viper.Get("server.addr") == nil {
viper.Set("server.addr", "0.0.0.0")
viper.WriteConfig()
}
if viper.Get("server.port") == nil {
viper.Set("server.port", 3000)
viper.WriteConfig()
}
if viper.Get("server.session.maxage") == nil {
viper.Set("server.session.maxage", 3600) // 1 hour
viper.WriteConfig()
}
if viper.Get("db.conn") == nil {
viper.Set("db.type", "mysql")
viper.Set("db.conn", "root@tcp(boulder-mysql:3306)/boulder_sa_integration")
viper.WriteConfig()
}
dbConn = viper.GetString("db.conn")
dbType = viper.GetString("db.type")
version = viper.GetString("version")
updateAvailable = false
}
func main() {
tmpls.Parse()
keys_auth, err := base64.StdEncoding.DecodeString(viper.GetString("keys.auth"))
if err != nil {
log.Fatalf("cannot decode configured 'keys.auth': %s\n", err)
}
keys_enc, err := base64.StdEncoding.DecodeString(viper.GetString("keys.enc"))
if err != nil {
log.Fatalf("cannot decode configured 'keys.enc': %s\n", err)
}
sessionStore = sessions.NewCookieStore(keys_auth, keys_enc)
sessionStore.Options = &sessions.Options{
Path: "/",
MaxAge: viper.GetInt("server.session.maxage") * 1,
HttpOnly: true,
}
r := mux.NewRouter()
r.HandleFunc("/", rootHandler).Methods("GET")
r.HandleFunc("/about", aboutHandler).Methods("GET")
r.HandleFunc("/manage", manageHandler).Methods("GET", "POST")
r.HandleFunc("/final", finalHandler).Methods("GET")
r.HandleFunc("/error", showErrorHandler).Methods("GET")
r.HandleFunc("/login", loginHandler).Methods("GET", "POST")
r.HandleFunc("/logout", logoutHandler).Methods("GET")
r.HandleFunc("/logs/{type}", logsHandler).Methods("GET")
r.HandleFunc("/restart", restartHandler).Methods("GET")
r.HandleFunc("/setup", setupHandler).Methods("GET", "POST")
r.HandleFunc("/wait", waitHandler).Methods("GET")
r.HandleFunc("/ws", wsHandler).Methods("GET")
r.HandleFunc("/accounts", accountsHandler).Methods("GET")
r.HandleFunc("/accounts/{id}", accountHandler).Methods("GET")
r.HandleFunc("/orders", ordersHandler).Methods("GET")
r.HandleFunc("/orders/{id}", orderHandler).Methods("GET")
r.HandleFunc("/authz", authzHandler).Methods("GET")
r.HandleFunc("/authz/{id}", authHandler).Methods("GET")
r.HandleFunc("/challenges", challengesHandler).Methods("GET")
r.HandleFunc("/challenges/{id}", challengeHandler).Methods("GET")
r.HandleFunc("/certificates", certificatesHandler).Methods("GET")
r.HandleFunc("/certificates/{id}", certificateHandler).Methods("GET")
r.HandleFunc("/certificates/{id}", certRevokeHandler).Methods("POST")
r.NotFoundHandler = http.HandlerFunc(notFoundHandler)
if isDev {
r.PathPrefix("/accounts/static/").Handler(http.StripPrefix("/accounts/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/authz/static/").Handler(http.StripPrefix("/authz/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/challenges/static/").Handler(http.StripPrefix("/challenges/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/certificates/static/").Handler(http.StripPrefix("/certificates/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/orders/static/").Handler(http.StripPrefix("/orders/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/logs/static/").Handler(http.StripPrefix("/logs/static/", http.FileServer(http.Dir("../static"))))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("../static"))))
}
r.Use(authorized)
log.Printf("Listening on %s:%d...\n", viper.GetString("server.addr"), viper.GetInt("server.port"))
srv := &http.Server{
Handler: r,
Addr: viper.GetString("server.addr") + ":" + viper.GetString("server.port"),
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}