Compare commits

...

3 Commits

Author SHA1 Message Date
wpetit 1029cc8f0f feat: load zip bundle from io.ReaderAt
arcad/edge/pipeline/head This commit looks good Details
2024-05-02 09:43:22 +02:00
wpetit 0f673671b8 feat: generalize defaut user middleware 2024-05-02 09:42:56 +02:00
wpetit 6aec6da078 feat: use github.com/wlynxg/anet instead of net
See https://github.com/golang/go/issues/40569
2024-05-02 09:40:55 +02:00
9 changed files with 189 additions and 161 deletions

View File

@ -39,6 +39,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/wlynxg/anet"
_ "embed" _ "embed"
@ -251,7 +252,7 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
fetchModule.Mount(), fetchModule.Mount(),
), ),
appHTTP.WithHTTPMiddlewares( appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(key, jwa.HS256), authModuleMiddleware.DefaultUser(key, jwa.HS256, authModuleMiddleware.WithAnonymousUser()),
), ),
) )
if err := handler.Load(ctx, bundle); err != nil { if err := handler.Load(ctx, bundle); err != nil {
@ -360,13 +361,13 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
return defaultAddr, nil return defaultAddr, nil
} }
ifaces, err := net.Interfaces() ifaces, err := anet.Interfaces()
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
for _, ifa := range ifaces { for _, ifa := range ifaces {
addrs, err := ifa.Addrs() addrs, err := anet.InterfaceAddrsByInterface(&ifa)
if err != nil { if err != nil {
logger.Error( logger.Error(
ctx, "could not retrieve iface adresses", ctx, "could not retrieve iface adresses",

2
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/lestrrat-go/jwx/v2 v2.0.8 github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/ulikunitz/xz v0.5.11 github.com/ulikunitz/xz v0.5.11
github.com/wlynxg/anet v0.0.1
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
modernc.org/sqlite v1.20.4 modernc.org/sqlite v1.20.4
) )
@ -43,7 +44,6 @@ require (
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/wlynxg/anet v0.0.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/sync v0.5.0 // indirect golang.org/x/sync v0.5.0 // indirect

View File

@ -16,7 +16,7 @@ func TestBundle(t *testing.T) {
bundles := []Bundle{ bundles := []Bundle{
NewDirectoryBundle("testdata/bundle"), NewDirectoryBundle("testdata/bundle"),
NewTarBundle("testdata/bundle.tar.gz"), NewTarBundle("testdata/bundle.tar.gz"),
NewZipBundle("testdata/bundle.zip"), Must(NewZipBundleFromPath("testdata/bundle.zip")),
} }
for _, b := range bundles { for _, b := range bundles {

View File

@ -54,7 +54,7 @@ func matchArchivePattern(archivePath string) (Bundle, error) {
} }
if matches { if matches {
return NewZipBundle(archivePath), nil return NewZipBundleFromPath(archivePath)
} }
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base) matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base)

View File

@ -13,15 +13,10 @@ import (
) )
type ZipBundle struct { type ZipBundle struct {
archivePath string reader *zip.Reader
} }
func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) { func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
reader, err := b.openArchive()
if err != nil {
return nil, nil, err
}
ctx := logger.With( ctx := logger.With(
context.Background(), context.Background(),
logger.F("filename", filename), logger.F("filename", filename),
@ -29,7 +24,7 @@ func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
logger.Debug(ctx, "opening file") logger.Debug(ctx, "opening file")
f, err := reader.Open(filename) f, err := b.reader.Open(filename)
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return nil, nil, errors.WithStack(err)
} }
@ -43,21 +38,10 @@ func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
} }
func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) { func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
reader, err := b.openArchive()
if err != nil {
return nil, err
}
defer func() {
if err := reader.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
files := make([]os.FileInfo, 0) files := make([]os.FileInfo, 0)
ctx := context.Background() ctx := context.Background()
for _, f := range reader.File { for _, f := range b.reader.File {
if !strings.HasPrefix(f.Name, dirname) { if !strings.HasPrefix(f.Name, dirname) {
continue continue
} }
@ -82,17 +66,35 @@ func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
return files, nil return files, nil
} }
func (b *ZipBundle) openArchive() (*zip.ReadCloser, error) { func NewZipBundleFromReader(reader io.ReaderAt, size int64) (*ZipBundle, error) {
zr, err := zip.OpenReader(b.archivePath) zipReader, err := zip.NewReader(reader, size)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath) return nil, errors.WithStack(err)
} }
return zr, nil
}
func NewZipBundle(archivePath string) *ZipBundle {
return &ZipBundle{ return &ZipBundle{
archivePath: archivePath, reader: zipReader,
} }, nil
}
func NewZipBundleFromPath(filename string) (*ZipBundle, error) {
file, err := os.Open(filename)
if err != nil {
return nil, errors.WithStack(err)
}
stat, err := file.Stat()
if err != nil {
return nil, errors.WithStack(err)
}
return NewZipBundleFromReader(file, stat.Size())
}
func Must(bundle Bundle, err error) Bundle {
if err != nil {
panic(errors.WithStack(err))
}
return bundle
} }

