@ -8,34 +8,33 @@ import (
"os"
"os/exec"
"regexp"
"time"
"strings"
"text/template"
// "reflect"
"bufio"
"net"
// "io"
// "encoding/binary"
"encoding/json"
"net/http"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
listenHost = kingpin . Flag ( "listen.host" , "host for openvpn-admin" ) . Default ( "127.0.0.1" ) . String ( )
listenPort = kingpin . Flag ( "listen.port" , "port for openvpn-admin" ) . Default ( "8080" ) . String ( )
easyrsaPath = kingpin . Flag ( "easyrsa.path" , "path to easyrsa dir" ) . Default ( "/etc/openvpn/easyrsa" ) . String ( )
indexTxtPath = kingpin . Flag ( "easyrsa.index-path" , "path to easyrsa index file." ) . Default ( "/etc/openvpn/easyrsa/pki/index.txt" ) . String ( )
ccdCustom = kingpin . Flag ( "ccd.custom" , "enable or disable custom routes" ) . Default ( "false" ) . Bool ( )
ccdDir = kingpin . Flag ( "ccd.path" , "path to client-config-dir" ) . Default ( "/etc/openvpn/ccd" ) . String ( )
staticPath = kingpin . Flag ( "static.path" , "path to static dir" ) . Default ( "./static" ) . String ( )
listenHost = kingpin . Flag ( "listen.host" , "host for openvpn-admin" ) . Default ( "0.0.0.0" ) . String ( )
listenPort = kingpin . Flag ( "listen.port" , "port for openvpn-admin" ) . Default ( "8080" ) . String ( )
openvpnServerHost = kingpin . Flag ( "ovpn.host" , "host for openvpn server" ) . Default ( "127.0.0.1" ) . String ( )
openvpnServerPort = kingpin . Flag ( "ovpn.port" , "port for openvpn server" ) . Default ( "7777" ) . String ( )
openvpnNetwork = kingpin . Flag ( "ovpn.network" , "network for openvpn server" ) . Default ( "172.16.100.0/24" ) . String ( )
mgmtListenHost = kingpin . Flag ( "mgmt.host" , "host for mgmt" ) . Default ( "127.0.0.1" ) . String ( )
mgmtListenPort = kingpin . Flag ( "mgmt.port" , "port for mgmt" ) . Default ( "8989" ) . String ( )
easyrsaDirPath = kingpin . Flag ( "easyrsa.path" , "path to easyrsa dir" ) . Default ( "/mnt/easyrsa" ) . String ( )
indexTxtPath = kingpin . Flag ( "easyrsa.index-path" , "path to easyrsa index file." ) . Default ( "/mnt/easyrsa/pki/index.txt" ) . String ( )
ccdDir = kingpin . Flag ( "ccd.path" , "path to client-config-dir" ) . Default ( "/mnt/ccd" ) . String ( )
staticPath = kingpin . Flag ( "static.path" , "path to static dir" ) . Default ( "./static" ) . String ( )
debug = kingpin . Flag ( "debug" , "Enable debug mode." ) . Default ( "false" ) . Bool ( )
)
const (
usernameRegexp = ` ^([a-zA-Z0-9_.-])+$ `
openvpnServerHost = "127.0.0.1"
openvpnServerPort = "7777"
mgmtListenHost = "127.0.0.1"
mgmtListenPort = "7788"
)
type openvpnClientConfig struct {
@ -47,14 +46,24 @@ type openvpnClientConfig struct {
TLS string
}
type ccdLine struct {
addr string ` json:"addr" `
mask string ` json:"mask" `
desc string ` json:"desc" `
type openvpnClient struct {
Identity string ` json:"Identity" `
AccountStatus string ` json:"AccountStatus" `
ExpirationDate string ` json:"ExpirationDate" `
RevocationDate string ` json:"RevocationDate" `
ConnectionStatus string ` json:"ConnectionStatus" `
}
type ccdFile struct {
lines [ ] ccdLine
type ccdRoute struct {
Address string ` json:"Address" `
Mask string ` json:"Mask" `
Description string ` json:"Description" `
}
type Ccd struct {
User string ` json:"User" `
ClientAddress string ` json:"ClientAddress" `
CustomRoutes [ ] ccdRoute ` json:"CustomRoutes" `
}
type indexTxtLine struct {
@ -80,13 +89,21 @@ type clientStatus struct {
}
func userListHandler ( w http . ResponseWriter , r * http . Request ) {
userList , _ := json . Marshal ( indexTxtParser ( fRead ( * indexTxtPath ) ) )
fmt . Fprintf ( w , "%s" , userList )
users List , _ := json . Marshal ( usersList ( ) )
fmt . Fprintf ( w , "%s" , users List )
}
func userCreateHandler ( w http . ResponseWriter , r * http . Request ) {
r . ParseForm ( )
fmt . Fprintf ( w , "%s" , userCreate ( r . FormValue ( "username" ) ) )
userCreated , userCreateStatus := userCreate ( r . FormValue ( "username" ) )
if userCreated {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , userCreateStatus )
return
} else {
http . Error ( w , userCreateStatus , http . StatusUnprocessableEntity )
}
}
func userRevokeHandler ( w http . ResponseWriter , r * http . Request ) {
@ -101,20 +118,42 @@ func userUnrevokeHandler(w http.ResponseWriter, r *http.Request) {
func userShowConfigHandler ( w http . ResponseWriter , r * http . Request ) {
r . ParseForm ( )
fmt . Printf ( "username: %v\n%s\n" , r . PostForm , r . FormValue ( "username" ) )
fmt . Fprintf ( w , "%s" , renderClientConfig ( r . FormValue ( "username" ) ) )
}
func userDisconnectHandler ( w http . ResponseWriter , r * http . Request ) {
r . ParseForm ( )
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
fmt . Fprintf ( w , "%s" , r . FormValue ( "username" ) )
}
func userShowCcdHandler ( w http . ResponseWriter , r * http . Request ) {
r . ParseForm ( )
fmt . Printf ( "username: %v\n%s\n" , r . PostForm , r . FormValue ( "username" ) )
fmt . Fprintf ( w , "%s" , renderCcdConfig ( r . FormValue ( "username" ) ) )
ccd , _ := json . Marshal ( getCcd ( r . FormValue ( "username" ) ) )
fmt . Fprintf ( w , "%s" , ccd )
}
func userApplyCcdHandler ( w http . ResponseWriter , r * http . Request ) {
r . ParseForm ( )
fmt . Printf ( "username: %v\n%s\n" , r . PostForm , r . FormValue ( "username" ) )
fmt . Fprintf ( w , "%s" , ccdFileModify ( r . FormValue ( "username" ) , ccdFileParser ( r . FormValue ( "ccd" ) ) ) )
var ccd Ccd
if r . Body == nil {
http . Error ( w , "Please send a request body" , 400 )
return
}
err := json . NewDecoder ( r . Body ) . Decode ( & ccd )
if err != nil {
log . Println ( err )
}
ccdApplied , applyStatus := modifyCcd ( ccd )
if ccdApplied {
w . WriteHeader ( http . StatusOK )
fmt . Fprintf ( w , applyStatus )
return
} else {
http . Error ( w , applyStatus , http . StatusUnprocessableEntity )
}
}
func main ( ) {
@ -122,34 +161,26 @@ func main() {
fmt . Println ( "Bind: http://" + * listenHost + ":" + * listenPort )
fs := http . FileServer ( http . Dir ( * staticPath ) )
fs := CacheControlWrapper ( http . FileServer ( http . Dir ( * staticPath ) ) )
http . Handle ( "/" , fs )
http . HandleFunc ( "/api/users/list" , userListHandler )
http . HandleFunc ( "/api/user/create" , userCreateHandler )
http . HandleFunc ( "/api/user/revoke" , userRevokeHandler )
http . HandleFunc ( "/api/user/unrevoke" , userUnrevokeHandler )
http . HandleFunc ( "/api/user/showconfig" , userShowConfigHandler )
http . HandleFunc ( "/api/user/ccd/list" , userShowCcdHandler )
http . HandleFunc ( "/api/user/config/show" , userShowConfigHandler )
http . HandleFunc ( "/api/user/disconnect" , userDisconnectHandler )
http . HandleFunc ( "/api/user/ccd" , userShowCcdHandler )
http . HandleFunc ( "/api/user/ccd/apply" , userApplyCcdHandler )
log . Fatal ( http . ListenAndServe ( * listenHost + ":" + * listenPort , nil ) )
}
func fRead ( path string ) string {
content , err := ioutil . ReadFile ( path )
if err != nil {
log . Fatal ( err )
}
return string ( content )
}
func fWrite ( path , content string ) {
err := ioutil . WriteFile ( path , [ ] byte ( content ) , 0644 )
if err != nil {
log . Fatal ( err )
}
func CacheControlWrapper ( h http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Cache-Control" , "max-age=2592000" ) // 30 days
h . ServeHTTP ( w , r )
} )
}
func indexTxtParser ( txt string ) [ ] indexTxtLine {
@ -163,13 +194,12 @@ func indexTxtParser(txt string) []indexTxtLine {
switch {
// case strings.HasPrefix(str[0], "E"):
case strings . HasPrefix ( str [ 0 ] , "V" ) :
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 : ] } )
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 : ] } )
case strings . HasPrefix ( str [ 0 ] , "R" ) :
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 : ] } )
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 : ] } )
}
}
}
return indexTxt
}
@ -178,159 +208,211 @@ func renderIndexTxt(data []indexTxtLine) string {
for _ , line := range data {
switch {
case line . Flag == "V" :
// if line.distinguishedName != "/CN=server" {
// fmt.Printf("%s\t%s\t\t%s\t%s\t%s\n", line.flag, line.expirationDate, line.serialNumber, line.filename, line.distinguishedName)
indexTxt += fmt . Sprintf ( "%s\t%s\t\t%s\t%s\t%s\n" , line . Flag , line . ExpirationDate , line . SerialNumber , line . Filename , line . DistinguishedName )
// }
indexTxt += fmt . Sprintf ( "%s\t%s\t\t%s\t%s\t%s\n" , line . Flag , line . ExpirationDate , line . SerialNumber , line . Filename , line . DistinguishedName )
case line . Flag == "R" :
// if line.distinguishedName != "/CN=server" {
// fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\n", line.flag, line.expirationDate, line.revocationDate, line.serialNumber, line.filename, line.distinguishedName)
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":
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":
}
}
return ( indexTxt )
return indexTxt
}
func renderClientConfig ( username string ) string {
if checkUserExist ( username ) {
conf := openvpnClientConfig { }
conf . Host = openvpnServerHost
conf . Port = openvpnServerPort
conf . CA = fRead ( * easyrsaPath + "/pki/ca.crt" )
conf . Cert = fRead ( * easyrsaPath + "/pki/issued/" + username + ".crt" )
conf . Key = fRead ( * easyrsaPath + "/pki/private/" + username + ".key" )
conf . TLS = fRead ( * easyrsaPath + "/pki/ta.key" )
conf . Host = * openvpnServerHost
conf . Port = * openvpnServerPort
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" )
t , _ := template . ParseFiles ( "client.conf.tpl" )
var tmp bytes . Buffer
t . Execute ( & tmp , conf )
// fmt.Printf("%+v\n", err)
fmt . Printf ( "%+v\n" , tmp . String ( ) )
return ( fmt . Sprintf ( "%+v\n" , tmp . String ( ) ) )
}
fmt . Printf ( "User \"%s\" not found" , username )
return ( fmt . Sprintf ( "User \"%s\" not found" , username ) )
return fmt . Sprintf ( "User \"%s\" not found" , username )
}
func ccdFileParser ( txt string ) ccdFile {
ccdFile := ccdFile { }
func parseCcd ( username string ) Ccd {
ccd := Ccd { }
ccd . User = username
ccd . ClientAddress = "dynamic"
ccd . CustomRoutes = [ ] ccdRoute { }
txtLinesArray := strings . Split ( txt , "\n" )
txtLinesArray := strings . Split ( fRead ( * ccdDir + "/" + username ) , "\n" )
for _ , v := range txtLinesArray {
str := strings . Fields ( v )
if len ( str ) > 0 {
switch {
case strings . HasPrefix ( str [ 0 ] , "ifconfig-push" ) :
ccdFile . lines = append ( ccdFile . lines , ccdLine { addr : str [ 2 ] , mask : str [ 3 ] , desc : "Client Address" } )
ccd . ClientAddress = str [ 1 ]
case strings . HasPrefix ( str [ 0 ] , "push" ) :
ccdFile . lines = append ( ccdFile . lines , ccdLine { addr : str [ 2 ] , m ask: str [ 3 ] , desc : strings . Join ( str [ 4 : ] , "" ) } )
ccd . CustomRoutes = append ( ccd . CustomRoutes , ccdRoute { Address : strings . Trim ( str [ 2 ] , "\"" ) , M ask: strings . Trim ( str [ 3 ] , "\"" ) , Description : strings . Trim ( strings . Join ( str [ 4 : ] , "" ) , "# " ) } )
}
}
}
return ccdFile
return ccd
}
func modifyCcd ( ccd Ccd ) ( bool , string ) {
if fCreate ( * ccdDir + "/" + ccd . User ) {
ccdValid , ccdErr := validateCcd ( ccd )
if ccdErr != "" {
return false , ccdErr
}
func renderCcdConfig ( username string ) string {
if checkCcdExist ( username ) {
ccdFileParser ( fRead ( * ccdDir + "/" + username ) )
if ccdValid {
t , _ := template . ParseFiles ( "ccd.tpl" )
var tmp bytes . Buffer
t . Execute ( & tmp , ccd )
fWrite ( * ccdDir + "/" + ccd . User , tmp . String ( ) )
return true , "ccd updated successfully"
}
}
fmt . Printf ( "ccd for user \"%s\" not found" , username )
return ( fmt . Sprintf ( "ccd for user \"%s\" not found" , username ) )
return false , "something goes wrong"
}
func validateCcd ( ccd Ccd ) ( bool , string ) {
ccdErr := ""
func ccdFileModify ( username string , ccdFile ccdFile ) bool {
if checkCcdExist ( username ) {
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" , 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" , 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" , ccdErr )
}
return false , ccdErr
}
}
return true
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" , 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" , ccd . User , ccdErr )
}
return false , ccdErr
}
}
return true , ccdErr
}
// https://community.openvpn.net/openvpn/ticket/623
func crlFix ( ) {
os . Chmod ( * easyrsaPath + "/pki" , 0755 )
err := os . Chmod ( * easyrsaPath + "/pki/crl.pem" , 0640 )
if err != nil {
log . Println ( err )
}
func getCcd ( username string ) Ccd {
ccd := Ccd { }
ccd . User = username
ccd . ClientAddress = "dynamic"
ccd . CustomRoutes = [ ] ccdRoute { }
if fCreate ( * ccdDir + "/" + username ) {
ccd = parseCcd ( username )
}
return ccd
}
func runBash ( script string ) string {
fmt . Println ( script )
cmd := exec . Command ( "bash" , "-c" , script )
stdout , err := cmd . CombinedOutput ( )
if err != nil {
return ( fmt . Sprint ( err ) + " : " + string ( stdout ) )
}
return ( string ( stdout ) )
func checkStaticAddressIsFree ( staticAddress string , username string ) bool {