Initial commit
This commit is contained in:
commit
35c48136fa
29 changed files with 11635 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*_vfsdata.go
|
||||||
|
certman
|
||||||
|
db.sqlite3
|
75
.gitlab-ci.yml
Normal file
75
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
image: golang:latest
|
||||||
|
|
||||||
|
variables:
|
||||||
|
REPO_NAME: git.klink.asia/paul/certman
|
||||||
|
|
||||||
|
# The problem is that to be able to use go get, one needs to put
|
||||||
|
# the repository in the $GOPATH. So for example if your gitlab domain
|
||||||
|
# is gitlab.com, and that your repository is namespace/project, and
|
||||||
|
# the default GOPATH being /go, then you'd need to have your
|
||||||
|
# repository in /go/src/gitlab.com/namespace/project
|
||||||
|
# Thus, making a symbolic link corrects this.
|
||||||
|
before_script:
|
||||||
|
- mkdir -p $GOPATH/src/$REPO_NAME
|
||||||
|
- ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
|
||||||
|
- cd $GOPATH/src/$REPO_NAME
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
- release
|
||||||
|
|
||||||
|
format:
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
script:
|
||||||
|
# we use tags="dev" so there is no dependency on the prebuilt assets yet
|
||||||
|
- go get -tags="dev" -v $(go list ./... | grep -v /vendor/) # get missing dependencies
|
||||||
|
- go fmt $(go list ./... | grep -v /vendor/)
|
||||||
|
- go vet $(go list ./... | grep -v /vendor/)
|
||||||
|
- go test -tags="dev" -race $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt
|
||||||
|
# Use coverage parsing regex: ^coverage:\s(\d+(?:\.\d+)?%)
|
||||||
|
|
||||||
|
compile:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
script:
|
||||||
|
# we use tags="dev" so there is no dependency on the prebuilt assets yet
|
||||||
|
- go get -tags="dev" -v $(go list ./... | grep -v /vendor/) # get missing dependencies
|
||||||
|
|
||||||
|
# generate assets
|
||||||
|
- go get github.com/shurcooL/vfsgen/cmd/vfsgendev
|
||||||
|
- go generate git.klink.asia/paul/certman/assets
|
||||||
|
|
||||||
|
# build binaries -- list of supported plattforms is here:
|
||||||
|
# https://stackoverflow.com/a/20728862
|
||||||
|
- GOOS=linux GOARCH=amd64 go build -o $CI_PROJECT_DIR/certman
|
||||||
|
- GOOS=linux GOARCH=arm GOARM=6 go build -o $CI_PROJECT_DIR/certman.arm
|
||||||
|
- GOOS=windows GOARCH=amd64 go build -o $CI_PROJECT_DIR/certman.exe
|
||||||
|
artifacts:
|
||||||
|
expire_in: "8 hrs"
|
||||||
|
paths:
|
||||||
|
- certman
|
||||||
|
- certman.arm
|
||||||
|
- certman.exe
|
||||||
|
|
||||||
|
minify:
|
||||||
|
stage: release
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
dependencies:
|
||||||
|
- compile
|
||||||
|
image:
|
||||||
|
name: znly/upx:latest
|
||||||
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
|
script:
|
||||||
|
- upx --best --brute $CI_PROJECT_DIR/certman certman.arm certman.exe
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- certman
|
||||||
|
- certman.arm
|
||||||
|
- certman.exe
|
||||||
|
only:
|
||||||
|
- tags
|
28
assets/dev.go
Normal file
28
assets/dev.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// +build dev
|
||||||
|
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/build"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/shurcooL/httpfs/union"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assets contains project assets.
|
||||||
|
var Assets = union.New(map[string]http.FileSystem{
|
||||||
|
"/static": http.Dir(importPathToDir("git.klink.asia/paul/certman/assets/static")),
|
||||||
|
"/templates": http.Dir(importPathToDir("git.klink.asia/paul/certman/assets/templates")),
|
||||||
|
})
|
||||||
|
|
||||||
|
// importPathToDir is a helper function that resolves the absolute path of
|
||||||
|
// modules, so they can be used both in dev mode (`-tags="dev"`) or with a
|
||||||
|
// generated static asset file (`go generate`).
|
||||||
|
func importPathToDir(importPath string) string {
|
||||||
|
p, err := build.Import(importPath, "", build.FindOnly)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
return p.Dir
|
||||||
|
}
|
6
assets/doc.go
Normal file
6
assets/doc.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
//go:generate vfsgendev -source="git.klink.asia/paul/certman/assets".Assets
|
||||||
|
|
||||||
|
// Package assets contains assets the service, that will be embedded into
|
||||||
|
// the binary.
|
||||||
|
// Regenerate by running `go generate git.klink.asia/paul/certman/assets`.
|
||||||
|
package assets
|
0
assets/static/css/main.css
Normal file
0
assets/static/css/main.css
Normal file
BIN
assets/static/img/logo.png
Normal file
BIN
assets/static/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
6
assets/static/js/main.js
Normal file
6
assets/static/js/main.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
$.ajaxSetup({
|
||||||
|
// Initialize headers from the csrf-token meta tag.
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
10663
assets/static/vendor/bulma.css
vendored
Normal file
10663
assets/static/vendor/bulma.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
4
assets/static/vendor/jquery-3.2.1.min.js
vendored
Normal file
4
assets/static/vendor/jquery-3.2.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
assets/templates/errors/401.gohtml
Normal file
20
assets/templates/errors/401.gohtml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Insufficient permissions</title>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="hero is-info is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-8-desktop is-offset-2-desktop">
|
||||||
|
<h1 class="title is-2 is-spaced">Insufficient permissions</h1>
|
||||||
|
<h2 class="subtitle is-4">
|
||||||
|
Sorry, you don't have permissione to access this ressource.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
20
assets/templates/errors/404.gohtml
Normal file
20
assets/templates/errors/404.gohtml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Page not found</title>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="hero is-info is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-8-desktop is-offset-2-desktop">
|
||||||
|
<h1 class="title is-2 is-spaced">Page not found</h1>
|
||||||
|
<h2 class="subtitle is-4">
|
||||||
|
You requested a page that could not be fetched by the web server.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
21
assets/templates/errors/500.gohtml
Normal file
21
assets/templates/errors/500.gohtml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Sorry, something went wrong.</title>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="hero is-warning is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-8-desktop is-offset-2-desktop">
|
||||||
|
<h1 class="title is-2 is-spaced">Sorry, there was an error while processing your request.</h1>
|
||||||
|
<h2 class="subtitle is-4">
|
||||||
|
Maybe you can try again at a later time.<br>
|
||||||
|
We have logged the error and will try to make sure that this does not happen in the future.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
39
assets/templates/layouts/admin.gohtml
Normal file
39
assets/templates/layouts/admin.gohtml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{{ define "base" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ .CSRF_TOKEN }}" />
|
||||||
|
<meta name="csrf-param" content="csrf_token" />
|
||||||
|
|
||||||
|
<title>Admin</title>
|
||||||
|
<link rel="stylesheet" href="{{ assetURL "css/admin-style" }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="admin">
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<div id="flash-container">
|
||||||
|
{{range .flashes}}
|
||||||
|
<div class="{{ .Class }}"{{ .Message }}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container main-container">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ template "sink" .}}
|
||||||
|
<script src="/public/assets/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "header"}}{{end}}
|
||||||
|
{{ define "content"}}{{end}}
|
||||||
|
{{ define "footer"}}{{end}}
|
||||||
|
{{ define "sink"}}{{end}}
|
46
assets/templates/layouts/application.gohtml
Normal file
46
assets/templates/layouts/application.gohtml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{{ define "base" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ .CSRF_TOKEN }}" />
|
||||||
|
<meta name="csrf-param" content="csrf_token" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href='{{ assetURL "vendor/bulma.css" }}'>
|
||||||
|
<link rel="stylesheet" href='{{ assetURL "css/style.css" }}'>
|
||||||
|
{{ template "meta" . }}
|
||||||
|
{{ if eq .Meta.Env "production" }}
|
||||||
|
<!-- Add meta tags specific to production environment (analytics etc) -->
|
||||||
|
{{ end }}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{ template "header" . }}
|
||||||
|
<div id="flash-container">
|
||||||
|
{{range .flashes}}
|
||||||
|
<div class="{{ .Class }}">{{ .Message }}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ template "sink" .}}
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||||
|
<script>window.jQuery || document.write('<script src="{{ assetURL "vendor/jquery-3.2.1.min.js" }}"><\/script>')</script>
|
||||||
|
<script src='{{ assetURL "js/main.js" }}' async></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "meta"}}{{end}}
|
||||||
|
{{ define "header"}}{{end}}
|
||||||
|
{{ define "content"}}{{end}}
|
||||||
|
{{ define "footer"}}{{end}}
|
||||||
|
{{ define "sink"}}{{end}}
|
13
assets/templates/shared/footer.gohtml
Normal file
13
assets/templates/shared/footer.gohtml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{{ define "footer" }}
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-third">
|
||||||
|
<p>
|
||||||
|
© 2017 OneOffTech
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{{ end }}
|
25
assets/templates/shared/header.gohtml
Normal file
25
assets/templates/shared/header.gohtml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{{ define "header" }}
|
||||||
|
<nav class="navbar container" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="#">
|
||||||
|
<img src="{{ assetURL "img/logo.png" }}" alt="Logo" width="112" height="28">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="button navbar-burger">
|
||||||
|
<!-- Burger patties -->
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item">Certificates</a>
|
||||||
|
<a class="navbar-item">Change Password</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
</div>
|
||||||
|
<!-- navbar start, navbar end -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{{ end }}
|
17
assets/templates/views/debug.gohtml
Normal file
17
assets/templates/views/debug.gohtml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Landing Page</title>
|
||||||
|
<meta name="description" content="Test boilerplate" />
|
||||||
|
{{ end}}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h1>Hello, World!</h1>
|
||||||
|
<p>Have some variables: {{.}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end}}
|
24
assets/templates/views/login.gohtml
Normal file
24
assets/templates/views/login.gohtml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Log in</title>
|
||||||
|
<meta name="description" content="Test boilerplate" />
|
||||||
|
{{ end}}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="content">
|
||||||
|
<div class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h1>Hello, World!</h1>
|
||||||
|
<form action="" method="POST">
|
||||||
|
<input name="username" type="text" value=""/>
|
||||||
|
<input name="password" type="text" value=""/>
|
||||||
|
{{ .csrfField }}
|
||||||
|
<input class="button is-primary" type="submit" value="Log in">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end}}
|
115
handlers/gencert.go
Normal file
115
handlers/gencert.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/views"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListCertHandler(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
v := views.New(req)
|
||||||
|
v.Render(w, "cert_list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenCertHandler(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
v := views.New(req)
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not generate keypair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
|
||||||
|
if err != nil {
|
||||||
|
v.Render(w, "500")
|
||||||
|
log.Fatalf("error loading ca keyfiles: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
derBytes, err := CreateCertificate(key, caCert, caKey)
|
||||||
|
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
|
||||||
|
pkBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkBytes})
|
||||||
|
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, cr := pem.Decode(cf)
|
||||||
|
fmt.Println(string(cr))
|
||||||
|
kpb, kr := pem.Decode(kf)
|
||||||
|
fmt.Println(string(kr))
|
||||||
|
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(key interface{}, caCert *x509.Certificate, caKey interface{}) ([]byte, error) {
|
||||||
|
subj := caCert.Subject
|
||||||
|
// .. except for the common name
|
||||||
|
subj.CommonName = "clientName"
|
||||||
|
|
||||||
|
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(),
|
||||||
|
NotAfter: time.Now().Add(24 * time.Hour * 356 * 5),
|
||||||
|
|
||||||
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||||
|
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
17
handlers/handlers.go
Normal file
17
handlers/handlers.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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)
|
||||||
|
}
|
33
handlers/login.go
Normal file
33
handlers/login.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/models"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginHandler(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get parameters
|
||||||
|
username := req.Form.Get("username")
|
||||||
|
password := req.Form.Get("password")
|
||||||
|
|
||||||
|
user := models.User{}
|
||||||
|
|
||||||
|
err := db.Where(&models.User{Username: username}).Find(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
// could not find user
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.CheckPassword(password); err != nil {
|
||||||
|
// wrong password
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// user is logged in
|
||||||
|
// set cookie
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
36
main.go
Normal file
36
main.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/models"
|
||||||
|
"git.klink.asia/paul/certman/router"
|
||||||
|
"git.klink.asia/paul/certman/views"
|
||||||
|
|
||||||
|
// import sqlite3 driver once
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// Connect to the database
|
||||||
|
db, err := gorm.Open("sqlite3", "db.sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not open database: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Migrate
|
||||||
|
db.AutoMigrate(models.User{}, models.ClientConf{})
|
||||||
|
|
||||||
|
// load and parse template files
|
||||||
|
views.LoadTemplates()
|
||||||
|
|
||||||
|
mux := router.HandleRoutes(db)
|
||||||
|
|
||||||
|
err = http.ListenAndServe(":8000", mux)
|
||||||
|
log.Fatalf(err.Error())
|
||||||
|
}
|
26
middleware/panic.go
Normal file
26
middleware/panic.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recoverer Listens for panic() calls and logs the stacktrace.
|
||||||
|
func Recoverer(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if rvr := recover(); rvr != nil {
|
||||||
|
log.Println(rvr)
|
||||||
|
log.Println(string(debug.Stack()))
|
||||||
|
handlers.ErrorHandler(w, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
25
middleware/requirelogin.go
Normal file
25
middleware/requirelogin.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequireLogin(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if rvr := recover(); rvr != nil {
|
||||||
|
log.Println(rvr)
|
||||||
|
log.Println(string(debug.Stack()))
|
||||||
|
handlers.ErrorHandler(w, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
42
models/models.go
Normal file
42
models/models.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotImplemented gets thrown if some action was not attempted,
|
||||||
|
// because it is not implemented in the code yet.
|
||||||
|
ErrNotImplemented = errors.New("Not implemented")
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a User of the system which is able to log in
|
||||||
|
type User struct {
|
||||||
|
gorm.Model
|
||||||
|
Username string
|
||||||
|
HashedPassword []byte
|
||||||
|
IsAdmin bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPassword sets the password of an user struct, but does not save it yet
|
||||||
|
func (u *User) SetPassword(password string) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPassword compares a supplied plain text password with the internally
|
||||||
|
// stored password hash, returns error=nil on success.
|
||||||
|
func (u *User) CheckPassword(password string) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConf represent the OpenVPN client configuration
|
||||||
|
type ClientConf struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string
|
||||||
|
User User
|
||||||
|
Cert []byte
|
||||||
|
PublicKey []byte
|
||||||
|
PrivateKey []byte
|
||||||
|
}
|
92
router/router.go
Normal file
92
router/router.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/assets"
|
||||||
|
"git.klink.asia/paul/certman/handlers"
|
||||||
|
"git.klink.asia/paul/certman/views"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
|
mw "git.klink.asia/paul/certman/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TODO: make this configurable
|
||||||
|
csrfCookieName = "csrf"
|
||||||
|
csrfFieldName = "csrf_token"
|
||||||
|
csrfKey = []byte("7Oj4DllZ9lTsxJnisTuWiiQBGQIzi6gX")
|
||||||
|
cookieKey = []byte("osx70sMD8HZG2ouUl8uKI4wcMugiJ2WH")
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleRoutes(db *gorm.DB) http.Handler {
|
||||||
|
mux := chi.NewMux()
|
||||||
|
|
||||||
|
// mux.Use(middleware.RequestID)
|
||||||
|
mux.Use(middleware.Logger)
|
||||||
|
mux.Use(middleware.RealIP)
|
||||||
|
mux.Use(middleware.RedirectSlashes)
|
||||||
|
mux.Use(mw.Recoverer)
|
||||||
|
|
||||||
|
// we are serving the static files directly from the assets package
|
||||||
|
fileServer(mux, "/static", assets.Assets)
|
||||||
|
|
||||||
|
mux.Route("/", func(r chi.Router) {
|
||||||
|
if os.Getenv("ENVIRONMENT") != "test" {
|
||||||
|
r.Use(csrf.Protect(
|
||||||
|
csrfKey,
|
||||||
|
csrf.Secure(false),
|
||||||
|
csrf.CookieName(csrfCookieName),
|
||||||
|
csrf.FieldName(csrfFieldName),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
view := views.New(req)
|
||||||
|
view.Render(w, "debug")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/login", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
view := views.New(req)
|
||||||
|
view.Render(w, "login")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/certs", handlers.ListCertHandler(db))
|
||||||
|
r.HandleFunc("/certs/new", handlers.GenCertHandler(db))
|
||||||
|
|
||||||
|
r.HandleFunc("/500", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
panic("500")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// what should happen if no route matches
|
||||||
|
mux.NotFound(handlers.NotFoundHandler)
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileServer sets up a http.FileServer handler to serve
|
||||||
|
// static files from a http.FileSystem.
|
||||||
|
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
||||||
|
if strings.ContainsAny(path, "{}*") {
|
||||||
|
panic("FileServer does not permit URL parameters.")
|
||||||
|
}
|
||||||
|
|
||||||
|
//fs := http.StripPrefix(path, http.FileServer(root))
|
||||||
|
fs := http.FileServer(root)
|
||||||
|
|
||||||
|
if path != "/" && path[len(path)-1] != '/' {
|
||||||
|
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
path += "*"
|
||||||
|
|
||||||
|
r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
}
|
68
views/funcs.go
Normal file
68
views/funcs.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var funcs = template.FuncMap{
|
||||||
|
"assetURL": assetURLFn,
|
||||||
|
"lower": lower,
|
||||||
|
"upper": upper,
|
||||||
|
"date": dateFn,
|
||||||
|
"humanDate": readableDateFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
func lower(input string) string {
|
||||||
|
return strings.ToLower(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upper(input string) string {
|
||||||
|
return strings.ToUpper(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assetURLFn(input string) string {
|
||||||
|
url := "/static/" //os.Getenv("ASSET_URL")
|
||||||
|
return fmt.Sprintf("%s%s", url, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateFn(format string, input interface{}) string {
|
||||||
|
var t time.Time
|
||||||
|
switch date := input.(type) {
|
||||||
|
default:
|
||||||
|
t = time.Now()
|
||||||
|
case time.Time:
|
||||||
|
t = date
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readableDateFn(t time.Time) string {
|
||||||
|
if time.Now().Before(t) {
|
||||||
|
return "in the future"
|
||||||
|
}
|
||||||
|
diff := time.Now().Sub(t)
|
||||||
|
day := 24 * time.Hour
|
||||||
|
month := 30 * day
|
||||||
|
year := 12 * month
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case diff < time.Second:
|
||||||
|
return "just now"
|
||||||
|
case diff < 5*time.Minute:
|
||||||
|
return "a few minutes ago"
|
||||||
|
case diff < time.Hour:
|
||||||
|
return fmt.Sprintf("%d minutes ago", diff/time.Minute)
|
||||||
|
case diff < day:
|
||||||
|
return fmt.Sprintf("%d hours ago", diff/time.Hour)
|
||||||
|
case diff < month:
|
||||||
|
return fmt.Sprintf("%d days ago", diff/day)
|
||||||
|
case diff < year:
|
||||||
|
return fmt.Sprintf("%d months ago", diff/month)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d years ago", diff/year)
|
||||||
|
}
|
||||||
|
}
|
88
views/templates.go
Normal file
88
views/templates.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// map of all parsed templates, by template name
|
||||||
|
var templates map[string]*template.Template
|
||||||
|
|
||||||
|
// LoadTemplates initializes the templates map, parsing all defined templates.
|
||||||
|
func LoadTemplates() {
|
||||||
|
templates = map[string]*template.Template{
|
||||||
|
"401": newTemplate("layouts/application.gohtml", "errors/401.gohtml"),
|
||||||
|
"404": newTemplate("layouts/application.gohtml", "errors/404.gohtml"),
|
||||||
|
"500": newTemplate("layouts/application.gohtml", "errors/500.gohtml"),
|
||||||
|
|
||||||
|
"debug": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/debug.gohtml"),
|
||||||
|
"login": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/login.gohtml"),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTemplate returns a new template from the assets
|
||||||
|
func newTemplate(filenames ...string) *template.Template {
|
||||||
|
f := []string{}
|
||||||
|
prefix := "/templates"
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
f = append(f, filepath.Join(prefix, filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseTemplate := template.New("base").Funcs(funcs)
|
||||||
|
tmpl, err := parseAssets(baseTemplate, assets.Assets, f...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not parse template: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpl
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAssets is a helper function to generate a template from multiple
|
||||||
|
// assets. If the argument template is nil, it is created from the first
|
||||||
|
// parameter that is passed (first file).
|
||||||
|
func parseAssets(t *template.Template, fs http.FileSystem, assets ...string) (*template.Template, error) {
|
||||||
|
if len(assets) == 0 {
|
||||||
|
// Not really a problem, but be consistent.
|
||||||
|
return nil, fmt.Errorf("no templates supplied in call to parseAssets")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range assets {
|
||||||
|
f, err := fs.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(f)
|
||||||
|
s := buf.String()
|
||||||
|
|
||||||
|
name := filepath.Base(filename)
|
||||||
|
// First template becomes return value if not already defined,
|
||||||
|
// and we use that one for subsequent New calls to associate
|
||||||
|
// all the templates together.
|
||||||
|
var tmpl *template.Template
|
||||||
|
if t == nil {
|
||||||
|
t = template.New(name)
|
||||||
|
}
|
||||||
|
if name == t.Name() {
|
||||||
|
tmpl = t
|
||||||
|
} else {
|
||||||
|
tmpl = t.New(name)
|
||||||
|
}
|
||||||
|
_, err = tmpl.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
83
views/views.go
Normal file
83
views/views.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type View struct {
|
||||||
|
Vars map[string]interface{}
|
||||||
|
Request *http.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(req *http.Request) *View {
|
||||||
|
return &View{
|
||||||
|
Request: req,
|
||||||
|
Vars: map[string]interface{}{
|
||||||
|
"CSRF_TOKEN": csrf.Token(req),
|
||||||
|
"csrfField": csrf.TemplateField(req),
|
||||||
|
"Meta": map[string]interface{}{
|
||||||
|
"Path": req.URL.Path,
|
||||||
|
"Env": "develop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) Render(w http.ResponseWriter, name string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
t, err := GetTemplate(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("the template '%s' does not exist.", name)
|
||||||
|
view.RenderError(w, 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
t.Execute(w, view.Vars)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) RenderError(w http.ResponseWriter, status int) {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
name = "404"
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
name = "401"
|
||||||
|
case http.StatusForbidden:
|
||||||
|
name = "403"
|
||||||
|
default:
|
||||||
|
name = "500"
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := GetTemplate(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("the error template '%s' does not exist.", name)
|
||||||
|
fmt.Fprintf(w, "Error page for status '%d' could not be rendered.", status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
t.Execute(w, view.Vars)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemplate returns a parsed template. The template ,ap needs to be
|
||||||
|
// Initialized by calling `LoadTemplates()` first.
|
||||||
|
func GetTemplate(name string) (*template.Template, error) {
|
||||||
|
if tmpl, ok := templates[name]; ok {
|
||||||
|
return tmpl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Template not found")
|
||||||
|
}
|
Loading…
Reference in a new issue