ovpn-admin/backend/utils.go

558 lines
12 KiB
Go

package backend
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
)
var (
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
)
func validateCcd(ccd CCD) (bool, string) {
ccdErr := ""
if ccd.ClientAddress != "dynamic" {
_, ovpnNet, err := net.ParseCIDR(*openvpnNetwork)
if err != nil {
log.Error(err)
}
if !checkStaticAddressIsFree(ccd.ClientAddress, ccd.User) {
ccdErr = fmt.Sprintf("ClientAddress \"%s\" already assigned to another user", ccd.ClientAddress)
log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr)
return false, ccdErr
}
if net.ParseIP(ccd.ClientAddress) == nil {
ccdErr = fmt.Sprintf("ClientAddress \"%s\" not a valid IP address", ccd.ClientAddress)
log.Debugf("modify ccd for user %s: %s", 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)
log.Debugf("modify ccd for user %s: %s", 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)
log.Debugf("modify ccd for user %s: %s", 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)
log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr)
return false, ccdErr
}
}
return true, ccdErr
}
func checkStaticAddressIsFree(staticAddress string, username string) bool {
o := runBash(fmt.Sprintf("grep -rl ' %s ' %s | grep -vx %s/%s | wc -l", staticAddress, *CcdDir, *CcdDir, username))
if strings.TrimSpace(o) == "0" {
return true
}
return false
}
func validateUsername(username string) error {
var validUsername = regexp.MustCompile(usernameRegexp)
if validUsername.MatchString(username) {
return nil
} else {
return errors.New(fmt.Sprintf("Username can only contains %s", usernameRegexp))
}
}
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))
} else {
return nil
}
}
func checkUserExist(username string) bool {
for _, u := range IndexTxtParser(fRead(*IndexTxtPath)) {
if u.DistinguishedName == ("/CN=" + username) {
return true
}
}
return false
}
func isUserConnected(username string, connectedUsers []ClientStatus) (bool, []string) {
var connections []string
var connected = false
for _, connectedUser := range connectedUsers {
if connectedUser.CommonName == username {
connected = true
connections = append(connections, connectedUser.ConnectedTo)
}
}
return connected, connections
}
func archiveCerts() {
err := createArchiveFromDir(*EasyrsaDirPath+"/pki", certsArchivePath)
if err != nil {
log.Warnf("archiveCerts(): %s", err)
}
}
func archiveCcd() {
err := createArchiveFromDir(*CcdDir, ccdArchivePath)
if err != nil {
log.Warnf("archiveCcd(): %s", err)
}
}
func unArchiveCerts() {
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)
}
}
func unArchiveCcd() {
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)
}
}
func getOvpnServerHostsFromKubeApi() ([]OpenvpnServer, error) {
var hosts []OpenvpnServer
var lbHost string
config, err := rest.InClusterConfig()
if err != nil {
log.Errorf("%s", err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Errorf("%s", err.Error())
}
for _, serviceName := range *openvpnServiceName {
service, err := clientset.CoreV1().Services(fRead(KubeNamespaceFilePath)).Get(context.TODO(), serviceName, metav1.GetOptions{})
if err != nil {
log.Error(err)
}
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)
lbIngress := service.Status.LoadBalancer.Ingress
if len(lbIngress) > 0 {
if lbIngress[0].Hostname != "" {
lbHost = lbIngress[0].Hostname
}
if lbIngress[0].IP != "" {
lbHost = lbIngress[0].IP
}
}
hosts = append(hosts, OpenvpnServer{lbHost, strconv.Itoa(int(service.Spec.Ports[0].Port)), strings.ToLower(string(service.Spec.Ports[0].Protocol))})
}
if len(hosts) == 0 {
return []OpenvpnServer{{Host: "kubernetes services not found"}}, err
}
return hosts, nil
}
func getOvpnCaCertExpireDate() time.Time {
caCertPath := *EasyrsaDirPath + "/pki/ca.crt"
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
cert, err := x509.ParseCertificate(certPemBytes)
if err != nil {
log.Errorf("error parse certificate ca.crt: %s", err.Error())
return time.Now()
}
return cert.NotAfter
}
// https://community.openvpn.net/openvpn/ticket/623
func crlFix() {
err := os.Chmod(*EasyrsaDirPath+"/pki", 0755)
if err != nil {
log.Error(err)
}
err = os.Chmod(*EasyrsaDirPath+"/pki/crl.pem", 0644)
if err != nil {
log.Error(err)
}
}
func parseDate(layout, datetime string) time.Time {
t, err := time.Parse(layout, datetime)
if err != nil {
log.Errorln(err)
}
return t
}
func parseDateToString(layout, datetime, format string) string {
return parseDate(layout, datetime).Format(format)
}
func parseDateToUnix(layout, datetime string) int64 {
return parseDate(layout, datetime).Unix()
}
func runBash(script string) string {
log.Debugln(script)
cmd := exec.Command("bash", "-c", script)
stdout, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprint(err) + " : " + string(stdout)
}
return string(stdout)
}
func fExist(path string) bool {
var _, err = os.Stat(path)
if os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatalf("fExist: %s", err)
return false
}
return true
}
func fRead(path string) string {
content, err := ioutil.ReadFile(path)
if err != nil {
log.Warning(err)
return ""
}
return string(content)
}
func fCreate(path string) error {
var _, err = os.Stat(path)
if os.IsNotExist(err) {
var file, err = os.Create(path)
if err != nil {
log.Errorln(err)
return err
}
defer file.Close()
}
return nil
}
func fWrite(path, content string) error {
err := ioutil.WriteFile(path, []byte(content), 0644)
if err != nil {
log.Fatal(err)
}
return nil
}
func fDelete(path string) error {
err := os.Remove(path)
if err != nil {
log.Fatal(err)
}
return nil
}
func fCopy(src, dst string) error {
sfi, err := os.Stat(src)
if err != nil {
return err
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
return fmt.Errorf("fCopy: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("fCopy: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return err
}
}
if err = os.Link(src, dst); err == nil {
return err
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return err
}
err = out.Sync()
return err
}
func fMove(src, dst string) error {
err := fCopy(src, dst)
if err != nil {
log.Warn(err)
return err
}
err = fDelete(src)
if err != nil {
log.Warn(err)
return err
}
return nil
}
func fDownload(path, url string, basicAuth bool) error {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if basicAuth {
req.SetBasicAuth(*MasterBasicAuthUser, *MasterBasicAuthPassword)
}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
log.Warnf("WARNING: Download file operation for url %s finished with status code %d\n", url, resp.StatusCode)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = fCreate(path)
if err != nil {
return err
}
err = fWrite(path, string(body))
if err != nil {
return err
}
return nil
}
func createArchiveFromDir(dir, path string) error {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Warn(err)
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
})
if err != nil {
log.Warn(err)
}
out, err := os.Create(path)
if err != nil {
log.Errorf("Error writing archive %s: %s", path, err)
return err
}
defer out.Close()
gw := gzip.NewWriter(out)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// Iterate over files and add them to the tar archive
for _, filePath := range files {
file, err := os.Open(filePath)
if err != nil {
log.Warnf("Error writing archive %s: %s", path, err)
return err
}
// Get FileInfo about our file providing file size, mode, etc.
info, err := file.Stat()
if err != nil {
file.Close()
return err
}
// Create a tar Header from the FileInfo data
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
file.Close()
return err
}
header.Name = strings.Replace(filePath, dir+"/", "", 1)
// Write file header to the tar archive
err = tw.WriteHeader(header)
if err != nil {
file.Close()
return err
}
// Copy file content to tar archive
_, err = io.Copy(tw, file)
if err != nil {
file.Close()
return err
}
file.Close()
}
return nil
}
func extractFromArchive(archive, path string) error {
// Open the file which will be written into the archive
file, err := os.Open(archive)
if err != nil {
return err
}
defer file.Close()
// Write file header to the tar archive
uncompressedStream, err := gzip.NewReader(file)
if err != nil {
log.Fatal("extractFromArchive(): NewReader failed")
}
tarReader := tar.NewReader(uncompressedStream)
for true {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("extractFromArchive: Next() failed: %s", err.Error())
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path+"/"+header.Name, 0755); err != nil {
log.Fatalf("extractFromArchive: Mkdir() failed: %s", err.Error())
}
case tar.TypeReg:
outFile, err := os.Create(path + "/" + header.Name)
if err != nil {
log.Fatalf("extractFromArchive: Create() failed: %s", err.Error())
}
if _, err := io.Copy(outFile, tarReader); err != nil {
log.Fatalf("extractFromArchive: Copy() failed: %s", err.Error())
}
outFile.Close()
default:
log.Fatalf(
"extractFromArchive: uknown type: %s in %s", header.Typeflag, header.Name)
}
}
return nil
}
func randStr(strSize int, randType string) string {
var dictionary string
if randType == "alphanum" {
dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "alpha" {
dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "number" {
dictionary = "0123456789"
}
var bytes = make([]byte, strSize)
for k, v := range bytes {
bytes[k] = dictionary[v%byte(len(dictionary))]
}
return string(bytes)
}