diff --git a/controllers/record.go b/controllers/record.go
new file mode 100644
index 0000000..0fa201f
--- /dev/null
+++ b/controllers/record.go
@@ -0,0 +1,108 @@
+// Copyright 2024 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 (
+ "encoding/json"
+
+ "github.com/casbin/caswaf/object"
+)
+
+func (c *ApiController) GetRecords() {
+ if c.RequireSignedIn() {
+ return
+ }
+
+ owner := c.Input().Get("owner")
+ if owner == "admin" {
+ owner = ""
+ }
+
+ sites, err := object.GetRecords(owner)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ // object.GetMaskedSites(sites, util.GetHostname())
+ c.ResponseOk(sites)
+}
+
+func (c *ApiController) DeleteRecord() {
+ if c.RequireSignedIn() {
+ return
+ }
+
+ var record object.Record
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.Data["json"] = wrapActionResponse(object.DeleteRecord(&record))
+ c.ServeJSON()
+}
+
+func (c *ApiController) UpdateRecord() {
+ if c.RequireSignedIn() {
+ return
+ }
+
+ owner := c.Input().Get("owner")
+ id := c.Input().Get("id")
+
+ var record object.Record
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.Data["json"] = wrapActionResponse(object.UpdateRecord(owner, id, &record))
+ c.ServeJSON()
+}
+
+func (c *ApiController) GetRecord() {
+ if c.RequireSignedIn() {
+ return
+ }
+
+ owner := c.Input().Get("owner")
+ id := c.Input().Get("id")
+ record, err := object.GetRecord(owner, id)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(record)
+}
+
+func (c *ApiController) AddRecord() {
+ if c.RequireSignedIn() {
+ return
+ }
+
+ var record object.Record
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.Data["json"] = wrapActionResponse(object.AddRecord(&record))
+ c.ServeJSON()
+}
diff --git a/object/ormer.go b/object/ormer.go
index 3b12a19..40ac973 100644
--- a/object/ormer.go
+++ b/object/ormer.go
@@ -188,4 +188,9 @@ func (a *Ormer) createTable() {
if err != nil {
panic(err)
}
+
+ err = a.Engine.Sync2(new(Record))
+ if err != nil {
+ panic(err)
+ }
}
diff --git a/object/record.go b/object/record.go
new file mode 100644
index 0000000..a833ab4
--- /dev/null
+++ b/object/record.go
@@ -0,0 +1,97 @@
+// Copyright 2024 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 (
+ "strconv"
+
+ "github.com/xorm-io/core"
+)
+
+type Record struct {
+ Id int64 `xorm:"int notnull pk autoincr" json:"id"`
+ Owner string `xorm:"varchar(100) notnull" json:"owner"`
+ CreatedTime string `xorm:"varchar(100) notnull" json:"createdTime"`
+
+ Method string `xorm:"varchar(100)" json:"method"`
+ Host string `xorm:"varchar(100)" json:"host"`
+ Path string `xorm:"varchar(100)" json:"path"`
+ ClientIp string `xorm:"varchar(100)" json:"clientIp"`
+ UserAgent string `xorm:"varchar(512)" json:"userAgent"`
+}
+
+func GetRecords(owner string) ([]*Record, error) {
+ records := []*Record{}
+ err := ormer.Engine.Asc("id").Asc("host").Find(&records, &Record{Owner: owner})
+ if err != nil {
+ return nil, err
+ }
+
+ return records, nil
+}
+
+func AddRecord(record *Record) (bool, error) {
+ affected, err := ormer.Engine.Insert(record)
+ if err != nil {
+ return false, err
+ }
+
+ return affected != 0, nil
+}
+
+func DeleteRecord(record *Record) (bool, error) {
+ affected, err := ormer.Engine.ID(core.PK{record.Id}).Delete(&Record{})
+ if err != nil {
+ return false, err
+ }
+
+ return affected != 0, nil
+}
+
+func UpdateRecord(owner string, id string, record *Record) (bool, error) {
+ affected, err := ormer.Engine.ID(core.PK{record.Id}).AllCols().Update(record)
+ if err != nil {
+ return false, err
+ }
+
+ return affected != 0, nil
+}
+
+func GetRecord(owner string, id string) (*Record, error) {
+ idNum, err := strconv.Atoi(id)
+ if err != nil {
+ return nil, err
+ }
+
+ record, err := getRecord(owner, int64(idNum))
+ if err != nil {
+ return nil, err
+ }
+
+ return record, nil
+}
+
+func getRecord(owner string, id int64) (*Record, error) {
+ record := Record{Owner: owner, Id: id}
+ existed, err := ormer.Engine.Get(&record)
+ if err != nil {
+ return nil, err
+ }
+
+ if existed {
+ return &record, nil
+ }
+ return nil, nil
+}
diff --git a/routers/router.go b/routers/router.go
index 8720161..89de865 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -52,4 +52,11 @@ func initAPI() {
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications")
+
+ beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
+ beego.Router("/api/get-record", &controllers.ApiController{}, "GET:GetRecord")
+ beego.Router("/api/delete-record", &controllers.ApiController{}, "POST:DeleteRecord")
+ beego.Router("/api/update-record", &controllers.ApiController{}, "POST:UpdateRecord")
+ beego.Router("/api/add-record", &controllers.ApiController{}, "POST:AddRecord")
+
}
diff --git a/service/proxy.go b/service/proxy.go
index 24b92f8..7277d66 100644
--- a/service/proxy.go
+++ b/service/proxy.go
@@ -17,7 +17,6 @@ package service
import (
"crypto/tls"
"fmt"
- httptx "github.com/corazawaf/coraza/v3/http"
"net"
"net/http"
"net/http/httputil"
@@ -28,6 +27,7 @@ import (
"github.com/beego/beego"
"github.com/casbin/caswaf/object"
"github.com/casbin/caswaf/util"
+ httptx "github.com/corazawaf/coraza/v3/http"
)
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
@@ -66,6 +66,41 @@ func getHostNonWww(host string) string {
return res
}
+func getClientIp(r *http.Request) string {
+ forwarded := r.Header.Get("X-Forwarded-For")
+ if forwarded != "" {
+ clientIP := strings.Split(forwarded, ",")[0]
+ return strings.TrimSpace(clientIP)
+ }
+
+ realIP := r.Header.Get("X-Real-IP")
+ if realIP != "" {
+ return realIP
+ }
+
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return r.RemoteAddr
+ }
+ return ip
+}
+
+func logRequest(clientIp string, r *http.Request) {
+ if !strings.Contains(r.UserAgent(), "Uptime-Kuma") {
+ fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\n", r.RemoteAddr, r.Method, r.Host, r.RequestURI, r.UserAgent())
+ record := object.Record{
+ Owner: "admin",
+ CreatedTime: util.GetCurrentTime(),
+ Method: r.Method,
+ Host: r.Host,
+ Path: r.RequestURI,
+ ClientIp: clientIp,
+ UserAgent: r.UserAgent(),
+ }
+ object.AddRecord(&record)
+ }
+}
+
func redirectToHttps(w http.ResponseWriter, r *http.Request) {
targetUrl := fmt.Sprintf("https://%s", joinPath(r.Host, r.RequestURI))
http.Redirect(w, r, targetUrl, http.StatusMovedPermanently)
@@ -82,9 +117,8 @@ func redirectToHost(w http.ResponseWriter, r *http.Request, host string) {
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
- if !strings.Contains(r.UserAgent(), "Uptime-Kuma") {
- fmt.Printf("handleRequest: %s\t%s\t%s\t%s\t%s\n", r.RemoteAddr, r.Method, r.Host, r.RequestURI, r.UserAgent())
- }
+ clientIp := getClientIp(r)
+ logRequest(clientIp, r)
site := getSiteByDomainWithWww(r.Host)
if site == nil {
diff --git a/web/src/App.js b/web/src/App.js
index b23b7ff..40d0b05 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -27,8 +27,9 @@ import SiteEditPage from "./SiteEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import SigninPage from "./SigninPage";
+import RecordListPage from "./RecordListPage";
+import RecordEditPage from "./RecordEditPage";
import i18next from "i18next";
-// import SelectLanguageBox from "./SelectLanguageBox";
const {Header, Footer} = Layout;
@@ -71,6 +72,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/sites"});
} else if (uri.includes("/certs")) {
this.setState({selectedMenuKey: "/certs"});
+ } else if (uri.includes("/records")) {
+ this.setState({selectedMenuKey: "/records"});
} else {
this.setState({selectedMenuKey: "null"});
}
@@ -253,6 +256,13 @@ class App extends Component {
);
+ res.push(
+
+
+ {i18next.t("general:Records")}
+
+
+ );
return res;
}
@@ -310,6 +320,9 @@ class App extends Component {
this.renderSigninIfNotSignedIn()} />
this.renderSigninIfNotSignedIn()} />
this.renderSigninIfNotSignedIn()} />
+
+ this.renderSigninIfNotSignedIn()} />
+ this.renderSigninIfNotSignedIn()} />
);
diff --git a/web/src/BaseListPage.js b/web/src/BaseListPage.js
new file mode 100644
index 0000000..d6bb90b
--- /dev/null
+++ b/web/src/BaseListPage.js
@@ -0,0 +1,64 @@
+// Copyright 2024 The CasWAF 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 React from "react";
+import {Button, Result} from "antd";
+import i18next from "i18next";
+
+class BaseListPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ classes: props,
+ data: [],
+ pagination: {
+ current: 1,
+ pageSize: 10,
+ },
+ loading: false,
+ isAuthorized: true,
+ };
+ }
+
+ UNSAFE_componentWillMount() {
+ this.fetch();
+ }
+
+ handleTableChange = () => {
+ this.fetch();
+ };
+
+ render() {
+ if (!this.state.isAuthorized) {
+ return (
+ }
+ />
+ );
+ }
+
+ return (
+