View File

@ -5,101 +5,45 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"net/http" "net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
const AnonIssuer = "anon" const AnonIssuer = "anon"
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler { func WithAnonymousUser(funcs ...DefaultUserOptionFunc) DefaultUserOptionFunc {
opts := defaultAnonymousUserOptions() return func(opts *DefaultUserOptions) {
for _, fn := range funcs { opts.GetSubject = getAnonymousSubject
fn(opts) opts.GetPreferredUsername = getAnonymousPreferredUsername
} opts.Issuer = AnonIssuer
return func(next http.Handler) http.Handler { for _, fn := range funcs {
handler := func(w http.ResponseWriter, r *http.Request) { fn(opts)
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders(
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(auth.CookieName),
jwtutil.FindTokenFromCookie(auth.CookieName),
))
// If request already has a raw token, we do nothing
if rawToken != "" && err == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
uuid, err := uuid.NewUUID()
if err != nil {
logger.Error(ctx, "could not generate uuid for anonymous user", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil {
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
claims := map[string]any{
auth.ClaimSubject: subject,
auth.ClaimIssuer: AnonIssuer,
auth.ClaimPreferredUsername: preferredUsername,
auth.ClaimEdgeRole: opts.Role,
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
auth.ClaimEdgeTenant: opts.Tenant,
}
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := opts.GetCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(opts.CookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r)
} }
return http.HandlerFunc(handler)
} }
} }
func getAnonymousSubject(r *http.Request) (string, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.Wrap(err, "could not generate uuid for anonymous user")
}
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
return subject, nil
}
func getAnonymousPreferredUsername(r *http.Request) (string, error) {
preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil {
return "", errors.Wrap(err, "could not generate preferred username for anonymous user")
}
return preferredUsername, nil
}
func generateRandomPreferredUsername(size int) (string, error) { func generateRandomPreferredUsername(size int) (string, error) {
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
max := big.NewInt(int64(len(letters))) max := big.NewInt(int64(len(letters)))

View File

@ -0,0 +1,94 @@
package middleware
import (
"net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func DefaultUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...DefaultUserOptionFunc) func(next http.Handler) http.Handler {
opts := defaultUserOptions()
for _, fn := range funcs {
fn(opts)
}
return func(next http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders(
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(auth.CookieName),
jwtutil.FindTokenFromCookie(auth.CookieName),
))
// If request already has a raw token, we do nothing
if rawToken != "" && err == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
subject, err := opts.GetSubject(r)
if err != nil {
logger.Error(ctx, "could not retrieve user subject", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
preferredUsername, err := opts.GetPreferredUsername(r)
if err != nil {
logger.Error(ctx, "could not retrieve user preferred username", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
claims := map[string]any{
auth.ClaimSubject: subject,
auth.ClaimIssuer: opts.Issuer,
auth.ClaimPreferredUsername: preferredUsername,
auth.ClaimEdgeRole: opts.Role,
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
auth.ClaimEdgeTenant: opts.Tenant,
}
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := opts.GetCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(opts.CookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(handler)
}
}

View File

@ -11,47 +11,52 @@ func defaultGetCookieDomain(r *http.Request) (string, error) {
return "", nil return "", nil
} }
type AnonymousUserOptions struct { type DefaultUserOptions struct {
GetCookieDomain GetCookieDomainFunc GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration CookieDuration time.Duration
Tenant string Tenant string
Entrypoint string Entrypoint string
Role string Role string
Issuer string
GetPreferredUsername func(r *http.Request) (string, error)
GetSubject func(r *http.Request) (string, error)
} }
type AnonymousUserOptionFunc func(*AnonymousUserOptions) type DefaultUserOptionFunc func(opts *DefaultUserOptions)
func defaultAnonymousUserOptions() *AnonymousUserOptions { func defaultUserOptions() *DefaultUserOptions {
return &AnonymousUserOptions{ return &DefaultUserOptions{
GetCookieDomain: defaultGetCookieDomain, GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour, CookieDuration: 24 * time.Hour,
Tenant: "", Tenant: "",
Entrypoint: "", Entrypoint: "",
Role: "", Role: "",
GetSubject: getAnonymousSubject,
GetPreferredUsername: getAnonymousPreferredUsername,
} }
} }
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc { func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.GetCookieDomain = getCookieDomain opts.GetCookieDomain = getCookieDomain
opts.CookieDuration = duration opts.CookieDuration = duration
} }
} }
func WithTenant(tenant string) AnonymousUserOptionFunc { func WithTenant(tenant string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Tenant = tenant opts.Tenant = tenant
} }
} }
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc { func WithEntrypoint(entrypoint string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Entrypoint = entrypoint opts.Entrypoint = entrypoint
} }
} }
func WithRole(role string) AnonymousUserOptionFunc { func WithRole(role string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Role = role opts.Role = role
} }
} }

