Initial import
This commit is contained in:
324
proxy/match/config.go
Normal file
324
proxy/match/config.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user