mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
refactor(repo): consolidate openflare-server to root and move subprojects to internal/apps
- Merge all files inside openflare-server to the repository root directory. - Relocate agent, relay, and flared subprojects from internal/ to internal/apps/. - Combine docker-compose files and update build context paths to root. - Update GitHub workflows and Dockerfiles to refer to new directories and package names. - Rewrite Go package imports across all files. - Resolve database renew test race condition and clean up docs.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy_route
|
||||
|
||||
const (
|
||||
errProxyRouteNotFound = "proxy route not found"
|
||||
errProxyRouteIdentityExists = "proxy route identity already exists"
|
||||
errProxyRouteSiteNameExists = "site_name already exists"
|
||||
errProxyRouteDomainExists = "domain %s already exists"
|
||||
errProxyRouteSiteNameEmpty = "site_name cannot be empty"
|
||||
errProxyRouteDomainRequired = "at least one domain is required"
|
||||
errProxyRouteDomainInvalid = "domain format is invalid"
|
||||
errProxyRouteDomainMismatch = "domain must match domains[0]"
|
||||
errProxyRouteOriginEmpty = "origin_url cannot be empty"
|
||||
errProxyRouteOriginInvalid = "origin URL format is invalid"
|
||||
errProxyRouteOriginScheme = "origin URL must start with http:// or https://"
|
||||
errProxyRouteOriginHostInvalid = "origin_host format is invalid"
|
||||
errProxyRouteUpstreamRequired = "at least one upstream is required"
|
||||
errProxyRouteUpstreamScheme = "all upstreams must use the same scheme"
|
||||
errProxyRouteUpstreamPath = "multi-upstream mode does not support origin paths"
|
||||
errProxyRouteUpstreamQuery = "multi-upstream mode does not support origin query strings"
|
||||
errProxyRouteOriginNotFound = "selected origin does not exist"
|
||||
errProxyRouteCertNotFound = "selected certificate does not exist"
|
||||
errProxyRouteCertRequired = "must select a certificate when HTTPS is enabled"
|
||||
errProxyRouteCertDomainLength = "domain_cert_ids must match domains length"
|
||||
errProxyRouteRedirectHTTP = "redirect_http requires enable_https"
|
||||
errProxyRouteBasicAuth = "basic_auth_username and basic_auth_password cannot be empty when basic auth is enabled"
|
||||
errProxyRouteLimitRate = "limit_rate must be a number or use the 512k / 1m format"
|
||||
errProxyRouteCachePolicy = "cache policy is not supported"
|
||||
errProxyRouteCacheSuffix = "cache suffix format is invalid"
|
||||
errProxyRouteCachePath = "cache path rule format is invalid"
|
||||
errProxyRouteCacheSuffixReq = "at least one suffix is required"
|
||||
errProxyRouteCachePrefixReq = "at least one path prefix is required"
|
||||
errProxyRouteCacheExactReq = "at least one exact path is required"
|
||||
errProxyRouteHeaderKeyEmpty = "custom header key cannot be empty"
|
||||
errProxyRouteHeaderKeyInvalid = "custom header key format is invalid"
|
||||
errProxyRouteHeaderNewline = "custom headers cannot contain newlines"
|
||||
errProxyRouteTunnelNodeReq = "tunnel_node_id is required for tunnel upstream"
|
||||
errProxyRouteTunnelNodeMissing = "tunnel client node does not exist"
|
||||
errProxyRouteTunnelNodeType = "tunnel_node_id must reference a tunnel_client node"
|
||||
errProxyRouteTunnelAddrReq = "tunnel_target_addr is required for tunnel upstream"
|
||||
errProxyRouteTunnelProtocol = "tunnel_target_protocol must be http or https"
|
||||
errProxyRoutePagesProjectReq = "pages_project_id is required for Pages upstream"
|
||||
errProxyRoutePagesNotFound = "Pages 项目不存在"
|
||||
errProxyRoutePagesDisabled = "Pages 项目未启用"
|
||||
errProxyRoutePagesNoDeploy = "Pages 项目没有激活部署"
|
||||
errProxyRouteOriginSchemeOnly = "源站协议仅支持 http 或 https"
|
||||
errProxyRouteOriginPort = "端口格式不合法"
|
||||
errProxyRouteOriginPortEmpty = "端口不能为空"
|
||||
errProxyRouteOriginURI = "源站路径需以 / 或 ? 开头"
|
||||
errProxyRouteOriginURIProto = "源站路径不能包含协议"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,430 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy_route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
// CustomHeaderInput 自定义响应头。
|
||||
type CustomHeaderInput struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Input 代理规则创建/更新请求。
|
||||
type Input struct {
|
||||
SiteName string `json:"site_name"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
OriginID *uint `json:"origin_id"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginScheme string `json:"origin_scheme"`
|
||||
OriginAddress string `json:"origin_address"`
|
||||
OriginPort string `json:"origin_port"`
|
||||
OriginURI string `json:"origin_uri"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
CertIDs []uint `json:"cert_ids"`
|
||||
DomainCertIDs []uint `json:"domain_cert_ids"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
LimitRate string `json:"limit_rate"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy"`
|
||||
CacheRules []string `json:"cache_rules"`
|
||||
CustomHeaders []CustomHeaderInput `json:"custom_headers"`
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled"`
|
||||
BasicAuthUsername string `json:"basic_auth_username"`
|
||||
BasicAuthPassword string `json:"basic_auth_password"`
|
||||
Remark string `json:"remark"`
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id"`
|
||||
TunnelID *uint `json:"tunnel_id"`
|
||||
TunnelTargetAddr string `json:"tunnel_target_addr"`
|
||||
TunnelTargetProtocol string `json:"tunnel_target_protocol"`
|
||||
PagesProjectID *uint `json:"pages_project_id"`
|
||||
}
|
||||
|
||||
// View 代理规则视图。
|
||||
type View struct {
|
||||
ID uint `json:"id"`
|
||||
SiteName string `json:"site_name"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
PrimaryDomain string `json:"primary_domain"`
|
||||
DomainCount int `json:"domain_count"`
|
||||
OriginID *uint `json:"origin_id"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Upstreams string `json:"upstreams"`
|
||||
UpstreamList []string `json:"upstream_list"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
CertIDs []uint `json:"cert_ids"`
|
||||
DomainCertIDs []uint `json:"domain_cert_ids"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
LimitRate string `json:"limit_rate"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy"`
|
||||
CacheRules string `json:"cache_rules"`
|
||||
CacheRuleList []string `json:"cache_rule_list"`
|
||||
CustomHeaders string `json:"custom_headers"`
|
||||
CustomHeaderList []CustomHeaderInput `json:"custom_header_list"`
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled"`
|
||||
BasicAuthUsername string `json:"basic_auth_username"`
|
||||
BasicAuthPassword string `json:"basic_auth_password"`
|
||||
Remark string `json:"remark"`
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id"`
|
||||
TunnelID *uint `json:"tunnel_id"`
|
||||
TunnelTargetAddr string `json:"tunnel_target_addr"`
|
||||
TunnelTargetProtocol string `json:"tunnel_target_protocol"`
|
||||
PagesProjectID *uint `json:"pages_project_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListProxyRoutes 列出全部代理规则。
|
||||
func ListProxyRoutes(ctx context.Context) ([]*View, error) {
|
||||
routes, err := model.ListProxyRoutes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteViews(ctx, routes)
|
||||
}
|
||||
|
||||
// GetProxyRoute 获取代理规则详情。
|
||||
func GetProxyRoute(ctx context.Context, id uint) (*View, error) {
|
||||
route, err := model.GetProxyRouteByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteView(ctx, route)
|
||||
}
|
||||
|
||||
// CreateProxyRoute 创建代理规则。
|
||||
func CreateProxyRoute(ctx context.Context, input Input) (*View, error) {
|
||||
route, err := buildProxyRoute(ctx, nil, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = model.CreateProxyRouteRecord(ctx, route); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New(errProxyRouteIdentityExists)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteView(ctx, route)
|
||||
}
|
||||
|
||||
// UpdateProxyRoute 更新代理规则。
|
||||
func UpdateProxyRoute(ctx context.Context, id uint, input Input) (*View, error) {
|
||||
route, err := model.GetProxyRouteByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route, err = buildProxyRoute(ctx, route, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = model.UpdateProxyRouteRecord(ctx, route); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New(errProxyRouteIdentityExists)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteView(ctx, route)
|
||||
}
|
||||
|
||||
// DeleteProxyRoute 删除代理规则。
|
||||
func DeleteProxyRoute(ctx context.Context, id uint) error {
|
||||
if _, err := model.GetProxyRouteByID(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return model.DeleteProxyRouteRecord(ctx, id)
|
||||
}
|
||||
|
||||
func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) (*model.ProxyRoute, error) {
|
||||
domains, err := normalizeProxyRouteDomainsInput(route, input.Domain, input.Domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domain := domains[0]
|
||||
siteName := normalizeProxyRouteSiteNameInput(route, input.SiteName, domain)
|
||||
|
||||
upstreamType := normalizeUpstreamType(input.UpstreamType)
|
||||
var originURL string
|
||||
var originID *uint
|
||||
var upstreams []string
|
||||
|
||||
if upstreamType == "tunnel" {
|
||||
originURL = "http://127.0.0.1"
|
||||
upstreams = []string{originURL}
|
||||
} else if upstreamType == "pages" {
|
||||
if err := validatePagesRouteInput(ctx, input.PagesProjectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originURL = "http://127.0.0.1"
|
||||
upstreams = []string{originURL}
|
||||
} else {
|
||||
originURL, originID, err = resolveProxyRoutePrimaryOrigin(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreams, err = normalizeUpstreams(originURL, input.Upstreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
originHost := strings.TrimSpace(input.OriginHost)
|
||||
remark := strings.TrimSpace(input.Remark)
|
||||
cachePolicy := strings.TrimSpace(input.CachePolicy)
|
||||
cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customHeaders, err := normalizeCustomHeaders(input.CustomHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitConnPerServer, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerServer, "limit_conn_per_server")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitConnPerIP, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerIP, "limit_conn_per_ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitRate, err := normalizeProxyRouteLimitRate(input.LimitRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheRulesJSON, err := json.Marshal(cacheRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreamsJSON, err := json.Marshal(upstreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customHeadersJSON, err := json.Marshal(customHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !input.EnableHTTPS {
|
||||
input.RedirectHTTP = false
|
||||
input.CertID = nil
|
||||
input.CertIDs = nil
|
||||
input.DomainCertIDs = nil
|
||||
}
|
||||
domainCertIDs, certIDs, primaryCertID, err := normalizeProxyRouteDomainCertificateIDs(
|
||||
ctx,
|
||||
domains,
|
||||
input.EnableHTTPS,
|
||||
input.DomainCertIDs,
|
||||
input.CertID,
|
||||
input.CertIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateProxyRouteDomainCertificateCoverage(ctx, domains, domainCertIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certIDsJSON, err := json.Marshal(certIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainCertIDsJSON, err := json.Marshal(domainCertIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainsJSON, err := json.Marshal(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateProxyRouteSiteName(siteName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateProxyRouteIdentityUniqueness(ctx, route, siteName, domains); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateOriginHost(originHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.DomainCertIDs = domainCertIDs
|
||||
input.CertIDs = certIDs
|
||||
input.CertID = primaryCertID
|
||||
if input.RedirectHTTP && !input.EnableHTTPS {
|
||||
return nil, errors.New(errProxyRouteRedirectHTTP)
|
||||
}
|
||||
|
||||
if input.BasicAuthEnabled {
|
||||
input.BasicAuthUsername = strings.TrimSpace(input.BasicAuthUsername)
|
||||
input.BasicAuthPassword = strings.TrimSpace(input.BasicAuthPassword)
|
||||
if input.BasicAuthUsername == "" || input.BasicAuthPassword == "" {
|
||||
return nil, errors.New(errProxyRouteBasicAuth)
|
||||
}
|
||||
} else {
|
||||
input.BasicAuthUsername = ""
|
||||
input.BasicAuthPassword = ""
|
||||
}
|
||||
|
||||
if route == nil {
|
||||
route = &model.ProxyRoute{}
|
||||
}
|
||||
route.SiteName = siteName
|
||||
route.Domain = domain
|
||||
route.Domains = string(domainsJSON)
|
||||
route.OriginID = originID
|
||||
route.OriginURL = upstreams[0]
|
||||
route.OriginHost = originHost
|
||||
route.Upstreams = string(upstreamsJSON)
|
||||
route.Enabled = input.Enabled
|
||||
route.EnableHTTPS = input.EnableHTTPS
|
||||
route.CertID = input.CertID
|
||||
route.CertIDs = string(certIDsJSON)
|
||||
route.DomainCertIDs = string(domainCertIDsJSON)
|
||||
route.RedirectHTTP = input.RedirectHTTP
|
||||
route.LimitConnPerServer = limitConnPerServer
|
||||
route.LimitConnPerIP = limitConnPerIP
|
||||
route.LimitRate = limitRate
|
||||
route.CacheEnabled = input.CacheEnabled
|
||||
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
|
||||
route.CacheRules = string(cacheRulesJSON)
|
||||
route.CustomHeaders = string(customHeadersJSON)
|
||||
route.BasicAuthEnabled = input.BasicAuthEnabled
|
||||
route.BasicAuthUsername = input.BasicAuthUsername
|
||||
route.BasicAuthPassword = input.BasicAuthPassword
|
||||
route.Remark = remark
|
||||
route.UpstreamType = upstreamType
|
||||
if upstreamType == "tunnel" {
|
||||
tunnelNodeID, err := normalizeTunnelNodeID(input.TunnelNodeID, input.TunnelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateTunnelRouteInput(ctx, tunnelNodeID, input.TunnelTargetAddr, input.TunnelTargetProtocol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route.TunnelNodeID = tunnelNodeID
|
||||
route.TunnelTargetAddr = strings.TrimSpace(input.TunnelTargetAddr)
|
||||
route.TunnelTargetProtocol = normalizeTunnelTargetProtocol(input.TunnelTargetProtocol)
|
||||
route.PagesProjectID = nil
|
||||
} else if upstreamType == "pages" {
|
||||
route.TunnelNodeID = nil
|
||||
route.TunnelTargetAddr = ""
|
||||
route.TunnelTargetProtocol = ""
|
||||
route.PagesProjectID = input.PagesProjectID
|
||||
} else {
|
||||
route.TunnelNodeID = nil
|
||||
route.TunnelTargetAddr = ""
|
||||
route.TunnelTargetProtocol = ""
|
||||
route.PagesProjectID = nil
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func buildProxyRouteViews(ctx context.Context, routes []*model.ProxyRoute) ([]*View, error) {
|
||||
views := make([]*View, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
view, err := buildProxyRouteView(ctx, route)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views = append(views, view)
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, error) {
|
||||
if route == nil {
|
||||
return nil, errors.New("proxy route is nil")
|
||||
}
|
||||
domains, err := decodeStoredDomains(route.Domains, route.Domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customHeaders, err := decodeStoredCustomHeaders(route.CustomHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certIDs, err := decodeStoredCertIDs(route.CertIDs, route.CertID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainCertIDs, err := resolveProxyRouteDomainCertIDs(ctx, route, domains, certIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var certID *uint
|
||||
if len(certIDs) > 0 {
|
||||
certID = &certIDs[0]
|
||||
}
|
||||
primaryDomain := domains[0]
|
||||
return &View{
|
||||
ID: route.ID,
|
||||
SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, primaryDomain),
|
||||
Domain: primaryDomain,
|
||||
Domains: domains,
|
||||
PrimaryDomain: primaryDomain,
|
||||
DomainCount: len(domains),
|
||||
OriginID: route.OriginID,
|
||||
OriginURL: route.OriginURL,
|
||||
OriginHost: route.OriginHost,
|
||||
Upstreams: route.Upstreams,
|
||||
UpstreamList: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
CertID: certID,
|
||||
CertIDs: certIDs,
|
||||
DomainCertIDs: domainCertIDs,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
LimitConnPerIP: route.LimitConnPerIP,
|
||||
LimitRate: route.LimitRate,
|
||||
CacheEnabled: route.CacheEnabled,
|
||||
CachePolicy: route.CachePolicy,
|
||||
CacheRules: route.CacheRules,
|
||||
CacheRuleList: cacheRules,
|
||||
CustomHeaders: route.CustomHeaders,
|
||||
CustomHeaderList: customHeaders,
|
||||
BasicAuthEnabled: route.BasicAuthEnabled,
|
||||
BasicAuthUsername: route.BasicAuthUsername,
|
||||
BasicAuthPassword: route.BasicAuthPassword,
|
||||
Remark: route.Remark,
|
||||
UpstreamType: route.UpstreamType,
|
||||
TunnelNodeID: route.TunnelNodeID,
|
||||
TunnelID: route.TunnelNodeID,
|
||||
TunnelTargetAddr: route.TunnelTargetAddr,
|
||||
TunnelTargetProtocol: route.TunnelTargetProtocol,
|
||||
PagesProjectID: route.PagesProjectID,
|
||||
CreatedAt: route.CreatedAt,
|
||||
UpdatedAt: route.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy_route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupProxyRouteTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.Origin{}))
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
return func() {
|
||||
db.SetDB(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyRoute(t *testing.T) {
|
||||
cleanup := setupProxyRouteTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
view, err := CreateProxyRoute(ctx, Input{
|
||||
SiteName: "example-site",
|
||||
Domain: "example.com",
|
||||
OriginURL: "http://origin.example.com:8080",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, view.ID)
|
||||
assert.Equal(t, "example-site", view.SiteName)
|
||||
assert.Equal(t, "example.com", view.Domain)
|
||||
assert.Equal(t, []string{"example.com"}, view.Domains)
|
||||
assert.Equal(t, "http://origin.example.com:8080", view.OriginURL)
|
||||
assert.Equal(t, []string{"http://origin.example.com:8080"}, view.UpstreamList)
|
||||
assert.True(t, view.Enabled)
|
||||
|
||||
_, err = CreateProxyRoute(ctx, Input{
|
||||
SiteName: "duplicate-site",
|
||||
Domain: "example.com",
|
||||
OriginURL: "http://origin.example.com:8080",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already exists")
|
||||
}
|
||||
|
||||
func TestListProxyRoutes(t *testing.T) {
|
||||
cleanup := setupProxyRouteTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
first, err := CreateProxyRoute(ctx, Input{
|
||||
SiteName: "first-site",
|
||||
Domain: "first.example.com",
|
||||
OriginURL: "http://origin-a.internal:80",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
second, err := CreateProxyRoute(ctx, Input{
|
||||
SiteName: "second-site",
|
||||
Domain: "second.example.com",
|
||||
OriginURL: "http://origin-b.internal:80",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
routes, err := ListProxyRoutes(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, routes, 2)
|
||||
assert.Equal(t, second.ID, routes[0].ID)
|
||||
assert.Equal(t, first.ID, routes[1].ID)
|
||||
assert.Equal(t, "second.example.com", routes[0].Domain)
|
||||
assert.Equal(t, "first.example.com", routes[1].Domain)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy_route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, errProxyRouteNotFound)
|
||||
}
|
||||
|
||||
// GetProxyRoutes 列出全部代理规则。
|
||||
// @Summary 获取代理规则列表
|
||||
// @Description 返回所有代理规则配置,需要管理员权限
|
||||
// @Tags openflare-proxy-route
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]proxy_route.View} "代理规则列表"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Router /api/v1/d/proxy-routes [get]
|
||||
func GetProxyRoutes(c *gin.Context) {
|
||||
routes, err := ListProxyRoutes(c.Request.Context())
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(routes))
|
||||
}
|
||||
|
||||
// GetProxyRouteHandler 获取代理规则详情。
|
||||
// @Summary 获取代理规则详情
|
||||
// @Description 返回指定代理规则的完整配置,需要管理员权限
|
||||
// @Tags openflare-proxy-route
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "代理规则 ID"
|
||||
// @Success 200 {object} response.Any{data=proxy_route.View} "代理规则详情"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或规则不存在"
|
||||
// @Router /api/v1/d/proxy-routes/{id} [get]
|
||||
func GetProxyRouteHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
route, err := GetProxyRoute(c.Request.Context(), id)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(route))
|
||||
}
|
||||
|
||||
// CreateProxyRouteHandler 创建代理规则。
|
||||
// @Summary 创建代理规则
|
||||
// @Description 创建新的反向代理规则,需要管理员权限
|
||||
// @Tags openflare-proxy-route
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param body body proxy_route.Input true "代理规则参数"
|
||||
// @Success 200 {object} response.Any{data=proxy_route.View} "创建成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Router /api/v1/d/proxy-routes [post]
|
||||
func CreateProxyRouteHandler(c *gin.Context) {
|
||||
var input Input
|
||||
if !apiutil.BindJSON(c, &input) {
|
||||
return
|
||||
}
|
||||
route, err := CreateProxyRoute(c.Request.Context(), input)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(route))
|
||||
}
|
||||
|
||||
// UpdateProxyRouteHandler 更新代理规则。
|
||||
// @Summary 更新代理规则
|
||||
// @Description 更新指定代理规则的配置,需要管理员权限
|
||||
// @Tags openflare-proxy-route
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "代理规则 ID"
|
||||
// @Param body body proxy_route.Input true "代理规则参数"
|
||||
// @Success 200 {object} response.Any{data=proxy_route.View} "更新成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或规则不存在"
|
||||
// @Router /api/v1/d/proxy-routes/{id}/update [post]
|
||||
func UpdateProxyRouteHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input Input
|
||||
if !apiutil.BindJSON(c, &input) {
|
||||
return
|
||||
}
|
||||
route, err := UpdateProxyRoute(c.Request.Context(), id, input)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(route))
|
||||
}
|
||||
|
||||
// DeleteProxyRouteHandler 删除代理规则。
|
||||
// @Summary 删除代理规则
|
||||
// @Description 删除指定代理规则,需要管理员权限
|
||||
// @Tags openflare-proxy-route
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "代理规则 ID"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或规则不存在"
|
||||
// @Router /api/v1/d/proxy-routes/{id}/delete [post]
|
||||
func DeleteProxyRouteHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := DeleteProxyRoute(c.Request.Context(), id); handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
Reference in New Issue
Block a user