实现 gost 网关
This commit is contained in:
215
web/globals/gost.go
Normal file
215
web/globals/gost.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package globals
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"platform/web/core"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrGostNotFound = errors.New("gost resource not found")
|
||||
|
||||
func IsGostNotFound(err error) bool {
|
||||
return errors.Is(err, ErrGostNotFound)
|
||||
}
|
||||
|
||||
type GostClient interface {
|
||||
GetChain(name string) (*GostChainConfig, error)
|
||||
CreateService(service *GostServiceConfig) error
|
||||
DeleteService(name string) error
|
||||
CreateAuther(auther *GostAutherConfig) error
|
||||
DeleteAuther(name string) error
|
||||
CreateAdmission(admission *GostAdmissionConfig) error
|
||||
DeleteAdmission(name string) error
|
||||
}
|
||||
|
||||
type gostClient struct {
|
||||
baseURL string
|
||||
pathPrefix string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient {
|
||||
baseURL := strings.TrimSpace(host)
|
||||
if !strings.Contains(baseURL, "://") {
|
||||
baseURL = fmt.Sprintf("http://%s:%d", baseURL, port)
|
||||
}
|
||||
|
||||
return &gostClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
pathPrefix: normalizeGostPathPrefix(pathPrefix),
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGost(host string, port int, pathPrefix, username, password string) GostClient {
|
||||
return GostInitializer(host, port, pathPrefix, username, password)
|
||||
}
|
||||
|
||||
type GostChainConfig struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type GostServiceConfig struct {
|
||||
Name string `json:"name"`
|
||||
Addr string `json:"addr"`
|
||||
Admission string `json:"admission,omitempty"`
|
||||
Handler GostHandlerConfig `json:"handler"`
|
||||
Listener GostListenerConfig `json:"listener"`
|
||||
}
|
||||
|
||||
type GostHandlerConfig struct {
|
||||
Type string `json:"type"`
|
||||
Chain string `json:"chain,omitempty"`
|
||||
Auther string `json:"auther,omitempty"`
|
||||
}
|
||||
|
||||
type GostListenerConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GostAutherConfig struct {
|
||||
Name string `json:"name"`
|
||||
Auths []GostAuthConfig `json:"auths"`
|
||||
}
|
||||
|
||||
type GostAuthConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type GostAdmissionConfig struct {
|
||||
Name string `json:"name"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
Matchers []string `json:"matchers"`
|
||||
}
|
||||
|
||||
func (c *gostClient) GetChain(name string) (*GostChainConfig, error) {
|
||||
body, err := c.get("/config/chains/" + url.PathEscape(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return &GostChainConfig{Name: name}, nil
|
||||
}
|
||||
|
||||
var direct GostChainConfig
|
||||
if err := json.Unmarshal(body, &direct); err == nil && direct.Name != "" {
|
||||
return &direct, nil
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
Data *GostChainConfig `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Data != nil && wrapper.Data.Name != "" {
|
||||
return wrapper.Data, nil
|
||||
}
|
||||
|
||||
return &GostChainConfig{Name: name}, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateService(service *GostServiceConfig) error {
|
||||
return c.create("/config/services", service)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteService(name string) error {
|
||||
return c.delete("/config/services/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateAuther(auther *GostAutherConfig) error {
|
||||
return c.create("/config/authers", auther)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteAuther(name string) error {
|
||||
return c.delete("/config/authers/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateAdmission(admission *GostAdmissionConfig) error {
|
||||
return c.create("/config/admissions", admission)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteAdmission(name string) error {
|
||||
return c.delete("/config/admissions/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) create(path string, payload any) error {
|
||||
_, err := c.request(http.MethodPost, path, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *gostClient) get(path string) ([]byte, error) {
|
||||
body, err := c.request(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) delete(path string) error {
|
||||
_, err := c.request(http.MethodDelete, path, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *gostClient) request(method string, path string, payload any) ([]byte, error) {
|
||||
var bodyReader io.Reader
|
||||
if payload != nil {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.endpoint(path), bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := core.Fetch(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body))
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("gost api %s %s failed: %d %s", method, path, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) endpoint(path string) string {
|
||||
return c.baseURL + c.pathPrefix + path
|
||||
}
|
||||
|
||||
func normalizeGostPathPrefix(prefix string) string {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
return strings.TrimRight(prefix, "/")
|
||||
}
|
||||
96
web/globals/gost_test.go
Normal file
96
web/globals/gost_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package globals
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGostClientCreateServiceUsesBasicAuthAndPathPrefix(t *testing.T) {
|
||||
var (
|
||||
gotPath string
|
||||
gotAuth string
|
||||
gotBody GostServiceConfig
|
||||
)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("Decode failed: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewGost(server.URL, 0, "/api", "user", "pass")
|
||||
err := client.CreateService(&GostServiceConfig{
|
||||
Name: "svc-1",
|
||||
Addr: ":10000",
|
||||
Handler: GostHandlerConfig{
|
||||
Type: "auto",
|
||||
Chain: "chain-a",
|
||||
Auther: "auther-a",
|
||||
},
|
||||
Listener: GostListenerConfig{Type: "tcp"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateService returned error: %v", err)
|
||||
}
|
||||
|
||||
if gotPath != "/api/config/services" {
|
||||
t.Fatalf("unexpected path: %s", gotPath)
|
||||
}
|
||||
if gotAuth != "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass")) {
|
||||
t.Fatalf("unexpected auth header: %s", gotAuth)
|
||||
}
|
||||
if gotBody.Name != "svc-1" || gotBody.Handler.Type != "auto" || gotBody.Handler.Chain != "chain-a" {
|
||||
t.Fatalf("unexpected request body: %+v", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGostClientDeleteServiceTreats404AsIdempotent(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewGost(server.URL, 0, "", "user", "pass")
|
||||
if err := client.DeleteService("svc-1"); !IsGostNotFound(err) {
|
||||
t.Fatalf("expected gost not found error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGostClientGetChainReadsTopLevelName(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/config/chains/chain-a" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"name":"chain-a"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewGost(server.URL, 0, "", "user", "pass")
|
||||
chain, err := client.GetChain("chain-a")
|
||||
if err != nil {
|
||||
t.Fatalf("GetChain returned error: %v", err)
|
||||
}
|
||||
if chain.Name != "chain-a" {
|
||||
t.Fatalf("unexpected chain: %+v", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGostPathPrefix(t *testing.T) {
|
||||
if got := normalizeGostPathPrefix("api/"); got != "/api" {
|
||||
t.Fatalf("unexpected prefix: %s", got)
|
||||
}
|
||||
if got := normalizeGostPathPrefix(""); got != "" {
|
||||
t.Fatalf("unexpected empty prefix: %s", got)
|
||||
}
|
||||
if !strings.HasPrefix(normalizeGostPathPrefix("/v1"), "/") {
|
||||
t.Fatal("expected normalized prefix to start with slash")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user