Initial commit

This commit is contained in:
paul 2018-04-22 16:55:50 +02:00
commit 2422e3108f
37 changed files with 12691 additions and 0 deletions

75
handlers/auth.go Normal file
View file

@ -0,0 +1,75 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"os"
"git.klink.asia/paul/certman/views"
"golang.org/x/oauth2"
"git.klink.asia/paul/certman/services"
)
func OAuth2Endpoint(p *services.Provider, config *oauth2.Config) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
v := views.NewWithSession(req, p.Sessions)
code := req.FormValue("code")
// exchange code for token
accessToken, err := config.Exchange(oauth2.NoContext, code)
if err != nil {
fmt.Println(err)
http.NotFound(w, req)
return
}
if accessToken.Valid() {
// generate a client using the access token
httpClient := config.Client(oauth2.NoContext, accessToken)
apiRequest, err := http.NewRequest("GET", os.Getenv("USER_ENDPOINT"), nil)
if err != nil {
v.RenderError(w, http.StatusNotFound)
return
}
resp, err := httpClient.Do(apiRequest)
if err != nil {
fmt.Println(err.Error())
v.RenderError(w, http.StatusInternalServerError)
return
}
var user struct {
Username string `json:"username"`
}
err = json.NewDecoder(resp.Body).Decode(&user)
if err != nil {
fmt.Println(err.Error())
v.RenderError(w, http.StatusInternalServerError)
return
}
if user.Username != "" {
p.Sessions.SetUsername(w, req, user.Username)
http.Redirect(w, req, "/certs", http.StatusFound)
return
}
fmt.Println(err.Error())
v.RenderError(w, http.StatusInternalServerError)
return
}
}
}
func GetLoginHandler(p *services.Provider, config *oauth2.Config) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
authURL := config.AuthCodeURL("", oauth2.AccessTypeOnline)
http.Redirect(w, req, authURL, http.StatusFound)
}
}

253
handlers/cert.go Normal file
View file

