mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 21:10:30 +01:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d47eb0e91 | ||
|
|
af7c59b5c2 | ||
|
|
693bf68864 | ||
|
|
c9ddf3d165 | ||
|
|
1549b56866 | ||
|
|
2cd1f22e68 | ||
|
|
688f38943d | ||
|
|
043bbd7a11 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: yusing # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: yusingwysq # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
59
README.md
59
README.md
@@ -1,19 +1,25 @@
|
||||
<div align="center">
|
||||
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
A lightweight, simple, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||
|
||||
For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
**EN** | <a href="README_CHT.md">中文</a>
|
||||
|
||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
|
||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
||||
|
||||

|
||||
|
||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
</div>
|
||||
|
||||
## Table of content
|
||||
|
||||
@@ -22,9 +28,8 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
- [GoDoxy](#godoxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Features](#key-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
@@ -53,18 +58,14 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Getting Started
|
||||
|
||||
For full documentation, **[See Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
1. Pull the latest docker images
|
||||
|
||||
@@ -78,27 +79,11 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. _(Optional)_ setup WebUI login (skip if you use OIDC)
|
||||
3. _(Optional)_ setup `docker-socket-proxy` other docker nodes (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) then add them inside `config.yml`
|
||||
|
||||
- set random JWT secret
|
||||
4. Start the container `docker compose up -d`
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
- change username and password for WebUI authentication
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
|
||||
4. _(Optional)_ setup `docker-socket-proxy` other docker nodes (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) then add them inside `config.yml`
|
||||
|
||||
5. Start the container `docker compose up -d`
|
||||
|
||||
6. You may now do some extra configuration on WebUI `https://gp.y.z`
|
||||
5. You may now do some extra configuration on WebUI `https://godoxy.domain.com`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<div align="center">
|
||||
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
輕量、易用、 [高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
||||
|
||||
完整文檔請查閱 **[Wiki](https://github.com/yusing/go-proxy/wiki)**(暫未有中文翻譯)
|
||||
|
||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||
|
||||
[English Documentation](README.md)
|
||||
<a href="README.md">EN</a> | **中文**
|
||||
|
||||
一個輕量級、易於使用且[高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks)的反向代理,具有網頁介面和儀表板。
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
|
||||

|
||||
|
||||
_加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
</div>
|
||||
|
||||
## 目錄
|
||||
|
||||
@@ -22,9 +28,8 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
- [GoDoxy](#godoxy)
|
||||
- [目錄](#目錄)
|
||||
- [主要特點](#主要特點)
|
||||
- [入門指南](#入門指南)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [在 VSCode 中使用 JSON Schema](#在-vscode-中使用-json-schema)
|
||||
@@ -43,6 +48,7 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
- 容器狀態/配置文件變更時自動熱重載
|
||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||
- HTTP(s) 反向代理
|
||||
- OpenID Connect 支持
|
||||
- [HTTP 中介軟體支援](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [自訂錯誤頁面支援](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP 和 UDP 埠轉發
|
||||
@@ -52,18 +58,14 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
## 入門指南
|
||||
|
||||
完整文檔請參見 **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
### 前置需求
|
||||
## 前置需求
|
||||
|
||||
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||
|
||||
- A 記錄:`*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄:`*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### 安裝
|
||||
## 安裝
|
||||
|
||||
1. 拉取最新的 Docker 映像
|
||||
|
||||
@@ -77,29 +79,11 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. _(可選)_ 設置網頁介面登入
|
||||
3. _(可選)_ 設置其他 Docker 節點的 `docker-socket-proxy`(參見 [多 Docker 節點設置](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)),然後在 `config.yml` 中添加它們
|
||||
|
||||
- 設置隨機 JWT 密鑰
|
||||
4. 啟動容器 `docker compose up -d`
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
- 更改網頁介面認證的使用者名稱和密碼
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
|
||||
4. _(可選)_ 設置其他 Docker 節點的 `docker-socket-proxy`(參見 [多 Docker 節點設置](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)),然後在 `config.yml` 中添加它們
|
||||
|
||||
5. 啟動容器 `docker compose up -d`
|
||||
|
||||
6. 現在您可以進行額外的配置
|
||||
- 使用文字編輯器(如 Visual Studio Code)
|
||||
- 通過網頁介面 `https://gp.y.z`
|
||||
5. 大功告成!可前往WebUI `https://gp.domain.com` 進行額外的配置
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ services:
|
||||
- app
|
||||
# modify below to fit your needs
|
||||
labels:
|
||||
proxy.aliases: gp
|
||||
proxy.#1.port: 3000
|
||||
# proxy.#1.middlewares.cidr_whitelist.status: 403
|
||||
# proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
|
||||
# proxy.#1.middlewares.cidr_whitelist.allow: |
|
||||
proxy.aliases: godoxy
|
||||
proxy.godoxy.port: 3000
|
||||
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||
# status: 403
|
||||
# message: IP not allowed
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - 10.0.0.0/8
|
||||
# - 192.168.0.0/16
|
||||
|
||||
@@ -2,19 +2,19 @@ package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
var validMethods = map[string]struct{}{
|
||||
http.MethodGet: {},
|
||||
http.MethodHead: {},
|
||||
http.MethodPost: {},
|
||||
http.MethodPut: {},
|
||||
http.MethodPatch: {},
|
||||
http.MethodDelete: {},
|
||||
http.MethodConnect: {},
|
||||
http.MethodOptions: {},
|
||||
http.MethodTrace: {},
|
||||
}
|
||||
|
||||
func IsMethodValid(method string) bool {
|
||||
_, ok := validMethods[method]
|
||||
return ok
|
||||
switch method {
|
||||
case http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodOptions,
|
||||
http.MethodTrace:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ var (
|
||||
ErrInvalidCommandSequence = E.New("invalid command sequence")
|
||||
ErrInvalidSetTarget = E.New("invalid `rule.set` target")
|
||||
|
||||
ErrExpectNoArg = ErrInvalidArguments.Withf("expect no arg")
|
||||
ErrExpectOneArg = ErrInvalidArguments.Withf("expect 1 arg")
|
||||
ErrExpectTwoArgs = ErrInvalidArguments.Withf("expect 2 args")
|
||||
ErrExpectNoArg = E.New("expect no arg")
|
||||
ErrExpectOneArg = E.New("expect 1 arg")
|
||||
ErrExpectTwoArgs = E.New("expect 2 args")
|
||||
ErrExpectKVOptionalV = E.New("expect 'key' or 'key value'")
|
||||
)
|
||||
|
||||
@@ -20,9 +20,8 @@ func (h *Help) String() string {
|
||||
sb.WriteString(h.command)
|
||||
sb.WriteString(" ")
|
||||
for arg := range h.args {
|
||||
sb.WriteRune('<')
|
||||
sb.WriteString(arg)
|
||||
sb.WriteString("> ")
|
||||
sb.WriteString(strings.ToUpper(arg))
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
if h.description != "" {
|
||||
sb.WriteString("\n\t")
|
||||
@@ -32,7 +31,7 @@ func (h *Help) String() string {
|
||||
sb.WriteRune('\n')
|
||||
for arg, desc := range h.args {
|
||||
sb.WriteRune('\t')
|
||||
sb.WriteString(arg)
|
||||
sb.WriteString(strings.ToUpper(arg))
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(desc)
|
||||
sb.WriteRune('\n')
|
||||
|
||||
@@ -34,15 +34,25 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnHeader,
|
||||
args: map[string]string{
|
||||
"key": "the header key",
|
||||
"value": "the header value",
|
||||
"key": "the header key",
|
||||
"[value]": "the header value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return len(r.Header[k]) > 0
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.Header.Get(k) == v
|
||||
for _, vv := range r.Header[k] {
|
||||
if v == vv {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -50,13 +60,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnQuery,
|
||||
args: map[string]string{
|
||||
"key": "the query key",
|
||||
"value": "the query value",
|
||||
"key": "the query key",
|
||||
"[value]": "the query value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return len(cached.GetQueries(r)[k]) > 0
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
queries := cached.GetQueries(r)[k]
|
||||
for _, query := range queries {
|
||||
@@ -72,13 +87,24 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnCookie,
|
||||
args: map[string]string{
|
||||
"key": "the cookie key",
|
||||
"value": "the cookie value",
|
||||
"key": "the cookie key",
|
||||
"[value]": "the cookie value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
cookies := cached.GetCookies(r)
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
cookies := cached.GetCookies(r)
|
||||
for _, cookie := range cookies {
|
||||
@@ -95,13 +121,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnForm,
|
||||
args: map[string]string{
|
||||
"key": "the form key",
|
||||
"value": "the form value",
|
||||
"key": "the form key",
|
||||
"[value]": "the form value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.FormValue(k) != ""
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.FormValue(k) == v
|
||||
}
|
||||
@@ -111,13 +142,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnPostForm,
|
||||
args: map[string]string{
|
||||
"key": "the form key",
|
||||
"value": "the form value",
|
||||
"key": "the form key",
|
||||
"[value]": "the form value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.PostFormValue(k) != ""
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.PostFormValue(k) == v
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestParseOn(t *testing.T) {
|
||||
@@ -15,25 +20,50 @@ func TestParseOn(t *testing.T) {
|
||||
}{
|
||||
// header
|
||||
{
|
||||
name: "header_valid",
|
||||
name: "header_valid_kv",
|
||||
input: "header Connection Upgrade",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "header_invalid",
|
||||
name: "header_valid_k",
|
||||
input: "header Connection",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "header_missing_arg",
|
||||
input: "header",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
// query
|
||||
{
|
||||
name: "query_valid",
|
||||
name: "query_valid_kv",
|
||||
input: "query key value",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "query_invalid",
|
||||
name: "query_valid_k",
|
||||
input: "query key",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "query_missing_arg",
|
||||
input: "query",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
{
|
||||
name: "cookie_valid_kv",
|
||||
input: "cookie key value",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cookie_valid_k",
|
||||
input: "cookie key",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cookie_missing_arg",
|
||||
input: "cookie",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
// method
|
||||
{
|
||||
@@ -43,9 +73,14 @@ func TestParseOn(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "method_invalid",
|
||||
input: "method",
|
||||
input: "method invalid",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "method_missing_arg",
|
||||
input: "method",
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
// path
|
||||
{
|
||||
name: "path_valid",
|
||||
@@ -53,9 +88,9 @@ func TestParseOn(t *testing.T) {
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "path_invalid",
|
||||
name: "path_missing_arg",
|
||||
input: "path",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
// remote
|
||||
{
|
||||
@@ -65,9 +100,14 @@ func TestParseOn(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "remote_invalid",
|
||||
input: "remote",
|
||||
input: "remote abcd",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "remote_missing_arg",
|
||||
input: "remote",
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
{
|
||||
name: "unknown_target",
|
||||
input: "unknown",
|
||||
@@ -87,3 +127,152 @@ func TestParseOn(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testCorrectness struct {
|
||||
name string
|
||||
checker string
|
||||
input *http.Request
|
||||
want bool
|
||||
}
|
||||
|
||||
func genCorrectnessTestCases(field string, genRequest func(k, v string) *http.Request) []testCorrectness {
|
||||
return []testCorrectness{
|
||||
{
|
||||
name: field + "_match",
|
||||
checker: field + " foo bar",
|
||||
input: genRequest("foo", "bar"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: field + "_no_match",
|
||||
checker: field + " foo baz",
|
||||
input: genRequest("foo", "bar"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: field + "_exists",
|
||||
checker: field + " foo",
|
||||
input: genRequest("foo", "abcd"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: field + "_not_exists",
|
||||
checker: field + " foo",
|
||||
input: genRequest("bar", "abcd"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnCorrectness(t *testing.T) {
|
||||
tests := []testCorrectness{
|
||||
{
|
||||
name: "method_match",
|
||||
checker: "method GET",
|
||||
input: &http.Request{Method: http.MethodGet},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "method_no_match",
|
||||
checker: "method GET",
|
||||
input: &http.Request{Method: http.MethodPost},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "path_exact_match",
|
||||
checker: "path /example",
|
||||
input: &http.Request{
|
||||
URL: &url.URL{Path: "/example"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "path_wildcard_match",
|
||||
checker: "path /example/*",
|
||||
input: &http.Request{
|
||||
URL: &url.URL{Path: "/example/123"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "remote_match",
|
||||
checker: "remote 192.168.1.0/24",
|
||||
input: &http.Request{
|
||||
RemoteAddr: "192.168.1.5",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "remote_no_match",
|
||||
checker: "remote 192.168.1.0/24",
|
||||
input: &http.Request{
|
||||
RemoteAddr: "192.168.2.5",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "basic_auth_correct",
|
||||
checker: "basic_auth user " + string(E.Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost))),
|
||||
input: &http.Request{
|
||||
Header: http.Header{
|
||||
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user:password"))}, // "user:password"
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "basic_auth_incorrect",
|
||||
checker: "basic_auth user " + string(E.Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost))),
|
||||
input: &http.Request{
|
||||
Header: http.Header{
|
||||
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user:incorrect"))}, // "user:wrong"
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
tests = append(tests, genCorrectnessTestCases("header", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{k: []string{v}}}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("query", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
URL: &url.URL{
|
||||
RawQuery: fmt.Sprintf("%s=%s", k, v),
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("cookie", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{
|
||||
"Cookie": {fmt.Sprintf("%s=%s", k, v)},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("form", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Form: url.Values{
|
||||
k: []string{v},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("postform", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
PostForm: url.Values{
|
||||
k: []string{v},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
on, err := parseOn(tt.checker)
|
||||
ExpectNoError(t, err)
|
||||
got := on.Check(Cache{}, tt.input)
|
||||
if tt.want != got {
|
||||
t.Errorf("want %v, got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ func toStrTuple(args []string) (any, E.Error) {
|
||||
return &StrTuple{args[0], args[1]}, nil
|
||||
}
|
||||
|
||||
// toKVOptionalV returns *StrTuple that value is optional.
|
||||
func toKVOptionalV(args []string) (any, E.Error) {
|
||||
switch len(args) {
|
||||
case 1:
|
||||
return &StrTuple{args[0], ""}, nil
|
||||
case 2:
|
||||
return &StrTuple{args[0], args[1]}, nil
|
||||
default:
|
||||
return nil, ErrExpectKVOptionalV
|
||||
}
|
||||
}
|
||||
|
||||
// validateURL returns types.URL with the URL validated.
|
||||
func validateURL(args []string) (any, E.Error) {
|
||||
if len(args) != 1 {
|
||||
|
||||
@@ -428,7 +428,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
|
||||
src = strings.TrimSpace(src)
|
||||
isMultiline := strings.ContainsRune(src, '\n')
|
||||
// one liner is comma separated list
|
||||
if !isMultiline {
|
||||
if !isMultiline && src[0] != '-' {
|
||||
values := strutils.CommaSeperatedList(src)
|
||||
dst.Set(reflect.MakeSlice(dst.Type(), len(values), len(values)))
|
||||
errs := E.NewBuilder("invalid slice values")
|
||||
|
||||
@@ -186,6 +186,13 @@ func TestStringToSlice(t *testing.T) {
|
||||
ExpectNoError(t, err)
|
||||
ExpectDeepEqual(t, dst, []string{"a", "b", "c"})
|
||||
})
|
||||
t.Run("single-line-yaml-like", func(t *testing.T) {
|
||||
dst := make([]string, 0)
|
||||
convertible, err := ConvertString("- a", reflect.ValueOf(&dst))
|
||||
ExpectTrue(t, convertible)
|
||||
ExpectNoError(t, err)
|
||||
ExpectDeepEqual(t, dst, []string{"a"})
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkStringToSlice(b *testing.B) {
|
||||
|
||||
Reference in New Issue
Block a user