View File

@ -3,8 +3,6 @@ package chromecast
import ( import (
"context" "context"
"net" "net"
"regexp"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -15,6 +13,7 @@ import (
"github.com/barnybug/go-cast/log" "github.com/barnybug/go-cast/log"
"github.com/hashicorp/mdns" "github.com/hashicorp/mdns"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/wlynxg/anet"
) )
const ( const (
@ -151,28 +150,11 @@ func (d *Discovery) listener(ctx context.Context) {
case d.found <- client: case d.found <- client:
case <-time.After(time.Second): case <-time.After(time.Second):
case <-ctx.Done(): case <-ctx.Done():
break return
} }
} }
} }
func decodeDnsEntry(text string) string {
text = strings.Replace(text, `\.`, ".", -1)
text = strings.Replace(text, `\ `, " ", -1)
re := regexp.MustCompile(`([\\][0-9][0-9][0-9])`)
text = re.ReplaceAllStringFunc(text, func(source string) string {
i, err := strconv.Atoi(source[1:])
if err != nil {
return ""
}
return string([]byte{byte(i)})
})
return text
}
func decodeTxtRecord(txt string) map[string]string { func decodeTxtRecord(txt string) map[string]string {
m := make(map[string]string) m := make(map[string]string)
@ -196,7 +178,7 @@ func isIPv6(ip net.IP) bool {
} }
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) { func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
ifaces, err := net.Interfaces() ifaces, err := anet.Interfaces()
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
@ -223,7 +205,7 @@ func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
} }
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) { func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
adresses, err := iface.Addrs() adresses, err := anet.InterfaceAddrsByInterface(&iface)
if err != nil { if err != nil {
return false, false, errors.WithStack(err) return false, false, errors.WithStack(err)
} }