Skip to content

Commit

Permalink
feat: support health check and alert for sites (#79)
Browse files Browse the repository at this point in the history
* feat: support health check for sites

* feat: refactor out health-check and provider

* feat: clean up code and support SMS

* feat: support send alert with specific providers
  • Loading branch information
love98ooo authored Sep 27, 2024
1 parent 69b6af2 commit 92a0dc8
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 0 deletions.
27 changes: 27 additions & 0 deletions controllers/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import "github.com/casdoor/casdoor-go-sdk/casdoorsdk"

func (c *ApiController) GetProviders() {
providers, err := casdoorsdk.GetProviders()
if err != nil {
c.ResponseError(err.Error())
return
}

c.ResponseOk(providers)
}
4 changes: 4 additions & 0 deletions object/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type Site struct {
NeedRedirect bool `json:"needRedirect"`
DisableVerbose bool `json:"disableVerbose"`
Rules []string `xorm:"varchar(500)" json:"rules"`
EnableAlert bool `json:"enableAlert"`
AlertInterval int `json:"alertInterval"`
AlertTryTimes int `json:"alertTryTimes"`
AlertProviders []string `xorm:"varchar(500)" json:"alertProviders"`
Challenges []string `xorm:"mediumtext" json:"challenges"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
Expand Down
6 changes: 6 additions & 0 deletions object/site_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

var siteMap = map[string]*Site{}
var certMap = map[string]*Cert{}
var healthCheckNeededDomains []string

func InitSiteMap() {
err := refreshSiteMap()
Expand Down Expand Up @@ -75,6 +76,7 @@ func refreshSiteMap() error {
}

newSiteMap := map[string]*Site{}
newHealthCheckNeededDomains := make([]string, 0)
sites, err := GetGlobalSites()
if err != nil {
return err
Expand Down Expand Up @@ -105,6 +107,9 @@ func refreshSiteMap() error {
}

newSiteMap[strings.ToLower(site.Domain)] = site
if !shouldStopHealthCheck(site) {
newHealthCheckNeededDomains = append(newHealthCheckNeededDomains, strings.ToLower(site.Domain))
}
for _, domain := range site.OtherDomains {
if domain != "" {
newSiteMap[strings.ToLower(domain)] = site
Expand All @@ -113,6 +118,7 @@ func refreshSiteMap() error {
}

siteMap = newSiteMap
healthCheckNeededDomains = newHealthCheckNeededDomains
return nil
}

Expand Down
2 changes: 2 additions & 0 deletions object/site_timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func StartMonitorSitesLoop() {
continue
}

startHealthCheckLoop()

time.Sleep(5 * time.Second)
}
}()
Expand Down
106 changes: 106 additions & 0 deletions object/site_timer_health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

import (
"fmt"
"strings"
"time"

"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)

var healthCheckTryTimesMap = map[string]int{}

func healthCheck(site *Site, domain string) error {
var isHealth bool
var pingResponse string
urlHttps := "https://" + domain
urlHttp := "http://" + domain
switch site.SslMode {
case "HTTPS Only":
isHealth, pingResponse = pingUrl(urlHttps)
case "HTTP":
isHealth, pingResponse = pingUrl(urlHttp)
case "HTTPS and HTTP":
isHttpsHealth, httpsPingResponse := pingUrl(urlHttps)
isHttpHealth, httpPingResponse := pingUrl(urlHttp)
isHealth = isHttpsHealth || isHttpHealth
pingResponse = httpsPingResponse + httpPingResponse
}

if isHealth {
healthCheckTryTimesMap[domain] = GetSiteByDomain(domain).AlertTryTimes
return nil
}

healthCheckTryTimesMap[domain]--
if healthCheckTryTimesMap[domain] != 0 {
return nil
}

pingResponse = fmt.Sprintf("CasWAF health check failed for domain %s, %s", domain, pingResponse)
user, err := casdoorsdk.GetUser(site.Owner)
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("user not found")
}
for _, provider := range site.AlertProviders {
if strings.HasPrefix(provider, "Email/") {
err := casdoorsdk.SendEmailByProvider("CasWAF HealthCheck Check Alert", pingResponse, "CasWAF", provider[6:], user.Email)
if err != nil {
fmt.Println(err)
}
}
if strings.HasPrefix(provider, "SMS/") {
err := casdoorsdk.SendSmsByProvider(pingResponse, provider[4:], user.Phone)
if err != nil {
fmt.Println(err)
}
}
}
return nil
}

func startHealthCheckLoop() {
for _, domain := range healthCheckNeededDomains {
domain := domain
if _, ok := healthCheckTryTimesMap[domain]; ok {
continue
}
healthCheckTryTimesMap[domain] = GetSiteByDomain(domain).AlertTryTimes
go func() {
for {
site := GetSiteByDomain(domain)
if shouldStopHealthCheck(site) {
delete(healthCheckTryTimesMap, domain)
return
}

err := healthCheck(site, domain)
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Duration(site.AlertInterval) * time.Second)
}
}()
}
}

func shouldStopHealthCheck(site *Site) bool {
return site == nil || !site.EnableAlert || site.Domain == "" || site.Status == "Inactive"
}
1 change: 1 addition & 0 deletions routers/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func initAPI() {
beego.Router("/api/signin", &controllers.ApiController{}, "POST:Signin")
beego.Router("/api/signout", &controllers.ApiController{}, "POST:Signout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")

beego.Router("/api/get-global-sites", &controllers.ApiController{}, "GET:GetGlobalSites")
beego.Router("/api/get-sites", &controllers.ApiController{}, "GET:GetSites")
Expand Down
87 changes: 87 additions & 0 deletions web/src/SiteEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as SiteBackend from "./backend/SiteBackend";
import * as CertBackend from "./backend/CertBackend";
import * as RuleBackend from "./backend/RuleBackend";
Expand All @@ -34,6 +35,7 @@ class SiteEditPage extends React.Component {
owner: props.match.params.owner,
siteName: props.match.params.siteName,
rules: [],
providers: [],
site: null,
certs: null,
applications: null,
Expand All @@ -45,6 +47,7 @@ class SiteEditPage extends React.Component {
this.getCerts();
this.getRules();
this.getApplications();
this.getAlertProviders();
}

getSite() {
Expand Down Expand Up @@ -99,6 +102,26 @@ class SiteEditPage extends React.Component {
});
}

getAlertProviders() {
ProviderBackend.getProviders()
.then((res) => {
if (res.status === "ok") {
const data = [];
for (let i = 0; i < res.data.length; i++) {
const provider = res.data[i];
if (provider.category === "SMS" || provider.category === "Email") {
data.push(provider.category + "/" + provider.name);
}
}
this.setState({
providers: data,
});
} else {
Setting.showMessage("error", `Failed to get providers: ${res.msg}`);
}
});
}

parseSiteField(key, value) {
if (["score"].includes(key)) {
value = Setting.myParseInt(value);
Expand All @@ -116,6 +139,16 @@ class SiteEditPage extends React.Component {
});
}

updateHealthCheckField(key, value) {
value = this.parseSiteField(key, value);

const site = this.state.site;
site.healthCheck[key] = value;
this.setState({
site: site,
});
}

renderSite() {
return (
<Card size="small" title={
Expand Down Expand Up @@ -210,6 +243,60 @@ class SiteEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col span={2} style={{marginTop: "5px"}}>
{i18next.t("site:Enable alert")}:
</Col>
<Col span={22} >
<Switch checked={this.state.site.enableAlert} onChange={checked => {
this.updateSiteField("enableAlert", checked);
}} />
</Col>
</Row>
{
this.state.site.enableAlert ? (
<Row style={{marginTop: "20px"}} >
<Col span={2} style={{marginTop: "5px"}}>
{i18next.t("site:Alert interval")}:
</Col>
<Col span={22} >
<InputNumber min={1} value={this.state.site.alertInterval} addonAfter={i18next.t("usage:seconds")} onChange={value => {
this.updateSiteField("alertInterval", value);
}} />
</Col>
</Row>
) : null
}
{
this.state.site.enableAlert ? (
<Row style={{marginTop: "20px"}} >
<Col span={2} style={{marginTop: "5px"}}>
{i18next.t("site:Alert try times")}:
</Col>
<Col span={22} >
<InputNumber min={1} value={this.state.site.alertTryTimes} onChange={value => {
this.updateSiteField("alertTryTimes", value);
}} />
</Col>
</Row>
) : null
}
{
this.state.site.enableAlert ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("site:Alert providers")}:
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.site.alertProviders} onChange={(value => {this.updateSiteField("alertProviders", value);})}>
{
this.state.providers.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("site:Challenges")}:
Expand Down
4 changes: 4 additions & 0 deletions web/src/SiteListPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class SiteListPage extends BaseListPage {
needRedirect: false,
disableVerbose: false,
rules: [],
enableAlert: false,
alertInterval: 60,
alertTryTimes: 3,
alertProviders: [],
challenges: [],
host: "",
port: 8000,
Expand Down
22 changes: 22 additions & 0 deletions web/src/backend/ProviderBackend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as Setting from "../Setting";

export function getProviders() {
return fetch(`${Setting.ServerUrl}/api/get-providers`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}

0 comments on commit 92a0dc8

Please sign in to comment.