From beae165fe5b5fa919bd0ca631e34f38851b5ee91 Mon Sep 17 00:00:00 2001 From: janusec Date: Fri, 22 Sep 2023 21:40:25 +0800 Subject: [PATCH 1/4] remove mips --- release_batch.sh | 18 +++++++++--------- utils/utils.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/release_batch.sh b/release_batch.sh index 2b3527f..db32850 100755 --- a/release_batch.sh +++ b/release_batch.sh @@ -46,14 +46,14 @@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build janusec.go buildFor arm64 printf "arm64 done!\n" -printf "Building mips64 ... \n" -CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build janusec.go -buildFor mips64 -printf "mips64 done!\n" - -printf "Building mips64le ... \n" -CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build janusec.go -buildFor mips64le -printf "mips64le done!\n" +#printf "Building mips64 ... \n" +#CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build janusec.go +#buildFor mips64 +#printf "mips64 done!\n" + +#printf "Building mips64le ... \n" +#CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build janusec.go +#buildFor mips64le +#printf "mips64le done!\n" printf "Done!\n" diff --git a/utils/utils.go b/utils/utils.go index ad8e5c8..8d9302a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -47,7 +47,7 @@ func InitLogger() { CheckError("InitLogger", err) os.Exit(1) } - logger = log.New(logFile, "[Janusec] ", log.LstdFlags) + logger = log.New(logFile, "[JANUSEC] ", log.LstdFlags) } // GetDirAll ... From 81e84ebd309882fcc02e28178e987cb1a11dd5d5 Mon Sep 17 00:00:00 2001 From: janusec Date: Sat, 28 Oct 2023 20:09:10 +0800 Subject: [PATCH 2/4] sync merge (add request headers for static files) to dev --- README.md | 2 +- gateway/gateway.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb3f98b..1f4c894 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ When listen=true in config.json : > https://`your_primary_node_domain_name:9443`/janusec-admin/ (after certificate configured) When using primary node only, any application domain name can be used for admin. -But if you have one or more replica nodes, you should apply for a seperate domain name for primary node. +But if you have one or more replica nodes, you should apply for a separate domain name for primary node. [Janusec Application Gateway Configuration](https://janusec.github.io/documentation/quick-start/) diff --git a/gateway/gateway.go b/gateway/gateway.go index 8d3f1eb..7c34a5c 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -469,6 +469,10 @@ func ReverseHandlerFunc(w http.ResponseWriter, r *http.Request) { utils.DebugPrintln("Check Update NewRequest", err) } if err == nil { + // copy request headers + for k := range r.Header { + req.Header.Set(k, r.Header.Get(k)) + } req.Header.Set("Host", domainStr) modTimeGMT := fi.ModTime().UTC().Format(http.TimeFormat) //If-Modified-Since: Sun, 14 Jun 2020 13:54:20 GMT From a1589620ba8f9ec17badde4d55fdf79f80e187d4 Mon Sep 17 00:00:00 2001 From: janusec Date: Sat, 23 Mar 2024 17:41:25 +0800 Subject: [PATCH 3/4] migrate DingTalk OAuth to new API and verify corpid --- data/setting.go | 14 ++-- gateway/auth_oauth.go | 7 ++ gateway/gateway.go | 20 +++++- models/data_config.go | 3 + usermgmt/oauth_dingtalk.go | 133 ++++++++++++++++++++++++++++++++++--- 5 files changed, 160 insertions(+), 17 deletions(-) diff --git a/data/setting.go b/data/setting.go index 4e44e7d..fbc55be 100644 --- a/data/setting.go +++ b/data/setting.go @@ -260,6 +260,10 @@ func InitDefaultSettings() { if !DAL.ExistsSetting("dingtalk_appsecret") { DAL.SaveStringSetting("dingtalk_appsecret", "crrALdXUIj4T0zBekYh4u9sU_T1GZT") } + if !DAL.ExistsSetting("dingtalk_corpid") { + // added on Mar 23, 2024 + DAL.SaveStringSetting("dingtalk_corpid", "xxxx") + } // AuthConfig feishu if !DAL.ExistsSetting("feishu_display_name") { DAL.SaveStringSetting("feishu_display_name", "Login with Feishu") @@ -495,11 +499,13 @@ func GetDingtalkConfig() *models.DingtalkConfig { callback := DAL.SelectStringSetting("dingtalk_callback") appID := DAL.SelectStringSetting("dingtalk_appid") appSecret := DAL.SelectStringSetting("dingtalk_appsecret") + corpID := DAL.SelectStringSetting("dingtalk_corpid") dingtalkConfig := &models.DingtalkConfig{ DisplayName: displayName, Callback: callback, AppID: appID, AppSecret: appSecret, + CorpID: corpID, } return dingtalkConfig } @@ -515,17 +521,11 @@ func UpdateDingtalkConfig(body []byte, clientIP string, authUser *models.AuthUse return nil, err } dingtalkConfig := rpcDingtalkConfigRequest.Object - /* - dingtalkConfig := param["object"].(map[string]interface{}) - displayName := dingtalkConfig["display_name"].(string) - callback := dingtalkConfig["callback"].(string) - appid := dingtalkConfig["appid"].(string) - appsecret := dingtalkConfig["appsecret"].(string) - */ DAL.SaveStringSetting("dingtalk_display_name", dingtalkConfig.DisplayName) DAL.SaveStringSetting("dingtalk_callback", dingtalkConfig.Callback) DAL.SaveStringSetting("dingtalk_appid", dingtalkConfig.AppID) DAL.SaveStringSetting("dingtalk_appsecret", dingtalkConfig.AppSecret) + DAL.SaveStringSetting("dingtalk_corpid", dingtalkConfig.CorpID) NodeSetting.AuthConfig.Dingtalk = dingtalkConfig go utils.OperationLog(clientIP, authUser.Username, "Update Dingtalk Config", dingtalkConfig.DisplayName) diff --git a/gateway/auth_oauth.go b/gateway/auth_oauth.go index 0922a24..4dd7945 100644 --- a/gateway/auth_oauth.go +++ b/gateway/auth_oauth.go @@ -79,9 +79,16 @@ func GetOAuthInfo() (*OAuthInfo, error) { oauthInfo.EntranceURL = entranceURL return &oauthInfo, nil case "dingtalk": + /* API v1 entranceURL := fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=admin&redirect_uri=%s", data.NodeSetting.AuthConfig.Dingtalk.AppID, data.NodeSetting.AuthConfig.Dingtalk.Callback) + */ + // API v2, added on Mar 23, 2024 + entranceURL := fmt.Sprintf(`https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&corpId=%s&client_id=%s&scope=openid%%20corpid&state=admin&prompt=consent`, + data.NodeSetting.AuthConfig.Dingtalk.Callback, + data.NodeSetting.AuthConfig.Dingtalk.CorpID, + data.NodeSetting.AuthConfig.Dingtalk.AppID) oauthInfo.UseOAuth = true oauthInfo.DisplayName = data.NodeSetting.AuthConfig.Dingtalk.DisplayName oauthInfo.EntranceURL = entranceURL diff --git a/gateway/gateway.go b/gateway/gateway.go index 7c34a5c..41c1aa5 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -577,10 +577,28 @@ func getOAuthEntrance(state string) (entranceURL string, err error) { data.NodeSetting.AuthConfig.Wxwork.Callback, state) case "dingtalk": - entranceURL = fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=%s&redirect_uri=%s", + /* This is the api v1 + entranceURL = fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=%s&redirect_uri=%s", data.NodeSetting.AuthConfig.Dingtalk.AppID, state, data.NodeSetting.AuthConfig.Dingtalk.Callback) + */ + // API V2, added on Mar 23, 2024 + // doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information + // Entrance URL Format: https://login.dingtalk.com/oauth2/auth? + // redirect_uri=https://test.janusec.com/oauth/dingtalk + // &response_type=code + // &client_id=... + // &scope=openid + // &state=... + // &prompt=consent + // &corpId=... + entranceURL = fmt.Sprintf(`https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&corpId=%s&client_id=%s&scope=openid%%20corpid&state=%s&prompt=consent`, + data.NodeSetting.AuthConfig.Dingtalk.Callback, + data.NodeSetting.AuthConfig.Dingtalk.CorpID, + data.NodeSetting.AuthConfig.Dingtalk.AppID, + state) + case "feishu": entranceURL = fmt.Sprintf("https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=%s&app_id=%s&state=%s", data.NodeSetting.AuthConfig.Feishu.Callback, diff --git a/models/data_config.go b/models/data_config.go index 8ebdbc3..9b8cfb8 100644 --- a/models/data_config.go +++ b/models/data_config.go @@ -75,6 +75,9 @@ type DingtalkConfig struct { Callback string `json:"callback"` AppID string `json:"appid"` AppSecret string `json:"appsecret"` + + // CorpID for API v2, added on Mar 23, 2024 + CorpID string `json:"corpid"` } type FeishuConfig struct { diff --git a/usermgmt/oauth_dingtalk.go b/usermgmt/oauth_dingtalk.go index 9b11076..80efa78 100644 --- a/usermgmt/oauth_dingtalk.go +++ b/usermgmt/oauth_dingtalk.go @@ -8,16 +8,10 @@ package usermgmt import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "net/http" - "net/url" - "strconv" - "time" + "strings" "janusec/data" "janusec/models" @@ -27,18 +21,25 @@ import ( "github.com/patrickmn/go-cache" ) -type DingtalkResponse struct { +// DingtalkResponse V1 +/* +type DingtalkResponseV1 struct { ErrCode int64 `json:"errcode"` ErrMsg string `json:"errmsg"` UserInfo DingtalkUserInfo `json:"user_info"` } +*/ +// DingtalkUserInfo V1 & V2 +// Doc: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information type DingtalkUserInfo struct { Nick string `json:"nick"` OpenID string `json:"openid"` UnionID string `json:"unionid"` } +/* +// GetSignature for API v1 func GetSignature(msg []byte, key []byte) string { hmac := hmac.New(sha256.New, key) _, err := hmac.Write(msg) @@ -48,11 +49,23 @@ func GetSignature(msg []byte, key []byte) string { digest := hmac.Sum(nil) return url.QueryEscape(base64.StdEncoding.EncodeToString(digest)) } +*/ +// AccessToken Response V2, added on Mar 23, 2024 +// Doc: https://open.dingtalk.com/document/orgapp/obtain-user-token#h2-hxj-mpf-5bd +type AccessTokenResp struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + ExpireIn int64 `json:"expireIn"` + CorpId string `json:"corpId"` +} + +// This is the API v1, instead by v2 // https://ding-doc.dingtalk.com/doc#/serverapi3/mrugr3 // Step 1: To https://oapi.dingtalk.com/connect/qrconnect?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI // If state==admin, for janusec-admin; else for frontend applications -func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) { +/* +func DingtalkCallbackWithCodeV1(w http.ResponseWriter, r *http.Request) { // Step 2.1: Callback with code, https://gate.janusec.com/janusec-admin/oauth/dingtalk?code=BM8k8U6RwtQtNY&state=admin code := r.FormValue("code") state := r.FormValue("state") @@ -129,3 +142,105 @@ func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) { //fmt.Println("Time expired") http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } +*/ + +// This is the API v2, added on Mar 23, 2024 +// Doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information +// Step 1: https://login.dingtalk.com/oauth2/auth?redirect_uri=https://.../oauth/dingtalk&response_type=code&client_id=...&scope=openid&state=...&prompt=consent +// If state==admin, for janusec-admin; else for frontend applications +func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) { + // Step 2.1: Callback with code, GET + // https://test.janusec.com/oauth/dingtalk?authCode=18025489140734ecb0ccf637ee5439f9&state=... + authCode := r.FormValue("authCode") + state := r.FormValue("state") + // Step 2.2: Within Callback, acquire token + // Doc: https://open.dingtalk.com/document/orgapp/obtain-user-token + // POST https://api.dingtalk.com/v1.0/oauth2/userAccessToken + // body { "clientId": "...AppKey", "clientSecret": "...AppSecret", "code": "...authCode", "grantType": "authorization_code"} + accessTokenURL := `https://api.dingtalk.com/v1.0/oauth2/userAccessToken` + body := fmt.Sprintf(`{"clientId":"%s","clientSecret":"%s","code":"%s","grantType": "authorization_code"}`, + data.NodeSetting.AuthConfig.Dingtalk.AppID, + data.NodeSetting.AuthConfig.Dingtalk.AppSecret, + authCode) + request, _ := http.NewRequest("POST", accessTokenURL, strings.NewReader(body)) + request.Header.Set("Content-Type", "application/json") + resp, err := utils.GetResponse(request) + if err != nil { + utils.DebugPrintln("DingtalkCallbackWithCode GetResponse accessToken", err) + } + accessTokenResp := AccessTokenResp{} + err = json.Unmarshal(resp, &accessTokenResp) + if err != nil { + utils.DebugPrintln("DingtalkCallbackWithCode json.Unmarshal AccessTokenResp error", err) + } + // check wether the corpid is valid + if accessTokenResp.CorpId != data.NodeSetting.AuthConfig.Dingtalk.CorpID { + w.WriteHeader(403) + w.Write([]byte("Error: You are not a member of the corporation now!")) + return + } + + // Step 2.3: Get UserInfo + // Doc: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information + // GET https://api.dingtalk.com/v1.0/contact/users/{unionId} + // Header x-acs-dingtalk-access-token:String + request, _ = http.NewRequest("GET", `https://api.dingtalk.com/v1.0/contact/users/me`, nil) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("x-acs-dingtalk-access-token", accessTokenResp.AccessToken) + resp, err = utils.GetResponse(request) + if err != nil { + utils.DebugPrintln("DingtalkCallbackWithCode GetResponse userinfo", err) + } + dingtalkUser := DingtalkUserInfo{} + err = json.Unmarshal(resp, &dingtalkUser) + if err != nil { + utils.DebugPrintln("DingtalkCallbackWithCode json.Unmarshal userinfo error", err) + } + if state == "admin" { + appUser := data.DAL.SelectAppUserByName(dingtalkUser.Nick) + var userID int64 + if appUser == nil { + // Insert into db if not existed + userID, err = data.DAL.InsertIfNotExistsAppUser(dingtalkUser.Nick, "", "", "", false, false, false, false) + if err != nil { + w.WriteHeader(403) + w.Write([]byte("Error: " + err.Error())) + return + } + } else { + userID = appUser.ID + } + // create session + authUser := &models.AuthUser{ + UserID: userID, + Username: dingtalkUser.Nick, + Logged: true, + IsSuperAdmin: appUser.IsSuperAdmin, + IsCertAdmin: appUser.IsCertAdmin, + IsAppAdmin: appUser.IsAppAdmin, + NeedModifyPWD: false} + session, _ := store.Get(r, "sessionid") + session.Values["authuser"] = authUser + session.Options = &sessions.Options{Path: "/janusec-admin/", MaxAge: 86400} + err = session.Save(r, w) + if err != nil { + utils.DebugPrintln("DingtalkCallbackWithCode session save error", err) + } + RecordAuthLog(r, authUser.Username, "DingTalk", data.CFG.PrimaryNode.Admin.Portal) + http.Redirect(w, r, data.CFG.PrimaryNode.Admin.Portal, http.StatusTemporaryRedirect) + return + } + // Gateway OAuth for employees and internal application + oauthStateI, found := OAuthCache.Get(state) + if found { + oauthState := oauthStateI.(models.OAuthState) + oauthState.UserID = dingtalkUser.Nick + oauthState.AccessToken = "N/A" + OAuthCache.Set(state, oauthState, cache.DefaultExpiration) + RecordAuthLog(r, oauthState.UserID, "DingTalk", oauthState.CallbackURL) + http.Redirect(w, r, oauthState.CallbackURL, http.StatusTemporaryRedirect) + return + } + //fmt.Println("Time expired") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} From 2cc4d56b45b064e990ac9770c17801b0932508fb Mon Sep 17 00:00:00 2001 From: janusec Date: Sat, 23 Mar 2024 23:13:33 +0800 Subject: [PATCH 4/4] optimize offline detection --- README.md | 44 +++++++++++++------------- backend/application.go | 2 ++ backend/destination.go | 63 ++++++++++++++++++++++++++++++++++---- backend/k8s.go | 4 +-- backend/vip_app.go | 40 ++---------------------- backend/vip_target.go | 49 +++++++++++++++++++++++++++-- data/data.go | 2 +- data/setting.go | 6 +--- firewall/cc.go | 2 +- gateway/auth_oauth.go | 2 +- gateway/gateway.go | 52 +++++++++---------------------- gateway/webssh.go | 26 ++++++++-------- models/backend.go | 1 + models/data_config.go | 2 +- release_batch.sh | 2 +- usermgmt/oauth_dingtalk.go | 4 +-- 16 files changed, 168 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 1f4c894..a1f7737 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [Janusec Application Gateway / JANUSEC应用网关](https://www.janusec.com/)   [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Protect%20web%20applications%20from%20network%20attacks%20with%20open%20source%20Janusec%20Application%20Gateway&url=https://github.com/Janusec/janusec&via=janusec&hashtags=waf,web,application,firewall,gateway) +# Janusec Application Gateway / JANUSEC应用网关 [![Build Status](https://travis-ci.org/Janusec/janusec.svg?branch=master)](https://travis-ci.org/Janusec/janusec) @@ -52,21 +52,20 @@ ## 产品网站 -https://doc.janusec.com/cn/ +https://janusec.github.io/cn/ + ## 需求 -* PostgreSQL 10/11/12+ (开发环境,及生产环境主节点需要) -* Debian 9/10+, CentOS/RHEL 7/8+, 首选Debian 10+ +* PostgreSQL 10/11/12/13/14+ (开发环境,及生产环境主节点需要) +* Debian 9/10/11+, CentOS/RHEL 7/8+, 首选Debian 10+ * systemd * nftables * Golang 1.15+ (仅开发环境需要) ## 部署快速指引 -详细文档可在这里获取: [Janusec应用网关快速入门](https://doc.janusec.com/cn/quick-start/) - -如希望快速体验,可尝试使用 [Docker镜像](https://www.janusec.com/articles/opensource/1615470598.html) +详细文档可在这里获取: [Janusec应用网关快速入门](https://janusec.github.io/cn/quick-start/) ## 开发快速指引 @@ -103,7 +102,7 @@ Janusec将自动加密数据库口令 只使用主节点时,任意应用域名均可用于访问管理入口。 如果使用了副本节点,应为主节点申请一个单独的域名。 -[Janusec应用网关配置](https://doc.janusec.com/cn/quick-start/) +[Janusec应用网关配置](https://janusec.github.io/cn/quick-start/) ## 发布 @@ -119,15 +118,15 @@ Janusec将自动加密数据库口令 Web化管理所需的文件在 `./static/janusec-admin/` 目录, 源码在 [Janusec-Admin Github](https://github.com/Janusec/janusec-admin) ,前端源码使用Angular 9. -## 许可证 +## 多许可证 -Janusec应用网关源文件使用GNU [AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html)授权. +JANUSEC应用网关开源版本的源文件使用GNU [AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html)授权. +专业增强特性版本闭源发布,增强特性包括:GSLB、Cookie合规(应用无需修改)等。 ## 支持 -* 产品网站 [https://doc.janusec.com/cn/](https://doc.janusec.com/cn/) -* 官方网站: [https://www.janusec.com/](https://www.janusec.com/) -* Email: `support#janusec.com` +* 产品网站 [https://janusec.github.io/cn/](https://janusec.github.io/cn/) +* Email: `support`**@**`janusec.com` * QQ群: 776900157 @@ -185,17 +184,15 @@ https://janusec.github.io/ ## Requirements -* PostgreSQL 10/11/12+ (Required by Development and Primary Node of Deployment) -* Debian 9/10+, CentOS/RHEL 7/8+, Debian 10+ is preferred +* PostgreSQL 10/11/12/13/14+ (Required by Development and Primary Node of Deployment) +* Debian 9/10/11+, CentOS/RHEL 7/8+, Debian 10+ is preferred * systemd * nftables * Golang 1.15+ (Required by Development Only) ## Quick Start for Deployment -Detailed documentation is available at: [Janusec Application Gateway Quick Start](https://janusec.github.io/documentation/quick-start/). - -You can also try it with [Docker Image](https://www.janusec.com/articles/opensource/1615470598.html) +Detailed documentation is available at: [Janusec Application Gateway Quick Start](https://janusec.github.io/documentation/quick-start/). ## Quick Start for Developer @@ -247,13 +244,14 @@ The release package is under `./dist` . Release directory is `./static/janusec-admin/` , and source code is available at [Janusec-Admin Github](https://github.com/Janusec/janusec-admin) with Angular 9. -## LICENSE +## Multiple LICENSES + +The open source files are made available under the terms of the GNU Affero General Public License ([GNU AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html)). -Janusec Application Gateway source files are made available under the terms of the GNU Affero General Public License ([GNU AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html)). +The professional enhanced version is released in closed source, and the enhanced features including GSLB, Cookie compliance (No need to modify applications), etc. ## Support -* Product: [https://janusec.github.io/](https://janusec.github.io/) -* Official site : [https://www.janusec.com/](https://www.janusec.com/) -* Email: `support#janusec.com` +* Product: [https://janusec.github.io/](https://janusec.github.io/) +* Email: `support`**@**`janusec.com` * QQ Group: 776900157 diff --git a/backend/application.go b/backend/application.go index a75aee7..5488eda 100644 --- a/backend/application.go +++ b/backend/application.go @@ -74,6 +74,8 @@ func SelectBackendRoute(app *models.Application, r *http.Request, srcIP string) // get online destinations var onlineDests = []*models.Destination{} for _, dest := range dests { + dest.Mutex.Lock() + defer dest.Mutex.Unlock() if dest.Online { onlineDests = append(onlineDests, dest) } diff --git a/backend/destination.go b/backend/destination.go index c4d186c..e34482a 100644 --- a/backend/destination.go +++ b/backend/destination.go @@ -9,11 +9,18 @@ package backend import ( "encoding/json" + "janusec/data" "janusec/models" "janusec/utils" "net" "net/http" "time" + + "github.com/patrickmn/go-cache" +) + +var ( + offlineCache = cache.New(30*time.Second, 30*time.Second) ) // ContainsDestinationID ... @@ -31,13 +38,17 @@ func ContainsDestinationID(destinations []*models.Destination, destID int64) boo func CheckOfflineDestinations(nowTimeStamp int64) { for _, app := range Apps { for _, dest := range app.Destinations { + dest.Mutex.Lock() + defer dest.Mutex.Unlock() if dest.RouteType == models.ReverseProxyRoute && !dest.Online { - go func(dest *models.Destination) { - conn, err := net.DialTimeout("tcp", dest.Destination, time.Second) + go func(dest2 *models.Destination) { + conn, err := net.DialTimeout("tcp", dest2.Destination, time.Second) if err == nil { defer conn.Close() - dest.Online = true - dest.CheckTime = nowTimeStamp + dest2.Mutex.Lock() + defer dest2.Mutex.Unlock() + dest2.Online = true + dest2.CheckTime = nowTimeStamp } }(dest) } else if dest.RouteType == models.K8S_Ingress && !dest.Online { @@ -54,8 +65,6 @@ func CheckOfflineDestinations(nowTimeStamp int64) { if err != nil { utils.DebugPrintln("Unmarshal K8S API", err) } - dest.Mutex.Lock() - defer dest.Mutex.Unlock() dest.Pods = "" for _, podItem := range pods.Items { if podItem.Status.Phase == "Running" { @@ -71,3 +80,45 @@ func CheckOfflineDestinations(nowTimeStamp int64) { } } } + +// SetDestinationOffline added on Mar 23, 2024, v1.5.0 +func SetDestinationOffline(dest *models.Destination) { + targetDest := dest.Destination + if dest.RouteType == models.K8S_Ingress { + targetDest = dest.PodsAPI + } + if count, ok := offlineCache.Get(targetDest); !ok { + offlineCache.Set(targetDest, int64(1), cache.DefaultExpiration) + } else { + nowCount := count.(int64) + int64(1) + if nowCount > 5 { + // more than 5 requests timeout + dest.Online = false + app, err := GetApplicationByID(dest.AppID) + if err == nil { + sendOfflineNotification(app, targetDest) + } + } + offlineCache.Set(targetDest, nowCount, cache.DefaultExpiration) + } +} + +// sendOfflineNotification ... +func sendOfflineNotification(app *models.Application, dest string) { + var emails string + if data.IsPrimary { + emails = data.DAL.GetAppAdminAndOwnerEmails(app.Owner) + } else { + emails = data.NodeSetting.SMTP.AdminEmails + } + mailBody := "Backend server: " + dest + " (" + app.Name + ") was offline." + if len(mailBody) > 0 && len(emails) > 0 { + go utils.SendEmail(data.NodeSetting.SMTP.SMTPServer, + data.NodeSetting.SMTP.SMTPPort, + data.NodeSetting.SMTP.SMTPAccount, + data.NodeSetting.SMTP.SMTPPassword, + emails, + "[JANUSEC] Backend server offline notification", + mailBody) + } +} diff --git a/backend/k8s.go b/backend/k8s.go index b52f51d..b1b8a0c 100644 --- a/backend/k8s.go +++ b/backend/k8s.go @@ -27,7 +27,7 @@ func UpdatePods(dest *models.Destination, nowTimeStamp int64) { if err != nil { utils.DebugPrintln("Check K8S API GetResponse", err) dest.CheckTime = nowTimeStamp - dest.Online = false + SetDestinationOffline(dest) } pods := models.PODS{} err = json.Unmarshal(resp, &pods) @@ -96,7 +96,7 @@ func SelectPodFromVIPTarget(dest *models.VipTarget, srcIP string) string { if err != nil { utils.DebugPrintln("Check K8S API GetResponse", err) dest.CheckTime = nowTimeStamp - dest.Online = false + SetVipTargetOffline(dest) } pods := models.PODS{} err = json.Unmarshal(resp, &pods) diff --git a/backend/vip_app.go b/backend/vip_app.go index f8cb42b..95158c3 100644 --- a/backend/vip_app.go +++ b/backend/vip_app.go @@ -104,22 +104,14 @@ func UDPForwarding(vipApp *models.VipApp, udpListenConn *net.UDPConn) { //fmt.Println("UDPForwarding ReadMsgUDP", err) break } - vipTarget := SelectVipTarget(vipApp, clientAddr.String()) - if err != nil { - //fmt.Println("UDPForwarding ResolveUDPAddr", err) - break - } if vipTarget != nil { vipTarget.CheckTime = time.Now().Unix() targetAddr, _ := net.ResolveUDPAddr("udp", vipTarget.Destination) udpTargetConn, err := net.DialUDP("udp", nil, targetAddr) if err != nil { utils.DebugPrintln("UDPForwarding DialUDP could not connect to target", vipTarget.Destination, err) - vipTarget.Online = false - if data.NodeSetting.SMTP.SMTPEnabled { - sendVIPOfflineNotification(vipApp, vipTarget.Destination) - } + SetVipTargetOffline(vipTarget) break } if udpTargetConn == nil { @@ -138,10 +130,7 @@ func UDPForwarding(vipApp *models.VipApp, udpListenConn *net.UDPConn) { for { n, _, err := udpTargetConn.ReadFromUDP(dataBuf) if err != nil { - vipTarget.Online = false - if data.NodeSetting.SMTP.SMTPEnabled { - sendVIPOfflineNotification(vipApp, vipTarget.Destination) - } + SetVipTargetOffline(vipTarget) break } // Response to client @@ -190,10 +179,7 @@ func TCPForwarding(vipApp *models.VipApp, vipListener net.Listener) { vipTarget.CheckTime = time.Now().Unix() if err != nil { utils.DebugPrintln("TCPForwarding could not connect to target", targetDest, err) - vipTarget.Online = false - if data.NodeSetting.SMTP.SMTPEnabled { - sendVIPOfflineNotification(vipApp, targetDest) - } + SetVipTargetOffline(vipTarget) continue } vipTarget.Online = true @@ -385,23 +371,3 @@ func GetVipAppIndex(vipAppID int64) int { } return -1 } - -// sendVIPOfflineNotification ... -func sendVIPOfflineNotification(app *models.VipApp, dest string) { - var emails string - if data.IsPrimary { - emails = data.DAL.GetAppAdminAndOwnerEmails(app.Owner) - } else { - emails = data.NodeSetting.SMTP.AdminEmails - } - mailBody := "Backend virtual IP server: " + dest + " (" + app.Name + ") was offline." - if len(mailBody) > 0 && len(emails) > 0 { - go utils.SendEmail(data.NodeSetting.SMTP.SMTPServer, - data.NodeSetting.SMTP.SMTPPort, - data.NodeSetting.SMTP.SMTPAccount, - data.NodeSetting.SMTP.SMTPPassword, - emails, - "[JANUSEC] Backend server offline notification", - mailBody) - } -} diff --git a/backend/vip_target.go b/backend/vip_target.go index 227958d..35e6479 100644 --- a/backend/vip_target.go +++ b/backend/vip_target.go @@ -13,6 +13,8 @@ import ( "janusec/utils" "net" "time" + + "github.com/patrickmn/go-cache" ) // DeleteVipTargetsByAppID delete backend targets for port forwarding @@ -42,7 +44,7 @@ func CheckOfflineVipTargets(nowTimeStamp int64) { targetAddr, _ := net.ResolveUDPAddr("udp", vTarget.Destination) udpTargetConn, err := net.DialUDP("udp", nil, targetAddr) if err != nil { - vTarget.Online = false + SetVipTargetOffline(vTarget) return } // udpTargetConn will be closed in go thread @@ -51,7 +53,7 @@ func CheckOfflineVipTargets(nowTimeStamp int64) { data := make([]byte, 2048) _, _, err := udpConn.ReadFromUDP(data) if err != nil { - vipTarget.Online = false + SetVipTargetOffline(vipTarget) } else { vipTarget.Online = true } @@ -61,7 +63,7 @@ func CheckOfflineVipTargets(nowTimeStamp int64) { // send test data to target _, err = udpTargetConn.Write([]byte("Hi")) if err != nil { - vTarget.Online = false + SetVipTargetOffline(vTarget) return } } @@ -79,3 +81,44 @@ func ContainsTargetID(targets []*models.VipTarget, targetID int64) bool { } return false } + +func SetVipTargetOffline(dest *models.VipTarget) { + target := dest.Destination + if dest.RouteType == models.K8S_Ingress { + target = dest.PodsAPI + } + if count, ok := offlineCache.Get(target); !ok { + offlineCache.Set(target, int64(1), cache.DefaultExpiration) + } else { + nowCount := count.(int64) + int64(1) + if nowCount > 5 { + // more than 5 requests timeout + dest.Online = false + app, err := GetVipAppByID(dest.VipAppID) + if err == nil { + sendVIPOfflineNotification(app, target) + } + } + offlineCache.Set(target, nowCount, cache.DefaultExpiration) + } +} + +// sendVIPOfflineNotification ... +func sendVIPOfflineNotification(app *models.VipApp, dest string) { + var emails string + if data.IsPrimary { + emails = data.DAL.GetAppAdminAndOwnerEmails(app.Owner) + } else { + emails = data.NodeSetting.SMTP.AdminEmails + } + mailBody := "Backend virtual IP: " + dest + " (" + app.Name + ") was offline." + if len(mailBody) > 0 && len(emails) > 0 { + go utils.SendEmail(data.NodeSetting.SMTP.SMTPServer, + data.NodeSetting.SMTP.SMTPPort, + data.NodeSetting.SMTP.SMTPAccount, + data.NodeSetting.SMTP.SMTPPassword, + emails, + "[JANUSEC] Backend server offline notification", + mailBody) + } +} diff --git a/data/data.go b/data/data.go index 7e2948a..894d2a4 100644 --- a/data/data.go +++ b/data/data.go @@ -35,7 +35,7 @@ var ( // IsPrimary i.e. Is Primary Node IsPrimary bool // Version of JANUSEC - Version = "1.4.2" + Version = "1.5.0" ) // InitConfig init Data Access Layer diff --git a/data/setting.go b/data/setting.go index fbc55be..c5fa16f 100644 --- a/data/setting.go +++ b/data/setting.go @@ -157,7 +157,6 @@ func InitDefaultSettings() { DAL.LoadInstanceKey() DAL.LoadNodesKey() DAL.LoadAPIKey() - var err error // Init PrimarySetting if !DAL.ExistsSetting("authenticator_enabled") { @@ -261,7 +260,7 @@ func InitDefaultSettings() { DAL.SaveStringSetting("dingtalk_appsecret", "crrALdXUIj4T0zBekYh4u9sU_T1GZT") } if !DAL.ExistsSetting("dingtalk_corpid") { - // added on Mar 23, 2024 + // added on Mar 23, 2024, v1.5.0 DAL.SaveStringSetting("dingtalk_corpid", "xxxx") } // AuthConfig feishu @@ -351,9 +350,6 @@ func InitDefaultSettings() { // 0.9.13 + _ = DAL.SaveIntSetting("init_time", time.Now().Unix()) } - if err != nil { - utils.DebugPrintln("InitDefaultSettings error", err) - } } // LoadSettings ... diff --git a/firewall/cc.go b/firewall/cc.go index 22451ab..9df922d 100644 --- a/firewall/cc.go +++ b/firewall/cc.go @@ -156,7 +156,7 @@ func InitCCPolicy() { } existCCPolicy := data.DAL.ExistsCCPolicy() if !existCCPolicy { - err = data.DAL.InsertCCPolicy(0, 100, 6, 900, models.Action_Block_100, true, false, false, true) + err = data.DAL.InsertCCPolicy(0, 100, 15, 900, models.Action_Block_100, true, false, false, true) if err != nil { utils.DebugPrintln("InitCCPolicy InsertCCPolicy", err) } diff --git a/gateway/auth_oauth.go b/gateway/auth_oauth.go index 4dd7945..874791c 100644 --- a/gateway/auth_oauth.go +++ b/gateway/auth_oauth.go @@ -84,7 +84,7 @@ func GetOAuthInfo() (*OAuthInfo, error) { data.NodeSetting.AuthConfig.Dingtalk.AppID, data.NodeSetting.AuthConfig.Dingtalk.Callback) */ - // API v2, added on Mar 23, 2024 + // API v2, added on Mar 23, 2024, v1.5.0 entranceURL := fmt.Sprintf(`https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&corpId=%s&client_id=%s&scope=openid%%20corpid&state=admin&prompt=consent`, data.NodeSetting.AuthConfig.Dingtalk.Callback, data.NodeSetting.AuthConfig.Dingtalk.CorpID, diff --git a/gateway/gateway.go b/gateway/gateway.go index 41c1aa5..94c2e7a 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -392,21 +392,20 @@ func ReverseHandlerFunc(w http.ResponseWriter, r *http.Request) { ExpectContinueTimeout: 10 * time.Second, MaxIdleConns: 100, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest.Mutex.Lock() + defer dest.Mutex.Unlock() dest.CheckTime = nowTimeStamp conn, err := net.Dial("tcp", targetDest) if err != nil { - dest.Mutex.Lock() - defer dest.Mutex.Unlock() - dest.Online = false + backend.SetDestinationOffline(dest) timeout := time.Now().Unix() - nowTimeStamp utils.DebugPrintln("DialContext error", err, timeout, "seconds") - if data.NodeSetting.SMTP.SMTPEnabled { - sendOfflineNotification(app, targetDest) - } - errInfo := &models.InternalErrorInfo{ - Description: "Internal Server Offline", + if !dest.Online { + errInfo := &models.InternalErrorInfo{ + Description: "Internal Server Offline", + } + GenerateInternalErrorResponse(w, errInfo) } - GenerateInternalErrorResponse(w, errInfo) } return conn, err }, @@ -414,16 +413,15 @@ func ReverseHandlerFunc(w http.ResponseWriter, r *http.Request) { dest.CheckTime = nowTimeStamp conn, err := net.Dial("tcp", targetDest) if err != nil { - dest.Online = false + backend.SetDestinationOffline(dest) timeout := time.Now().Unix() - nowTimeStamp utils.DebugPrintln("DialTLS error", err, timeout, "seconds") - if data.NodeSetting.SMTP.SMTPEnabled { - sendOfflineNotification(app, targetDest) - } - errInfo := &models.InternalErrorInfo{ - Description: "Internal Server Offline", + if !dest.Online { + errInfo := &models.InternalErrorInfo{ + Description: "Internal Server Offline", + } + GenerateInternalErrorResponse(w, errInfo) } - GenerateInternalErrorResponse(w, errInfo) return nil, err } cfg := &tls.Config{ @@ -583,7 +581,7 @@ func getOAuthEntrance(state string) (entranceURL string, err error) { state, data.NodeSetting.AuthConfig.Dingtalk.Callback) */ - // API V2, added on Mar 23, 2024 + // API V2, added on Mar 23, 2024, v1.5.0 // doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information // Entrance URL Format: https://login.dingtalk.com/oauth2/auth? // redirect_uri=https://test.janusec.com/oauth/dingtalk @@ -732,26 +730,6 @@ func CheckExpiringCertificates() { } } -// sendOfflineNotification ... -func sendOfflineNotification(app *models.Application, dest string) { - var emails string - if data.IsPrimary { - emails = data.DAL.GetAppAdminAndOwnerEmails(app.Owner) - } else { - emails = data.NodeSetting.SMTP.AdminEmails - } - mailBody := "Backend server: " + dest + " (" + app.Name + ") was offline." - if len(mailBody) > 0 && len(emails) > 0 { - go utils.SendEmail(data.NodeSetting.SMTP.SMTPServer, - data.NodeSetting.SMTP.SMTPPort, - data.NodeSetting.SMTP.SMTPAccount, - data.NodeSetting.SMTP.SMTPPassword, - emails, - "[JANUSEC] Backend server offline notification", - mailBody) - } -} - // Test ... func Test(w http.ResponseWriter, r *http.Request) { if utils.Debug { diff --git a/gateway/webssh.go b/gateway/webssh.go index 5c1a4de..df355d4 100644 --- a/gateway/webssh.go +++ b/gateway/webssh.go @@ -118,9 +118,9 @@ func WebSSHHandlerFunc(w http.ResponseWriter, r *http.Request) { } defer wsConn.Close() // Read SSH Parameters - _, msg, err := wsConn.ReadMessage() - if err != nil { - utils.DebugPrintln("ReadMessage SSH Parameters Error:", err) + _, msg, err2 := wsConn.ReadMessage() + if err2 != nil { + utils.DebugPrintln("ReadMessage SSH Parameters Error:", err2) return } if !data.PrimarySetting.WebSSHEnabled { @@ -135,16 +135,16 @@ func WebSSHHandlerFunc(w http.ResponseWriter, r *http.Request) { if err != nil { utils.DebugPrintln("WebSSHHandlerFunc json.Unmarshal error", err) } - if err := wsConn.WriteMessage(websocket.TextMessage, []byte("Connecting "+host.IP+":"+host.Port+" ... Please wait a moment!\r\n")); err != nil { + if err = wsConn.WriteMessage(websocket.TextMessage, []byte("Connecting "+host.IP+":"+host.Port+" ... Please wait a moment!\r\n")); err != nil { return } errChan := make(chan error) go SSH(&sshInput, &sshOutput, &host, errChan) err = <-errChan if err != nil { - err = wsConn.WriteMessage(websocket.TextMessage, []byte(err.Error())) - if err != nil { - utils.DebugPrintln("WebSSHHandlerFunc wsConn.WriteMessage error", err) + err2 := wsConn.WriteMessage(websocket.TextMessage, []byte(err.Error())) + if err2 != nil { + utils.DebugPrintln("WebSSHHandlerFunc wsConn.WriteMessage error", err2) } return } @@ -154,23 +154,23 @@ func WebSSHHandlerFunc(w http.ResponseWriter, r *http.Request) { for { select { case <-errChan: - err = wsConn.WriteMessage(websocket.TextMessage, []byte(err.Error())) - if err != nil { - utils.DebugPrintln("WebSSHHandlerFunc wsConn.WriteMessage error", err) + err2 := wsConn.WriteMessage(websocket.TextMessage, []byte(err.Error())) + if err2 != nil { + utils.DebugPrintln("WebSSHHandlerFunc wsConn.WriteMessage error", err2) } return default: if wsConn == nil { return } - _, msg, err := wsConn.ReadMessage() - if err != nil { + _, msg, err2 := wsConn.ReadMessage() + if err2 != nil { return } //log.Printf("Received: %s %v\n", string(msg), msg) if sshInput != nil { go CmdLog(&logBuf, username, &host, &msg) - if _, err := sshInput.Write(msg); err != nil { + if _, err = sshInput.Write(msg); err != nil { return } } diff --git a/models/backend.go b/models/backend.go index 3fec098..3e7f42f 100644 --- a/models/backend.go +++ b/models/backend.go @@ -139,6 +139,7 @@ type Destination struct { BackendRoute string `json:"backend_route"` // Destination is backend IP:Port , or static directory + // If RoutyType is K8S, this field is not used Destination string `json:"destination"` // PodsAPI example: http://127.0.0.1:8080/api/v1/namespaces/default/pods diff --git a/models/data_config.go b/models/data_config.go index 9b8cfb8..25551e3 100644 --- a/models/data_config.go +++ b/models/data_config.go @@ -76,7 +76,7 @@ type DingtalkConfig struct { AppID string `json:"appid"` AppSecret string `json:"appsecret"` - // CorpID for API v2, added on Mar 23, 2024 + // CorpID for API v2, added on Mar 23, 2024, v1.5.0 CorpID string `json:"corpid"` } diff --git a/release_batch.sh b/release_batch.sh index db32850..0cc6bca 100755 --- a/release_batch.sh +++ b/release_batch.sh @@ -2,7 +2,7 @@ printf "Creating installation package\n" printf "Checklist:\n" printf "* Angular Admin Version Check. \n" printf "* Janusec Version Check. \n" -version="1.4.2" +version="1.5.0" printf "Version: ${version} \n" read -r -p "Are You Sure? [Y/n] " option diff --git a/usermgmt/oauth_dingtalk.go b/usermgmt/oauth_dingtalk.go index 80efa78..d72e99a 100644 --- a/usermgmt/oauth_dingtalk.go +++ b/usermgmt/oauth_dingtalk.go @@ -51,7 +51,7 @@ func GetSignature(msg []byte, key []byte) string { } */ -// AccessToken Response V2, added on Mar 23, 2024 +// AccessToken Response V2, added on Mar 23, 2024, v1.5.0 // Doc: https://open.dingtalk.com/document/orgapp/obtain-user-token#h2-hxj-mpf-5bd type AccessTokenResp struct { AccessToken string `json:"accessToken"` @@ -144,7 +144,7 @@ func DingtalkCallbackWithCodeV1(w http.ResponseWriter, r *http.Request) { } */ -// This is the API v2, added on Mar 23, 2024 +// This is the API v2, added on Mar 23, 2024, v1.5.0 // Doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information // Step 1: https://login.dingtalk.com/oauth2/auth?redirect_uri=https://.../oauth/dingtalk&response_type=code&client_id=...&scope=openid&state=...&prompt=consent // If state==admin, for janusec-admin; else for frontend applications