diff --git a/config/error_page/CAPTCHA.html b/config/error_page/CAPTCHA.html
new file mode 100644
index 0000000..1210f28
--- /dev/null
+++ b/config/error_page/CAPTCHA.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ CAPTCHA - ⛩️Server Torii
+
+
+
+
+
+
+
Checking that you are not a robot
+
+
+
+
+
+
diff --git a/config/rules/CAPTCHA.yml b/config/rules/CAPTCHA.yml
new file mode 100644
index 0000000..df022a7
--- /dev/null
+++ b/config/rules/CAPTCHA.yml
@@ -0,0 +1,3 @@
+secret_key: "0378b0f84c4310279918d71a5647ba5d"
+captcha_validate_time: 60
+hcaptcha_secret: ""
\ No newline at end of file
diff --git a/config/torii.yml b/config/torii.yml
index 3403b1e..1e25075 100644
--- a/config/torii.yml
+++ b/config/torii.yml
@@ -1,8 +1,13 @@
port: "25555"
+web_path: "/torii"
rule_path: "/www/dev/server_torii/config/rules"
error_page: "/www/dev/server_torii/config/error_page"
node_name: "Server Torii"
+connecting_host_headers:
+ - "Torii-Real-Host"
connecting_ip_headers:
- - "X-Real-IP"
+ - "Torii-Real-IP"
connecting_uri_headers:
- - "X-Original-URI"
\ No newline at end of file
+ - "Torii-Original-URI"
+connecting_captcha_status_headers:
+ - "Torii-Captcha-Status"
\ No newline at end of file
diff --git a/internal/action/action.go b/internal/action/action.go
index 58c7fad..8b20df2 100644
--- a/internal/action/action.go
+++ b/internal/action/action.go
@@ -10,25 +10,32 @@ const (
// Decision saves the result of the decision
type Decision struct {
- HTTPCode string
- State checkState
- JumpIndex int
+ HTTPCode []byte
+ State checkState
+ ResponseData []byte
+ JumpIndex int
}
func NewDecision() *Decision {
- return &Decision{HTTPCode: "200", State: Continue, JumpIndex: -1}
+ return &Decision{HTTPCode: []byte("200"), State: Continue, ResponseData: nil, JumpIndex: -1}
}
func (d *Decision) Set(state checkState) {
d.State = state
}
-func (d *Decision) SetCode(state checkState, httpCode string) {
+func (d *Decision) SetCode(state checkState, httpCode []byte) {
d.State = state
d.HTTPCode = httpCode
}
-func (d *Decision) SetJump(state checkState, httpCode string, jumpIndex int) {
+func (d *Decision) SetResponse(state checkState, httpCode []byte, responseData []byte) {
+ d.State = state
+ d.HTTPCode = httpCode
+ d.ResponseData = responseData
+}
+
+func (d *Decision) SetJump(state checkState, httpCode []byte, jumpIndex int) {
d.State = state
d.HTTPCode = httpCode
d.JumpIndex = jumpIndex
diff --git a/internal/check/Captcha.go b/internal/check/Captcha.go
new file mode 100644
index 0000000..aed1835
--- /dev/null
+++ b/internal/check/Captcha.go
@@ -0,0 +1,121 @@
+package check
+
+import (
+ "crypto/hmac"
+ "crypto/sha512"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "server_torii/internal/action"
+ "server_torii/internal/config"
+ "server_torii/internal/dataType"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type HCaptchaResponse struct {
+ Success bool `json:"success"`
+ ChallengeTS string `json:"challenge_ts"`
+ Hostname string `json:"hostname"`
+ ErrorCodes []string `json:"error-codes"`
+}
+
+func Captcha(reqData dataType.UserRequest, ruleSet *config.RuleSet, decision *action.Decision) {
+ if !reqData.Captcha {
+ decision.Set(action.Continue)
+ return
+ }
+
+ if !verifyClearanceCookie(reqData, *ruleSet) {
+ decision.SetCode(action.Done, []byte("CAPTCHA"))
+ return
+ }
+
+ decision.Set(action.Continue)
+
+}
+
+func CheckCaptcha(r *http.Request, reqData dataType.UserRequest, ruleSet *config.RuleSet, decision *action.Decision) {
+ if r.Method != "POST" {
+ decision.SetResponse(action.Done, []byte("403"), nil)
+ return
+ }
+
+ hCaptchaResponse := r.FormValue("h-captcha-response")
+ if hCaptchaResponse == "" {
+ decision.SetResponse(action.Done, []byte("200"), []byte("bad"))
+ return
+ }
+
+ data := url.Values{}
+ data.Set("secret", ruleSet.CAPTCHARule.HCaptchaSecret)
+ data.Set("response", hCaptchaResponse)
+ data.Set("remoteip", reqData.RemoteIP)
+
+ resp, err := http.PostForm("https://api.hcaptcha.com/siteverify", data)
+ if err != nil {
+ decision.SetResponse(action.Done, []byte("500"), []byte("bad"))
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ decision.SetResponse(action.Done, []byte("500"), []byte("bad"))
+ return
+ }
+
+ var hCaptchaResp HCaptchaResponse
+ err = json.Unmarshal(body, &hCaptchaResp)
+ if err != nil {
+ decision.SetResponse(action.Done, []byte("500"), []byte("bad"))
+ return
+ }
+
+ if !hCaptchaResp.Success {
+ decision.SetResponse(action.Done, []byte("200"), []byte("bad4"))
+ return
+ }
+
+ decision.SetResponse(action.Done, []byte("200"), []byte("good"))
+ return
+
+}
+
+func GenClearance(reqData dataType.UserRequest, ruleSet config.RuleSet) []byte {
+ timeNow := time.Now().Unix()
+ mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey))
+ mac.Write([]byte(fmt.Sprintf("%d%s%s", timeNow, reqData.Host, reqData.UserAgent)))
+ return []byte(fmt.Sprintf("%s:%s", fmt.Sprintf("%d", time.Now().Unix()), fmt.Sprintf("%x", mac.Sum(nil))))
+}
+
+func verifyClearanceCookie(reqData dataType.UserRequest, ruleSet config.RuleSet) bool {
+ if reqData.ToriiClearance == "" {
+ return false
+ }
+ parts := strings.Split(reqData.ToriiClearance, ":")
+ if len(parts) != 2 {
+ return false
+ }
+ timestamp := parts[0]
+ expectedHash := parts[1]
+
+ timeNow := time.Now().Unix()
+ parsedTimestamp, err := strconv.ParseInt(timestamp, 10, 64)
+ if err != nil {
+ return false
+ }
+
+ if timeNow-parsedTimestamp > ruleSet.CAPTCHARule.CaptchaValidateTime {
+ return false
+ }
+
+ mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey))
+ mac.Write([]byte(fmt.Sprintf("%d%s%s", parsedTimestamp, reqData.Host, reqData.UserAgent)))
+ computedHash := fmt.Sprintf("%x", mac.Sum(nil))
+
+ return hmac.Equal([]byte(computedHash), []byte(expectedHash))
+
+}
diff --git a/internal/check/IPAllow.go b/internal/check/IPAllow.go
index 262c915..bc90a2b 100644
--- a/internal/check/IPAllow.go
+++ b/internal/check/IPAllow.go
@@ -16,7 +16,7 @@ func IPAllowList(reqData dataType.UserRequest, ruleSet *config.RuleSet, decision
return
}
if trie.Search(ip) {
- decision.SetCode(action.Done, "200")
+ decision.SetCode(action.Done, []byte("200"))
} else {
decision.Set(action.Continue)
}
diff --git a/internal/check/IPBlock.go b/internal/check/IPBlock.go
index 12911b3..291bafc 100644
--- a/internal/check/IPBlock.go
+++ b/internal/check/IPBlock.go
@@ -15,7 +15,7 @@ func IPBlockList(reqData dataType.UserRequest, ruleSet *config.RuleSet, decision
return
}
if trie.Search(ip) {
- decision.SetCode(action.Done, "403")
+ decision.SetCode(action.Done, []byte("403"))
} else {
decision.Set(action.Continue)
}
diff --git a/internal/check/URLAllow.go b/internal/check/URLAllow.go
index 6be1f5d..e80cf0c 100644
--- a/internal/check/URLAllow.go
+++ b/internal/check/URLAllow.go
@@ -10,7 +10,7 @@ func URLAllowList(reqData dataType.UserRequest, ruleSet *config.RuleSet, decisio
url := reqData.Uri
list := ruleSet.URLAllowList
if list.Match(url) {
- decision.SetCode(action.Done, "200")
+ decision.SetCode(action.Done, []byte("200"))
} else {
decision.Set(action.Continue)
}
diff --git a/internal/check/URLBlock.go b/internal/check/URLBlock.go
index 2f4b9ce..55f6be3 100644
--- a/internal/check/URLBlock.go
+++ b/internal/check/URLBlock.go
@@ -10,7 +10,7 @@ func URLBlockList(reqData dataType.UserRequest, ruleSet *config.RuleSet, decisio
url := reqData.Uri
list := ruleSet.URLBlockList
if list.Match(url) {
- decision.SetCode(action.Done, "403")
+ decision.SetCode(action.Done, []byte("403"))
} else {
decision.Set(action.Continue)
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 27ea85c..343a567 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -12,12 +12,15 @@ import (
)
type MainConfig struct {
- Port string `yaml:"port"`
- RulePath string `yaml:"rule_path"`
- ErrorPage string `yaml:"error_page"`
- NodeName string `yaml:"node_name"`
- ConnectingIPHeaders []string `yaml:"connecting_ip_headers"`
- ConnectingURIHeaders []string `yaml:"connecting_uri_headers"`
+ Port string `yaml:"port"`
+ WebPath string `yaml:"web_path"`
+ RulePath string `yaml:"rule_path"`
+ ErrorPage string `yaml:"error_page"`
+ NodeName string `yaml:"node_name"`
+ ConnectingHostHeaders []string `yaml:"connecting_host_headers"`
+ ConnectingIPHeaders []string `yaml:"connecting_ip_headers"`
+ ConnectingURIHeaders []string `yaml:"connecting_uri_headers"`
+ ConnectingCaptchaStatusHeaders []string `yaml:"connecting_captcha_status_headers"`
}
// LoadMainConfig Read the configuration file and return the configuration object
@@ -50,6 +53,7 @@ type RuleSet struct {
IPBlockTrie *dataType.TrieNode
URLAllowList *dataType.URLRuleList
URLBlockList *dataType.URLRuleList
+ CAPTCHARule *dataType.CaptchaRule
}
// LoadRules Load all rules from the specified path
@@ -59,6 +63,7 @@ func LoadRules(rulePath string) (*RuleSet, error) {
IPBlockTrie: &dataType.TrieNode{},
URLAllowList: &dataType.URLRuleList{},
URLBlockList: &dataType.URLRuleList{},
+ CAPTCHARule: &dataType.CaptchaRule{},
}
// Load IP Allow List
@@ -85,9 +90,29 @@ func LoadRules(rulePath string) (*RuleSet, error) {
return nil, err
}
+ // Load CAPTCHA Rule
+ captchaFile := rulePath + "/CAPTCHA.yml"
+ if err := loadCAPTCHARule(captchaFile, rs.CAPTCHARule); err != nil {
+ return nil, err
+ }
+
return &rs, nil
}
+func loadCAPTCHARule(file string, rule *dataType.CaptchaRule) error {
+ data, err := os.ReadFile(file)
+ if err != nil {
+ return err
+ }
+
+ if err := yaml.Unmarshal(data, &rule); err != nil {
+ return err
+ }
+
+ return nil
+
+}
+
// loadIPRules read the IP rule file and insert the rules into the trie
func loadIPRules(filePath string, trie *dataType.TrieNode) error {
file, err := os.Open(filePath)
diff --git a/internal/dataType/type.go b/internal/dataType/type.go
index af30942..db83d66 100644
--- a/internal/dataType/type.go
+++ b/internal/dataType/type.go
@@ -1,6 +1,17 @@
package dataType
type UserRequest struct {
- RemoteIP string
- Uri string
+ RemoteIP string
+ Uri string
+ Captcha bool
+ ToriiClearance string
+ ToriiSessionID string
+ UserAgent string
+ Host string
+}
+
+type CaptchaRule struct {
+ SecretKey string `yaml:"secret_key"`
+ CaptchaValidateTime int64 `yaml:"captcha_validate_time"`
+ HCaptchaSecret string `yaml:"hcaptcha_secret"`
}
diff --git a/internal/server/checker.go b/internal/server/checker.go
new file mode 100644
index 0000000..632cd9f
--- /dev/null
+++ b/internal/server/checker.go
@@ -0,0 +1,77 @@
+package server
+
+import (
+ "bytes"
+ "html/template"
+ "net/http"
+ "server_torii/internal/action"
+ "server_torii/internal/check"
+ "server_torii/internal/config"
+ "server_torii/internal/dataType"
+ "time"
+)
+
+type CheckFunc func(dataType.UserRequest, *config.RuleSet, *action.Decision)
+
+func CheckMain(w http.ResponseWriter, userRequestData dataType.UserRequest, ruleSet *config.RuleSet, cfg *config.MainConfig) {
+ decision := action.NewDecision()
+
+ checkFuncs := make([]CheckFunc, 0)
+ checkFuncs = append(checkFuncs, check.IPAllowList)
+ checkFuncs = append(checkFuncs, check.IPBlockList)
+ checkFuncs = append(checkFuncs, check.URLAllowList)
+ checkFuncs = append(checkFuncs, check.URLBlockList)
+ checkFuncs = append(checkFuncs, check.Captcha)
+
+ for _, checkFunc := range checkFuncs {
+ checkFunc(userRequestData, ruleSet, decision)
+ if decision.State == action.Done {
+ break
+ }
+ }
+
+ if bytes.Compare(decision.HTTPCode, []byte("200")) == 0 {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
+ } else if bytes.Compare(decision.HTTPCode, []byte("403")) == 0 {
+ tpl, err := template.ParseFiles(cfg.ErrorPage + "/403.html")
+ if err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ data := struct {
+ EdgeTag string
+ ConnectIP string
+ Date string
+ }{
+ EdgeTag: cfg.NodeName,
+ ConnectIP: userRequestData.RemoteIP,
+ Date: time.Now().Format("2006-01-02 15:04:05"),
+ }
+ w.WriteHeader(http.StatusForbidden)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err = tpl.Execute(w, data); err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ } else if bytes.Compare(decision.HTTPCode, []byte("CAPTCHA")) == 0 {
+ tpl, err := template.ParseFiles(cfg.ErrorPage + "/CAPTCHA.html")
+ if err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusServiceUnavailable)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err = tpl.Execute(w, nil); err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ } else {
+ //should never happen
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index a5a6b3e..2f1188a 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -1,71 +1,26 @@
package server
import (
- "html/template"
"log"
"net"
"net/http"
- "server_torii/internal/action"
- "server_torii/internal/check"
"server_torii/internal/config"
"server_torii/internal/dataType"
"strings"
- "time"
)
-type CheckFunc func(dataType.UserRequest, *config.RuleSet, *action.Decision)
-
// StartServer starts the HTTP server
func StartServer(cfg *config.MainConfig, ruleSet *config.RuleSet) error {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
userRequestData := processRequestData(cfg, r)
- decision := action.NewDecision()
-
- checkFuncs := make([]CheckFunc, 0)
- checkFuncs = append(checkFuncs, check.IPAllowList)
- checkFuncs = append(checkFuncs, check.IPBlockList)
- checkFuncs = append(checkFuncs, check.URLAllowList)
- checkFuncs = append(checkFuncs, check.URLBlockList)
-
- for _, checkFunc := range checkFuncs {
- checkFunc(userRequestData, ruleSet, decision)
- if decision.State == action.Done {
- break
- }
- }
-
- if decision.HTTPCode == "200" {
- w.WriteHeader(http.StatusOK)
- w.Write([]byte("OK"))
- } else if decision.HTTPCode == "403" {
- tpl, err := template.ParseFiles(cfg.ErrorPage + "/" + decision.HTTPCode + ".html")
- if err != nil {
- http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
- return
- }
-
- data := struct {
- EdgeTag string
- ConnectIP string
- Date string
- }{
- EdgeTag: cfg.NodeName,
- ConnectIP: userRequestData.RemoteIP,
- Date: time.Now().Format("2006-01-02 15:04:05"),
- }
- w.WriteHeader(http.StatusForbidden)
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- if err = tpl.Execute(w, data); err != nil {
- http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
- return
- }
-
+ if strings.HasPrefix(userRequestData.Uri, cfg.WebPath) {
+ CheckTorii(w, r, userRequestData, ruleSet, cfg)
} else {
- // should not reach here
- w.WriteHeader(http.StatusInternalServerError)
+ CheckMain(w, userRequestData, ruleSet, cfg)
}
+
})
log.Printf("HTTP Server listening on :%s ...", cfg.Port)
@@ -74,6 +29,55 @@ func StartServer(cfg *config.MainConfig, ruleSet *config.RuleSet) error {
func processRequestData(cfg *config.MainConfig, r *http.Request) dataType.UserRequest {
+ userRequest := dataType.UserRequest{
+ RemoteIP: getClientIP(cfg, r),
+ Uri: getReqURI(cfg, r),
+ Captcha: getCaptchaStatus(cfg, r),
+ ToriiClearance: getHeader(r, "__torii_clearance"),
+ ToriiSessionID: getHeader(r, "__torii_session_id"),
+ UserAgent: r.UserAgent(),
+ Host: getReqHost(cfg, r),
+ }
+ return userRequest
+}
+
+func getHeader(r *http.Request, headerName string) string {
+ cookie, err := r.Cookie(headerName)
+ if err != nil {
+ return ""
+ }
+ return cookie.Value
+}
+
+func getCaptchaStatus(cfg *config.MainConfig, r *http.Request) bool {
+ captchaStatus := false
+ for _, headerName := range cfg.ConnectingCaptchaStatusHeaders {
+ if captchaVal := r.Header.Get(headerName); captchaVal != "" {
+ if captchaVal == "on" {
+ captchaStatus = true
+ }
+ break
+ }
+ }
+ return captchaStatus
+
+}
+
+func getReqURI(cfg *config.MainConfig, r *http.Request) string {
+ var clientURI string
+ for _, headerName := range cfg.ConnectingURIHeaders {
+ if uriVal := r.Header.Get(headerName); uriVal != "" {
+ clientURI = uriVal
+ break
+ }
+ }
+ if clientURI == "" {
+ clientURI = r.RequestURI
+ }
+ return clientURI
+}
+
+func getClientIP(cfg *config.MainConfig, r *http.Request) string {
var clientIP string
for _, headerName := range cfg.ConnectingIPHeaders {
if ipVal := r.Header.Get(headerName); ipVal != "" {
@@ -96,21 +100,16 @@ func processRequestData(cfg *config.MainConfig, r *http.Request) dataType.UserRe
clientIP = ipStr
}
}
+ return clientIP
+}
- var clientURI string
- for _, headerName := range cfg.ConnectingURIHeaders {
- if uriVal := r.Header.Get(headerName); uriVal != "" {
- clientURI = uriVal
+func getReqHost(cfg *config.MainConfig, r *http.Request) string {
+ var clientHost = ""
+ for _, headerName := range cfg.ConnectingHostHeaders {
+ if hostVal := r.Header.Get(headerName); hostVal != "" {
+ clientHost = hostVal
break
}
}
- if clientURI == "" {
- clientURI = r.RequestURI
- }
-
- userRequest := dataType.UserRequest{
- RemoteIP: clientIP,
- Uri: clientURI,
- }
- return userRequest
+ return clientHost
}
diff --git a/internal/server/torii.go b/internal/server/torii.go
new file mode 100644
index 0000000..2d566e4
--- /dev/null
+++ b/internal/server/torii.go
@@ -0,0 +1,58 @@
+package server
+
+import (
+ "bytes"
+ "html/template"
+ "net/http"
+ "server_torii/internal/action"
+ "server_torii/internal/check"
+ "server_torii/internal/config"
+ "server_torii/internal/dataType"
+ "time"
+)
+
+func CheckTorii(w http.ResponseWriter, r *http.Request, reqData dataType.UserRequest, ruleSet *config.RuleSet, cfg *config.MainConfig) {
+ decision := action.NewDecision()
+
+ decision.SetCode(action.Continue, []byte("403"))
+ if reqData.Uri == cfg.WebPath+"/captcha" {
+ check.CheckCaptcha(r, reqData, ruleSet, decision)
+ }
+ if bytes.Compare(decision.HTTPCode, []byte("200")) == 0 {
+ if bytes.Compare(decision.ResponseData, []byte("bad")) == 0 {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("bad"))
+ return
+ } else if bytes.Compare(decision.ResponseData, []byte("good")) == 0 {
+ w.Header().Set("Set-Cookie", "__torii_clearance="+string(check.GenClearance(reqData, *ruleSet))+"; Path=/; HttpOnly")
+ w.WriteHeader(http.StatusOK)
+ w.Write(decision.ResponseData)
+ } else {
+ //should not be here
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("500 - Internal Server Error"))
+ }
+ } else {
+ tpl, err := template.ParseFiles(cfg.ErrorPage + "/403.html")
+ if err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ data := struct {
+ EdgeTag string
+ ConnectIP string
+ Date string
+ }{
+ EdgeTag: cfg.NodeName,
+ ConnectIP: reqData.RemoteIP,
+ Date: time.Now().Format("2006-01-02 15:04:05"),
+ }
+ w.WriteHeader(http.StatusForbidden)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err = tpl.Execute(w, data); err != nil {
+ http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+}