diff --git a/httpd.go b/httpd.go index 318b99c..d70666f 100644 --- a/httpd.go +++ b/httpd.go @@ -8,21 +8,48 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "log" "net/http" "os" + "strings" ) +type jsonInput struct { + Action string `json:"action"` + Params jsonInputParams `json:"params"` +} + +type jsonInputParams struct { + Id int `json:"id"` + Session string `json:"session"` +} + type HttpServer struct { - Port string - ovpn *OpenVpnMgt - key string - cert string - certPool *x509.CertPool + Port string + ovpn *OpenVpnMgt + key string + cert string + minProfile string + neededProfile string + certPool *x509.CertPool +} + +func parseJsonQuery(r *http.Request) (*jsonInput, error) { + var in jsonInput + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal(body, &in); err != + nil { + return nil, err + } + return &in, nil } func (h *HttpServer) handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) + fmt.Fprintf(w, "nothing here\n") } func (h *HttpServer) versionHandler(w http.ResponseWriter, r *http.Request) { @@ -50,15 +77,91 @@ func (h *HttpServer) helpHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s", jsonStr) } -func NewHTTPServer(port, key, cert, ca string, s *OpenVpnMgt) { +func (h *HttpServer) ajaxHandler(w http.ResponseWriter, r *http.Request) { + var sslUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} + + // deactivate if there is no https auth + if h.key == "" || h.cert == "" || h.certPool == nil { + http.Error(w, "No security, deactivated", 403) + return + } + + // add CORS headers + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "content-type, accept, origin, user-agent, Accept-Encoding") + + // stop here if the method is OPTIONS, to allow CORS to work + if r.Method == "OPTIONS" { + return + } + + // stop here if the method is OPTIONS, to allow CORS to work + if r.Method != "POST" { + http.Error(w, "post only", 405) + return + } + + // ssl auth + if len(r.TLS.PeerCertificates) == 0 { + log.Println(len(r.TLS.PeerCertificates)) + http.Error(w, "Need certificate", 403) + return + } + opts := x509.VerifyOptions{Roots: h.certPool, KeyUsages: sslUsage} + if _, err := r.TLS.PeerCertificates[0].Verify(opts); err != nil { + http.Error(w, "Bad certificate", 403) + return + } + + profile, _ := h.ovpn.AuthLoop(h.minProfile, + strings.Replace(r.TLS.PeerCertificates[0].Subject.CommonName, " ", "", -1), "", false) + if profile != h.neededProfile { + http.Error(w, fmt.Sprintf("You need the %s profile", h.neededProfile), 403) + return + } + + req, err := parseJsonQuery(r) + if err != nil { + log.Println(err) + http.Error(w, "Invalid request", 500) + return + } + + switch req.Action { + case "stats": + jsonStr, err := json.Marshal(h.ovpn.Stats()) + if err != nil { + fmt.Fprintf(w, "Error : %s", err) + } + fmt.Fprintf(w, "%s", jsonStr) + + case "kill": + if err := h.ovpn.Kill(req.Params.Session, req.Params.Id); err != nil { + http.Error(w, fmt.Sprintf("%s", err), 500) + } + + default: + http.Error(w, "Invalid request", 500) + + } + + return +} + +func NewHTTPServer(port, key, cert, ca, minProfile, neededProfile string, s *OpenVpnMgt) { h := &HttpServer{ - Port: port, - ovpn: s, - key: key, - cert: cert, + Port: port, + ovpn: s, + key: key, + cert: cert, + minProfile: minProfile, + neededProfile: neededProfile, } http.HandleFunc("/help", h.helpHandler) + http.HandleFunc("/ajax", h.ajaxHandler) http.HandleFunc("/version", h.versionHandler) http.HandleFunc("/", h.handler) diff --git a/ldap.go b/ldap.go index c3eb1e2..7cb2278 100644 --- a/ldap.go +++ b/ldap.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net" + "regexp" "strings" "time" @@ -43,6 +44,61 @@ func (l *ldapConfig) addIPRange(s string) error { return nil } +// auth loop. Try all auth profiles from startProfile +// return the last possible profile and the mail if we found a mail like login +func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck bool) (string, string) { + login := []string{user} + profile := startProfile + mail := "" + + re := regexp.MustCompile("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$") + + for { + if re.MatchString(login[0]) && mail == "" { + mail = login[0] + } + n := profile + + for k, ldap := range s.ldap { + if ldap.upgradeFrom != profile { + continue + } + err, userOk, passOk, secondary := ldap.Auth(login, pass) + + // if there is an error, try the other configurations + if err != nil { + log.Printf("user %s not validated as %s\n", user, k) + continue + } + + // we did find a valid User + if userOk { + // the login for the new auth level is given by the current one + login = secondary + + if passOk && profile != "" { + // it's at least the second auth level, and we have a valid + // password on 2 different auth system. It's a dupplicate + // password, let's log it + log.Printf("User %s has a dupplicate password\n", user) + } + + // we have either a positive auth ok a previous valid one + if passOk || profile != "" || overridePwdCheck { + profile = k + } + } + } + + // no profile update this turn, no need to continue + if n == profile { + break + } + } + + return profile, mail +} + // override the real DialTLS function func myDialTLS(network, addr string, config *tls.Config) (*ldap.Conn, error) { dc, err := net.DialTimeout(network, addr, 3*time.Second) diff --git a/main.go b/main.go index adc084c..c08b1e3 100644 --- a/main.go +++ b/main.go @@ -86,9 +86,11 @@ func main() { // time to start the listeners go server.Run() NewHTTPServer( - config.GetString("config.httpPort", "127.0.0.01:8080"), - config.GetString("config.httpKey", ""), - config.GetString("config.httpCert", ""), - config.GetString("config.httpCa", ""), + config.GetString("config.http.port", "127.0.0.01:8080"), + config.GetString("config.http.key", ""), + config.GetString("config.http.cert", ""), + config.GetString("config.http.ca", ""), + config.GetString("config.http.startAuth", "CORP"), + config.GetString("config.http.reqAuth", "ADMINS"), server) } diff --git a/openvpn-dm-mgt-server.conf.example b/openvpn-dm-mgt-server.conf.example index 4fe1789..1582360 100644 --- a/openvpn-dm-mgt-server.conf.example +++ b/openvpn-dm-mgt-server.conf.example @@ -62,10 +62,6 @@ config mfa: "" cert: "optionnal" IPRange: "192.168.204.1-192.168.206.254" - routes: - [ - "10.190.32.51 255.255.255.255", - ] } ADMINS: { @@ -82,14 +78,20 @@ config } } openvpnPort: "127.0.0.1:4000" - httpPort: ":8443" - ipRouteScript: "./iproute" - httpCa: "/usr/local/share/ca-certificates/Dailymotion.crt" - httpKey: "/etc/ssl/private/server-key.pem" - httpCert: "/etc/ssl/certs/server-bundle.pem" + ipRouteScript: "/usr/local/bin/iproute" + http: + { + port: ":8443" + ca: "/usr/local/share/ca-certificates/Dailymotion.crt" + key: "/etc/ssl/private/server-key.pem" + cert: "/etc/ssl/certs/server-bundle.pem" + startAuth: "CORP" + reqAuth: "ADMINS" + } + cacheDir: "/var/run/openvpn/" authCa: "/usr/local/share/ca-certificates/Dailymotion.crt" - masterSecrets: [ "********************************"] + masterSecrets: [ "********************************" ] vpnLogUrl: "https://install.dm.gg/vpn-log.php" mailRelay: "mailrelay.dailymotion.com:25" mailFrom: "engineering-infra@dailymotion.com" diff --git a/vpnserver.go b/vpnserver.go index 6d81d6b..67118cf 100644 --- a/vpnserver.go +++ b/vpnserver.go @@ -3,6 +3,7 @@ package main import ( "bufio" "errors" + "fmt" "io" "log" "net" @@ -117,6 +118,22 @@ func (s *OpenVpnMgt) sendCommand(msg []string, remote string) (error, []string) return nil, ret } +// send the list of all connected clients +func (s *OpenVpnMgt) Stats() map[string]map[int]*vpnSession { + return s.clients +} + +func (s *OpenVpnMgt) Kill(session string, id int) error { + if _, ok := s.clients[session]; !ok { + return errors.New("unknown session") + } + if _, ok := s.clients[session][id]; !ok { + return errors.New("unknown session id") + } + err, msg := s.sendCommand([]string{fmt.Sprintf("client-kill %d", id)}, session) + return err +} + // send the help command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) { ret := make(map[string]map[string]string) @@ -139,7 +156,7 @@ func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) { return nil, ret } -// send the verson command on all vpn servers. Kind of useless +// send the version command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Version() (error, map[string][]string) { ret := make(map[string][]string) for remote := range s.buf { @@ -295,7 +312,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { } // ask for statistics - if _, err := s.buf[remote].WriteString("bytecount 10\r\n"); err != nil { + if _, err := s.buf[remote].WriteString("bytecount 30\r\n"); err != nil { log.Println(err) return } @@ -334,7 +351,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { } // manage all "terminator" lines - for _, terminator := range []string{"END", ">CLIENT:ENV,END", "SUCCESS"} { + for _, terminator := range []string{"END", ">CLIENT:ENV,END", "SUCCESS", "ERROR"} { if strings.HasPrefix(line, terminator) { s.ret <- response response = nil @@ -359,6 +376,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { // new bloc for a connect event. // We start the receiving handler, which will wait for the Channel message case strings.HasPrefix(line, ">CLIENT:ADDRESS"): + case strings.HasPrefix(line, ">CLIENT:ESTABLISHED"): go s.ClientValidated(line, remote) case strings.HasPrefix(line, ">CLIENT:CONNECT"): diff --git a/vpnsession.go b/vpnsession.go index 6e13af5..65127d3 100644 --- a/vpnsession.go +++ b/vpnsession.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "os" "os/exec" "regexp" @@ -117,6 +116,14 @@ func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error { c.PrivIP = r.ReplaceAllString(p[1], "") case "ifconfig_local": c.localIP = r.ReplaceAllString(p[1], "") + case "bytes_received": + if c.BwWrite, err = strconv.Atoi(p[1]); err != nil { + break + } + case "bytes_sent": + if c.BwRead, err = strconv.Atoi(p[1]); err != nil { + break + } case "password": switch { case strings.HasPrefix(p[1], "CRV1"): @@ -129,8 +136,7 @@ func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error { if c.otpCode == "" { c.otpCode = "***" } - // don't check that password agains the ibp database - + // don't check that password against the ibp database case strings.HasPrefix(p[1], "SCRV1"): split := strings.Split(p[1], ":") if len(split) != 3 { @@ -257,52 +263,7 @@ func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) { } } - c.Profile = "" - login := []string{c.Login} - pass := c.password - - for { - n := c.Profile - for k, ldap := range s.ldap { - if ldap.upgradeFrom != c.Profile { - continue - } - err, userOk, passOk, secondary := ldap.Auth(login, pass) - - // if there is an error, try the other configurations - if err != nil { - c.LogPrintln(err) - continue - } - - // we did find a valid User - if userOk { - // the login for the new auth level is given by the current one - login = secondary - - if c.Mail == "" { - c.Mail = secondary[0] - } - - if passOk && c.Profile != "" { - // it's at least the second auth level, and we have a valid - // password on 2 different auth system. It's a dupplicate - // password, let's log it - log.Printf("User %s has a dupplicate password\n", c.Login) - } - - // we have either a positive auth ok a previous valid one - if passOk || c.Profile != "" || tokenPasswordOk { - c.Profile = k - } - } - } - - // no profile update this turn, no need to continue - if n == c.Profile { - break - } - } + c.Profile, c.Mail = s.AuthLoop("", c.Login, c.password, tokenPasswordOk) // no profile validated, we stop here if c.Profile == "" {