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"
2020-11-17 12:48:26 -05:00
"encoding/json"
2020-05-14 19:13:33 -04:00
"fmt"
"log"
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"
"text/template"
2020-11-17 12:48:26 -05:00
"time"
2021-02-26 07:11:13 -05:00
2021-10-05 11:09:29 -04:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
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"
"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_.-@])+$ `
passwordRegexp = ` ^([a-zA-Z0-9_.-@])+$ `
passwordMinLength = 6
downloadCertsApiUrl = "/api/data/certs/download"
downloadCcdApiUrl = "/api/data/ccd/download"
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-02-20 10:46:30 -05:00
ovpnStatusDateLayout = "2006-01-02 15:04:05"
2021-10-21 06:01:03 -04:00
kubeTokenFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
kubeNamespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
2020-10-15 12:12:31 -04:00
)
var (
2021-10-21 06:01:03 -04:00
listenHost = kingpin . Flag ( "listen.host" , "host for ovpn-admin" ) . Default ( "0.0.0.0" ) . Envar ( "OVPN_LISTEN_HOST" ) . String ( )
2021-10-22 00:35:16 -04:00
listenPort = kingpin . Flag ( "listen.port" , "port for ovpn-admin" ) . Default ( "8080" ) . Envar ( "OVPN_LISTEN_PORT" ) . String ( )
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 ( )
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-10-22 00:35:16 -04: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 ( )
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" ) . String ( )
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 ( )
2021-10-21 06:01:03 -04:00
easyrsaDirPath = kingpin . Flag ( "easyrsa.path" , "path to easyrsa dir" ) . Default ( "./easyrsa/" ) . Envar ( "EASYRSA_PATH" ) . String ( )
2021-10-22 00:35:16 -04:00
indexTxtPath = kingpin . Flag ( "easyrsa.index-path" , "path to easyrsa index file" ) . Default ( "./easyrsa/pki/index.txt" ) . Envar ( "OVPN_INDEX_PATH" ) . String ( )
ccdEnabled = kingpin . Flag ( "ccd" , "enable client-config-dir" ) . Default ( "false" ) . Envar ( "OVPN_CCD" ) . Bool ( )
2021-10-21 06:01:03 -04:00
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-10-22 00:35:16 -04: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 ( )
debug = kingpin . Flag ( "debug" , "enable debug mode" ) . Default ( "false" ) . Envar ( "OVPN_DEBUG" ) . Bool ( )
verbose = kingpin . Flag ( "verbose" , "enable verbose mode" ) . Default ( "false" ) . Envar ( "OVPN_VERBOSE" ) . Bool ( )
2021-10-05 11:09:29 -04:00
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
2021-11-29 03:34:34 -05:00
version = "1.7.5"
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" ,
Help : "connected openvpn users" ,
} ,
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
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-10-05 11:09:29 -04:00
Identity string ` json:"Identity" `
AccountStatus string ` json:"AccountStatus" `
ExpirationDate string ` json:"ExpirationDate" `
RevocationDate string ` json:"RevocationDate" `
ConnectionStatus string ` json:"ConnectionStatus" `
ConnectionServer string ` json:"ConnectionServer" `
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 ) {
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 ) {
2020-11-27 02:23:59 -05:00
r . ParseForm ( )
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 ) {
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
}
2020-05-14 19:13:33 -04: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 {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , userCreateStatus )
return
} else {
http . Error ( w , userCreateStatus , http . StatusUnprocessableEntity )
}
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 ) {
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
}
2020-05-14 19:13:33 -04:00
r . ParseForm ( )
2020-11-27 02:23:59 -05:00
fmt . Fprintf ( w , "%s" , oAdmin . userRevoke ( r . FormValue ( "username" ) ) )
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 ) {
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
}
2020-05-14 19:13:33 -04:00
r . ParseForm ( )
2020-11-27 02:23:59 -05:00
fmt . Fprintf ( w , "%s" , oAdmin . userUnrevoke ( r . FormValue ( "username" ) ) )
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 ) {
2021-02-15 01:03:38 -05:00
r . ParseForm ( )
if * authByPassword {
passwordChanged , passwordChangeMessage := oAdmin . userChangePassword ( r . FormValue ( "username" ) , r . FormValue ( "password" ) )
if passwordChanged {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , ` { "status":"ok", "message": "%s"} ` , passwordChangeMessage )
return
} else {
w . WriteHeader ( http . StatusInternalServerError )
fmt . Fprintf ( w , ` { "status":"error", "message": "%s"} ` , passwordChangeMessage )
return
}
} 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 ) {
2020-05-14 19:13:33 -04: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 ) {
2020-10-29 06:50:19 -04: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 ) {
2020-10-15 12:12:31 -04: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 ) {
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 {
log . Println ( err )
}
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 ) {
2021-02-26 07:11:13 -05:00
enabledModules , enabledModulesErr := json . Marshal ( oAdmin . modules )
if enabledModulesErr != nil {
2021-07-21 18:06:34 -04:00
log . Printf ( "ERROR: %s\n" , 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 ) {
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 ) {
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 ) {
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
}
r . ParseForm ( )
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 ) {
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
}
r . ParseForm ( )
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
}
2020-05-14 19:13:33 -04:00
func main ( ) {
2021-02-20 07:48:41 -05:00
kingpin . Version ( version )
kingpin . Parse ( )
2021-03-17 06:44:12 -04:00
ovpnAdmin := new ( OvpnAdmin )
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 { }
2020-11-27 02:23:59 -05:00
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 ]
}
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 {
ovpnAdmin . modules = append ( ovpnAdmin . modules , "passwdAuth" )
}
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-10-21 06:01:03 -04:00
if * debug {
log . Println ( "Runnnig in debug mode" )
}
if * verbose {
log . Println ( "Runnnig in verbose mode" )
}
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-02-26 07:11:13 -05:00
http . Handle ( "/" , static )
http . HandleFunc ( "/api/server/settings" , ovpnAdmin . serverSettingsHandler )
2020-11-27 02:23:59 -05:00
http . HandleFunc ( "/api/users/list" , ovpnAdmin . userListHandler )
http . HandleFunc ( "/api/user/create" , ovpnAdmin . userCreateHandler )
2021-02-15 01:03:38 -05:00
http . HandleFunc ( "/api/user/change-password" , ovpnAdmin . userChangePasswordHandler )
2020-11-27 02:23:59 -05:00
http . HandleFunc ( "/api/user/revoke" , ovpnAdmin . userRevokeHandler )
http . HandleFunc ( "/api/user/unrevoke" , ovpnAdmin . userUnrevokeHandler )
http . HandleFunc ( "/api/user/config/show" , ovpnAdmin . userShowConfigHandler )
http . HandleFunc ( "/api/user/disconnect" , ovpnAdmin . userDisconnectHandler )
http . HandleFunc ( "/api/user/statistic" , ovpnAdmin . userStatisticHandler )
http . HandleFunc ( "/api/user/ccd" , ovpnAdmin . userShowCcdHandler )
http . HandleFunc ( "/api/user/ccd/apply" , ovpnAdmin . userApplyCcdHandler )
http . HandleFunc ( "/api/sync/last/try" , ovpnAdmin . lastSyncTimeHandler )
http . HandleFunc ( "/api/sync/last/successful" , ovpnAdmin . lastSuccessfulSyncTimeHandler )
http . HandleFunc ( downloadCertsApiUrl , ovpnAdmin . downloadCertsHandler )
2021-02-15 01:03:38 -05:00
http . HandleFunc ( downloadCcdApiUrl , ovpnAdmin . downloadCcdHandler )
2020-11-27 02:23:59 -05:00
http . Handle ( * metricsPath , promhttp . HandlerFor ( ovpnAdmin . promRegistry , promhttp . HandlerOpts { } ) )
http . HandleFunc ( "/ping" , func ( w http . ResponseWriter , r * http . Request ) {
fmt . Fprintf ( w , "pong" )
} )
2020-11-17 12:48:26 -05:00
2021-10-21 06:01:03 -04:00
log . Printf ( "Bind: http://%s:%s\n" , * listenHost , * listenPort )
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 )
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 ( )
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 {
log . Println ( "ERROR: clientConfigTpl not found in templates box" )
}
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-10-05 11:09:29 -04:00
if * openvpnServerBehindLB {
hosts = getOvpnServerHostsFromKubeApi ( )
}
2021-03-10 09:54:27 -05:00
if * debug {
2021-07-21 18:06:34 -04:00
log . Printf ( "WARNING: hosts for %s\n %v" , username , hosts )
2021-03-10 09:54:27 -05:00
}
2020-11-17 12:48:26 -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 . Cert = fRead ( * easyrsaDirPath + "/pki/issued/" + username + ".crt" )
conf . Key = fRead ( * easyrsaDirPath + "/pki/private/" + username + ".key" )
conf . TLS = fRead ( * easyrsaDirPath + "/pki/ta.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 {
2021-10-15 00:54:24 -04:00
log . Printf ( "ERROR: something goes wrong during rendering config for %s\n" , username )
2021-03-10 09:54:27 -05:00
if * debug {
2021-10-15 00:54:24 -04:00
log . Printf ( "DEBUG: rendering config for %s failed with error %v\n" , username , err )
2021-03-10 09:54:27 -05:00
}
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-10-15 00:54:24 -04:00
if * verbose {
log . Printf ( "INFO: Rendered config for user %s: %+v\n" , username , tmp . String ( ) )
}
2020-11-27 02:23:59 -05:00
return fmt . Sprintf ( "%+v\n" , tmp . String ( ) )
2020-05-14 19:13:33 -04:00
}
2021-10-15 00:54:24 -04:00
log . Printf ( "WARNING: User \"%s\" not found" , username )
2020-10-29 06:50:19 -04:00
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 {
log . Printf ( "ERROR: ccdTpl not found in templates box" )
}
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
2021-07-21 18:06:34 -04:00
txtLinesArray := strings . Split ( fRead ( * ccdDir + "/" + username ) , "\n" )
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 ) {
2020-11-17 12:48:26 -05:00
ccdErr := "something goes wrong"
2020-10-29 06:50:19 -04:00
2021-07-21 18:06:34 -04:00
if fCreate ( * ccdDir + "/" + ccd . User ) {
ccdValid , ccdErr := validateCcd ( ccd )
if ccdErr != "" {
return false , ccdErr
}
2021-02-26 07:11:13 -05:00
2021-07-21 18:06:34 -04:00
if ccdValid {
t := oAdmin . getCcdTemplate ( )
var tmp bytes . Buffer
tplErr := t . Execute ( & tmp , ccd )
2020-11-17 12:48:26 -05:00
if tplErr != nil {
log . Println ( tplErr )
}
2021-07-21 18:06:34 -04:00
fWrite ( * ccdDir + "/" + ccd . User , tmp . String ( ) )
return true , "ccd updated successfully"
}
}
2020-10-15 12:12:31 -04:00
2020-11-17 12:48:26 -05:00
return false , ccdErr
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-10-05 11:09:29 -04:00
ccdErr := ""
if ccd . ClientAddress != "dynamic" {
_ , ovpnNet , err := net . ParseCIDR ( * openvpnNetwork )
if err != nil {
log . Println ( err )
}
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\n" , ccd . User , ccdErr )
}
return false , ccdErr
}
if net . ParseIP ( ccd . ClientAddress ) == nil {
ccdErr = fmt . Sprintf ( "ClientAddress \"%s\" not a valid IP address" , ccd . ClientAddress )
if * debug {
log . Printf ( "ERROR: Modify ccd for user %s: %s\n" , ccd . User , ccdErr )
}
return false , ccdErr
}
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\n" , ccd . User , ccdErr )
}
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 )
if * debug {
log . Printf ( "ERROR: Modify ccd for user %s: %s\n" , ccd . User , ccdErr )
}
return false , ccdErr
}
if net . ParseIP ( route . Mask ) == nil {
ccdErr = fmt . Sprintf ( "CustomRoute.Mask \"%s\" must be a valid IP address" , route . Mask )
if * debug {
log . Printf ( "ERROR: Modify ccd for user %s: %s\n" , ccd . User , ccdErr )
}
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 { }
2021-07-21 18:06:34 -04:00
if fCreate ( * ccdDir + "/" + username ) {
ccd = oAdmin . parseCcd ( username )
}
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
}
func validateUsername ( username string ) bool {
var validUsername = regexp . MustCompile ( usernameRegexp )
2020-10-29 06:50:19 -04:00
return validUsername . MatchString ( username )
2020-05-14 19:13:33 -04:00
}
2021-02-15 01:03:38 -05:00
func validatePassword ( password string ) bool {
if len ( password ) < passwordMinLength {
return false
} else {
return true
}
}
2020-05-14 19:13:33 -04:00
func checkUserExist ( username string ) bool {
2020-10-15 12:12:31 -04: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
connectedUsers := 0
apochNow := time . Now ( ) . Unix ( )
2020-11-02 11:30:36 -05:00
2020-10-15 12:12:31 -04:00
for _ , line := range indexTxtParser ( fRead ( * indexTxtPath ) ) {
2021-07-21 18:06:34 -04:00
if line . Identity != "server" {
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"
ovpnClientCertificateExpire . WithLabelValues ( line . Identity ) . Set ( float64 ( ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) / 3600 / 24 ) )
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 )
ovpnClientCertificateExpire . WithLabelValues ( line . Identity ) . Set ( float64 ( ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) / 3600 / 24 ) )
revokedCerts += 1
case line . Flag == "E" :
ovpnClient . AccountStatus = "Expired"
ovpnClientCertificateExpire . WithLabelValues ( line . Identity ) . Set ( float64 ( ( parseDateToUnix ( indexTxtDateLayout , line . ExpirationDate ) - apochNow ) / 3600 / 24 ) )
expiredCerts += 1
}
2020-11-27 02:23:59 -05:00
2021-02-15 01:03:38 -05:00
ovpnClient . ConnectionServer = ""
2021-07-21 18:06:34 -04:00
userConnected , userConnectedTo := isUserConnected ( line . Identity , oAdmin . activeClients )
if userConnected {
ovpnClient . ConnectionStatus = "Connected"
ovpnClient . ConnectionServer = userConnectedTo
2020-11-27 02:23:59 -05:00
connectedUsers += 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 {
2021-10-05 11:09:29 -04:00
log . Printf ( "WARNING: there are %d otherCerts\n" , otherCerts )
2020-11-27 02:23:59 -05:00
}
ovpnClientsTotal . Set ( float64 ( totalCerts ) )
ovpnClientsRevoked . Set ( float64 ( revokedCerts ) )
ovpnClientsExpired . Set ( float64 ( expiredCerts ) )
ovpnClientsConnected . Set ( float64 ( connectedUsers ) )
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
if checkUserExist ( username ) {
ucErr = fmt . Sprintf ( "User \"%s\" already exists\n" , username )
if * debug {
2021-10-05 11:09:29 -04:00
log . Printf ( "ERROR: userCreate: %s\n" , ucErr )
2021-02-15 01:03:38 -05:00
}
return false , ucErr
}
2021-07-21 18:06:34 -04:00
if ! validateUsername ( username ) {
2020-11-03 11:32:37 -05:00
ucErr = fmt . Sprintf ( "Username \"%s\" incorrect, you can use only %s\n" , username , usernameRegexp )
2021-02-15 01:03:38 -05:00
if * debug {
2021-10-05 11:09:29 -04:00
log . Printf ( "ERROR: userCreate: %s\n" , ucErr )
2021-02-15 01:03:38 -05:00
}
2020-11-03 11:32:37 -05:00
return false , ucErr
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 {
if ! validatePassword ( password ) {
ucErr = fmt . Sprintf ( "Password too short, password length must be greater or equal %d" , passwordMinLength )
if * debug {
log . Printf ( "ERROR: userCreate: %s\n" , ucErr )
}
return false , ucErr
2021-02-15 01:03:38 -05:00
}
2020-05-14 19:13:33 -04:00
}
2021-02-15 01:03:38 -05:00
2020-10-29 06:50:19 -04:00
o := runBash ( fmt . Sprintf ( "date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass" , * easyrsaDirPath , username ) )
2021-10-05 11:09:29 -04:00
log . Println ( o )
2021-02-15 01:03:38 -05:00
if * authByPassword {
o = runBash ( fmt . Sprintf ( "openvpn-user create --db.path %s --user %s --password %s" , * authDatabase , username , password ) )
2021-10-05 11:09:29 -04:00
log . Println ( o )
2021-02-15 01:03:38 -05:00
}
if * verbose {
2021-10-05 11:09:29 -04:00
log . Printf ( "INFO: user created: %s\n" , username )
2020-11-27 02:23:59 -05:00
}
2021-02-15 01:03:38 -05:00
2020-11-27 02:23:59 -05: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
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userChangePassword ( username , password string ) ( bool , 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 ) )
2021-10-05 11:09:29 -04:00
log . Println ( o )
2021-02-15 01:03:38 -05:00
2021-07-21 18:06:34 -04:00
if ! validatePassword ( password ) {
2021-02-15 01:03:38 -05:00
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 )
}
return false , ucpErr
}
if strings . TrimSpace ( o ) == "0" {
2021-10-05 11:09:29 -04:00
log . Println ( "Creating user in users.db" )
2021-02-15 01:03:38 -05:00
o = runBash ( fmt . Sprintf ( "openvpn-user create --db.path %s --user %s --password %s" , * authDatabase , username , password ) )
2021-10-05 11:09:29 -04:00
log . Println ( 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 ) )
2021-10-05 11:09:29 -04:00
log . Println ( o )
2021-02-15 01:03:38 -05:00
if * verbose {
2021-10-05 11:09:29 -04:00
log . Printf ( "INFO: password for user %s was changed\n" , username )
2021-02-15 01:03:38 -05:00
}
return true , "Password changed"
}
return false , "User does not exist"
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) getUserStatistic ( username string ) clientStatus {
2020-11-27 02:23:59 -05:00
for _ , u := range oAdmin . activeClients {
if u . CommonName == username {
return u
}
}
return clientStatus { }
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userRevoke ( username string ) string {
2020-05-14 19:13:33 -04:00
if checkUserExist ( username ) {
// check certificate valid flag 'V'
2020-10-29 06:50:19 -04:00
o := runBash ( fmt . Sprintf ( "date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | easyrsa revoke %s && easyrsa gen-crl" , * easyrsaDirPath , username ) )
2021-02-15 01:03:38 -05:00
if * authByPassword {
o = runBash ( fmt . Sprintf ( "openvpn-user revoke --db-path %s --user %s" , * authDatabase , username ) )
//fmt.Println(o)
}
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 )
if userConnected {
oAdmin . mgmtKillUserConnection ( username , userConnectedTo )
log . Printf ( "Session for user \"%s\" session killed\n" , username )
}
2020-11-27 02:23:59 -05:00
oAdmin . clients = oAdmin . usersList ( )
2020-10-29 06:50:19 -04:00
return fmt . Sprintln ( o )
2020-05-14 19:13:33 -04:00
}
2021-10-05 11:09:29 -04:00
log . Printf ( "User \"%s\" not found\n" , username )
2020-10-29 06:50:19 -04:00
return fmt . Sprintf ( "User \"%s\" not found" , username )
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) userUnrevoke ( username string ) string {
2020-05-14 19:13:33 -04:00
if checkUserExist ( username ) {
// check certificate revoked flag 'R'
2020-10-15 12:12:31 -04:00
usersFromIndexTxt := indexTxtParser ( fRead ( * indexTxtPath ) )
2020-05-14 19:13:33 -04:00
for i := range usersFromIndexTxt {
if usersFromIndexTxt [ i ] . DistinguishedName == ( "/CN=" + username ) {
2021-07-21 18:06:34 -04:00
if usersFromIndexTxt [ i ] . Flag == "R" {
usersFromIndexTxt [ i ] . Flag = "V"
usersFromIndexTxt [ i ] . RevocationDate = ""
o := runBash ( fmt . Sprintf ( "cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/issued/%s.crt" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber , username ) )
//fmt.Println(o)
o = runBash ( fmt . Sprintf ( "cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/certs_by_serial/%s.pem" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber , usersFromIndexTxt [ i ] . SerialNumber ) )
//fmt.Println(o)
o = runBash ( fmt . Sprintf ( "cd %s && cp pki/revoked/private_by_serial/%s.key pki/private/%s.key" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber , username ) )
//fmt.Println(o)
o = runBash ( fmt . Sprintf ( "cd %s && cp pki/revoked/reqs_by_serial/%s.req pki/reqs/%s.req" , * easyrsaDirPath , usersFromIndexTxt [ i ] . SerialNumber , username ) )
//fmt.Println(o)
fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
o = runBash ( fmt . Sprintf ( "cd %s && easyrsa gen-crl" , * easyrsaDirPath ) )
//fmt.Println(o)
2021-02-15 01:03:38 -05:00
if * authByPassword {
o = runBash ( fmt . Sprintf ( "openvpn-user restore --db-path %s --user %s" , * authDatabase , username ) )
//fmt.Println(o)
}
2021-07-21 18:06:34 -04:00
crlFix ( )
o = ""
2020-11-27 02:23:59 -05:00
fmt . Println ( o )
2021-07-21 18:06:34 -04:00
break
}
2020-05-14 19:13:33 -04:00
}
}
2020-10-15 12:12:31 -04:00
fWrite ( * indexTxtPath , renderIndexTxt ( usersFromIndexTxt ) )
2020-05-14 19:13:33 -04:00
fmt . Print ( renderIndexTxt ( usersFromIndexTxt ) )
crlFix ( )
2020-11-27 02:23:59 -05:00
oAdmin . clients = oAdmin . usersList ( )
2020-10-29 06:50:19 -04:00
return fmt . Sprintf ( "{\"msg\":\"User %s successfully unrevoked\"}" , username )
2020-05-14 19:13:33 -04:00
}
2020-10-29 06:50:19 -04:00
return fmt . Sprintf ( "{\"msg\":\"User \"%s\" not found\"}" , username )
2020-05-14 19:13:33 -04:00
}
2021-03-17 06:44:12 -04:00
func ( oAdmin * OvpnAdmin ) mgmtRead ( conn net . Conn ) string {
2020-05-14 19:13:33 -04:00
buf := make ( [ ] byte , 32768 )
2020-11-17 12:48:26 -05:00
bufLen , _ := conn . Read ( buf )
s := string ( buf [ : bufLen ] )
2020-10-29 06:50:19 -04:00
return s
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 )
2020-11-27 02:23:59 -05:00
ovpnClientConnectionFrom . WithLabelValues ( userName , userAddress ) . Set ( float64 ( parseDateToUnix ( ovpnStatusDateLayout , userConnectedSince ) ) )
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 ]
2020-11-27 02:23:59 -05:00
ovpnClientConnectionInfo . WithLabelValues ( user [ 1 ] , user [ 0 ] ) . Set ( float64 ( parseDateToUnix ( ovpnStatusDateLayout , 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 {
2021-02-20 07:48:41 -05:00
log . Printf ( "WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n" , 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 {
2021-02-20 07:48:41 -05:00
log . Printf ( "WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n" , srv , addr )
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
}
2021-02-15 01:03:38 -05:00
func isUserConnected ( username string , connectedUsers [ ] clientStatus ) ( bool , string ) {
2021-07-21 18:06:34 -04:00
for _ , connectedUser := range connectedUsers {
if connectedUser . CommonName == username {
return true , connectedUser . ConnectedTo
}
}
return false , ""
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 ) {
fDelete ( certsArchivePath )
}
2021-07-21 18:06:34 -04:00
err := fDownload ( certsArchivePath , * masterHost + downloadCertsApiUrl + "?token=" + oAdmin . masterSyncToken , oAdmin . masterHostBasicAuth )
if err != nil {
2020-11-19 12:08:55 -05:00
log . Println ( 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 ) {
fDelete ( ccdArchivePath )
2020-10-29 06:50:19 -04:00
}
2021-07-21 18:06:34 -04:00
err := fDownload ( ccdArchivePath , * masterHost + downloadCcdApiUrl + "?token=" + oAdmin . masterSyncToken , oAdmin . masterHostBasicAuth )
2020-10-29 06:50:19 -04:00
if err != nil {
2020-11-19 12:08:55 -05:00
log . Println ( 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 ( ) {
2021-07-21 18:06:34 -04:00
o := runBash ( fmt . Sprintf ( "cd %s && tar -czf %s *" , * easyrsaDirPath + "/pki" , certsArchivePath ) )
2020-11-17 12:48:26 -05:00
fmt . Println ( o )
}
func archiveCcd ( ) {
2021-07-21 18:06:34 -04:00
o := runBash ( fmt . Sprintf ( "cd %s && tar -czf %s *" , * ccdDir , ccdArchivePath ) )
2020-11-17 12:48:26 -05:00
fmt . Println ( o )
}
func unArchiveCerts ( ) {
2021-07-21 18:06:34 -04:00
runBash ( fmt . Sprintf ( "mkdir -p %s" , * easyrsaDirPath + "/pki" ) )
o := runBash ( fmt . Sprintf ( "cd %s && tar -xzf %s" , * easyrsaDirPath + "/pki" , certsArchivePath ) )
2020-11-17 12:48:26 -05:00
fmt . Println ( o )
}
func unArchiveCcd ( ) {
runBash ( fmt . Sprintf ( "mkdir -p %s" , * ccdDir ) )
2021-07-21 18:06:34 -04:00
o := runBash ( fmt . Sprintf ( "cd %s && tar -xzf %s" , * ccdDir , ccdArchivePath ) )
2020-11-17 12:48:26 -05:00
fmt . Println ( o )
}
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
certsDownloadRetries := 0
ccdDownloadRetries := 0
for certsDownloadFailed && certsDownloadRetries < retryCountMax {
certsDownloadRetries += 1
2021-10-05 11:09:29 -04:00
log . Printf ( "Downloading certs archive from master. Attempt %d\n" , certsDownloadRetries )
2020-11-27 02:23:59 -05:00
if oAdmin . downloadCerts ( ) {
2020-11-19 12:08:55 -05:00
certsDownloadFailed = false
log . Println ( "Decompression certs archive from master" )
unArchiveCerts ( )
} else {
2021-10-05 11:09:29 -04:00
log . Printf ( "WARNING: something goes wrong during downloading certs from master. Attempt %d\n" , certsDownloadRetries )
2020-11-19 12:08:55 -05:00
}
}
2020-11-17 12:48:26 -05:00
2020-11-19 12:08:55 -05:00
for ccdDownloadFailed && ccdDownloadRetries < retryCountMax {
ccdDownloadRetries += 1
2021-10-05 11:09:29 -04:00
log . Printf ( "Downloading ccd archive from master. Attempt %d\n" , ccdDownloadRetries )
2020-11-27 02:23:59 -05:00
if oAdmin . downloadCcd ( ) {
2020-11-19 12:08:55 -05:00
ccdDownloadFailed = false
log . Println ( "Decompression ccd archive from master" )
unArchiveCcd ( )
} else {
2021-10-05 11:09:29 -04:00
log . Printf ( "WARNING: something goes wrong during downloading certs from master. Attempt %d\n" , ccdDownloadRetries )
2020-11-19 12:08:55 -05:00
}
}
2020-11-17 12:48:26 -05:00
2020-11-27 02:23:59 -05:00
oAdmin . lastSyncTime = time . Now ( ) . Format ( "2006-01-02 15:04:05" )
2020-11-19 12:08:55 -05:00
if ! ccdDownloadFailed && ! certsDownloadFailed {
2020-11-27 02:23:59 -05:00
oAdmin . lastSuccessfulSyncTime = time . Now ( ) . Format ( "2006-01-02 15:04:05" )
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-10-05 11:09:29 -04:00
func getOvpnServerHostsFromKubeApi ( ) [ ] OpenvpnServer {
var hosts [ ] OpenvpnServer
var lbHost string
config , err := rest . InClusterConfig ( )
if err != nil {
log . Printf ( "ERROR: %s\n" , err . Error ( ) )
}
clientset , err := kubernetes . NewForConfig ( config )
if err != nil {
log . Printf ( "ERROR: %s\n" , err . Error ( ) )
}
service , err := clientset . CoreV1 ( ) . Services ( fRead ( kubeNamespaceFilePath ) ) . Get ( context . TODO ( ) , * openvpnServiceName , metav1 . GetOptions { } )
if err != nil {
log . Printf ( "ERROR: %s\n" , err . Error ( ) )
}
if * debug {
log . Printf ( "Debug: service from kube api %v\n" , service )
log . Printf ( "Debug: service.Status from kube api %v\n" , service . Status )
log . Printf ( "Debug: service.Status.LoadBalancer from kube api %v\n" , service . Status . LoadBalancer )
}
if service . Status . LoadBalancer . Ingress [ 0 ] . Hostname != "" {
lbHost = service . Status . LoadBalancer . Ingress [ 0 ] . Hostname
}
if service . Status . LoadBalancer . Ingress [ 0 ] . IP != "" {
lbHost = service . Status . LoadBalancer . Ingress [ 0 ] . IP
}
2021-10-14 01:11:07 -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
return hosts
}
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"
caCertExpireDate := runBash ( fmt . Sprintf ( "openssl x509 -in %s -noout -enddate | awk -F \"=\" {'print $2'}" , caCertPath ) )
dateLayout := "Jan 2 15:04:05 2006 MST"
t , err := time . Parse ( dateLayout , strings . TrimSpace ( caCertExpireDate ) )
if err != nil {
2021-10-05 11:09:29 -04:00
log . Printf ( "WARNING: can`t parse expire date for CA cert: %v\n" , err )
2020-11-27 02:23:59 -05:00
return time . Now ( )
}
return t
}
2020-11-17 12:48:26 -05:00
// https://community.openvpn.net/openvpn/ticket/623
func crlFix ( ) {
2021-07-21 18:06:34 -04:00
err1 := os . Chmod ( * easyrsaDirPath + "/pki" , 0755 )
2020-11-27 02:23:59 -05:00
if err1 != nil {
log . Println ( err1 )
}
2021-07-21 18:06:34 -04:00
err2 := os . Chmod ( * easyrsaDirPath + "/pki/crl.pem" , 0644 )
2020-11-27 02:23:59 -05:00
if err2 != nil {
log . Println ( err2 )
2020-10-29 06:50:19 -04:00
}
2020-11-27 02:23:59 -05:00
}