add option to specify custom user template path

This commit is contained in:
Andrii Veklychev 2021-07-22 01:06:34 +03:00
parent 05d7462a79
commit fa9022ee1b
3 changed files with 320 additions and 282 deletions

24
.editorconfig Normal file
View File

@ -0,0 +1,24 @@
; https://editorconfig.org/
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
eclint_indent_style = unset
[Dockerfile]
indent_size = 4

View File

@ -94,6 +94,9 @@ Flags:
path to easyrsa index file.
--ccd Enable client-config-dir.
--ccd.path="./ccd" path to client-config-dir
--templates.clientconfig-path=""
path to custom client.config.tpl file
--templates.ccd-path="" path to custom ccd.tpl file
--auth.password Enable additional password authorization.
--auth.db="./easyrsa/pki/users.db"
Database path fort password authorization.

133
main.go
View File

@ -5,9 +5,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/alecthomas/kingpin.v2"
"log"
"net"
"net/http"
@ -19,6 +16,10 @@ import (
"time"
"github.com/gobuffalo/packr/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/alecthomas/kingpin.v2"
)
const (
@ -32,26 +33,28 @@ const (
indexTxtDateLayout = "060102150405Z"
stringDateFormat = "2006-01-02 15:04:05"
ovpnStatusDateLayout = "2006-01-02 15:04:05"
version = "1.6.2"
version = "1.6.3"
)
var (
listenHost = kingpin.Flag("listen.host","host for ovpn-admin").Default("0.0.0.0").String()
listenPort = kingpin.Flag("listen.port","port for ovpn-admin").Default("8080").String()
serverRole = kingpin.Flag("role","server role master or slave").Default("master").HintOptions("master", "slave").String()
masterHost = kingpin.Flag("master.host","url for master server").Default("http://127.0.0.1").String()
masterBasicAuthUser = kingpin.Flag("master.basic-auth.user","user for basic auth on master server url").Default("").String()
masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password","password for basic auth on master server url").Default("").String()
listenHost = kingpin.Flag("listen.host", "host for ovpn-admin").Default("0.0.0.0").String()
listenPort = kingpin.Flag("listen.port", "port for ovpn-admin").Default("8080").String()
serverRole = kingpin.Flag("role", "server role master or slave").Default("master").HintOptions("master", "slave").String()
masterHost = kingpin.Flag("master.host", "url for master server").Default("http://127.0.0.1").String()
masterBasicAuthUser = kingpin.Flag("master.basic-auth.user", "user for basic auth on master server url").Default("").String()
masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password", "password for basic auth on master server url").Default("").String()
masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds.").Default("600").Int()
masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("VerySecureToken").PlaceHolder("TOKEN").String()
openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String()
openvpnServer = kingpin.Flag("ovpn.server","comma separated addresses for openvpn servers").Default("127.0.0.1:7777:tcp").PlaceHolder("HOST:PORT:PROTOCOL").Strings()
mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=address) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings()
openvpnNetwork = kingpin.Flag("ovpn.network", "network for openvpn server").Default("172.16.100.0/24").String()
openvpnServer = kingpin.Flag("ovpn.server", "comma separated addresses for openvpn servers").Default("127.0.0.1:7777:tcp").PlaceHolder("HOST:PORT:PROTOCOL").Strings()
mgmtAddress = kingpin.Flag("mgmt", "comma separated (alias=address) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings()
metricsPath = kingpin.Flag("metrics.path", "URL path for surfacing collected metrics").Default("/metrics").String()
easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("./easyrsa/").String()
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("./easyrsa/pki/index.txt").String()
ccdEnabled = kingpin.Flag("ccd", "Enable client-config-dir.").Default("false").Bool()
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("./ccd").String()
clientConfigTemplatePath = kingpin.Flag("templates.clientconfig-path", "path to custom client.conf.tpl").Default("").String()
ccdTemplatePath = kingpin.Flag("templates.ccd-path", "path to custom ccd.tpl").Default("").String()
authByPassword = kingpin.Flag("auth.password", "Enable additional password authorization.").Default("false").Bool()
authDatabase = kingpin.Flag("auth.db", "Database path fort password authorization.").Default("./easyrsa/pki/users.db").String()
debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool()
@ -59,11 +62,9 @@ var (
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
)
var (
ovpnServerCertExpire = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "ovpn_server_cert_expire",
Help: "openvpn server certificate expire time in days",
@ -134,7 +135,6 @@ var (
},
[]string{"client"},
)
)
type OvpnAdmin struct {
@ -271,7 +271,7 @@ func (oAdmin *OvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *htt
return
}
} else {
http.Error(w, `{"status":"error"}`, http.StatusNotImplemented )
http.Error(w, `{"status":"error"}`, http.StatusNotImplemented)
}
}
@ -283,7 +283,7 @@ func (oAdmin *OvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Re
func (oAdmin *OvpnAdmin) userDisconnectHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
fmt.Fprintf(w, "%s", r.FormValue("username"))
}
@ -323,7 +323,7 @@ func (oAdmin *OvpnAdmin) userApplyCcdHandler(w http.ResponseWriter, r *http.Requ
func (oAdmin *OvpnAdmin) serverSettingsHandler(w http.ResponseWriter, r *http.Request) {
enabledModules, enabledModulesErr := json.Marshal(oAdmin.modules)
if enabledModulesErr != nil {
log.Printf("ERROR: %s\n",enabledModulesErr)
log.Printf("ERROR: %s\n", enabledModulesErr)
}
fmt.Fprintf(w, `{"status":"ok", "serverRole": "%s", "modules": %s }`, oAdmin.role, string(enabledModules))
}
@ -350,8 +350,8 @@ func (oAdmin *OvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.Req
}
archiveCerts()
w.Header().Set("Content-Disposition", "attachment; filename=" + certsArchiveFileName)
http.ServeFile(w,r, certsArchivePath)
w.Header().Set("Content-Disposition", "attachment; filename="+certsArchiveFileName)
http.ServeFile(w, r, certsArchivePath)
}
func (oAdmin *OvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) {
@ -368,15 +368,14 @@ func (oAdmin *OvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Reque
}
archiveCcd()
w.Header().Set("Content-Disposition", "attachment; filename=" + ccdArchiveFileName)
http.ServeFile(w,r, ccdArchivePath)
w.Header().Set("Content-Disposition", "attachment; filename="+ccdArchiveFileName)
http.ServeFile(w, r, ccdArchivePath)
}
func main() {
kingpin.Version(version)
kingpin.Parse()
ovpnAdmin := new(OvpnAdmin)
ovpnAdmin.lastSyncTime = "unknown"
ovpnAdmin.role = *serverRole
@ -388,7 +387,7 @@ func main() {
ovpnAdmin.mgmtInterfaces = make(map[string]string)
for _, mgmtInterface := range *mgmtAddress {
parts := strings.SplitN(mgmtInterface, "=",2)
parts := strings.SplitN(mgmtInterface, "=", 2)
ovpnAdmin.mgmtInterfaces[parts[0]] = parts[len(parts)-1]
}
@ -447,7 +446,7 @@ func main() {
})
fmt.Println("Bind: http://" + *listenHost + ":" + *listenPort)
log.Fatal(http.ListenAndServe(*listenHost + ":" + *listenPort, nil))
log.Fatal(http.ListenAndServe(*listenHost+":"+*listenPort, nil))
}
func CacheControlWrapper(h http.Handler) http.Handler {
@ -524,16 +523,28 @@ func renderIndexTxt(data []indexTxtLine) string {
return indexTxt
}
func (oAdmin *OvpnAdmin) getClientConfigTemplate() *template.Template {
if *clientConfigTemplatePath != "" {
return template.Must(template.ParseFiles(*clientConfigTemplatePath))
} else {
clientConfigTpl, clientConfigTplErr := oAdmin.templates.FindString("client.conf.tpl")
if clientConfigTplErr != nil {
log.Println("ERROR: clientConfigTpl not found in templates box")
}
return template.Must(template.New("client-config").Parse(clientConfigTpl))
}
}
func (oAdmin *OvpnAdmin) renderClientConfig(username string) string {
if checkUserExist(username) {
var hosts []OpenvpnServer
for _, server := range *openvpnServer {
parts := strings.SplitN(server, ":",3)
parts := strings.SplitN(server, ":", 3)
hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1], Protocol: parts[2]})
}
if *debug {
log.Printf("WARNING: hosts for %s\n %v", username, hosts )
log.Printf("WARNING: hosts for %s\n %v", username, hosts)
}
conf := openvpnClientConfig{}
@ -544,18 +555,14 @@ func (oAdmin *OvpnAdmin) renderClientConfig(username string) string {
conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key")
conf.PasswdAuth = *authByPassword
clientConfigTpl, clientConfigTplErr := oAdmin.templates.FindString("client.conf.tpl")
if clientConfigTplErr != nil {
log.Println("ERROR: clientConfigTpl not found in templates box")
}
t := oAdmin.getClientConfigTemplate()
t := template.Must(template.New("client-config").Parse(clientConfigTpl))
var tmp bytes.Buffer
err := t.Execute(&tmp, conf)
if err != nil {
log.Printf("WARNING: something goes wrong during rendering config for %s", username )
log.Printf("WARNING: something goes wrong during rendering config for %s", username)
if *debug {
log.Printf("ERROR: rendering config for %s failed \n %v", username, err )
log.Printf("ERROR: rendering config for %s failed \n %v", username, err)
}
}
@ -568,13 +575,25 @@ func (oAdmin *OvpnAdmin) renderClientConfig(username string) string {
return fmt.Sprintf("User \"%s\" not found", username)
}
func (oAdmin *OvpnAdmin) getCcdTemplate() *template.Template {
if *ccdTemplatePath != "" {
return template.Must(template.ParseFiles(*ccdTemplatePath))
} else {
ccdTpl, ccdTplErr := oAdmin.templates.FindString("ccd.tpl")
if ccdTplErr != nil {
log.Printf("ERROR: ccdTpl not found in templates box")
}
return template.Must(template.New("ccd").Parse(ccdTpl))
}
}
func (oAdmin *OvpnAdmin) parseCcd(username string) Ccd {
ccd := Ccd{}
ccd.User = username
ccd.ClientAddress = "dynamic"
ccd.CustomRoutes = []ccdRoute{}
txtLinesArray := strings.Split(fRead(*ccdDir + "/" + username), "\n")
txtLinesArray := strings.Split(fRead(*ccdDir+"/"+username), "\n")
for _, v := range txtLinesArray {
str := strings.Fields(v)
@ -601,20 +620,13 @@ func (oAdmin *OvpnAdmin) modifyCcd(ccd Ccd) (bool, string) {
}
if ccdValid {
ccdTpl, ccdTplErr := oAdmin.templates.FindString("ccd.tpl")
if ccdTplErr != nil {
ccdErr = "ccdTpl not found in templates box"
log.Printf("ERROR: %s\n",ccdErr)
return false, ccdErr
}
t := template.Must(template.New("ccd").Parse(ccdTpl))
t := oAdmin.getCcdTemplate()
var tmp bytes.Buffer
tplErr := t.Execute(&tmp, ccd)
if tplErr != nil {
log.Println(tplErr)
}
fWrite(*ccdDir + "/" + ccd.User, tmp.String())
fWrite(*ccdDir+"/"+ccd.User, tmp.String())
return true, "ccd updated successfully"
}
}
@ -631,7 +643,7 @@ func validateCcd(ccd Ccd) (bool, string) {
log.Println(err)
}
if ! checkStaticAddressIsFree(ccd.ClientAddress, ccd.User) {
if !checkStaticAddressIsFree(ccd.ClientAddress, ccd.User) {
ccdErr = fmt.Sprintf("ClientAddress \"%s\" already assigned to another user", ccd.ClientAddress)
if *debug {
log.Printf("ERROR: Modify ccd for user %s: %s", ccd.User, ccdErr)
@ -647,7 +659,7 @@ func validateCcd(ccd Ccd) (bool, string) {
return false, ccdErr
}
if ! ovpnNet.Contains(net.ParseIP(ccd.ClientAddress)) {
if !ovpnNet.Contains(net.ParseIP(ccd.ClientAddress)) {
ccdErr = fmt.Sprintf("ClientAddress \"%s\" not belongs to openvpn server network", ccd.ClientAddress)
if *debug {
log.Printf("ERROR: Modify ccd for user %s: %s", ccd.User, ccdErr)
@ -791,7 +803,7 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
return false, ucErr
}
if ! validateUsername(username) {
if !validateUsername(username) {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
@ -832,7 +844,7 @@ func (oAdmin *OvpnAdmin) userChangePassword(username, password string) (bool, st
o := runBash(fmt.Sprintf("openvpn-user check --db.path %s --user %s | grep %s | wc -l", *authDatabase, username, username))
fmt.Println(o)
if ! validatePassword(password) {
if !validatePassword(password) {
ucpErr := fmt.Sprintf("Password for too short, password length must be greater or equal %d", passwordMinLength)
if *debug {
log.Printf("ERROR: userChangePassword: %s\n", ucpErr)
@ -924,7 +936,6 @@ func (oAdmin *OvpnAdmin) userUnrevoke(username string) string {
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
}
func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string {
buf := make([]byte, 32768)
bufLen, _ := conn.Read(buf)
@ -960,8 +971,8 @@ func (oAdmin *OvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []cli
userName := user[0]
userAddress := user[1]
userBytesReceived:= user[2]
userBytesSent:= user[3]
userBytesReceived := user[2]
userBytesSent := user[3]
userConnectedSince := user[4]
userStatus := clientStatus{CommonName: userName, RealAddress: userAddress, BytesReceived: userBytesReceived, BytesSent: userBytesSent, ConnectedSince: userConnectedSince, ConnectedTo: serverName}
@ -1029,7 +1040,7 @@ func (oAdmin *OvpnAdmin) downloadCerts() bool {
if fExist(certsArchivePath) {
fDelete(certsArchivePath)
}
err := fDownload(certsArchivePath, *masterHost + downloadCertsApiUrl + "?token=" + oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
err := fDownload(certsArchivePath, *masterHost+downloadCertsApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
if err != nil {
log.Println(err)
return false
@ -1043,7 +1054,7 @@ func (oAdmin *OvpnAdmin) downloadCcd() bool {
fDelete(ccdArchivePath)
}
err := fDownload(ccdArchivePath, *masterHost + downloadCcdApiUrl + "?token=" + oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
err := fDownload(ccdArchivePath, *masterHost+downloadCcdApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth)
if err != nil {
log.Println(err)
return false
@ -1053,24 +1064,24 @@ func (oAdmin *OvpnAdmin) downloadCcd() bool {
}
func archiveCerts() {
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *easyrsaDirPath + "/pki", certsArchivePath ))
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *easyrsaDirPath+"/pki", certsArchivePath))
fmt.Println(o)
}
func archiveCcd() {
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *ccdDir, ccdArchivePath ))
o := runBash(fmt.Sprintf("cd %s && tar -czf %s *", *ccdDir, ccdArchivePath))
fmt.Println(o)
}
func unArchiveCerts() {
runBash(fmt.Sprintf("mkdir -p %s", *easyrsaDirPath + "/pki"))
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *easyrsaDirPath + "/pki", certsArchivePath ))
runBash(fmt.Sprintf("mkdir -p %s", *easyrsaDirPath+"/pki"))
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *easyrsaDirPath+"/pki", certsArchivePath))
fmt.Println(o)
}
func unArchiveCcd() {
runBash(fmt.Sprintf("mkdir -p %s", *ccdDir))
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *ccdDir, ccdArchivePath ))
o := runBash(fmt.Sprintf("cd %s && tar -xzf %s", *ccdDir, ccdArchivePath))
fmt.Println(o)
}
@ -1134,11 +1145,11 @@ func getOvpnCaCertExpireDate() time.Time {
// https://community.openvpn.net/openvpn/ticket/623
func crlFix() {
err1 := os.Chmod(*easyrsaDirPath + "/pki", 0755)
err1 := os.Chmod(*easyrsaDirPath+"/pki", 0755)
if err1 != nil {
log.Println(err1)
}
err2 := os.Chmod(*easyrsaDirPath + "/pki/crl.pem", 0644)
err2 := os.Chmod(*easyrsaDirPath+"/pki/crl.pem", 0644)
if err2 != nil {
log.Println(err2)
}