Initial import

This commit is contained in:
2025-09-26 08:49:53 +02:00
commit a76650da35
35 changed files with 4660 additions and 0 deletions

324
proxy/match/config.go Normal file
View File

@@ -0,0 +1,324 @@
package match
import (
"fmt"
"net"
"net/http"
"os"
"regexp"
"slices"
"strconv"
"strings"
"time"
"git.maze.io/maze/styx/internal/log"
"git.maze.io/maze/styx/internal/netutil"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)
type Config struct {
Path string `hcl:"path,optional"`
Refresh time.Duration `hcl:"refresh,optional"`
Domain []*Domain `hcl:"domain,block"`
Network []*Network `hcl:"network,block"`
Content []*Content `hcl:"content,block"`
}
func (config Config) Matchers() (Matchers, error) {
all := make(Matchers)
if config.Domain != nil {
all["domain"] = make(map[string]Matcher)
for _, domain := range config.Domain {
m, err := domain.Matcher()
if err != nil {
return nil, fmt.Errorf("matcher domain %q invalid: %w", domain.Name, err)
}
all["domain"][domain.Name] = m
}
}
if config.Network != nil {
all["network"] = make(map[string]Matcher)
for _, network := range config.Network {
m, err := network.Matcher(true)
if err != nil {
return nil, fmt.Errorf("matcher network %q invalid: %w", network.Name, err)
}
all["network"][network.Name] = m
}
}
return all, nil
}
type Content struct {
Name string `hcl:"name,label"`
Type string `hcl:"type"`
Body hcl.Body `hcl:",remain"`
}
type contentHeader struct {
Key string `hcl:"name"`
Value string `hcl:"value,optional"`
List []string `hcl:"list,optional"`
name string
keyRe *regexp.Regexp
valueRe *regexp.Regexp
}
func (m contentHeader) Name() string { return m.name }
func (m contentHeader) MatchesResponse(r *http.Response) bool {
for k, vv := range r.Header {
if m.keyRe.MatchString(k) {
for _, v := range vv {
if slices.Contains(m.List, v) {
return true
}
if m.valueRe != nil && m.valueRe.MatchString(v) {
return true
}
}
}
}
return false
}
type contentType struct {
List []string `hcl:"list"`
name string
}
func (m contentType) Name() string { return m.name }
func (m contentType) MatchesResponse(r *http.Response) bool {
return slices.Contains(m.List, r.Header.Get("Content-Type"))
}
type contentSizeLargerThan struct {
Size int64 `hcl:"size"`
name string
}
func (m contentSizeLargerThan) Name() string { return m.name }
func (m contentSizeLargerThan) MatchesResponse(r *http.Response) bool {
size, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
if err != nil {
return false
}
return size >= m.Size
}
type contentStatus struct {
Code []int `hcl:"code"`
name string
}
func (m contentStatus) Name() string { return m.name }
func (m contentStatus) MatchesResponse(r *http.Response) bool {
return slices.Contains(m.Code, r.StatusCode)
}
func (config Content) Matcher() (Response, error) {
switch strings.ToLower(config.Type) {
case "content", "contenttype", "content-type", "type":
var matcher = contentType{name: config.Name}
if err := gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
return matcher, nil
case "header":
var (
matcher = contentHeader{name: config.Name}
err error
)
if err = gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
if matcher.Value == "" && len(matcher.List) == 0 {
return nil, fmt.Errorf("invalid content %q: must contain either list or value", config.Name)
}
if matcher.keyRe, err = regexp.Compile(matcher.Key); err != nil {
return nil, fmt.Errorf("invalid regular expression on content %q key: %w", config.Name, err)
}
if matcher.Value != "" {
if matcher.valueRe, err = regexp.Compile(matcher.Value); err != nil {
return nil, fmt.Errorf("invalid regular expression on content %q value: %w", config.Name, err)
}
}
return matcher, nil
case "size":
var matcher = contentSizeLargerThan{name: config.Name}
if err := gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
return matcher, nil
case "status":
var matcher = contentStatus{name: config.Name}
if err := gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
return matcher, nil
default:
return nil, fmt.Errorf("unknown content matcher type %q", config.Type)
}
}
type Domain struct {
Name string `hcl:"name,label"`
Type string `hcl:"type"`
Body hcl.Body `hcl:",remain"`
}
func (config Domain) Matcher() (Request, error) {
switch config.Type {
case "list":
var matcher = domainList{Title: config.Name}
if err := gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
matcher.list = netutil.NewDomainList(matcher.List...)
return matcher, nil
case "adblock", "dnsmasq", "hosts", "detect", "domains":
var matcher = DomainFile{
Title: config.Name,
Type: config.Type,
}
if err := gohcl.DecodeBody(config.Body, nil, &matcher); err != nil {
return nil, err
}
if matcher.Path == "" && matcher.From == "" {
return nil, fmt.Errorf("matcher: domain %q must have either file or from configured", config.Name)
}
if err := matcher.Update(); err != nil {
return nil, err
}
return matcher, nil
default:
return nil, fmt.Errorf("unknown domain matcher type %q", config.Type)
}
}
type domainList struct {
Title string `json:"title"`
List []string `hcl:"list" json:"list"`
list *netutil.DomainTree
}
func (m domainList) Name() string {
return m.Title
}
func (m domainList) MatchesRequest(r *http.Request) bool {
host := netutil.Host(r.URL.Host)
log.Debug().Str("host", host).Msgf("match domain list (%d domains)", len(m.List))
return m.list.Contains(host)
}
type DomainFile struct {
Title string `json:"name"`
Type string `json:"type"`
Path string `hcl:"path,optional" json:"path,omitempty"`
From string `hcl:"from,optional" json:"from,omitempty"`
Refresh time.Duration `hcl:"refresh,optional" json:"refresh"`
}
func (m DomainFile) Name() string {
return m.Title
}
func (m DomainFile) MatchesRequest(_ *http.Request) bool {
return false
}
func (m *DomainFile) Update() (err error) {
var data []byte
if m.Path != "" {
if data, err = os.ReadFile(m.Path); err != nil {
return
}
} else {
/*
var response *http.Response
if response, err = http.DefaultClient.Get(m.From); err != nil {
return
}
defer func() { _ = response.Body.Close() }()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("match: domain %q update failed: %s", m.name, response.Status)
}
if data, err = io.ReadAll(response.Body); err != nil {
return
}
*/
}
switch m.Type {
case "hosts":
}
_ = data
return nil
}
type Network struct {
Name string `hcl:"name,label"`
Type string `hcl:"type"`
Body hcl.Body `hcl:",remain"`
}
func (config *Network) Matcher(target bool) (Matcher, error) {
switch config.Type {
case "list":
var (
matcher = networkList{Title: config.Name}
err error
)
if diag := gohcl.DecodeBody(config.Body, nil, &matcher); diag.HasErrors() {
return nil, diag
}
if matcher.tree, err = netutil.NewNetworkTree(matcher.List...); err != nil {
return nil, err
}
return &matcher, nil
default:
return nil, fmt.Errorf("unknown network matcher type %q", config.Type)
}
}
type networkList struct {
Title string `json:"name"`
List []string `hcl:"list" json:"list"`
tree *netutil.NetworkTree
target bool
}
func (m *networkList) Name() string {
return m.Title
}
func (m *networkList) MatchesIP(ip net.IP) bool {
return m.tree.Contains(ip)
}
func (m *networkList) MatchesRequest(r *http.Request) bool {
var (
host string
err error
)
if m.target {
host, _, err = net.SplitHostPort(r.URL.Host)
} else {
host, _, err = net.SplitHostPort(r.RemoteAddr)
}
if err != nil {
return false
}
ip := net.ParseIP(host)
return m.MatchesIP(ip)
}