Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tianguyin authored Jan 4, 2025
1 parent 31fbb8a commit ca14bb3
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 0 deletions.
72 changes: 72 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cli

import (
"errors"
"fmt"
"strings"
)

func Run(args []string) error {
if len(args) == 0 {
return errors.New("no arguments provided. Use --help for usage")
}

switch args[0] {
case "-V", "--version":
fmt.Println("WebProxy CLI version 1.0.0")
case "-h", "--help":
printHelp()
default:
// 调用子命令
return executeCommand(args)
}
return nil
}

func printHelp() {
fmt.Println(`WebProxy CLI
Commands:
proxy Run the proxy server
--server_port 设置监听端口(必须,否则无法启动)
--proxy_port 设置代理端口(必须,否则无法启动)
--proxy_ip 设置代理ip(非必须,默认为127.0.0.1)
--log_mode 设置日志模式默认 cli(仅仅控制台打印) save(保存到指定路径文件)
--log_path 设置日志文件路径(如果log_mode为save则必须要填,反之则一定不要填写)
--waf_rules 设置waf规则文件路径(非必须,不填写则不启动waf功能)
-V, --version Show the CLI version
-h, --help Show this help message`)
}

func executeCommand(args []string) error {
command := args[0]
parsedArgs := parseArgs(args[1:])

switch command {
case "proxy":
return runProxy(parsedArgs)
default:
return fmt.Errorf("unknown command: %s. Use --help for usage", command)
}
}

// parseArgs 将参数解析为键值对
func parseArgs(args []string) map[string]string {
parsed := make(map[string]string)
for i := 0; i < len(args); i++ {
arg := args[i]
if strings.HasPrefix(arg, "--") {
key := strings.TrimPrefix(arg, "--")
// 检查是否有下一个值,并且下一个值不是另一个选项
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
parsed[key] = args[i+1]
i++
} else {
parsed[key] = "" // 没有值的选项
}
} else {
// 非选项参数,按顺序存储
parsed[arg] = ""
}
}
return parsed
}
152 changes: 152 additions & 0 deletions cli/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package cli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)

// LogEntry 定义日志结构
type LogEntry struct {
Timestamp string `json:"timestamp"`
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body string `json:"body,omitempty"`
}

func runProxy(args map[string]string) error {
// 获取 server_port 和 proxy_port 参数
serverPort := args["server_port"]
if serverPort == "" {
fmt.Println("server_port is required, server failed to start")
return nil
}

proxyPort := args["proxy_port"]
if proxyPort == "" {
fmt.Println("proxy_port is required, proxy failed to start")
return nil
}

proxyIP := args["proxy_ip"]
if proxyIP == "" {
proxyIP = "127.0.0.1" // 默认代理 IP
}
rulesFile := args["waf_rules"] // 获取 WAF 规则文件路径

// 获取 log_mode 和 log_path 参数
logMode := args["log_mode"]
logPath := args["log_path"]
if logMode == "" {
logMode = "cli"
}
// 启动代理服务器
fmt.Printf("Starting HTTP proxy server on port %s, forwarding to %s:%s...\n", serverPort, proxyIP, proxyPort)
return server(serverPort, proxyIP, proxyPort, logMode, logPath, rulesFile)
}

func server(serverPort, proxyIP, proxyPort, logMode, logPath, rulesFile string) error {
// 构造目标地址
proxyURL := fmt.Sprintf("http://%s:%s", proxyIP, proxyPort)
targetURL, err := url.Parse(proxyURL)
if err != nil {
return fmt.Errorf("failed to parse proxy URL: %v", err)
}

// 创建日志处理器
var logWriter io.Writer
if logMode == "cli" {
logWriter = os.Stdout
} else if logMode == "save" {
if logPath == "" {
return fmt.Errorf("log_path is required in 'save' mode")
}
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %v", err)
}
defer logFile.Close()
logWriter = logFile
} else {
logWriter = nil // 不记录日志
}

