2020-05-14 19:13:33 -04:00
package main
import (
2020-11-17 12:48:26 -05:00
"bufio"
2020-05-14 19:13:33 -04:00
"bytes"
2021-10-05 11:09:29 -04:00
"context"
2022-01-20 09:49:03 -05:00
"crypto/x509"
2020-11-17 12:48:26 -05:00
"encoding/json"
2022-01-20 09:49:03 -05:00
"encoding/pem"
2022-08-12 06:52:45 -04:00
"errors"
2020-05-14 19:13:33 -04:00
"fmt"
2022-07-21 11:17:53 -04:00
"github.com/google/uuid"
2022-01-20 09:49:03 -05:00
"io/ioutil"
2022-07-21 11:17:53 -04:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
2020-11-17 12:48:26 -05:00
"net"
"net/http"
2020-05-14 19:13:33 -04:00
"os"
"regexp"
2020-11-27 02:23:59 -05:00
"strconv"
2020-05-14 19:13:33 -04:00
"strings"
2022-08-02 10:19:27 -04:00
"sync"
2020-05-14 19:13:33 -04:00
"text/template"
2020-11-17 12:48:26 -05:00
"time"
2022-08-12 06:52:45 -04:00
"unicode/utf8"
2021-02-26 07:11:13 -05:00
"github.com/gobuffalo/packr/v2"
2021-07-21 18:06:34 -04:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
2021-12-30 03:24:44 -05:00
log "github.com/sirupsen/logrus"
2021-07-21 18:06:34 -04:00
"gopkg.in/alecthomas/kingpin.v2"
2020-11-17 12:48:26 -05:00
)
const (
2021-07-21 18:06:34 -04:00
usernameRegexp = ` ^([a-zA-Z0-9_.-@])+$ `
passwordMinLength = 6
2020-11-17 12:48:26 -05:00
certsArchiveFileName = "certs.tar.gz"
2021-07-21 18:06:34 -04:00
ccdArchiveFileName = "ccd.tar.gz"
indexTxtDateLayout = "060102150405Z"
stringDateFormat = "2006-01-02 15:04:05"
2021-12-07 10:12:57 -05:00
downloadCertsApiUrl = "api/data/certs/download"
downloadCcdApiUrl = "api/data/ccd/download"
2021-10-21 06:01:03 -04:00
kubeNamespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
2020-10-15 12:12:31 -04:00
)
var (
2021-12-30 03:24:44 -05:00
listenHost = kingpin . Flag ( "listen.host" , "host for ovpn-admin" ) . Default ( "0.0.0.0" ) . Envar ( "OVPN_LISTEN_HOST" ) . String ( )
listenPort = kingpin . Flag ( "listen.port" , "port for ovpn-admin" ) . Default ( "8080" ) . Envar ( "OVPN_LISTEN_PORT" ) . String ( )
2022-11-02 09:29:22 -04:00
listenBaseUrl = kingpin . Flag ( "listen.base-url" , "base url for ovpn-admin" ) . Default ( "/" ) . Envar ( "OVPN_LISTEN_BASE_URL" ) . String ( )
2021-12-30 03:24:44 -05:00
serverRole = kingpin . Flag ( "role" , "server role, master or slave" ) . Default ( "master" ) . Envar ( "OVPN_ROLE" ) . HintOptions ( "master" , "slave" ) . String ( )
masterHost = kingpin . Flag ( "master.host" , "URL for the master server" ) . Default ( "http://127.0.0.1" ) . Envar ( "OVPN_MASTER_HOST" ) . String ( )
masterBasicAuthUser = kingpin . Flag ( "master.basic-auth.user" , "user for master server's Basic Auth" ) . Default ( "" ) . Envar ( "OVPN_MASTER_USER" ) . String ( )
masterBasicAuthPassword = kingpin . Flag ( "master.basic-auth.password" , "password for master server's Basic Auth" ) . Default ( "" ) . Envar ( "OVPN_MASTER_PASSWORD" ) . String ( )
2021-10-22 00:35:16 -04:00
masterSyncFrequency = kingpin . Flag ( "master.sync-frequency" , "master host data sync frequency in seconds" ) . Default ( "600" ) . Envar ( "OVPN_MASTER_SYNC_FREQUENCY" ) . Int ( )
2021-10-21 06:01:03 -04:00
masterSyncToken = kingpin . Flag ( "master.sync-token" , "master host data sync security token" ) . Default ( "VerySecureToken" ) . Envar ( "OVPN_MASTER_TOKEN" ) . PlaceHolder ( "TOKEN" ) . String ( )
2021-12-30 03:24:44 -05:00
openvpnNetwork = kingpin . Flag ( "ovpn.network" , "NETWORK/MASK_PREFIX for OpenVPN server" ) . Default ( "172.16.100.0/24" ) . Envar ( "OVPN_NETWORK" ) . String ( )
openvpnServer = kingpin . Flag ( "ovpn.server" , "HOST:PORT:PROTOCOL for OpenVPN server; can have multiple values" ) . Default ( "127.0.0.1:7777:tcp" ) . Envar ( "OVPN_SERVER" ) . PlaceHolder ( "HOST:PORT:PROTOCOL" ) . Strings ( )
openvpnServerBehindLB = kingpin . Flag ( "ovpn.server.behindLB" , "enable if your OpenVPN server is behind Kubernetes Service having the LoadBalancer type" ) . Default ( "false" ) . Envar ( "OVPN_LB" ) . Bool ( )
2022-04-27 06:42:59 -04:00
openvpnServiceName = kingpin . Flag ( "ovpn.service" , "the name of Kubernetes Service having the LoadBalancer type if your OpenVPN server is behind it" ) . Default ( "openvpn-external" ) . Envar ( "OVPN_LB_SERVICE" ) . Strings ( )
2021-12-30 03:24:44 -05:00
mgmtAddress = kingpin . Flag ( "mgmt" , "ALIAS=HOST:PORT for OpenVPN server mgmt interface; can have multiple values" ) . Default ( "main=127.0.0.1:8989" ) . Envar ( "OVPN_MGMT" ) . Strings ( )
metricsPath = kingpin . Flag ( "metrics.path" , "URL path for exposing collected metrics" ) . Default ( "/metrics" ) . Envar ( "OVPN_METRICS_PATH" ) . String ( )
easyrsaDirPath = kingpin . Flag ( "easyrsa.path" , "path to easyrsa dir" ) . Default ( "./easyrsa" ) . Envar ( "EASYRSA_PATH" ) . String ( )
indexTxtPath = kingpin . Flag ( "easyrsa.index-path" , "path to easyrsa index file" ) . Default ( "" ) . Envar ( "OVPN_INDEX_PATH" ) . String ( )
2022-11-02 09:29:22 -04:00
easyrsaBinPath = kingpin . Flag ( "easyrsa.bin-path" , "path to easyrsa script" ) . Default ( "easyrsa" ) . Envar ( "EASYRSA_BIN_PATH" ) . String ( )
2021-12-30 03:24:44 -05:00
ccdEnabled = kingpin . Flag ( "ccd" , "enable client-config-dir" ) . Default ( "false" ) . Envar ( "OVPN_CCD" ) . Bool ( )
ccdDir = kingpin . Flag ( "ccd.path" , "path to client-config-dir" ) . Default ( "./ccd" ) . Envar ( "OVPN_CCD_PATH" ) . String ( )
2021-10-22 00:35:16 -04:00
clientConfigTemplatePath = kingpin . Flag ( "templates.clientconfig-path" , "path to custom client.conf.tpl" ) . Default ( "" ) . Envar ( "OVPN_TEMPLATES_CC_PATH" ) . String ( )
2021-10-21 06:01:03 -04:00
ccdTemplatePath = kingpin . Flag ( "templates.ccd-path" , "path to custom ccd.tpl" ) . Default ( "" ) . Envar ( "OVPN_TEMPLATES_CCD_PATH" ) . String ( )
2021-12-30 03:24:44 -05:00
authByPassword = kingpin . Flag ( "auth.password" , "enable additional password authentication" ) . Default ( "false" ) . Envar ( "OVPN_AUTH" ) . Bool ( )
authDatabase = kingpin . Flag ( "auth.db" , "database path for password authentication" ) . Default ( "./easyrsa/pki/users.db" ) . Envar ( "OVPN_AUTH_DB_PATH" ) . String ( )
2022-01-20 11:42:36 -05:00
logLevel = kingpin . Flag ( "log.level" , "set log level: trace, debug, info, warn, error (default info)" ) . Default ( "info" ) . Envar ( "LOG_LEVEL" ) . String ( )
logFormat = kingpin . Flag ( "log.format" , "set log format: text, json (default text)" ) . Default ( "text" ) . Envar ( "LOG_FORMAT" ) . String ( )
storageBackend = kingpin . Flag ( "storage.backend" , "storage backend: filesystem, kubernetes.secrets (default filesystem)" ) . Default ( "filesystem" ) . Envar ( "STORAGE_BACKEND" ) . String ( )
2021-10-05 11:09:29 -04:00
2021-12-30 03:24:44 -05:00
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
2021-10-05 11:09:29 -04:00
2022-07-21 11:17:53 -04:00
version = "2.0.0"
2020-11-27 02:23:59 -05:00
)
2021-12-30 03:24:44 -05:00
var logLevels = map [ string ] log . Level {
2022-01-20 11:42:36 -05:00
"trace" : log . TraceLevel ,
2021-12-30 03:24:44 -05:00
"debug" : log . DebugLevel ,
"info" : log . InfoLevel ,
"warn" : log . WarnLevel ,
"error" : log . ErrorLevel ,
}
2022-01-20 11:42:36 -05:00
var logFormats = map [ string ] log . Formatter {
"text" : & log . TextFormatter { } ,
"json" : & log . JSONFormatter { } ,
}
2020-11-27 02:23:59 -05:00
var (
ovpnServerCertExpire = prometheus . NewGauge ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_server_cert_expire" ,
Help : "openvpn server certificate expire time in days" ,
} ,
2020-11-27 02:23:59 -05:00
)
ovpnServerCaCertExpire = prometheus . NewGauge ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_server_ca_cert_expire" ,
Help : "openvpn server CA certificate expire time in days" ,
} ,
2020-11-27 02:23:59 -05:00
)
ovpnClientsTotal = prometheus . NewGauge ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_clients_total" ,
Help : "total openvpn users" ,
} ,
2020-11-27 02:23:59 -05:00
)
ovpnClientsRevoked = prometheus . NewGauge ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_clients_revoked" ,
Help : "revoked openvpn users" ,
} ,
2020-11-27 02:23:59 -05:00
)
ovpnClientsExpired = prometheus . NewGauge ( prometheus . GaugeOpts {
Name : "ovpn_clients_expired" ,
Help : "expired openvpn users" ,
} ,
)
ovpnClientsConnected = prometheus . NewGauge ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_clients_connected" ,
2022-07-21 11:17:53 -04:00
Help : "total connected openvpn clients" ,
} ,
)
ovpnUniqClientsConnected = prometheus . NewGauge ( prometheus . GaugeOpts {
Name : "ovpn_uniq_clients_connected" ,
Help : "uniq connected openvpn clients" ,
2021-07-21 18:06:34 -04:00
} ,
2020-11-27 02:23:59 -05:00
)
ovpnClientCertificateExpire = prometheus . NewGaugeVec ( prometheus . GaugeOpts {
Name : "ovpn_client_cert_expire" ,
Help : "openvpn user certificate expire time in days" ,
} ,
[ ] string { "client" } ,
)
ovpnClientConnectionInfo = prometheus . NewGaugeVec ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_client_connection_info" ,
Help : "openvpn user connection info. ip - assigned address from ovpn network. value - last time when connection was refreshed in unix format" ,
} ,
2020-11-27 02:23:59 -05:00
[ ] string { "client" , "ip" } ,
)
ovpnClientConnectionFrom = prometheus . NewGaugeVec ( prometheus . GaugeOpts {
Name : "ovpn_client_connection_from" ,
Help : "openvpn user connection info. ip - from which address connection was initialized. value - time when connection was initialized in unix format" ,
} ,
[ ] string { "client" , "ip" } ,
)
ovpnClientBytesReceived = prometheus . NewGaugeVec ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_client_bytes_received" ,
Help : "openvpn user bytes received" ,
} ,
2020-11-27 02:23:59 -05:00
[ ] string { "client" } ,
)
ovpnClientBytesSent = prometheus . NewGaugeVec ( prometheus . GaugeOpts {
2021-07-21 18:06:34 -04:00
Name : "ovpn_client_bytes_sent" ,
Help : "openvpn user bytes sent" ,
} ,
2020-11-27 02:23:59 -05:00
[ ] string { "client" } ,
)
2020-05-14 19:13:33 -04:00
)
2021-03-17 06:44:12 -04:00
type OvpnAdmin struct {
2021-07-21 18:06:34 -04:00
role string
lastSyncTime string
lastSuccessfulSyncTime string
masterHostBasicAuth bool
masterSyncToken string
clients [ ] OpenvpnClient
activeClients [ ] clientStatus
promRegistry * prometheus . Registry
mgmtInterfaces map [ string ] string
templates * packr . Box
modules [ ] string
2022-01-20 11:42:36 -05:00
mgmtStatusTimeFormat string
2022-08-02 10:19:27 -04:00
createUserMutex * sync . Mutex
2020-11-27 02:23:59 -05:00
}
2020-11-17 12:48:26 -05:00
type OpenvpnServer struct {
2021-07-21 18:06:34 -04:00
Host string
Port string
2021-02-20 07:48:41 -05:00
Protocol string
2020-11-17 12:48:26 -05:00
}
type openvpnClientConfig struct {
2021-07-21 18:06:34 -04:00
Hosts [ ] OpenvpnServer
CA string
Cert string
Key string
TLS string
2021-02-15 01:03:38 -05:00
PasswdAuth bool
2020-05-14 19:13:33 -04:00
}
2020-11-17 12:48:26 -05:00
type OpenvpnClient struct {
2021-12-30 03:24:44 -05:00
Identity string ` json:"Identity" `
AccountStatus string ` json:"AccountStatus" `
ExpirationDate string ` json:"ExpirationDate" `
RevocationDate string ` json:"RevocationDate" `
ConnectionStatus string ` json:"ConnectionStatus" `
2022-07-21 11:17:53 -04:00
Connections int ` json:"Connections" `
2020-10-15 12:12:31 -04:00
}
2020-10-29 06:50:19 -04:00
type ccdRoute struct {
2021-07-21 18:06:34 -04:00
Address string ` json:"Address" `
Mask string ` json:"Mask" `
Description string ` json:"Description" `
2020-10-29 06:50:19 -04:00
}
type Ccd struct {
2021-07-21 18:06:34 -04:00
User string ` json:"User" `
ClientAddress string ` json:"ClientAddress" `
CustomRoutes [ ] ccdRoute ` json:"CustomRoutes" `
2020-10-15 12:12:31 -04:00
}
2020-05-14 19:13:33 -04:00
type indexTxtLine struct {
Flag string
ExpirationDate string
RevocationDate string
SerialNumber string
Filename string
DistinguishedName string
Identity string
}
type clientStatus struct {
2021-02-15 01:03:38 -05:00
CommonName string
RealAddress string
BytesReceived string
BytesSent string
ConnectedSince string
VirtualAddress string
LastRef string
2020-11-27 02:23:59 -05:00
ConnectedSinceFormatted string
LastRefFormatted string
2021-02-15 01:03:38 -05:00
ConnectedTo string
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userListHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2023-04-27 02:38:01 -04:00
if * storageBackend == "kubernetes.secrets" {
err := app . updateIndexTxtOnDisk ( )
if err != nil {
log . Errorln ( err )
}
oAdmin . clients = oAdmin . usersList ( )
}
2020-11-27 02:23:59 -05:00
usersList , _ := json . Marshal ( oAdmin . clients )
2020-10-29 06:50:19 -04:00
fmt . Fprintf ( w , "%s" , usersList )
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userStatisticHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2020-11-27 02:23:59 -05:00
userStatistic , _ := json . Marshal ( oAdmin . getUserStatistic ( r . FormValue ( "username" ) ) )
fmt . Fprintf ( w , "%s" , userStatistic )
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userCreateHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2021-02-15 01:03:38 -05:00
userCreated , userCreateStatus := oAdmin . userCreate ( r . FormValue ( "username" ) , r . FormValue ( "password" ) )
2020-10-29 06:50:19 -04:00
2021-07-21 18:06:34 -04:00
if userCreated {
2022-07-21 11:17:53 -04:00
oAdmin . clients = oAdmin . usersList ( )
2021-07-21 18:06:34 -04:00
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , userCreateStatus )
return
} else {
http . Error ( w , userCreateStatus , http . StatusUnprocessableEntity )
}
2020-05-14 19:13:33 -04:00
}
2022-07-21 11:17:53 -04:00
func ( oAdmin * OvpnAdmin ) userRotateHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2022-07-21 11:17:53 -04:00
if oAdmin . role == "slave" {
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
_ = r . ParseForm ( )
2022-08-12 06:52:45 -04:00
err , msg := oAdmin . userRotate ( r . FormValue ( "username" ) , r . FormValue ( "password" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
} else {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , msg )
}
2022-07-21 11:17:53 -04:00
}
func ( oAdmin * OvpnAdmin ) userDeleteHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2022-07-21 11:17:53 -04:00
if oAdmin . role == "slave" {
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
_ = r . ParseForm ( )
2022-08-12 06:52:45 -04:00
err , msg := oAdmin . userDelete ( r . FormValue ( "username" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
} else {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , msg )
}
2022-07-21 11:17:53 -04:00
}
2020-05-14 19:13:33 -04:00
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userRevokeHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2022-08-12 06:52:45 -04:00
err , msg := oAdmin . userRevoke ( r . FormValue ( "username" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
} else {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , msg )
}
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userUnrevokeHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2022-08-12 06:52:45 -04:00
err , msg := oAdmin . userUnrevoke ( r . FormValue ( "username" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
} else {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , msg )
}
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userChangePasswordHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2021-02-15 01:03:38 -05:00
if * authByPassword {
2022-08-12 06:52:45 -04:00
err , msg := oAdmin . userChangePassword ( r . FormValue ( "username" ) , r . FormValue ( "password" ) )
if err != nil {
2021-02-15 01:03:38 -05:00
w . WriteHeader ( http . StatusInternalServerError )
2022-08-12 06:52:45 -04:00
fmt . Fprintf ( w , ` { "status":"error", "message": "%s"} ` , msg )
} else {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , ` { "status":"ok", "message": "%s"} ` , msg )
2021-02-15 01:03:38 -05:00
}
} else {
2021-07-21 18:06:34 -04:00
http . Error ( w , ` { "status":"error"} ` , http . StatusNotImplemented )
2021-02-15 01:03:38 -05:00
}
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userShowConfigHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2020-11-27 02:23:59 -05:00
fmt . Fprintf ( w , "%s" , oAdmin . renderClientConfig ( r . FormValue ( "username" ) ) )
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userDisconnectHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2021-07-21 18:06:34 -04:00
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
2020-10-29 06:50:19 -04:00
fmt . Fprintf ( w , "%s" , r . FormValue ( "username" ) )
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userShowCcdHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2020-11-27 02:23:59 -05:00
ccd , _ := json . Marshal ( oAdmin . getCcd ( r . FormValue ( "username" ) ) )
2020-10-29 06:50:19 -04:00
fmt . Fprintf ( w , "%s" , ccd )
2020-10-15 12:12:31 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userApplyCcdHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusLocked )
return
}
2021-07-21 18:06:34 -04:00
var ccd Ccd
if r . Body == nil {
http . Error ( w , "Please send a request body" , http . StatusBadRequest )
return
}
err := json . NewDecoder ( r . Body ) . Decode ( & ccd )
if err != nil {
2021-12-30 03:24:44 -05:00
log . Errorln ( err )
2021-07-21 18:06:34 -04:00
}
ccdApplied , applyStatus := oAdmin . modifyCcd ( ccd )
if ccdApplied {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , applyStatus )
return
} else {
http . Error ( w , applyStatus , http . StatusUnprocessableEntity )
}
2020-10-15 12:12:31 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) serverSettingsHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2021-02-26 07:11:13 -05:00
enabledModules , enabledModulesErr := json . Marshal ( oAdmin . modules )
if enabledModulesErr != nil {
2021-12-30 03:24:44 -05:00
log . Errorln ( enabledModulesErr )
2021-02-26 07:11:13 -05:00
}
fmt . Fprintf ( w , ` { "status":"ok", "serverRole": "%s", "modules": %s } ` , oAdmin . role , string ( enabledModules ) )
2020-11-17 12:48:26 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) lastSyncTimeHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Debug ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
fmt . Fprint ( w , oAdmin . lastSyncTime )
2020-11-17 12:48:26 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) lastSuccessfulSyncTimeHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Debug ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
fmt . Fprint ( w , oAdmin . lastSuccessfulSyncTime )
2020-11-19 12:08:55 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) downloadCertsHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2022-08-02 10:19:27 -04:00
http . Error ( w , ` { "status":"error"} ` , http . StatusBadRequest )
return
}
if * storageBackend == "kubernetes.secrets" {
http . Error ( w , ` { "status":"error"} ` , http . StatusBadRequest )
2020-11-17 12:48:26 -05:00
return
}
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2020-11-17 12:48:26 -05:00
token := r . Form . Get ( "token" )
2020-11-27 02:23:59 -05:00
if token != oAdmin . masterSyncToken {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusForbidden )
return
}
archiveCerts ( )
2021-07-21 18:06:34 -04:00
w . Header ( ) . Set ( "Content-Disposition" , "attachment; filename=" + certsArchiveFileName )
http . ServeFile ( w , r , certsArchivePath )
2020-11-17 12:48:26 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) downloadCcdHandler ( w http . ResponseWriter , r * http . Request ) {
2022-08-02 10:19:27 -04:00
log . Info ( r . RemoteAddr , " " , r . RequestURI )
2020-11-27 02:23:59 -05:00
if oAdmin . role == "slave" {
2022-08-02 10:19:27 -04:00
http . Error ( w , ` { "status":"error"} ` , http . StatusBadRequest )
return
}
if * storageBackend == "kubernetes.secrets" {
http . Error ( w , ` { "status":"error"} ` , http . StatusBadRequest )
2020-11-17 12:48:26 -05:00
return
}
2021-12-30 03:24:44 -05:00
_ = r . ParseForm ( )
2020-11-17 12:48:26 -05:00
token := r . Form . Get ( "token" )
2020-11-27 02:23:59 -05:00
if token != oAdmin . masterSyncToken {
2020-11-17 12:48:26 -05:00
http . Error ( w , ` { "status":"error"} ` , http . StatusForbidden )
return
}
archiveCcd ( )
2021-07-21 18:06:34 -04:00
w . Header ( ) . Set ( "Content-Disposition" , "attachment; filename=" + ccdArchiveFileName )
http . ServeFile ( w , r , ccdArchivePath )
2020-11-17 12:48:26 -05:00
}
2021-12-30 03:24:44 -05:00
var app OpenVPNPKI
2020-05-14 19:13:33 -04:00
func main ( ) {
2021-02-20 07:48:41 -05:00
kingpin . Version ( version )
kingpin . Parse ( )
2021-12-30 03:24:44 -05:00
log . SetLevel ( logLevels [ * logLevel ] )
2022-01-20 11:42:36 -05:00
log . SetFormatter ( logFormats [ * logFormat ] )
2021-12-30 03:24:44 -05:00
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
2021-12-30 03:24:44 -05:00
err := app . run ( )
if err != nil {
log . Error ( err )
}
}
if * indexTxtPath == "" {
* indexTxtPath = * easyrsaDirPath + "/pki/index.txt"
}
2021-03-17 06:44:12 -04:00
ovpnAdmin := new ( OvpnAdmin )
2022-01-20 11:42:36 -05:00
2020-11-27 02:23:59 -05:00
ovpnAdmin . lastSyncTime = "unknown"
ovpnAdmin . role = * serverRole
ovpnAdmin . lastSuccessfulSyncTime = "unknown"
ovpnAdmin . masterSyncToken = * masterSyncToken
ovpnAdmin . promRegistry = prometheus . NewRegistry ( )
2021-02-26 07:11:13 -05:00
ovpnAdmin . modules = [ ] string { }
2022-08-02 10:19:27 -04:00
ovpnAdmin . createUserMutex = & sync . Mutex { }
2021-02-15 01:03:38 -05:00
ovpnAdmin . mgmtInterfaces = make ( map [ string ] string )
for _ , mgmtInterface := range * mgmtAddress {
2021-07-21 18:06:34 -04:00
parts := strings . SplitN ( mgmtInterface , "=" , 2 )
2021-02-15 01:03:38 -05:00
ovpnAdmin . mgmtInterfaces [ parts [ 0 ] ] = parts [ len ( parts ) - 1 ]
}
2022-01-20 11:42:36 -05:00
ovpnAdmin . mgmtSetTimeFormat ( )
2020-11-27 02:23:59 -05:00
ovpnAdmin . registerMetrics ( )
ovpnAdmin . setState ( )
go ovpnAdmin . updateState ( )
2020-11-17 12:48:26 -05:00
if * masterBasicAuthPassword != "" && * masterBasicAuthUser != "" {
2020-11-27 02:23:59 -05:00
ovpnAdmin . masterHostBasicAuth = true
} else {
ovpnAdmin . masterHostBasicAuth = false
2020-11-17 12:48:26 -05:00
}
2021-02-26 07:11:13 -05:00
ovpnAdmin . modules = append ( ovpnAdmin . modules , "core" )
if * authByPassword {
2022-07-21 11:17:53 -04:00
if * storageBackend != "kubernetes.secrets" {
ovpnAdmin . modules = append ( ovpnAdmin . modules , "passwdAuth" )
} else {
log . Fatal ( "Right now the keys `--storage.backend=kubernetes.secret` and `--auth.password` are not working together. Please use only one of them " )
}
2021-02-26 07:11:13 -05:00
}
if * ccdEnabled {
ovpnAdmin . modules = append ( ovpnAdmin . modules , "ccd" )
}
2020-11-27 02:23:59 -05:00
if ovpnAdmin . role == "slave" {
ovpnAdmin . syncDataFromMaster ( )
2021-07-21 18:06:34 -04:00
go ovpnAdmin . syncWithMaster ( )
2020-11-17 12:48:26 -05:00
}
2021-02-26 07:11:13 -05:00
ovpnAdmin . templates = packr . New ( "template" , "./templates" )
staticBox := packr . New ( "static" , "./frontend/static" )
static := CacheControlWrapper ( http . FileServer ( staticBox ) )
2020-05-14 19:13:33 -04:00
2021-12-07 09:44:52 -05:00
http . Handle ( * listenBaseUrl , http . StripPrefix ( strings . TrimRight ( * listenBaseUrl , "/" ) , static ) )
http . HandleFunc ( * listenBaseUrl + "api/server/settings" , ovpnAdmin . serverSettingsHandler )
http . HandleFunc ( * listenBaseUrl + "api/users/list" , ovpnAdmin . userListHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/create" , ovpnAdmin . userCreateHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/change-password" , ovpnAdmin . userChangePasswordHandler )
2022-11-02 09:29:22 -04:00
http . HandleFunc ( * listenBaseUrl + "api/user/rotate" , ovpnAdmin . userRotateHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/delete" , ovpnAdmin . userDeleteHandler )
2021-12-07 09:44:52 -05:00
http . HandleFunc ( * listenBaseUrl + "api/user/revoke" , ovpnAdmin . userRevokeHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/unrevoke" , ovpnAdmin . userUnrevokeHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/config/show" , ovpnAdmin . userShowConfigHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/disconnect" , ovpnAdmin . userDisconnectHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/statistic" , ovpnAdmin . userStatisticHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/ccd" , ovpnAdmin . userShowCcdHandler )
http . HandleFunc ( * listenBaseUrl + "api/user/ccd/apply" , ovpnAdmin . userApplyCcdHandler )
http . HandleFunc ( * listenBaseUrl + "api/sync/last/try" , ovpnAdmin . lastSyncTimeHandler )
http . HandleFunc ( * listenBaseUrl + "api/sync/last/successful" , ovpnAdmin . lastSuccessfulSyncTimeHandler )
2021-12-07 10:12:57 -05:00
http . HandleFunc ( * listenBaseUrl + downloadCertsApiUrl , ovpnAdmin . downloadCertsHandler )
http . HandleFunc ( * listenBaseUrl + downloadCcdApiUrl , ovpnAdmin . downloadCcdHandler )
2020-11-27 02:23:59 -05:00
http . Handle ( * metricsPath , promhttp . HandlerFor ( ovpnAdmin . promRegistry , promhttp . HandlerOpts { } ) )
2021-12-07 09:44:52 -05:00
http . HandleFunc ( * listenBaseUrl + "ping" , func ( w http . ResponseWriter , r * http . Request ) {
2020-11-27 02:23:59 -05:00
fmt . Fprintf ( w , "pong" )
} )
2020-11-17 12:48:26 -05:00
2022-11-02 09:29:22 -04:00
log . Printf ( "Bind: http://%s:%s%s" , * listenHost , * listenPort , * listenBaseUrl )
2021-07-21 18:06:34 -04:00
log . Fatal ( http . ListenAndServe ( * listenHost + ":" + * listenPort , nil ) )
2020-05-14 19:13:33 -04:00
}
2020-10-29 06:50:19 -04:00
func CacheControlWrapper ( h http . Handler ) http . Handler {
2020-11-17 12:48:26 -05:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Cache-Control" , "max-age=2592000" ) // 30 days
h . ServeHTTP ( w , r )
} )
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) registerMetrics ( ) {
2020-11-27 02:23:59 -05:00
oAdmin . promRegistry . MustRegister ( ovpnServerCertExpire )
oAdmin . promRegistry . MustRegister ( ovpnServerCaCertExpire )
oAdmin . promRegistry . MustRegister ( ovpnClientsTotal )
oAdmin . promRegistry . MustRegister ( ovpnClientsRevoked )
oAdmin . promRegistry . MustRegister ( ovpnClientsConnected )
2022-07-21 11:17:53 -04:00
oAdmin . promRegistry . MustRegister ( ovpnUniqClientsConnected )
2020-11-27 02:23:59 -05:00
oAdmin . promRegistry . MustRegister ( ovpnClientsExpired )
oAdmin . promRegistry . MustRegister ( ovpnClientCertificateExpire )
oAdmin . promRegistry . MustRegister ( ovpnClientConnectionInfo )
oAdmin . promRegistry . MustRegister ( ovpnClientConnectionFrom )
oAdmin . promRegistry . MustRegister ( ovpnClientBytesReceived )
oAdmin . promRegistry . MustRegister ( ovpnClientBytesSent )
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) setState ( ) {
2020-11-27 02:23:59 -05:00
oAdmin . activeClients = oAdmin . mgmtGetActiveClients ( )
oAdmin . clients = oAdmin . usersList ( )
2021-02-15 01:03:38 -05:00
ovpnServerCaCertExpire . Set ( float64 ( ( getOvpnCaCertExpireDate ( ) . Unix ( ) - time . Now ( ) . Unix ( ) ) / 3600 / 24 ) )
2020-11-27 02:23:59 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) updateState ( ) {
2020-11-27 02:23:59 -05:00
for {
time . Sleep ( time . Duration ( 28 ) * time . Second )
ovpnClientBytesSent . Reset ( )
ovpnClientBytesReceived . Reset ( )
ovpnClientConnectionFrom . Reset ( )
ovpnClientConnectionInfo . Reset ( )
2022-07-21 11:17:53 -04:00
ovpnClientCertificateExpire . Reset ( )
2020-11-27 02:23:59 -05:00
go oAdmin . setState ( )
}
}
2020-05-14 19:13:33 -04:00
func indexTxtParser ( txt string ) [ ] indexTxtLine {
2020-11-17 12:48:26 -05:00
var indexTxt [ ] indexTxtLine
2020-05-14 19:13:33 -04:00
txtLinesArray := strings . Split ( txt , "\n" )
for _ , v := range txtLinesArray {
str := strings . Fields ( v )
if len ( str ) > 0 {
switch {
// case strings.HasPrefix(str[0], "E"):
case strings . HasPrefix ( str [ 0 ] , "V" ) :
2021-07-21 18:06:34 -04:00
indexTxt = append ( indexTxt , indexTxtLine { Flag : str [ 0 ] , ExpirationDate : str [ 1 ] , SerialNumber : str [ 2 ] , Filename : str [ 3 ] , DistinguishedName : str [ 4 ] , Identity : str [ 4 ] [ strings . Index ( str [ 4 ] , "=" ) + 1 : ] } )
2020-05-14 19:13:33 -04:00
case strings . HasPrefix ( str [ 0 ] , "R" ) :
2021-07-21 18:06:34 -04:00
indexTxt = append ( indexTxt , indexTxtLine { Flag : str [ 0 ] , ExpirationDate : str [ 1 ] , RevocationDate : str [ 2 ] , SerialNumber : str [ 3 ] , Filename : str [ 4 ] , DistinguishedName : str [ 5 ] , Identity : str [ 5 ] [ strings . Index ( str [ 5 ] , "=" ) + 1 : ] } )
2020-05-14 19:13:33 -04:00
}
}
}
2020-11-27 02:23:59 -05:00
2020-05-14 19:13:33 -04:00
return indexTxt
}
func renderIndexTxt ( data [ ] indexTxtLine ) string {
indexTxt := ""
for _ , line := range data {
switch {
case line . Flag == "V" :
2021-07-21 18:06:34 -04:00
indexTxt += fmt . Sprintf ( "%s\t%s\t\t%s\t%s\t%s\n" , line . Flag , line . ExpirationDate , line . SerialNumber , line . Filename , line . DistinguishedName )
2020-05-14 19:13:33 -04:00
case line . Flag == "R" :
2021-07-21 18:06:34 -04:00
indexTxt += fmt . Sprintf ( "%s\t%s\t%s\t%s\t%s\t%s\n" , line . Flag , line . ExpirationDate , line . RevocationDate , line . SerialNumber , line . Filename , line . DistinguishedName )
// case line.flag == "E":
2020-05-14 19:13:33 -04:00
}
}
2020-10-29 06:50:19 -04:00
return indexTxt
2020-05-14 19:13:33 -04:00
}
2021-07-21 18:06:34 -04:00
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 {
2022-01-20 11:42:36 -05:00
log . Error ( "clientConfigTpl not found in templates box" )
2021-07-21 18:06:34 -04:00
}
return template . Must ( template . New ( "client-config" ) . Parse ( clientConfigTpl ) )
}
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) renderClientConfig ( username string ) string {
2020-05-14 19:13:33 -04:00
if checkUserExist ( username ) {
2020-11-17 12:48:26 -05:00
var hosts [ ] OpenvpnServer
for _ , server := range * openvpnServer {
2021-07-21 18:06:34 -04:00
parts := strings . SplitN ( server , ":" , 3 )
2021-02-20 07:48:41 -05:00
hosts = append ( hosts , OpenvpnServer { Host : parts [ 0 ] , Port : parts [ 1 ] , Protocol : parts [ 2 ] } )
2020-11-17 12:48:26 -05:00
}
2021-12-30 03:24:44 -05:00
2021-10-05 11:09:29 -04:00
if * openvpnServerBehindLB {
2021-12-30 03:24:44 -05:00
var err error
hosts , err = getOvpnServerHostsFromKubeApi ( )
if err != nil {
log . Error ( err )
}
2021-10-05 11:09:29 -04:00
}
2022-01-20 11:42:36 -05:00
log . Tracef ( "hosts for %s\n %v" , username , hosts )
2021-12-30 03:24:44 -05:00
2020-05-14 19:13:33 -04:00
conf := openvpnClientConfig { }
2020-11-17 12:48:26 -05:00
conf . Hosts = hosts
2020-10-29 06:50:19 -04:00
conf . CA = fRead ( * easyrsaDirPath + "/pki/ca.crt" )
conf . TLS = fRead ( * easyrsaDirPath + "/pki/ta.key" )
2021-12-30 03:24:44 -05:00
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
conf . Cert , conf . Key = app . easyrsaGetClientCert ( username )
2021-12-30 03:24:44 -05:00
} else {
conf . Cert = fRead ( * easyrsaDirPath + "/pki/issued/" + username + ".crt" )
conf . Key = fRead ( * easyrsaDirPath + "/pki/private/" + username + ".key" )
}
2021-02-15 01:03:38 -05:00
conf . PasswdAuth = * authByPassword
2020-10-29 06:50:19 -04:00
2021-07-21 18:06:34 -04:00
t := oAdmin . getClientConfigTemplate ( )
2021-02-26 07:11:13 -05:00
2020-05-14 19:13:33 -04:00
var tmp bytes . Buffer
2020-11-27 02:23:59 -05:00
err := t . Execute ( & tmp , conf )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Errorf ( "something goes wrong during rendering config for %s" , username )
log . Debugf ( "rendering config for %s failed with error %v" , username , err )
2020-11-27 02:23:59 -05:00
}
2020-10-29 06:50:19 -04:00
2020-11-17 12:48:26 -05:00
hosts = nil
2021-12-30 03:24:44 -05:00
2022-01-20 11:42:36 -05:00
log . Tracef ( "Rendered config for user %s: %+v" , username , tmp . String ( ) )
2021-12-30 03:24:44 -05:00
2022-01-20 11:42:36 -05:00
return fmt . Sprintf ( "%+v" , tmp . String ( ) )
2020-05-14 19:13:33 -04:00
}
2022-01-20 11:42:36 -05:00
log . Warnf ( "user \"%s\" not found" , username )
return fmt . Sprintf ( "user \"%s\" not found" , username )
2020-05-14 19:13:33 -04:00
}
2021-07-21 18:06:34 -04:00
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 {
2021-12-30 03:24:44 -05:00
log . Errorf ( "ccdTpl not found in templates box" )
2021-07-21 18:06:34 -04:00
}
return template . Must ( template . New ( "ccd" ) . Parse ( ccdTpl ) )
}
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) parseCcd ( username string ) Ccd {
2020-10-29 06:50:19 -04:00
ccd := Ccd { }
ccd . User = username
ccd . ClientAddress = "dynamic"
ccd . CustomRoutes = [ ] ccdRoute { }
2020-10-15 12:12:31 -04:00
2022-01-20 11:42:36 -05:00
var txtLinesArray [ ] string
if * storageBackend == "kubernetes.secrets" {
txtLinesArray = strings . Split ( app . secretGetCcd ( ccd . User ) , "\n" )
} else {
2022-07-21 11:17:53 -04:00
if fExist ( * ccdDir + "/" + username ) {
txtLinesArray = strings . Split ( fRead ( * ccdDir + "/" + username ) , "\n" )
}
2022-01-20 11:42:36 -05:00
}
2020-10-15 12:12:31 -04:00
for _ , v := range txtLinesArray {
str := strings . Fields ( v )
if len ( str ) > 0 {
switch {
case strings . HasPrefix ( str [ 0 ] , "ifconfig-push" ) :
2021-07-21 18:06:34 -04:00
ccd . ClientAddress = str [ 1 ]
2020-10-15 12:12:31 -04:00
case strings . HasPrefix ( str [ 0 ] , "push" ) :
2021-07-21 18:06:34 -04:00
ccd . CustomRoutes = append ( ccd . CustomRoutes , ccdRoute { Address : strings . Trim ( str [ 2 ] , "\"" ) , Mask : strings . Trim ( str [ 3 ] , "\"" ) , Description : strings . Trim ( strings . Join ( str [ 4 : ] , "" ) , "#" ) } )
2020-10-15 12:12:31 -04:00
}
}
}
2020-10-29 06:50:19 -04:00
return ccd
2020-10-15 12:12:31 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) modifyCcd ( ccd Ccd ) ( bool , string ) {
2022-08-02 10:19:27 -04:00
ccdValid , err := validateCcd ( ccd )
if err != "" {
return false , err
2022-01-20 11:42:36 -05:00
}
2020-10-29 06:50:19 -04:00
2022-01-20 11:42:36 -05:00
if ccdValid {
t := oAdmin . getCcdTemplate ( )
var tmp bytes . Buffer
2022-08-02 10:19:27 -04:00
err := t . Execute ( & tmp , ccd )
if err != nil {
log . Error ( err )
2021-07-21 18:06:34 -04:00
}
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
app . secretUpdateCcd ( ccd . User , tmp . Bytes ( ) )
} else {
2022-08-02 10:19:27 -04:00
err = fWrite ( * ccdDir + "/" + ccd . User , tmp . String ( ) )
if err != nil {
log . Errorf ( "modifyCcd: fWrite(): %v" , err )
}
2021-07-21 18:06:34 -04:00
}
2022-01-20 11:42:36 -05:00
return true , "ccd updated successfully"
2021-07-21 18:06:34 -04:00
}
2020-10-15 12:12:31 -04:00
2022-01-20 11:42:36 -05:00
return false , "something goes wrong"
2020-10-15 12:12:31 -04:00
}
2020-10-29 06:50:19 -04:00
func validateCcd ( ccd Ccd ) ( bool , string ) {
2021-07-21 18:06:34 -04:00
2021-12-30 03:24:44 -05:00
ccdErr := ""
if ccd . ClientAddress != "dynamic" {
_ , ovpnNet , err := net . ParseCIDR ( * openvpnNetwork )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Error ( err )
2021-12-30 03:24:44 -05:00
}
if ! checkStaticAddressIsFree ( ccd . ClientAddress , ccd . User ) {
ccdErr = fmt . Sprintf ( "ClientAddress \"%s\" already assigned to another user" , ccd . ClientAddress )
2022-01-20 11:42:36 -05:00
log . Debugf ( "modify ccd for user %s: %s" , ccd . User , ccdErr )
2021-12-30 03:24:44 -05:00
return false , ccdErr
}
if net . ParseIP ( ccd . ClientAddress ) == nil {
ccdErr = fmt . Sprintf ( "ClientAddress \"%s\" not a valid IP address" , ccd . ClientAddress )
2022-01-20 11:42:36 -05:00
log . Debugf ( "modify ccd for user %s: %s" , ccd . User , ccdErr )
2021-12-30 03:24:44 -05:00
return false , ccdErr
}
if ! ovpnNet . Contains ( net . ParseIP ( ccd . ClientAddress ) ) {
ccdErr = fmt . Sprintf ( "ClientAddress \"%s\" not belongs to openvpn server network" , ccd . ClientAddress )
2022-01-20 11:42:36 -05:00
log . Debugf ( "modify ccd for user %s: %s" , ccd . User , ccdErr )
2021-12-30 03:24:44 -05:00
return false , ccdErr
}
}
for _ , route := range ccd . CustomRoutes {
if net . ParseIP ( route . Address ) == nil {
ccdErr = fmt . Sprintf ( "CustomRoute.Address \"%s\" must be a valid IP address" , route . Address )
2022-01-20 11:42:36 -05:00
log . Debugf ( "modify ccd for user %s: %s" , ccd . User , ccdErr )
2021-12-30 03:24:44 -05:00
return false , ccdErr
}
if net . ParseIP ( route . Mask ) == nil {
ccdErr = fmt . Sprintf ( "CustomRoute.Mask \"%s\" must be a valid IP address" , route . Mask )
2022-01-20 11:42:36 -05:00
log . Debugf ( "modify ccd for user %s: %s" , ccd . User , ccdErr )
2021-12-30 03:24:44 -05:00
return false , ccdErr
}
}
2020-10-29 06:50:19 -04:00
return true , ccdErr
2020-10-15 12:12:31 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) getCcd ( username string ) Ccd {
2020-10-29 06:50:19 -04:00
ccd := Ccd { }
ccd . User = username
ccd . ClientAddress = "dynamic"
ccd . CustomRoutes = [ ] ccdRoute { }
2022-01-20 11:42:36 -05:00
ccd = oAdmin . parseCcd ( username )
2021-07-21 18:06:34 -04:00
return ccd
2020-05-14 19:13:33 -04:00
}
2020-10-29 06:50:19 -04:00
func checkStaticAddressIsFree ( staticAddress string , username string ) bool {
2021-10-05 11:09:29 -04:00
o := runBash ( fmt . Sprintf ( "grep -rl ' %s ' %s | grep -vx %s/%s | wc -l" , staticAddress , * ccdDir , * ccdDir , username ) )
2020-10-29 06:50:19 -04:00
2021-07-21 18:06:34 -04:00
if strings . TrimSpace ( o ) == "0" {
return true
}
return false
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
func validateUsername ( username string ) error {
2020-05-14 19:13:33 -04:00
var validUsername = regexp . MustCompile ( usernameRegexp )
2022-08-12 06:52:45 -04:00
if validUsername . MatchString ( username ) {
return nil
} else {
return errors . New ( fmt . Sprintf ( "Username can only contains %s" , usernameRegexp ) )
}
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
func validatePassword ( password string ) error {
if utf8 . RuneCountInString ( password ) < passwordMinLength {
return errors . New ( fmt . Sprintf ( "Password too short, password length must be greater or equal %d" , passwordMinLength ) )
2021-02-15 01:03:38 -05:00
} else {
2022-08-12 06:52:45 -04:00
return nil
2021-02-15 01:03:38 -05:00
}
}
2020-05-14 19:13:33 -04:00
func checkUserExist ( username string ) bool {
2022-01-20 11:42:36 -05:00
for _ , u := range indexTxtParser ( fRead ( * indexTxtPath ) ) {
2020-05-14 19:13:33 -04:00
if u . DistinguishedName == ( "/CN=" + username ) {
2020-10-29 06:50:19 -04:00
return true
2020-05-14 19:13:33 -04:00
}
}
2020-10-29 06:50:19 -04:00
return false
2020-10-15 12:12:31 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) usersList ( ) [ ] OpenvpnClient {
2020-11-17 12:48:26 -05:00
var users [ ] OpenvpnClient
2020-11-27 02:23:59 -05:00
totalCerts := 0
validCerts := 0
revokedCerts := 0
expiredCerts := 0
2022-07-21 11:17:53 -04:00
connectedUniqUsers := 0
totalActiveConnections := 0
2020-11-27 02:23:59 -05:00
apochNow := time . Now ( ) . Unix ( )
2020-11-02 11:30:36 -05:00
2022-01-20 11:42:36 -05:00
for _ , line := range indexTxtParser ( fRead ( * indexTxtPath ) ) {
2022-08-02 10:19:27 -04:00
if line . Identity != "server" && ! strings . Contains ( line . Identity , "REVOKED" ) {
2020-11-27 02:23:59 -05:00
totalCerts += 1
2021-07-21 18:06:34 -04:00
ovpnClient := OpenvpnClient { Identity : line . Identity , ExpirationDate : parseDateToString ( indexTxtDateLayout , line . ExpirationDate , stringDateFormat ) }
switch {
case line . Flag == "V" :
ovpnClient . AccountStatus = "Active"
validCerts += 1
2020-11-27 02:23:59 -05:00
case line . Flag == "R" :
2021-07-21 18:06:34 -04:00
ovpnClient . AccountStatus = "Revoked"
ovpnClient . RevocationDate = parseDateToString ( indexTxtDateLayout , line . RevocationDate , stringDateFormat )
revokedCerts += 1
case line . Flag == "E" :
ovpnClient . AccountStatus = "Expired"
expiredCerts += 1
}
2020-11-27 02:23:59 -05:00
2022-07-21 11:17:53 -04:00
ovpnClientCertificateExpire . WithLabelValues ( line . Identity ) . Set ( float64 ( ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) / 3600 / 24 ) )
if ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) < 0 {
ovpnClient . AccountStatus = "Expired"
}
ovpnClient . Connections = 0
2021-02-15 01:03:38 -05:00
2021-07-21 18:06:34 -04:00
userConnected , userConnectedTo := isUserConnected ( line . Identity , oAdmin . activeClients )
if userConnected {
ovpnClient . ConnectionStatus = "Connected"
2022-08-02 10:19:27 -04:00
for range userConnectedTo {
2022-07-21 11:17:53 -04:00
ovpnClient . Connections += 1
totalActiveConnections += 1
}
connectedUniqUsers += 1
2021-07-21 18:06:34 -04:00
}
2020-11-27 02:23:59 -05:00
2021-07-21 18:06:34 -04:00
users = append ( users , ovpnClient )
2020-11-27 02:23:59 -05:00
2021-07-21 18:06:34 -04:00
} else {
2020-11-27 03:56:42 -05:00
ovpnServerCertExpire . Set ( float64 ( ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) / 3600 / 24 ) )
2020-11-27 02:23:59 -05:00
}
2020-05-14 19:13:33 -04:00
}
2020-11-27 02:23:59 -05:00
otherCerts := totalCerts - validCerts - revokedCerts - expiredCerts
if otherCerts != 0 {
2022-01-20 11:42:36 -05:00
log . Warnf ( "there are %d otherCerts" , otherCerts )
2020-11-27 02:23:59 -05:00
}
ovpnClientsTotal . Set ( float64 ( totalCerts ) )
ovpnClientsRevoked . Set ( float64 ( revokedCerts ) )
ovpnClientsExpired . Set ( float64 ( expiredCerts ) )
2022-07-21 11:17:53 -04:00
ovpnClientsConnected . Set ( float64 ( totalActiveConnections ) )
ovpnUniqClientsConnected . Set ( float64 ( connectedUniqUsers ) )
2020-11-27 02:23:59 -05:00
2020-10-29 06:50:19 -04:00
return users
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userCreate ( username , password string ) ( bool , string ) {
2021-07-21 18:06:34 -04:00
ucErr := fmt . Sprintf ( "User \"%s\" created" , username )
2021-02-15 01:03:38 -05:00
2022-08-02 10:19:27 -04:00
oAdmin . createUserMutex . Lock ( )
defer oAdmin . createUserMutex . Unlock ( )
2021-02-15 01:03:38 -05:00
if checkUserExist ( username ) {
ucErr = fmt . Sprintf ( "User \"%s\" already exists\n" , username )
2022-08-02 10:19:27 -04:00
log . Debugf ( "userCreate: checkUserExist(): %s" , ucErr )
2021-02-15 01:03:38 -05:00
return false , ucErr
}
2022-08-12 06:52:45 -04:00
if err := validateUsername ( username ) ; err != nil {
log . Debugf ( "userCreate: validateUsername(): %s" , err . Error ( ) )
return false , err . Error ( )
2020-05-14 19:13:33 -04:00
}
2021-02-15 01:03:38 -05:00
2021-03-17 06:44:12 -04:00
if * authByPassword {
2022-08-12 06:52:45 -04:00
if err := validatePassword ( password ) ; err != nil {
log . Debugf ( "userCreate: authByPassword(): %s" , err . Error ( ) )
return false , err . Error ( )
2021-02-15 01:03:38 -05:00
}
2020-05-14 19:13:33 -04:00
}
2021-02-15 01:03:38 -05:00
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
2021-12-30 03:24:44 -05:00
err := app . easyrsaBuildClient ( username )
if err != nil {
log . Error ( err )
}
} else {
2022-11-02 09:29:22 -04:00
o := runBash ( fmt . Sprintf ( "cd %s && %s build-client-full %s nopass 1>/dev/null" , * easyrsaDirPath , * easyrsaBinPath , username ) )
2022-01-20 11:42:36 -05:00
log . Debug ( o )
2021-12-30 03:24:44 -05:00
}
2021-02-15 01:03:38 -05:00
if * authByPassword {
2021-12-30 03:24:44 -05:00
o := runBash ( fmt . Sprintf ( "openvpn-user create --db.path %s --user %s --password %s" , * authDatabase , username , password ) )
2022-01-20 11:42:36 -05:00
log . Debug ( o )
2021-02-15 01:03:38 -05:00
}
2022-01-20 11:42:36 -05:00
log . Infof ( "Certificate for user %s issued" , username )
2021-02-15 01:03:38 -05:00
2022-07-21 11:17:53 -04:00
//oAdmin.clients = oAdmin.usersList()
2021-02-15 01:03:38 -05:00
2020-11-27 02:23:59 -05:00
return true , ucErr
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
func ( oAdmin * OvpnAdmin ) userChangePassword ( username , password string ) ( error , string ) {
2021-02-15 01:03:38 -05:00
if checkUserExist ( username ) {
o := runBash ( fmt . Sprintf ( "openvpn-user check --db.path %s --user %s | grep %s | wc -l" , * authDatabase , username , username ) )
2022-08-02 10:19:27 -04:00
log . Debug ( o )
2021-02-15 01:03:38 -05:00
2022-08-12 06:52:45 -04:00
if err := validatePassword ( password ) ; err != nil {
log . Warningf ( "userChangePassword: %s" , err . Error ( ) )
return err , err . Error ( )
2021-02-15 01:03:38 -05:00
}
if strings . TrimSpace ( o ) == "0" {
o = runBash ( fmt . Sprintf ( "openvpn-user create --db.path %s --user %s --password %s" , * authDatabase , username , password ) )
2022-08-02 10:19:27 -04:00
log . Debug ( o )
2021-02-15 01:03:38 -05:00
}
o = runBash ( fmt . Sprintf ( "openvpn-user change-password --db.path %s --user %s --password %s" , * authDatabase , username , password ) )
2022-08-02 10:19:27 -04:00
log . Debug ( o )
2021-02-15 01:03:38 -05:00
2022-08-02 10:19:27 -04:00
log . Infof ( "Password for user %s was changed" , username )
2021-12-30 03:24:44 -05:00
2022-08-12 06:52:45 -04:00
return nil , "Password changed"
2021-02-15 01:03:38 -05:00
}
2022-08-12 06:52:45 -04:00
return errors . New ( fmt . Sprintf ( "User \"%s\" not found}" , username ) ) , fmt . Sprintf ( "{\"msg\":\"User \"%s\" not found\"}" , username )
2021-02-15 01:03:38 -05:00
}
2022-07-21 11:17:53 -04:00
func ( oAdmin * OvpnAdmin ) getUserStatistic ( username string ) [ ] clientStatus {
var userStatistic [ ] clientStatus
2020-11-27 02:23:59 -05:00
for _ , u := range oAdmin . activeClients {
if u . CommonName == username {
2022-07-21 11:17:53 -04:00
userStatistic = append ( userStatistic , u )
2020-11-27 02:23:59 -05:00
}
}
2022-07-21 11:17:53 -04:00
return userStatistic
2020-11-27 02:23:59 -05:00
}
2022-08-12 06:52:45 -04:00
func ( oAdmin * OvpnAdmin ) userRevoke ( username string ) ( error , string ) {
2022-01-20 11:42:36 -05:00
log . Infof ( "Revoke certificate for user %s" , username )
2020-05-14 19:13:33 -04:00
if checkUserExist ( username ) {
// check certificate valid flag 'V'
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
2021-12-30 03:24:44 -05:00
err := app . easyrsaRevoke ( username )
if err != nil {
log . Error ( err )
}
} else {
2023-09-11 09:38:39 -04:00
o := runBash ( fmt . Sprintf ( "cd %s && echo yes | easyrsa revoke %s 1>/dev/null && %s gen-crl 1>/dev/null" , * easyrsaDirPath , username , * easyrsaBinPath ) )
2022-08-02 10:19:27 -04:00
log . Debugln ( o )
2021-12-30 03:24:44 -05:00
}
2021-02-15 01:03:38 -05:00
if * authByPassword {
2022-08-12 06:52:45 -04:00
o := runBash ( fmt . Sprintf ( "openvpn-user revoke --db-path %s --user %s" , * authDatabase , username ) )
log . Debug ( o )
2021-02-15 01:03:38 -05:00
}
2021-10-05 11:09:29 -04:00
2020-05-14 19:13:33 -04:00
crlFix ( )
2021-10-05 11:09:29 -04:00
userConnected , userConnectedTo := isUserConnected ( username , oAdmin . activeClients )
2022-01-20 11:42:36 -05:00
log . Tracef ( "User %s connected: %t" , username , userConnected )
2021-10-05 11:09:29 -04:00
if userConnected {
2022-07-21 11:17:53 -04:00
for _ , connection := range userConnectedTo {
oAdmin . mgmtKillUserConnection ( username , connection )
log . Infof ( "Session for user \"%s\" killed" , username )
}
2021-10-05 11:09:29 -04:00
}
2022-07-21 11:17:53 -04:00
oAdmin . setState ( )
2022-08-12 06:52:45 -04:00
return nil , fmt . Sprintf ( "user \"%s\" revoked" , username )
2020-05-14 19:13:33 -04:00
}
2022-01-20 11:42:36 -05:00
log . Infof ( "user \"%s\" not found" , username )
2022-08-12 06:52:45 -04:00
return errors . New ( fmt . Sprintf ( "User \"%s\" not found}" , username ) ) , fmt . Sprintf ( "User \"%s\" not found" , username )
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
func ( oAdmin * OvpnAdmin ) userUnrevoke ( username string ) ( error , string ) {
2020-05-14 19:13:33 -04:00
if checkUserExist ( username ) {
2022-01-20 11:42:36 -05:00
if * storageBackend == "kubernetes.secrets" {
2021-12-30 03:24:44 -05:00
err := app . easyrsaUnrevoke ( username )
if err != nil {
log . Error ( err )
}
} else {
// check certificate revoked flag 'R'
usersFromIndexTxt := indexTxtParser ( fRead ( * indexTxtPath ) )
for i := range usersFromIndexTxt {
2022-07-21 11:17:53 -04:00
if usersFromIndexTxt [ i ] . DistinguishedName == "/CN=" + username {
2021-12-30 03:24:44 -05:00
if usersFromIndexTxt [ i ] . Flag == "R" {
2022-07-21 11:17:53 -04:00
2021-12-30 03:24:44 -05:00
usersFromIndexTxt [ i ] . Flag = "V"
usersFromIndexTxt [ i ] . RevocationDate = ""
2022-07-21 11:17:53 -04:00
2022-08-12 06:52:45 -04:00
err := fMove ( fmt . Sprintf ( "%s/pki/revoked/certs_by_serial/%s.crt" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber ) , fmt . Sprintf ( "%s/pki/issued/%s.crt" , * easyrsaDirPath , username ) )
2022-08-02 10:19:27 -04:00
if err != nil {
log . Error ( err )
}
2022-08-12 06:52:45 -04:00
err = fMove ( fmt . Sprintf ( "%s/pki/revoked/certs_by_serial/%s.crt" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber ) , fmt . Sprintf ( "%s/pki/certs_by_serial/%s.pem" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber ) )
2022-08-02 10:19:27 -04:00
if err != nil {
log . Error ( err )
}
2022-08-12 06:52:45 -04:00
err = fMove ( fmt . Sprintf ( "%s/pki/revoked/private_by_serial/%s.key" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber ) , fmt . Sprintf ( "%s/pki/private/%s.key" , * easyrsaDirPath , username ) )
2022-08-02 10:19:27 -04:00
if err != nil {
log . Error ( err )
}
2022-08-12 06:52:45 -04:00
err = fMove ( fmt . Sprintf ( "%s/pki/revoked/reqs_by_serial/%s.req" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber ) , fmt . Sprintf ( "%s/pki/reqs/%s.req" , * easyrsaDirPath , username ) )
2022-08-02 10:19:27 -04:00
if err != nil {
log . Error ( err )
}
err = fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
2022-07-21 11:17:53 -04:00
2022-11-02 09:29:22 -04:00
_ = runBash ( fmt . Sprintf ( "cd %s && %s gen-crl 1>/dev/null" , * easyrsaDirPath , * easyrsaBinPath ) )
2022-07-21 11:17:53 -04:00
2021-12-30 03:24:44 -05:00
if * authByPassword {
2022-08-12 06:52:45 -04:00
o := runBash ( fmt . Sprintf ( "openvpn-user restore --db-path %s --user %s" , * authDatabase , username ) )
log . Debug ( o )
2021-12-30 03:24:44 -05:00
}
2022-07-21 11:17:53 -04:00
2021-12-30 03:24:44 -05:00
crlFix ( )
2022-07-21 11:17:53 -04:00
2021-12-30 03:24:44 -05:00
break
2021-02-15 01:03:38 -05:00
}
2021-07-21 18:06:34 -04:00
}
2020-05-14 19:13:33 -04:00
}
2022-08-02 10:19:27 -04:00
err := fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
2022-07-21 11:17:53 -04:00
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
2020-05-14 19:13:33 -04:00
}
crlFix ( )
2020-11-27 02:23:59 -05:00
oAdmin . clients = oAdmin . usersList ( )
2022-08-12 06:52:45 -04:00
return nil , fmt . Sprintf ( "{\"msg\":\"User %s successfully unrevoked\"}" , username )
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
return errors . New ( fmt . Sprintf ( "user \"%s\" not found" , username ) ) , fmt . Sprintf ( "{\"msg\":\"User \"%s\" not found\"}" , username )
2020-05-14 19:13:33 -04:00
}
2022-08-12 06:52:45 -04:00
func ( oAdmin * OvpnAdmin ) userRotate ( username , newPassword string ) ( error , string ) {
2022-07-21 11:17:53 -04:00
if checkUserExist ( username ) {
if * storageBackend == "kubernetes.secrets" {
err := app . easyrsaRotate ( username , newPassword )
if err != nil {
log . Error ( err )
}
} else {
var oldUserIndex , newUserIndex int
2022-08-12 06:52:45 -04:00
var oldUserSerial string
uniqHash := strings . Replace ( uuid . New ( ) . String ( ) , "-" , "" , - 1 )
2022-07-21 11:17:53 -04:00
usersFromIndexTxt := indexTxtParser ( fRead ( * indexTxtPath ) )
for i := range usersFromIndexTxt {
if usersFromIndexTxt [ i ] . DistinguishedName == "/CN=" + username {
2022-08-12 06:52:45 -04:00
oldUserSerial = usersFromIndexTxt [ i ] . SerialNumber
2022-08-02 10:19:27 -04:00
usersFromIndexTxt [ i ] . DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash
2022-08-12 06:52:45 -04:00
oldUserIndex = i
2022-07-21 11:17:53 -04:00
break
}
}
2022-08-02 10:19:27 -04:00
err := fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
2022-08-12 06:52:45 -04:00
if * authByPassword {
o := runBash ( fmt . Sprintf ( "openvpn-user delete --force --db.path %s --user %s" , * authDatabase , username ) )
log . Debug ( o )
}
userCreated , userCreateMessage := oAdmin . userCreate ( username , newPassword )
if ! userCreated {
usersFromIndexTxt = indexTxtParser ( fRead ( * indexTxtPath ) )
for i := range usersFromIndexTxt {
if usersFromIndexTxt [ i ] . SerialNumber == oldUserSerial {
usersFromIndexTxt [ i ] . DistinguishedName = "/CN=" + username
break
}
}
err = fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
return errors . New ( fmt . Sprintf ( "error rotaing user due: %s" , userCreateMessage ) ) , userCreateMessage
}
2022-07-21 11:17:53 -04:00
usersFromIndexTxt = indexTxtParser ( fRead ( * indexTxtPath ) )
for i := range usersFromIndexTxt {
if usersFromIndexTxt [ i ] . DistinguishedName == "/CN=" + username {
newUserIndex = i
}
2022-08-12 06:52:45 -04:00
if usersFromIndexTxt [ i ] . SerialNumber == oldUserSerial {
2022-07-21 11:17:53 -04:00
oldUserIndex = i
}
}
usersFromIndexTxt [ oldUserIndex ] , usersFromIndexTxt [ newUserIndex ] = usersFromIndexTxt [ newUserIndex ] , usersFromIndexTxt [ oldUserIndex ]
2022-08-02 10:19:27 -04:00
err = fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
2022-08-12 06:52:45 -04:00
2022-11-02 09:29:22 -04:00
_ = runBash ( fmt . Sprintf ( "cd %s && %s gen-crl 1>/dev/null" , * easyrsaDirPath , * easyrsaBinPath ) )
2022-07-21 11:17:53 -04:00
}
crlFix ( )
oAdmin . clients = oAdmin . usersList ( )
2022-08-12 06:52:45 -04:00
return nil , fmt . Sprintf ( "{\"msg\":\"User %s successfully rotated\"}" , username )
2022-07-21 11:17:53 -04:00
}
2022-08-12 06:52:45 -04:00
return errors . New ( fmt . Sprintf ( "user \"%s\" not found" , username ) ) , fmt . Sprintf ( "{\"msg\":\"User \"%s\" not found\"}" , username )
2022-07-21 11:17:53 -04:00
}
2022-08-12 06:52:45 -04:00
func ( oAdmin * OvpnAdmin ) userDelete ( username string ) ( error , string ) {
2022-07-21 11:17:53 -04:00
if checkUserExist ( username ) {
if * storageBackend == "kubernetes.secrets" {
err := app . easyrsaDelete ( username )
if err != nil {
log . Error ( err )
}
} else {
uniqHash := strings . Replace ( uuid . New ( ) . String ( ) , "-" , "" , - 1 )
usersFromIndexTxt := indexTxtParser ( fRead ( * indexTxtPath ) )
for i := range usersFromIndexTxt {
if usersFromIndexTxt [ i ] . DistinguishedName == "/CN=" + username {
2022-08-02 10:19:27 -04:00
usersFromIndexTxt [ i ] . DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash
2022-07-21 11:17:53 -04:00
break
}
}
2022-08-02 10:19:27 -04:00
if * authByPassword {
_ = runBash ( fmt . Sprintf ( "openvpn-user delete --force --db.path %s --user %s" , * authDatabase , username ) )
}
err := fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
if err != nil {
log . Error ( err )
}
2022-11-02 09:29:22 -04:00
_ = runBash ( fmt . Sprintf ( "cd %s && %s gen-crl 1>/dev/null " , * easyrsaDirPath , * easyrsaBinPath ) )
2022-07-21 11:17:53 -04:00
}
crlFix ( )
oAdmin . clients = oAdmin . usersList ( )
2022-08-12 06:52:45 -04:00
return nil , fmt . Sprintf ( "{\"msg\":\"User %s successfully deleted\"}" , username )
2022-07-21 11:17:53 -04:00
}
2022-08-12 06:52:45 -04:00
return errors . New ( fmt . Sprintf ( "User \"%s\" not found}" , username ) ) , fmt . Sprintf ( "{\"msg\":\"User \"%s\" not found\"}" , username )
2022-07-21 11:17:53 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) mgmtRead ( conn net . Conn ) string {
2022-07-21 11:17:53 -04:00
recvData := make ( [ ] byte , 32768 )
var out string
var n int
var err error
for {
n , err = conn . Read ( recvData )
if n <= 0 || err != nil {
break
} else {
out += string ( recvData [ : n ] )
if strings . Contains ( out , "type 'help' for more info" ) || strings . Contains ( out , "END" ) || strings . Contains ( out , "SUCCESS:" ) || strings . Contains ( out , "ERROR:" ) {
break
}
}
}
return out
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) mgmtConnectedUsersParser ( text , serverName string ) [ ] clientStatus {
2020-11-17 12:48:26 -05:00
var u [ ] clientStatus
2020-05-14 19:13:33 -04:00
isClientList := false
isRouteTable := false
scanner := bufio . NewScanner ( strings . NewReader ( text ) )
for scanner . Scan ( ) {
txt := scanner . Text ( )
if regexp . MustCompile ( ` ^Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since$ ` ) . MatchString ( txt ) {
isClientList = true
continue
}
if regexp . MustCompile ( ` ^ROUTING TABLE$ ` ) . MatchString ( txt ) {
isClientList = false
continue
}
if regexp . MustCompile ( ` ^Virtual Address,Common Name,Real Address,Last Ref$ ` ) . MatchString ( txt ) {
isRouteTable = true
continue
}
if regexp . MustCompile ( ` ^GLOBAL STATS$ ` ) . MatchString ( txt ) {
// isRouteTable = false // ineffectual assignment to isRouteTable (ineffassign)
break
}
if isClientList {
user := strings . Split ( txt , "," )
2020-11-27 02:23:59 -05:00
userName := user [ 0 ]
userAddress := user [ 1 ]
2021-07-21 18:06:34 -04:00
userBytesReceived := user [ 2 ]
userBytesSent := user [ 3 ]
2020-11-27 02:23:59 -05:00
userConnectedSince := user [ 4 ]
2021-02-15 01:03:38 -05:00
userStatus := clientStatus { CommonName : userName , RealAddress : userAddress , BytesReceived : userBytesReceived , BytesSent : userBytesSent , ConnectedSince : userConnectedSince , ConnectedTo : serverName }
2020-11-27 02:23:59 -05:00
u = append ( u , userStatus )
bytesSent , _ := strconv . Atoi ( userBytesSent )
2021-02-15 01:03:38 -05:00
bytesReceive , _ := strconv . Atoi ( userBytesReceived )
2022-01-20 11:42:36 -05:00
ovpnClientConnectionFrom . WithLabelValues ( userName , userAddress ) . Set ( float64 ( parseDateToUnix ( oAdmin . mgmtStatusTimeFormat , userConnectedSince ) ) )
2020-11-27 02:23:59 -05:00
ovpnClientBytesSent . WithLabelValues ( userName ) . Set ( float64 ( bytesSent ) )
ovpnClientBytesReceived . WithLabelValues ( userName ) . Set ( float64 ( bytesReceive ) )
2020-05-14 19:13:33 -04:00
}
if isRouteTable {
user := strings . Split ( txt , "," )
for i := range u {
if u [ i ] . CommonName == user [ 1 ] {
u [ i ] . VirtualAddress = user [ 0 ]
u [ i ] . LastRef = user [ 3 ]
2022-01-20 11:42:36 -05:00
ovpnClientConnectionInfo . WithLabelValues ( user [ 1 ] , user [ 0 ] ) . Set ( float64 ( parseDateToUnix ( oAdmin . mgmtStatusTimeFormat , user [ 3 ] ) ) )
2020-05-14 19:13:33 -04:00
break
}
}
}
}
2020-10-29 06:50:19 -04:00
return u
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) mgmtKillUserConnection ( username , serverName string ) {
2021-02-15 01:03:38 -05:00
conn , err := net . Dial ( "tcp" , oAdmin . mgmtInterfaces [ serverName ] )
2020-11-17 12:48:26 -05:00
if err != nil {
2022-01-20 11:42:36 -05:00
log . Errorf ( "openvpn mgmt interface for %s is not reachable by addr %s" , serverName , oAdmin . mgmtInterfaces [ serverName ] )
2020-11-17 12:48:26 -05:00
return
}
2020-11-27 02:23:59 -05:00
oAdmin . mgmtRead ( conn ) // read welcome message
2020-05-14 19:13:33 -04:00
conn . Write ( [ ] byte ( fmt . Sprintf ( "kill %s\n" , username ) ) )
2020-11-27 02:23:59 -05:00
fmt . Printf ( "%v" , oAdmin . mgmtRead ( conn ) )
2020-05-14 19:13:33 -04:00
conn . Close ( )
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) mgmtGetActiveClients ( ) [ ] clientStatus {
2021-02-15 01:03:38 -05:00
var activeClients [ ] clientStatus
for srv , addr := range oAdmin . mgmtInterfaces {
conn , err := net . Dial ( "tcp" , addr )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Warnf ( "openvpn mgmt interface for %s is not reachable by addr %s" , srv , addr )
2021-02-20 07:48:41 -05:00
break
2021-02-15 01:03:38 -05:00
}
oAdmin . mgmtRead ( conn ) // read welcome message
conn . Write ( [ ] byte ( "status\n" ) )
activeClients = append ( activeClients , oAdmin . mgmtConnectedUsersParser ( oAdmin . mgmtRead ( conn ) , srv ) ... )
conn . Close ( )
2020-11-17 12:48:26 -05:00
}
2020-10-29 06:50:19 -04:00
return activeClients
}
2022-01-20 11:42:36 -05:00
func ( oAdmin * OvpnAdmin ) mgmtSetTimeFormat ( ) {
// time format for version 2.5 and may be newer
oAdmin . mgmtStatusTimeFormat = "2006-01-02 15:04:05"
log . Debugf ( "mgmtStatusTimeFormat: %s" , oAdmin . mgmtStatusTimeFormat )
type serverVersion struct {
name string
version string
}
var serverVersions [ ] serverVersion
for srv , addr := range oAdmin . mgmtInterfaces {
var conn net . Conn
var err error
for connAttempt := 0 ; connAttempt < 10 ; connAttempt ++ {
conn , err = net . Dial ( "tcp" , addr )
if err == nil {
log . Debugf ( "mgmtSetTimeFormat: successful connection to %s/%s" , srv , addr )
break
}
log . Warnf ( "mgmtSetTimeFormat: openvpn mgmt interface for %s is not reachable by addr %s" , srv , addr )
time . Sleep ( time . Duration ( 2 ) * time . Second )
}
if err != nil {
break
}
oAdmin . mgmtRead ( conn ) // read welcome message
conn . Write ( [ ] byte ( "version\n" ) )
out := oAdmin . mgmtRead ( conn )
conn . Close ( )
log . Trace ( out )
for _ , s := range strings . Split ( out , "\n" ) {
if strings . Contains ( s , "OpenVPN Version:" ) {
serverVersions = append ( serverVersions , serverVersion { srv , strings . Split ( s , " " ) [ 3 ] } )
break
}
}
}
if len ( serverVersions ) == 0 {
return
}
firstVersion := serverVersions [ 0 ] . version
if strings . HasPrefix ( firstVersion , "2.4" ) {
oAdmin . mgmtStatusTimeFormat = time . ANSIC
log . Debugf ( "mgmtStatusTimeFormat changed: %s" , oAdmin . mgmtStatusTimeFormat )
}
warn := ""
for _ , v := range serverVersions {
if firstVersion != v . version {
warn = "mgmtSetTimeFormat: servers have different versions of openvpn, user connection status may not work"
log . Warn ( warn )
break
}
}
if warn != "" {
for _ , v := range serverVersions {
log . Infof ( "server name: %s, version: %s" , v . name , v . version )
}
}
}
2022-07-21 11:17:53 -04:00
func isUserConnected ( username string , connectedUsers [ ] clientStatus ) ( bool , [ ] string ) {
var connections [ ] string
var connected = false
2022-08-02 10:19:27 -04:00
2021-07-21 18:06:34 -04:00
for _ , connectedUser := range connectedUsers {
if connectedUser . CommonName == username {
2022-07-21 11:17:53 -04:00
connected = true
connections = append ( connections , connectedUser . ConnectedTo )
2021-07-21 18:06:34 -04:00
}
}
2022-07-21 11:17:53 -04:00
return connected , connections
2020-11-02 11:30:36 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) downloadCerts ( ) bool {
2020-11-17 12:48:26 -05:00
if fExist ( certsArchivePath ) {
2022-08-02 10:19:27 -04:00
err := fDelete ( certsArchivePath )
if err != nil {
log . Error ( err )
}
2020-11-17 12:48:26 -05:00
}
2022-08-02 10:19:27 -04:00
2021-12-07 10:12:57 -05:00
err := fDownload ( certsArchivePath , * masterHost + * listenBaseUrl + downloadCertsApiUrl + "?token=" + oAdmin . masterSyncToken , oAdmin . masterHostBasicAuth )
2021-07-21 18:06:34 -04:00
if err != nil {
2022-01-20 11:42:36 -05:00
log . Error ( err )
2020-11-17 12:48:26 -05:00
return false
2020-10-29 06:50:19 -04:00
}
2020-11-17 12:48:26 -05:00
return true
2020-10-29 06:50:19 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) downloadCcd ( ) bool {
2020-11-17 12:48:26 -05:00
if fExist ( ccdArchivePath ) {
2022-08-02 10:19:27 -04:00
err := fDelete ( ccdArchivePath )
if err != nil {
log . Error ( err )
}
2020-10-29 06:50:19 -04:00
}
2021-12-07 10:12:57 -05:00
err := fDownload ( ccdArchivePath , * masterHost + * listenBaseUrl + downloadCcdApiUrl + "?token=" + oAdmin . masterSyncToken , oAdmin . masterHostBasicAuth )
2020-10-29 06:50:19 -04:00
if err != nil {
2022-01-20 11:42:36 -05:00
log . Error ( err )
2020-11-17 12:48:26 -05:00
return false
2020-10-29 06:50:19 -04:00
}
return true
}
2020-11-17 12:48:26 -05:00
func archiveCerts ( ) {
2022-08-02 10:19:27 -04:00
err := createArchiveFromDir ( * easyrsaDirPath + "/pki" , certsArchivePath )
if err != nil {
log . Warnf ( "archiveCerts(): %s" , err )
}
2020-11-17 12:48:26 -05:00
}
func archiveCcd ( ) {
2022-08-02 10:19:27 -04:00
err := createArchiveFromDir ( * ccdDir , ccdArchivePath )
if err != nil {
log . Warnf ( "archiveCcd(): %s" , err )
}
2020-11-17 12:48:26 -05:00
}
func unArchiveCerts ( ) {
2022-08-02 10:19:27 -04:00
if err := os . MkdirAll ( * easyrsaDirPath + "/pki" , 0755 ) ; err != nil {
log . Warnf ( "unArchiveCerts(): error creating pki dir: %s" , err )
}
err := extractFromArchive ( certsArchivePath , * easyrsaDirPath + "/pki" )
if err != nil {
log . Warnf ( "unArchiveCerts: extractFromArchive() %s" , err )
}
2020-11-17 12:48:26 -05:00
}
func unArchiveCcd ( ) {
2022-08-02 10:19:27 -04:00
if err := os . MkdirAll ( * ccdDir , 0755 ) ; err != nil {
log . Warnf ( "unArchiveCcd(): error creating ccd dir: %s" , err )
}
err := extractFromArchive ( ccdArchivePath , * ccdDir )
if err != nil {
log . Warnf ( "unArchiveCcd: extractFromArchive() %s" , err )
}
2020-11-17 12:48:26 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) syncDataFromMaster ( ) {
2020-11-19 12:08:55 -05:00
retryCountMax := 3
certsDownloadFailed := true
ccdDownloadFailed := true
2022-08-02 10:19:27 -04:00
for certsDownloadRetries := 0 ; certsDownloadRetries < retryCountMax ; certsDownloadRetries ++ {
log . Infof ( "Downloading archive with certificates from master. Attempt %d" , certsDownloadRetries )
2020-11-27 02:23:59 -05:00
if oAdmin . downloadCerts ( ) {
2020-11-19 12:08:55 -05:00
certsDownloadFailed = false
2022-08-02 10:19:27 -04:00
log . Info ( "Decompressing archive with certificates from master" )
2020-11-19 12:08:55 -05:00
unArchiveCerts ( )
2022-08-02 10:19:27 -04:00
log . Info ( "Decompression archive with certificates from master completed" )
break
2020-11-19 12:08:55 -05:00
} else {
2022-08-02 10:19:27 -04:00
log . Warnf ( "Something goes wrong during downloading archive with certificates from master. Attempt %d" , certsDownloadRetries )
2020-11-19 12:08:55 -05:00
}
}
2020-11-17 12:48:26 -05:00
2022-08-02 10:19:27 -04:00
for ccdDownloadRetries := 0 ; ccdDownloadRetries < retryCountMax ; ccdDownloadRetries ++ {
log . Infof ( "Downloading archive with ccd from master. Attempt %d" , ccdDownloadRetries )
2020-11-27 02:23:59 -05:00
if oAdmin . downloadCcd ( ) {
2020-11-19 12:08:55 -05:00
ccdDownloadFailed = false
2022-08-02 10:19:27 -04:00
log . Info ( "Decompressing archive with ccd from master" )
2020-11-19 12:08:55 -05:00
unArchiveCcd ( )
2022-08-02 10:19:27 -04:00
log . Info ( "Decompression archive with ccd from master completed" )
break
2020-11-19 12:08:55 -05:00
} else {
2022-08-02 10:19:27 -04:00
log . Warnf ( "Something goes wrong during downloading archive with ccd from master. Attempt %d" , ccdDownloadRetries )
2020-11-19 12:08:55 -05:00
}
}
2020-11-17 12:48:26 -05:00
2022-01-20 11:42:36 -05:00
oAdmin . lastSyncTime = time . Now ( ) . Format ( stringDateFormat )
2020-11-19 12:08:55 -05:00
if ! ccdDownloadFailed && ! certsDownloadFailed {
2022-01-20 11:42:36 -05:00
oAdmin . lastSuccessfulSyncTime = time . Now ( ) . Format ( stringDateFormat )
2020-11-19 12:08:55 -05:00
}
2020-11-17 12:48:26 -05:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) syncWithMaster ( ) {
2021-07-21 18:06:34 -04:00
for {
2020-11-17 12:48:26 -05:00
time . Sleep ( time . Duration ( * masterSyncFrequency ) * time . Second )
2020-11-27 02:23:59 -05:00
oAdmin . syncDataFromMaster ( )
2021-07-21 18:06:34 -04:00
}
2020-11-17 12:48:26 -05:00
}
2021-12-30 03:24:44 -05:00
func getOvpnServerHostsFromKubeApi ( ) ( [ ] OpenvpnServer , error ) {
2021-10-05 11:09:29 -04:00
var hosts [ ] OpenvpnServer
var lbHost string
config , err := rest . InClusterConfig ( )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Errorf ( "%s" , err . Error ( ) )
2021-10-05 11:09:29 -04:00
}
clientset , err := kubernetes . NewForConfig ( config )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Errorf ( "%s" , err . Error ( ) )
2021-10-05 11:09:29 -04:00
}
2022-04-27 06:42:59 -04:00
for _ , serviceName := range * openvpnServiceName {
service , err := clientset . CoreV1 ( ) . Services ( fRead ( kubeNamespaceFilePath ) ) . Get ( context . TODO ( ) , serviceName , metav1 . GetOptions { } )
if err != nil {
log . Error ( err )
}
2021-10-05 11:09:29 -04:00
2022-08-02 10:19:27 -04:00
log . Tracef ( "service from kube api %v" , service )
log . Tracef ( "service.Status from kube api %v" , service . Status )
log . Tracef ( "service.Status.LoadBalancer from kube api %v" , service . Status . LoadBalancer )
2021-10-05 11:09:29 -04:00
2022-04-27 06:42:59 -04:00
lbIngress := service . Status . LoadBalancer . Ingress
if len ( lbIngress ) > 0 {
if lbIngress [ 0 ] . Hostname != "" {
lbHost = lbIngress [ 0 ] . Hostname
}
2022-01-20 11:42:36 -05:00
2022-04-27 06:42:59 -04:00
if lbIngress [ 0 ] . IP != "" {
lbHost = lbIngress [ 0 ] . IP
}
2022-01-20 11:42:36 -05:00
}
2022-04-27 06:42:59 -04:00
hosts = append ( hosts , OpenvpnServer { lbHost , strconv . Itoa ( int ( service . Spec . Ports [ 0 ] . Port ) ) , strings . ToLower ( string ( service . Spec . Ports [ 0 ] . Protocol ) ) } )
2021-10-05 11:09:29 -04:00
}
2022-01-20 11:42:36 -05:00
2022-04-27 06:42:59 -04:00
if len ( hosts ) == 0 {
return [ ] OpenvpnServer { { Host : "kubernetes services not found" } } , err
}
2021-10-05 11:09:29 -04:00
2021-12-30 03:24:44 -05:00
return hosts , nil
2021-10-05 11:09:29 -04:00
}
2021-02-15 01:03:38 -05:00
func getOvpnCaCertExpireDate ( ) time . Time {
2020-11-27 02:23:59 -05:00
caCertPath := * easyrsaDirPath + "/pki/ca.crt"
2022-01-20 09:49:03 -05:00
caCert , err := ioutil . ReadFile ( caCertPath )
if err != nil {
log . Errorf ( "error read file %s: %s" , caCertPath , err . Error ( ) )
}
certPem , _ := pem . Decode ( caCert )
certPemBytes := certPem . Bytes
2020-11-27 02:23:59 -05:00
2022-01-20 09:49:03 -05:00
cert , err := x509 . ParseCertificate ( certPemBytes )
2020-11-27 02:23:59 -05:00
if err != nil {
2022-01-20 09:49:03 -05:00
log . Errorf ( "error parse certificate ca.crt: %s" , err . Error ( ) )
2020-11-27 02:23:59 -05:00
return time . Now ( )
}
2022-01-20 09:49:03 -05:00
return cert . NotAfter
2020-11-27 02:23:59 -05:00
}
2020-11-17 12:48:26 -05:00
// https://community.openvpn.net/openvpn/ticket/623
func crlFix ( ) {
2021-12-30 03:24:44 -05:00
err := os . Chmod ( * easyrsaDirPath + "/pki" , 0755 )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Error ( err )
2020-11-27 02:23:59 -05:00
}
2021-12-30 03:24:44 -05:00
err = os . Chmod ( * easyrsaDirPath + "/pki/crl.pem" , 0644 )
if err != nil {
2022-01-20 11:42:36 -05:00
log . Error ( err )
2020-10-29 06:50:19 -04:00
}
2020-11-27 02:23:59 -05:00
}