@ -0,0 +1,253 @@
package handlers
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"html/template"
"io/ioutil"
"log"
"math/big"
"net/http"
"strings"
"time"
"git.klink.asia/paul/certman/models"
"git.klink.asia/paul/certman/services"
"github.com/go-chi/chi"
"git.klink.asia/paul/certman/views"
)
func ListClientsHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
v := views.NewWithSession(req, p.Sessions)
username := p.Sessions.GetUsername(req)
clients, _ := p.ClientCollection.ListClientsForUser(username)
v.Vars["Clients"] = clients
v.Render(w, "client_list")
}
}
func CreateCertHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
username := p.Sessions.GetUsername(req)
certname := req.FormValue("certname")
// Validate certificate Name
if !IsByteLength(certname, 2, 64) || !IsDNSName(certname) {
p.Sessions.Flash(w, req,
services.Flash{
Type: "danger",
Message: "The certificate name can only contain letters and numbers",
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
return
}
// lowercase the certificate name, to avoid problems with the case
// insensitive matching inside OpenVPN
certname = strings.ToLower(certname)
// Load CA master certificate
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
if err != nil {
log.Fatalf("error loading ca keyfiles: %s", err)
panic(err.Error())
}
// Generate Keypair
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatalf("Could not generate keypair: %s", err)
p.Sessions.Flash(w, req,
services.Flash{
Type: "danger",
Message: "The certificate key could not be generated",
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
return
}
// Generate Certificate
commonName := fmt.Sprintf("%s@%s", username, certname)
derBytes, err := CreateCertificate(commonName, key, caCert, caKey)
// Initialize new client config
client := models.Client{
Name: certname,
CreatedAt: time.Now(),
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
Cert: derBytes,
User: username,
}
// Insert client into database
if err := p.ClientCollection.CreateClient(&client); err != nil {
log.Println(err.Error())
p.Sessions.Flash(w, req,
services.Flash{
Type: "danger",
Message: "The certificate could not be added to the database",
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
return
}
p.Sessions.Flash(w, req,
services.Flash{
Type: "success",
Message: "The certificate was created successfully",
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
}
}
func DeleteCertHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
v := views.New(req)
// detemine own username
username := p.Sessions.GetUsername(req)
name := chi.URLParam(req, "name")
client, err := p.ClientCollection.GetClientByNameUser(name, username)
if err != nil {
v.RenderError(w, http.StatusNotFound)
return
}
err = p.ClientCollection.DeleteClient(client.ID)
if err != nil {
p.Sessions.Flash(w, req,
services.Flash{
Type: "danger",
Message: "Failed to delete certificate",
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
}
p.Sessions.Flash(w, req,
services.Flash{
Type: "success",
Message: template.HTML(fmt.Sprintf("Successfully deleted client <strong>%s</strong>", client.Name)),
},
)
http.Redirect(w, req, "/certs", http.StatusFound)
}
}
func DownloadCertHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
v := views.New(req)
// detemine own username
username := p.Sessions.GetUsername(req)
name := chi.URLParam(req, "name")
client, err := p.ClientCollection.GetClientByNameUser(name, username)
if err != nil {
v.RenderError(w, http.StatusNotFound)
return
}
// cbuf and kbuf are buffers in which the PEM certificates are
// rendered into
var cbuf = new(bytes.Buffer)
var kbuf = new(bytes.Buffer)
pem.Encode(cbuf, &pem.Block{Type: "CERTIFICATE", Bytes: client.Cert})
pem.Encode(kbuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: client.PrivateKey})
vars := map[string]string{
"Cert": cbuf.String(),
"Key": kbuf.String(),
}
t, err := views.GetTemplate("config.ovpn")
if err != nil {
log.Printf("Error loading certificate template: %s", err)
v.RenderError(w, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/x-openvpn-profile")
w.Header().Set("Content-Disposition", "attachment; filename=\"config.ovpn\"")
w.WriteHeader(http.StatusOK)
t.Execute(w, vars)
return
}
}
func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey, error) {
cf, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, nil, err
}
kf, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, nil, err
}
cpb, _ := pem.Decode(cf)
kpb, _ := pem.Decode(kf)
crt, err := x509.ParseCertificate(cpb.Bytes)
if err != nil {
return nil, nil, err
}
key, err := x509.ParsePKCS1PrivateKey(kpb.Bytes)
if err != nil {
return nil, nil, err
}
return crt, key, nil
}
// CreateCertificate creates a CA-signed certificate
func CreateCertificate(commonName string, key interface{}, caCert *x509.Certificate, caKey interface{}) ([]byte, error) {
subj := caCert.Subject
// .. except for the common name
subj.CommonName = commonName
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Obscure error in cert serial number generation: %s", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: subj,
NotBefore: time.Now().Add(-5 * time.Minute), // account for clock shift
NotAfter: time.Now().Add(24 * time.Hour * 356 * 5), // 5 years ought to be enough!
SignatureAlgorithm: x509.SHA256WithRSA,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
return x509.CreateCertificate(rand.Reader, &template, caCert, publicKey(key), caKey)
}
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}

64
handlers/converters.go Normal file
View file

@ -0,0 +1,64 @@
package handlers
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// ToString convert the input to a string.
func ToString(obj interface{}) string {
res := fmt.Sprintf("%v", obj)
return string(res)
}
// ToJSON convert the input to a valid JSON string
func ToJSON(obj interface{}) (string, error) {
res, err := json.Marshal(obj)
if err != nil {
res = []byte("")
}
return string(res), err
}
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
func ToFloat(str string) (float64, error) {
res, err := strconv.ParseFloat(str, 64)
if err != nil {
res = 0.0
}
return res, err
}
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
func ToInt(value interface{}) (res int64, err error) {
val := reflect.ValueOf(value)
switch value.(type) {
case int, int8, int16, int32, int64:
res = val.Int()
case uint, uint8, uint16, uint32, uint64:
res = int64(val.Uint())
case string:
if IsInt(val.String()) {
res, err = strconv.ParseInt(val.String(), 0, 64)
if err != nil {
res = 0
}
} else {
err = fmt.Errorf("math: square root of negative number %g", value)
res = 0
}
default:
err = fmt.Errorf("math: square root of negative number %g", value)
res = 0
}
return
}
// ToBoolean convert the input string to a boolean.
func ToBoolean(str string) (bool, error) {
return strconv.ParseBool(str)
}

22
handlers/handlers.go Normal file
View file

@ -0,0 +1,22 @@
package handlers
import (
"net/http"
"git.klink.asia/paul/certman/views"
)
func NotFoundHandler(w http.ResponseWriter, req *http.Request) {
view := views.New(req)
view.RenderError(w, http.StatusNotFound)
}
func ErrorHandler(w http.ResponseWriter, req *http.Request) {
view := views.New(req)
view.RenderError(w, http.StatusInternalServerError)
}
func CSRFErrorHandler(w http.ResponseWriter, req *http.Request) {
view := views.New(req)
view.RenderError(w, http.StatusForbidden)
}

