feat: add header decoder

This commit is contained in:
Stavros
2025-09-02 17:44:44 +03:00
parent ad4fc7ef5f
commit ca772ed24f
9 changed files with 312 additions and 41 deletions

View File

@@ -0,0 +1,122 @@
package decoders
import (
"fmt"
"sort"
"strings"
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
// Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License)
func DecodeHeaders(headers map[string]string) (config.App, error) {
var app config.App
err := decodeHeadersHelper(headers, &app, "tinyauth")
if err != nil {
return config.App{}, err
}
return app, nil
}
func decodeHeadersHelper(headers map[string]string, element any, rootName string, filters ...string) error {
node, err := decodeHeadersToNode(headers, rootName, filters...)
if err != nil {
return err
}
opts := parser.MetadataOpts{TagName: "header", AllowSliceAsStruct: true}
err = parser.AddMetadata(element, node, opts)
if err != nil {
return err
}
return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: true})
}
func decodeHeadersToNode(headers map[string]string, rootName string, filters ...string) (*parser.Node, error) {
sortedKeys := sortKeys(headers, filters)
var node *parser.Node
for i, key := range sortedKeys {
split := strings.Split(strings.ToLower(key), "-")
if split[0] != rootName {
return nil, fmt.Errorf("invalid header root %s", split[0])
}
var parts []string
for _, v := range split {
if v == "" {
return nil, fmt.Errorf("invalid element: %s", key)
}
parts = append(parts, v)
}
if i == 0 {
node = &parser.Node{}
}
decodeHeaderToNode(node, parts, headers[key])
}
return node, nil
}
func decodeHeaderToNode(root *parser.Node, path []string, value string) {
if len(root.Name) == 0 {
root.Name = path[0]
}
if len(path) > 1 {
node := containsNode(root.Children, path[1])
if node != nil {
decodeHeaderToNode(node, path[1:], value)
} else {
child := &parser.Node{Name: path[1]}
decodeHeaderToNode(child, path[1:], value)
root.Children = append(root.Children, child)
}
} else {
root.Value = value
}
}
func containsNode(nodes []*parser.Node, name string) *parser.Node {
for _, node := range nodes {
if strings.EqualFold(node.Name, name) {
return node
}
}
return nil
}
func sortKeys(headers map[string]string, filters []string) []string {
var sortedKeys []string
for key := range headers {
if len(filters) == 0 {
sortedKeys = append(sortedKeys, key)
continue
}
for _, filter := range filters {
if len(key) >= len(filter) && strings.EqualFold(key[:len(filter)], filter) {
sortedKeys = append(sortedKeys, key)
continue
}
}
}
sort.Strings(sortedKeys)
return sortedKeys
}

View File

@@ -0,0 +1,69 @@
package decoders_test
import (
"reflect"
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
)
func TestDecodeHeaders(t *testing.T) {
// Variables
expected := config.App{
Config: config.AppConfig{
Domain: "example.com",
},
Users: config.AppUsers{
Allow: "user1,user2",
Block: "user3",
},
OAuth: config.AppOAuth{
Whitelist: "somebody@example.com",
Groups: "group3",
},
IP: config.AppIP{
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
Block: []string{"10.10.10.10", "10.0.0.0/24"},
Bypass: []string{"192.168.1.1"},
},
Response: config.AppResponse{
Headers: []string{"X-Foo=Bar", "X-Baz=Qux"},
BasicAuth: config.AppBasicAuth{
Username: "admin",
Password: "password",
PasswordFile: "/path/to/passwordfile",
},
},
Path: config.AppPath{
Allow: "/public",
Block: "/private",
},
}
test := map[string]string{
"Tinyauth-Config-Domain": "example.com",
"Tinyauth-Users-Allow": "user1,user2",
"Tinyauth-Users-Block": "user3",
"Tinyauth-OAuth-Whitelist": "somebody@example.com",
"Tinyauth-OAuth-Groups": "group3",
"Tinyauth-IP-Allow": "10.71.0.1/24,10.71.0.2",
"Tinyauth-IP-Block": "10.10.10.10,10.0.0.0/24",
"Tinyauth-IP-Bypass": "192.168.1.1",
"Tinyauth-Response-Headers": "X-Foo=Bar,X-Baz=Qux",
"Tinyauth-Response-BasicAuth-Username": "admin",
"Tinyauth-Response-BasicAuth-Password": "password",
"Tinyauth-Response-BasicAuth-PasswordFile": "/path/to/passwordfile",
"Tinyauth-Path-Allow": "/public",
"Tinyauth-Path-Block": "/private",
}
// Test
result, err := decoders.DecodeHeaders(test)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v but got %v", expected, result)
}
}

View File

@@ -0,0 +1,19 @@
package decoders
import (
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
func DecodeLabels(labels map[string]string) (config.Labels, error) {
var appLabels config.Labels
err := parser.Decode(labels, &appLabels, "tinyauth")
if err != nil {
return config.Labels{}, err
}
return appLabels, nil
}

View File

@@ -0,0 +1,73 @@
package decoders_test
import (
"reflect"
"testing"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"
)
func TestDecodeLabels(t *testing.T) {
// Variables
expected := config.Labels{
Apps: map[string]config.App{
"foo": {
Config: config.AppConfig{
Domain: "example.com",
},
Users: config.AppUsers{
Allow: "user1,user2",
Block: "user3",
},
OAuth: config.AppOAuth{
Whitelist: "somebody@example.com",
Groups: "group3",
},
IP: config.AppIP{
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
Block: []string{"10.10.10.10", "10.0.0.0/24"},
Bypass: []string{"192.168.1.1"},
},
Response: config.AppResponse{
Headers: []string{"X-Foo=Bar", "X-Baz=Qux"},
BasicAuth: config.AppBasicAuth{
Username: "admin",
Password: "password",
PasswordFile: "/path/to/passwordfile",
},
},
Path: config.AppPath{
Allow: "/public",
Block: "/private",
},
},
},
}
test := map[string]string{
"tinyauth.apps.foo.config.domain": "example.com",
"tinyauth.apps.foo.users.allow": "user1,user2",
"tinyauth.apps.foo.users.block": "user3",
"tinyauth.apps.foo.oauth.whitelist": "somebody@example.com",
"tinyauth.apps.foo.oauth.groups": "group3",
"tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2",
"tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24",
"tinyauth.apps.foo.ip.bypass": "192.168.1.1",
"tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux",
"tinyauth.apps.foo.response.basicauth.username": "admin",
"tinyauth.apps.foo.response.basicauth.password": "password",
"tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile",
"tinyauth.apps.foo.path.allow": "/public",
"tinyauth.apps.foo.path.block": "/private",
}
// Test
result, err := decoders.DecodeLabels(test)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if reflect.DeepEqual(expected, result) == false {
t.Fatalf("Expected %v but got %v", expected, result)
}
}

View File

@@ -3,22 +3,8 @@ package utils
import (
"net/http"
"strings"
"tinyauth/internal/config"
"github.com/traefik/paerser/parser"
)
func GetLabels(labels map[string]string) (config.Labels, error) {
var labelsParsed config.Labels
err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps")
if err != nil {
return config.Labels{}, err
}
return labelsParsed, nil
}
func ParseHeaders(headers []string) map[string]string {
headerMap := make(map[string]string)
for _, header := range headers {