Initial commit
commit
080c1a3d06
@ -0,0 +1,3 @@
|
|||||||
|
out
|
||||||
|
examples/*
|
||||||
|
!examples/service.toml
|
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2019 eater
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
@ -0,0 +1,40 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZeroooConfig struct {
|
||||||
|
Location string `toml:"location"`
|
||||||
|
Endpoint string `toml:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Zerooo ZeroooConfig `toml:"zerooo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
config := Config{}
|
||||||
|
_, err := toml.DecodeFile(path, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateConfig(path string, cfg *Config) {
|
||||||
|
conf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
Check(err, "Couldn't open %q, err: %s", path, err)
|
||||||
|
encoder := toml.NewEncoder(conf)
|
||||||
|
log.Printf("ZeroooConfig: %#v", cfg)
|
||||||
|
err = encoder.Encode(cfg)
|
||||||
|
if cfg.Zerooo.Location[0] != '/' {
|
||||||
|
cfg.Zerooo.Location = filepath.Join(filepath.Dir(path), cfg.Zerooo.Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
Check(err, "Couldn't parse %q, err: %s", path, err)
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
[zerooo]
|
||||||
|
location = "/etc/zerooo"
|
||||||
|
endpoint = "https://zer.ooo"
|
@ -0,0 +1,217 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/blowfish"
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OK Status = iota
|
||||||
|
WAITING
|
||||||
|
ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseMessage struct {
|
||||||
|
Signature string
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
manager *Manager
|
||||||
|
status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCSRRequest struct {
|
||||||
|
*BaseMessage
|
||||||
|
Hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCSRResponse struct {
|
||||||
|
*BaseMessage
|
||||||
|
CSR string `json:"csr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateOpenVPNConfigRequest struct {
|
||||||
|
*BaseMessage
|
||||||
|
Config string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliverCertificateRequest struct {
|
||||||
|
*BaseMessage
|
||||||
|
Certificate string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpServer(manager *Manager) *httpServer {
|
||||||
|
return &httpServer{manager, WAITING}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *httpServer) Start() {
|
||||||
|
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
writer.Write([]byte("Don't"))
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/create-csr", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
req := &CreateCSRRequest{}
|
||||||
|
err := it.verifyRequest(request, req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := it.manager.CreateCSR(req.Hostname)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it.writeResponse(writer, CreateCSRResponse{
|
||||||
|
CSR: string(csr),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/deliver-crt", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
req := &DeliverCertificateRequest{}
|
||||||
|
err := it.verifyRequest(request, req)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = it.manager.UpdateCertificate(req.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it.manager.openVPN.Start()
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/update-openvpn-config", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
req := &UpdateOpenVPNConfigRequest{}
|
||||||
|
err := it.verifyRequest(request, req)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = it.manager.openVPN.UpdateConfig(req.Config)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it.manager.openVPN.Restart()
|
||||||
|
})
|
||||||
|
|
||||||
|
http.ListenAndServe(":7864", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *httpServer) writeResponse(writer http.ResponseWriter, v interface{}) error {
|
||||||
|
passwordAndIV := make([]byte, 48)
|
||||||
|
_, err := rand.Read(passwordAndIV)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encPasswordAndIV, err := rsa.EncryptPKCS1v15(rand.Reader, it.manager.CAPublicKey(), passwordAndIV)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.Write([]byte(hex.EncodeToString(encPasswordAndIV)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
password := passwordAndIV[:32]
|
||||||
|
iv := passwordAndIV[32:]
|
||||||
|
|
||||||
|
cipher, err := blowfish.NewSaltedCipher(password, iv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedFingerpint := sha256.Sum256(it.manager.GetServerFingerprint())
|
||||||
|
plainSignature, err := rsa.SignPKCS1v15(rand.Reader, it.manager.privateKey, 0, hashedFingerpint[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sign, ok := v.(*BaseMessage)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Given message can't be signed")
|
||||||
|
}
|
||||||
|
sign.Signature = hex.EncodeToString(plainSignature)
|
||||||
|
|
||||||
|
body, err := json.Marshal(v)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encBody := make([]byte, len(body))
|
||||||
|
cipher.Encrypt(encBody, body)
|
||||||
|
writer.Write([]byte(hex.EncodeToString(encBody)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *httpServer) verifyRequest(r *http.Request, v interface{}) (error) {
|
||||||
|
all, err := ioutil.ReadAll(hex.NewDecoder(r.Body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encPasswordAndIV := all[:256]
|
||||||
|
encBody := all[256:]
|
||||||
|
|
||||||
|
passwordAndIV, err := rsa.DecryptPKCS1v15(rand.Reader, it.manager.privateKey, encPasswordAndIV)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
password := passwordAndIV[:32]
|
||||||
|
iv := passwordAndIV[32:]
|
||||||
|
|
||||||
|
cipher, err := blowfish.NewSaltedCipher(password, iv)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := make([]byte, len(encBody))
|
||||||
|
cipher.Decrypt(body, encBody)
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, v)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseReq, ok := v.(*BaseMessage)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("non-base request")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := hex.DecodeString(baseReq.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint := it.manager.GetServerFingerprint()
|
||||||
|
fingerprintHashed := sha256.Sum256(fingerprint)
|
||||||
|
err = rsa.VerifyPKCS1v15(it.manager.CAPublicKey(), 0, fingerprintHashed[:], signature)
|
||||||
|
return err
|
||||||
|
}
|
@ -0,0 +1,339 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
Config Config
|
||||||
|
privateKey *rsa.PrivateKey
|
||||||
|
openVPN *OpenVPN
|
||||||
|
CA *x509.Certificate
|
||||||
|
HasCertificate bool
|
||||||
|
HasOpenVPNConfig bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(cfg Config) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(s string) bool {
|
||||||
|
if _, err := os.Stat(s); err == nil {
|
||||||
|
return true
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
Check(err, "Failed to check %q, err: %s", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetOpenVPNConfigLocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/server.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetCALocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/ca.crt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetCertificateLocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/server.crt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetKeyLocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/server.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetDHLocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/dh2048.pem"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetCRLLocation() string {
|
||||||
|
return it.Config.Zerooo.Location + "/crl.pem"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) EnsureFile(path string, generator func()) bool {
|
||||||
|
if !fileExists(path) {
|
||||||
|
generator()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func (it *Manager) Init() {
|
||||||
|
if !fileExists(it.Config.Zerooo.Location) {
|
||||||
|
os.MkdirAll(it.Config.Zerooo.Location, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
generated := it.EnsureFile(it.GetKeyLocation(), func() {
|
||||||
|
it.GenerateKey()
|
||||||
|
})
|
||||||
|
|
||||||
|
if !generated {
|
||||||
|
it.LoadKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
it.EnsureFile(it.GetCALocation(), func() {
|
||||||
|
it.DownloadCA()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.LoadCA()
|
||||||
|
|
||||||
|
it.EnsureFile(it.GetCRLLocation(), func() {
|
||||||
|
it.DownloadCRL()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.EnsureFile(it.GetDHLocation(), func() {
|
||||||
|
it.DownloadDH()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetServerFingerprint() []byte {
|
||||||
|
der := it.ExportPublicKeyDER()
|
||||||
|
fp := it.GetOpenSSLLikeFingerprint(der)
|
||||||
|
return []byte(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GetOpenSSLLikeFingerprint(input []byte) string {
|
||||||
|
output := md5.Sum(input)
|
||||||
|
items := make([]string, 16)
|
||||||
|
for i := range output {
|
||||||
|
items[i] = strings.ToLower(hex.EncodeToString([]byte{output[i]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(items, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) Register() {
|
||||||
|
fp := it.GetServerFingerprint()
|
||||||
|
|
||||||
|
log.Printf("%s\n", fp)
|
||||||
|
|
||||||
|
signed, err := rsa.SignPKCS1v15(rand.Reader, it.privateKey, 0, fp)
|
||||||
|
Check(err, "Failed signing fingerprint, err: %s", err)
|
||||||
|
|
||||||
|
signedHash := sha256.Sum256(signed)
|
||||||
|
|
||||||
|
publicKey := string(it.ExportPublicKey())
|
||||||
|
hexSignature := hex.EncodeToString(signedHash[:])
|
||||||
|
|
||||||
|
log.Printf("Registering with\n\nsignature: %s\npublic key:\n\n%s", hexSignature, publicKey)
|
||||||
|
|
||||||
|
resp, err := http.PostForm(it.Config.Zerooo.Endpoint+"/server/register", url.Values{
|
||||||
|
"publicKey": []string{publicKey},
|
||||||
|
"signature": []string{hexSignature},
|
||||||
|
})
|
||||||
|
|
||||||
|
Check(err, "Failed to sent register post to endpoint (%s/server/register), err: %s", it.Config.Zerooo.Endpoint, err)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
Error("Endpoint responded with %d, may not be actual zerooo endpoint", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
Check(err, "Failed to read response, err: %s", err)
|
||||||
|
|
||||||
|
strBody := string(body)
|
||||||
|
items := strings.SplitN(strBody, "\n", 2)
|
||||||
|
code, err := strconv.ParseInt(items[0], 10, 64)
|
||||||
|
Check(err, "Failed reading response success code, err: %s", err)
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
Error("Register failed, server responded with:\n\n%s", items[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Register succesful, server responded with: %s", items[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) ExportPublicKeyDER() []byte {
|
||||||
|
if (it.privateKey == nil) {
|
||||||
|
Error("No private key is loaded, can't export public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.MarshalPKIXPublicKey(&it.privateKey.PublicKey)
|
||||||
|
Check(err, "Failed converting public key to DER, err: %s", err)
|
||||||
|
return der
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) ExportPublicKey() []byte {
|
||||||
|
pemKey := &pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: it.ExportPublicKeyDER(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(pemKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) GenerateKey() {
|
||||||
|
log.Printf("No key exists, generating new 4096 RSA key")
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
Check(err, "Couldn't generate key, err: %s", err)
|
||||||
|
it.privateKey = priv
|
||||||
|
|
||||||
|
pemKey := &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||||
|
}
|
||||||
|
|
||||||
|
keyFile, err := os.OpenFile(it.GetKeyLocation(), os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
Check(err, "Failed creating file at %q for private key, err: %s", it.GetKeyLocation(), err)
|
||||||
|
defer keyFile.Close()
|
||||||
|
|
||||||
|
err = pem.Encode(keyFile, pemKey)
|
||||||
|
Check(err, "Failed encoding private key, err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) LoadKey() {
|
||||||
|
log.Printf("Loading generated key")
|
||||||
|
|
||||||
|
keyFile, err := os.Open(it.GetKeyLocation())
|
||||||
|
Check(err, "Failed opening private key at %q for reading, err: %s", it.GetKeyLocation(), err)
|
||||||
|
defer keyFile.Close()
|
||||||
|
|
||||||
|
stat, _ := keyFile.Stat()
|
||||||
|
var size int64 = stat.Size()
|
||||||
|
pembytes := make([]byte, size)
|
||||||
|
buffer := bufio.NewReader(keyFile)
|
||||||
|
_, err = buffer.Read(pembytes)
|
||||||
|
Check(err, "Failed to read private key at %q, err: %s", it.GetKeyLocation(), err)
|
||||||
|
|
||||||
|
data, _ := pem.Decode([]byte(pembytes))
|
||||||
|
if data == nil {
|
||||||
|
Error("No valid key found at %q", it.GetKeyLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
importedKey, err := x509.ParsePKCS1PrivateKey(data.Bytes)
|
||||||
|
Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err)
|
||||||
|
it.privateKey = importedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) Daemon() {
|
||||||
|
server := NewHttpServer(it)
|
||||||
|
go server.Start()
|
||||||
|
|
||||||
|
it.openVPN = NewOpenVPN(it)
|
||||||
|
|
||||||
|
it.openVPN.Start()
|
||||||
|
|
||||||
|
ch := make(chan os.Signal)
|
||||||
|
|
||||||
|
signal.Notify(ch, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT)
|
||||||
|
|
||||||
|
for sig := range ch {
|
||||||
|
log.Printf("Received signal %s, quitting.", sig)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) LoadCA() {
|
||||||
|
log.Printf("Loading CA certificate")
|
||||||
|
|
||||||
|
keyFile, err := os.Open(it.GetCALocation())
|
||||||
|
Check(err, "Failed opening CA certificate at %q for reading, err: %s", it.GetKeyLocation(), err)
|
||||||
|
defer keyFile.Close()
|
||||||
|
|
||||||
|
stat, _ := keyFile.Stat()
|
||||||
|
var size int64 = stat.Size()
|
||||||
|
pemBytes := make([]byte, size)
|
||||||
|
buffer := bufio.NewReader(keyFile)
|
||||||
|
_, err = buffer.Read(pemBytes)
|
||||||
|
Check(err, "Failed to read CA certificate at %q, err: %s", it.GetKeyLocation(), err)
|
||||||
|
|
||||||
|
data, _ := pem.Decode([]byte(pemBytes))
|
||||||
|
if data == nil {
|
||||||
|
Error("No valid CA certificate found at %q", it.GetKeyLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, err := x509.ParseCertificate(data.Bytes)
|
||||||
|
Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err)
|
||||||
|
it.CA = ca
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) DownloadCA() {
|
||||||
|
caUrl := it.Config.Zerooo.Endpoint + "/ca"
|
||||||
|
download("CA", it.GetCALocation(), caUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) CAPublicKey() (*rsa.PublicKey) {
|
||||||
|
key, ok := it.CA.PublicKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
Error("Can't read CA Certificate as public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) CreateCSR(hostname string) ([]byte, error) {
|
||||||
|
tpl := x509.CertificateRequest{Subject: pkix.Name{CommonName: hostname}}
|
||||||
|
csr, err := x509.CreateCertificateRequest(rand.Reader, &tpl, it.privateKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemCSR := &pem.Block{
|
||||||
|
Type: "CERTIFICATE REQUEST",
|
||||||
|
Bytes: csr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(pemCSR), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) UpdateCertificate(s string) error {
|
||||||
|
f, err := os.OpenFile(it.GetCertificateLocation(), os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(name string, path string, url string) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
Check(err, "Failed fetching %s at %q, err: %s", name, url, err)
|
||||||
|
cert, err := ioutil.ReadAll(resp.Body)
|
||||||
|
Check(err, "Failed fetching %s at %q, err: %s", name, url, err)
|
||||||
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
Check(err, "Failed open %s for writing at %q, err: %s", name, path, err)
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.Write(cert)
|
||||||
|
Check(err, "Failed writing %s at %q, err: %s", name, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) DownloadCRL() {
|
||||||
|
crlUrl := it.Config.Zerooo.Endpoint + "/crl"
|
||||||
|
download("CRL", it.GetCRLLocation(), crlUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) DownloadDH() {
|
||||||
|
dhUrl := it.Config.Zerooo.Endpoint + "/dh.pem"
|
||||||
|
download("DH", it.GetDHLocation(), dhUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *Manager) IsOpenVPNReady() bool {
|
||||||
|
hasConfig := fileExists(it.GetOpenVPNConfigLocation())
|
||||||
|
hasCert := fileExists(it.GetCertificateLocation())
|
||||||
|
return hasConfig && hasCert
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenVPN struct {
|
||||||
|
Config *ZeroooConfig
|
||||||
|
Manager *Manager
|
||||||
|
shouldStop bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenVPN(manager *Manager) *OpenVPN {
|
||||||
|
return &OpenVPN{
|
||||||
|
Config: &manager.Config.Zerooo,
|
||||||
|
Manager: manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *OpenVPN) Stop() {
|
||||||
|
it.shouldStop = true
|
||||||
|
if it.cmd != nil && it.cmd.Process != nil {
|
||||||
|
it.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *OpenVPN) GetConfigLocation() string {
|
||||||
|
return it.Config.Location + "/server.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *OpenVPN) Restart() {
|
||||||
|
if it.cmd != nil && it.cmd.Process != nil {
|
||||||
|
it.cmd.Process.Kill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *OpenVPN) Start() {
|
||||||
|
if !it.Manager.IsOpenVPNReady() {
|
||||||
|
log.Printf("OpenVPN not ready yet.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Starting OpenVPN")
|
||||||
|
it.cmd = exec.Command("openvpn", "--config", it.GetConfigLocation())
|
||||||
|
it.cmd.Start()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
it.cmd.Wait()
|
||||||
|
if it.shouldStop {
|
||||||
|
log.Printf("OpenVPN has stopped (exit code: %d)", it.cmd.ProcessState.ExitCode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("OpenVPN has stopped (exit code: %d), restarting", it.cmd.ProcessState.ExitCode())
|
||||||
|
it.Start()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *OpenVPN) UpdateConfig(s string) error {
|
||||||
|
f, err := os.OpenFile(it.GetConfigLocation(), os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString(s)
|
||||||
|
return err
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Check(err error, msg string, v ...interface{}) {
|
||||||
|
if err != nil {
|
||||||
|
Error(msg, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string, v ...interface{}) {
|
||||||
|
log.Fatalf(msg+"\n", v...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cijber.net/zer.ooo/service"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var daemonCmd = &cobra.Command{
|
||||||
|
Use: "daemon",
|
||||||
|
Short: "Starts the zer.ooo service daemon",
|
||||||
|
Long: "Will start running the zer.ooo service daemon, keeping OpenVPN up",
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
config, err := service.LoadConfig(ConfigPath)
|
||||||
|
service.Check(err, "Failed loading config, err: %s", err)
|
||||||
|
mgmt := service.NewManager(*config)
|
||||||
|
mgmt.Init()
|
||||||
|
mgmt.Daemon()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cijber.net/zer.ooo/service"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
endpoint string
|
||||||
|
location string
|
||||||
|
)
|
||||||
|
|
||||||
|
var initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialises an zer.ooo server install",
|
||||||
|
Long: "Init (zerooo init) will initialise a server",
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
config, err := service.LoadConfig(ConfigPath)
|
||||||
|
service.Check(err, "Failed reading config, err: %s", err)
|
||||||
|
|
||||||
|
if (cmd.Flag("endpoint").Changed) {
|
||||||
|
config.Zerooo.Endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.Flag("location").Changed) {
|
||||||
|
if location[0] != '/' {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
service.Check(err, "Can't get current working directory, err: %s", err)
|
||||||
|
location = filepath.Join(cwd, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Zerooo.Location = location
|
||||||
|
}
|
||||||
|
|
||||||
|
service.CreateConfig(ConfigPath, config)
|
||||||
|
mgmt := service.NewManager(*config)
|
||||||
|
mgmt.Init()
|
||||||
|
mgmt.Register()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
f := initCmd.Flags()
|
||||||
|
f.StringVar(&endpoint, "endpoint", "https://zer.ooo", "Endpoint to register this server")
|
||||||
|
f.StringVar(&location, "location", "/etc/zerooo", "Location of OpenVPN installation")
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2019 eater
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ConfigPath string
|
||||||
|
|
||||||
|
// rootCmd represents the base command when called without any subcommands
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "zerooo",
|
||||||
|
Short: "",
|
||||||
|
Long: `Zer.ooo service manager`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVar(&ConfigPath, "config", "/etc/zerooo/service.toml", "config file")
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
initCmd,
|
||||||
|
daemonCmd,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "git.cijber.net/zer.ooo/service/zerooo/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
Loading…
Reference in New Issue