138
handlers/validators.go Normal file
View file

@ -0,0 +1,138 @@
package handlers
import (
"net"
"regexp"
"strings"
)
const (
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$"
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
ISBN13 string = "^(?:[0-9]{13})$"
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
Alpha string = "^[a-zA-Z]+$"
Alphanumeric string = "^[a-zA-Z0-9]+$"
Numeric string = "^[0-9]+$"
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
Hexadecimal string = "^[0-9a-fA-F]+$"
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
ASCII string = "^[\x00-\x7F]+$"
Multibyte string = "[^\x00-\x7F]"
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
PrintableASCII string = "^[\x20-\x7E]+$"
DataURI string = "^data:.+\\/(.+);base64$"
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername string = `(\S+(:\S*)?@)`
URLPath string = `((\/|\?|#)[^\s]*)`
URLPort string = `(:(\d{1,5}))`
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
URLSubdomain string = `((www\.)|([a-zA-Z0-9]([-\.][-\._a-zA-Z0-9]+)*))`
URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
UnixPath string = `^(/[^/\x00]*)+/?$`
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
tagName string = "valid"
hasLowerCase string = ".*[[:lower:]]"
hasUpperCase string = ".*[[:upper:]]"
)
var (
rxEmail = regexp.MustCompile(Email)
rxCreditCard = regexp.MustCompile(CreditCard)
rxISBN10 = regexp.MustCompile(ISBN10)
rxISBN13 = regexp.MustCompile(ISBN13)
rxUUID3 = regexp.MustCompile(UUID3)
rxUUID4 = regexp.MustCompile(UUID4)
rxUUID5 = regexp.MustCompile(UUID5)
rxUUID = regexp.MustCompile(UUID)
rxAlpha = regexp.MustCompile(Alpha)
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
rxNumeric = regexp.MustCompile(Numeric)
rxInt = regexp.MustCompile(Int)
rxFloat = regexp.MustCompile(Float)
rxHexadecimal = regexp.MustCompile(Hexadecimal)
rxHexcolor = regexp.MustCompile(Hexcolor)
rxRGBcolor = regexp.MustCompile(RGBcolor)
rxASCII = regexp.MustCompile(ASCII)
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
rxMultibyte = regexp.MustCompile(Multibyte)
rxFullWidth = regexp.MustCompile(FullWidth)
rxHalfWidth = regexp.MustCompile(HalfWidth)
rxBase64 = regexp.MustCompile(Base64)
rxDataURI = regexp.MustCompile(DataURI)
rxLatitude = regexp.MustCompile(Latitude)
rxLongitude = regexp.MustCompile(Longitude)
rxDNSName = regexp.MustCompile(DNSName)
rxURL = regexp.MustCompile(URL)
rxSSN = regexp.MustCompile(SSN)
rxWinPath = regexp.MustCompile(WinPath)
rxUnixPath = regexp.MustCompile(UnixPath)
rxSemver = regexp.MustCompile(Semver)
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
)
// IsAlphanumeric check if the string contains only letters and numbers. Empty string is valid.
func IsAlphanumeric(str string) bool {
if IsNull(str) {
return true
}
return rxAlphanumeric.MatchString(str)
}
// ByteLength check string's length
func ByteLength(str string, params ...string) bool {
if len(params) == 2 {
min, _ := ToInt(params[0])
max, _ := ToInt(params[1])
return len(str) >= int(min) && len(str) <= int(max)
}
return false
}
// IsInt check if the string is an integer. Empty string is valid.
func IsInt(str string) bool {
if IsNull(str) {
return true
}
return rxInt.MatchString(str)
}
// IsNull check if the string is null.
func IsNull(str string) bool {
return len(str) == 0
}
// IsByteLength check if the string's length (in bytes) falls in a range.
func IsByteLength(str string, min, max int) bool {
return len(str) >= min && len(str) <= max
}
// IsDNSName will validate the given string as a DNS name
func IsDNSName(str string) bool {
if str == "" || len(strings.Replace(str, ".", "", -1)) > 255 {
// constraints already violated
return false
}
return !IsIP(str) && rxDNSName.MatchString(str)
}
// IsIP checks if a string is either IP version 4 or 6.
func IsIP(str string) bool {
return net.ParseIP(str) != nil
}