// 创建反向代理处理器
proxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 如果指定了 WAF 规则文件,进行 WAF 检查
if rulesFile != "" {
if err := waf(r, rulesFile); err != nil {
http.Error(w, fmt.Sprintf("WAF validation failed: %v", err), http.StatusForbidden)
return
}
}

// 记录请求头
headers := make(map[string]string)
for key, values := range r.Header {
headers[key] = values[0]
}

// 保存请求体(用于 POST/PUT 等方法记录 data)
var bodyData string
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
bodyBytes, err := io.ReadAll(r.Body)
if err == nil {
bodyData = string(bodyBytes)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // 恢复请求体供后续使用
} else {
bodyData = fmt.Sprintf("failed to read body: %v", err)
}
}

// 构造日志条目
logEntry := LogEntry{
Timestamp: time.Now().Format(time.RFC3339),
Method: r.Method,
URL: r.URL.String(),
Headers: headers,
Body: bodyData,
}

// 输出日志
if logWriter != nil {
logData, err := json.Marshal(logEntry)
if err == nil {
logData = append(logData, '\n') // 每行一个 JSON 对象
_, _ = logWriter.Write(logData)
}
}

// 转发请求到目标服务器
r.URL.Scheme = targetURL.Scheme
r.URL.Host = targetURL.Host
r.Host = targetURL.Host

resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
http.Error(w, fmt.Sprintf("proxy error: %v", err), http.StatusBadGateway)
return
}
defer resp.Body.Close()

// 将目标服务器的响应写回客户端
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
})

// 启动 HTTP 服务器
serverAddr := fmt.Sprintf(":%s", serverPort)
fmt.Printf("Proxy server is running at %s\n", serverAddr)
return http.ListenAndServe(serverAddr, proxy)
}
103 changes: 103 additions & 0 deletions cli/waf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cli

import (
"bytes"
"fmt"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
"regexp"
)

// WAFRules 定义 WAF 规则结构
type WAFRules struct {
Low WAFRuleSet `yaml:"low"`
High WAFRuleSet `yaml:"high"`
}

// WAFRuleSet 定义单个规则集
type WAFRuleSet struct {
Allow WAFRule `yaml:"allow"`
Disallow WAFRule `yaml:"disallow"`
}

// WAFRule 定义规则内容
type WAFRule struct {
Agent []string `yaml:"agent"`
Body []string `yaml:"body"`
URL []string `yaml:"url"`
}

// waf 函数加载规则并检查请求
func waf(r *http.Request, rulesFile string) error {
// 加载规则文件
data, err := os.ReadFile(rulesFile)
if err != nil {
return fmt.Errorf("failed to read rules file: %v", err)
}

// 解析 YAML 文件
var wafRules WAFRules
if err := yaml.Unmarshal(data, &wafRules); err != nil {
return fmt.Errorf("failed to parse rules file: %v", err)
}

// 1. 检查 URL
urlPath := r.URL.Path
if err := checkRuleWithHigh(urlPath, wafRules.Low.Allow.URL, wafRules.Low.Disallow.URL, wafRules.High.Allow.URL); err != nil {
return err
}

// 2. 检查 User-Agent
userAgent := r.Header.Get("User-Agent")
if err := checkRuleWithHigh(userAgent, wafRules.Low.Allow.Agent, wafRules.Low.Disallow.Agent, wafRules.High.Allow.Agent); err != nil {
return err
}

// 3. 检查 Body 内容
var bodyData string
if r.Body != nil {
bodyBytes, _ := io.ReadAll(r.Body)
bodyData = string(bodyBytes)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
if err := checkRuleWithHigh(bodyData, wafRules.Low.Allow.Body, wafRules.Low.Disallow.Body, wafRules.High.Allow.Body); err != nil {
return err
}

return nil
}

// checkRuleWithHigh 检查规则,并考虑 high 规则的覆盖
func checkRuleWithHigh(content string, allowRules []string, disallowRules []string, highAllowRules []string) error {
// 如果禁止规则匹配,直接返回错误
for _, rule := range disallowRules {
if match, _ := regexp.MatchString(rule, content); match {
// 如果 low 规则拒绝了,但 high 规则允许某些内容,继续检查 high 规则
for _, highAllowRule := range highAllowRules {
if match, _ := regexp.MatchString(highAllowRule, content); match {
// 如果 high 规则允许更大的词语,放行请求
return nil
}
}
return fmt.Errorf("content matches disallowed rule: %s", rule)
}
}

// 如果允许规则不为空且未匹配,返回错误
if len(allowRules) > 0 {
matched := false
for _, rule := range allowRules {
if match, _ := regexp.MatchString(rule, content); match {
matched = true
break
}
}
if !matched {
return fmt.Errorf("content does not match any allowed rules")
}
}

return nil
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module webproxy

go 1.23

require gopkg.in/yaml.v3 v3.0.1
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions logs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{"timestamp":"2025-01-04T14:52:12+08:00","method":"GET","url":"/api/allowed","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Cookie":"authToken=b504a6c8db6b4861","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"}}
{"timestamp":"2025-01-04T15:07:04+08:00","method":"POST","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Content-Length":"9","Content-Type":"application/x-www-form-urlencoded","Cookie":"authToken=b504a6c8db6b4861","Origin":"http://127.0.0.1:8080","Referer":"http://127.0.0.1:8080/api/allowed","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"},"body":"data=data"}
{"timestamp":"2025-01-04T15:07:55+08:00","method":"POST","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Content-Length":"14","Content-Type":"application/x-www-form-urlencoded","Cookie":"authToken=b504a6c8db6b4861","Origin":"http://127.0.0.1:8080","Referer":"http://127.0.0.1:8080/console/machines","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"},"body":"testhight=data"}
{"timestamp":"2025-01-04T15:08:24+08:00","method":"POST","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Content-Length":"14","Content-Type":"application/x-www-form-urlencoded","Cookie":"authToken=b504a6c8db6b4861","Origin":"http://127.0.0.1:8080","Referer":"http://127.0.0.1:8080/console/machines","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"},"body":"testhight=data"}
{"timestamp":"2025-01-04T15:08:58+08:00","method":"GET","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Cookie":"authToken=b504a6c8db6b4861","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"}}
{"timestamp":"2025-01-04T15:08:59+08:00","method":"GET","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Cookie":"authToken=b504a6c8db6b4861","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"}}
{"timestamp":"2025-01-04T15:09:04+08:00","method":"POST","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Content-Length":"14","Content-Type":"application/x-www-form-urlencoded","Cookie":"authToken=b504a6c8db6b4861","Origin":"http://127.0.0.1:8080","Referer":"http://127.0.0.1:8080/console/machines","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"},"body":"testhight=data"}
{"timestamp":"2025-01-04T15:09:17+08:00","method":"POST","url":"/console/machines","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"max-age=0","Connection":"keep-alive","Content-Length":"14","Content-Type":"application/x-www-form-urlencoded","Cookie":"authToken=b504a6c8db6b4861","Origin":"http://127.0.0.1:8080","Referer":"http://127.0.0.1:8080/console/machines","Sec-Ch-Ua":"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"Windows\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"},"body":"testhight=data"}
18 changes: 18 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"fmt"
"os"
"webproxy/cli"
)

func main() {
args := os.Args[1:]
if err := cli.Run(args); err != nil {
_, err := fmt.Fprintln(os.Stderr, err)
if err != nil {
return
}
os.Exit(1)
}
}
26 changes: 26 additions & 0 deletions waf_rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
low:
allow:
agent:
- ".*"
body:
- ".*"
url:
- ".*"
disallow:
agent:
- "curl"
body:
- "test"
- "malicious"
url:
- "/api/restricted"
high:
disallow:
agent:
body:
url:
allow:
agent:
url:
body:
- "testhight"

0 comments on commit ca14bb3

Please sign in to comment.