diff --git a/.gitignore b/.gitignore index c0a9cb6..79c36ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ logzio.yml tmp/ /build /ev +acct.yml diff --git a/app/webfinger/addr.go b/app/webfinger/addr.go new file mode 100644 index 0000000..035c031 --- /dev/null +++ b/app/webfinger/addr.go @@ -0,0 +1,35 @@ +package webfinger + +import ( + "net/url" + "strings" +) + +type Addr struct { + prefix []string + URL *url.URL +} + +func Parse(s string) *Addr { + addr := &Addr{} + + addr.URL, _ = url.Parse(s) + + if addr.URL.Opaque == "" { + return addr + } + + var hasPfx = true + pfx := addr.URL.Scheme + + for hasPfx { + addr.prefix = append(addr.prefix, pfx) + pfx, addr.URL.Opaque, hasPfx = strings.Cut(addr.URL.Opaque, ":") + } + + user, host, _ := strings.Cut(pfx, "@") + addr.URL.User = url.User(user) + addr.URL.Host = host + + return addr +} diff --git a/app/webfinger/jrd.go b/app/webfinger/jrd.go index b991728..d2982bd 100644 --- a/app/webfinger/jrd.go +++ b/app/webfinger/jrd.go @@ -1,6 +1,7 @@ package webfinger import ( + "bytes" "encoding/base64" "encoding/json" "fmt" @@ -8,7 +9,9 @@ import ( "sort" "github.com/sour-is/ev/pkg/es/event" + "github.com/sour-is/ev/pkg/set" "github.com/sour-is/ev/pkg/slice" + "gopkg.in/yaml.v3" ) func StreamID(subject string) string { @@ -20,13 +23,13 @@ func StreamID(subject string) string { // JRD is a JSON Resource Descriptor, specifying properties and related links // for a resource. type JRD struct { - Subject string `json:"subject,omitempty"` - Aliases []string `json:"aliases,omitempty"` - Properties map[string]*string `json:"properties,omitempty"` - Links Links `json:"links,omitempty"` + Subject string `json:"subject,omitempty" yaml:"subject,omitempty"` + Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"` + Properties map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"` + Links Links `json:"links,omitempty" yaml:"links,omitempty"` - deleted bool - event.AggregateRoot + deleted bool + event.AggregateRoot `yaml:"-"` } var _ event.Aggregate = (*JRD)(nil) @@ -86,6 +89,19 @@ func (jrd *JRD) GetLinkByRel(rel string) *Link { return nil } +// GetLinksByRel returns the first *Link with the specified rel value. +func (jrd *JRD) GetLinksByRel(rel ...string) []*Link { + var lis []*Link + rels := set.New(rel...) + + for _, link := range jrd.Links { + if rels.Has(link.Rel) { + lis = append(lis, link) + } + } + return lis +} + // GetProperty Returns the property value as a string. // Per spec a property value can be null, empty string is returned in this case. func (jrd *JRD) GetProperty(uri string) string { @@ -100,6 +116,12 @@ func (a *JRD) SetProperty(name string, value *string) { } a.Properties[name] = value } +func (a *JRD) DeleteProperty(name string) { + if a.Properties == nil { + return + } + delete(a.Properties, name) +} func (a *JRD) IsDeleted() bool { return a.deleted } @@ -118,6 +140,12 @@ func (link *Link) SetProperty(name string, value *string) { } link.Properties[name] = value } +func (link *Link) DeleteProperty(name string) { + if link.Properties == nil { + return + } + delete(link.Properties, name) +} // ApplyEvent implements event.Aggregate func (a *JRD) ApplyEvent(events ...event.Event) { @@ -133,6 +161,7 @@ func (a *JRD) ApplyEvent(events ...event.Event) { case *SubjectDeleted: a.deleted = true + a.Subject = e.Subject a.Aliases = a.Aliases[:0] a.Links = a.Links[:0] a.Properties = map[string]*string{} @@ -156,41 +185,43 @@ func (a *JRD) ApplyEvent(events ...event.Event) { } } -const NSpubkey = "https://sour.is/ns/pub" +const NSauth = "https://sour.is/ns/auth" +const NSpubkey = "https://sour.is/ns/pubkey" +const NSredirect = "https://sour.is/rel/redirect" -func (a *JRD) OnDelete(pubkey string, jrd *JRD) error { - if a.Version() == 0 || a.IsDeleted() { - return nil - } +func (a *JRD) OnAuth(claim, auth *JRD) error { + pubkey := claim.Properties[NSpubkey] - if v, ok := a.Properties[NSpubkey]; ok && v != nil && *v == pubkey { + if v, ok := auth.Properties[NSpubkey]; ok && v != nil && cmpPtr(v, pubkey) { // pubkey matches! } else { return fmt.Errorf("pubkey does not match") } - if a.Subject != jrd.Subject { + if a.Version() > 0 && !a.IsDeleted() && a.Subject != claim.Subject { return fmt.Errorf("subject does not match") } + if auth.Subject == claim.Subject { + claim.SetProperty(NSpubkey, pubkey) + } else { + claim.SetProperty(NSauth, &auth.Subject) + claim.DeleteProperty(NSpubkey) + } + + return nil +} + +func (a *JRD) OnDelete(jrd *JRD) error { + if a.Version() == 0 || a.IsDeleted() { + return nil + } + event.Raise(a, &SubjectDeleted{Subject: jrd.Subject}) return nil } -func (a *JRD) OnClaims(pubkey string, jrd *JRD) error { - if a.Version() > 0 && !a.IsDeleted() { - if v, ok := a.Properties[NSpubkey]; ok && v != nil && *v == pubkey { - // pubkey matches! - } else { - return fmt.Errorf("pubkey does not match") - } - - if a.Subject != jrd.Subject { - return fmt.Errorf("subject does not match") - } - } - - jrd.SetProperty(NSpubkey, &pubkey) +func (a *JRD) OnClaims(jrd *JRD) error { err := a.OnSubjectSet(jrd.Subject, jrd.Aliases, jrd.Properties) if err != nil { @@ -343,3 +374,11 @@ func cmpPtr[T comparable](l, r *T) bool { return *l == *r } + +func (a *JRD) String() string { + b := &bytes.Buffer{} + y := yaml.NewEncoder(b) + _ = y.Encode(a) + + return b.String() +} diff --git a/app/webfinger/jrd_test.go b/app/webfinger/jrd_test.go index 70e6787..108cb97 100644 --- a/app/webfinger/jrd_test.go +++ b/app/webfinger/jrd_test.go @@ -100,9 +100,6 @@ func TestEncodeJRD(t *testing.T) { } } -// { "properties":{"https://sour.is/ns/pubkey":"kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"},"links":[{"rel":"salty:public","type":"application/json+salty","href":"https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5","properties":{"pub":"kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"}}]} -//!= {"subject":"acct:me@sour.is","properties":{"https://sour.is/ns/pubkey":"kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"},"links":[{"rel":"salty:public","type":"application/json+salty","href":"https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5","properties":{"pub":"kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"}}]} - func TestApplyEvents(t *testing.T) { is := is.New(t) @@ -158,7 +155,7 @@ func TestApplyEvents(t *testing.T) { } t.Log(string(s)) - if string(s) != `{"subject":"acct:me@sour.is"}` { + if string(s) != `{}` { t.Fatal("output does not match") } } @@ -177,6 +174,7 @@ func TestCommands(t *testing.T) { "aliases": []string{"acct:xuu@sour.is"}, "properties": map[string]*string{ "https://example.com/ns/asdf": nil, + webfinger.NSpubkey: ptr(enc(pub)), }, "links": []map[string]any{{ "rel": "salty:public", @@ -214,6 +212,8 @@ func TestCommands(t *testing.T) { c.JRD.Subject = c.Subject c.StandardClaims.Subject = c.Subject + c.SetProperty(webfinger.NSpubkey, &c.PubKey) + pub, err := dec(c.PubKey) return ed25519.PublicKey(pub), err }, @@ -227,8 +227,49 @@ func TestCommands(t *testing.T) { t.Logf("%#v", c) a, err := ev.Upsert(ctx, es, webfinger.StreamID(c.Subject), func(ctx context.Context, a *webfinger.JRD) error { - a.OnClaims(c.PubKey, c.JRD) - return nil + var auth *webfinger.JRD + + // does the target have a pubkey for self auth? + if _, ok := a.Properties[webfinger.NSpubkey]; ok { + auth = a + } + + // Check current version for auth. + if authID, ok := a.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil { + auth = &webfinger.JRD{} + auth.SetStreamID(webfinger.StreamID(*authID)) + err := es.Load(ctx, auth) + if err != nil { + return err + } + } + if a.Version() == 0 || a.IsDeleted() { + // else does the new object claim auth from another object? + if authID, ok := c.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil { + auth = &webfinger.JRD{} + auth.SetStreamID(webfinger.StreamID(*authID)) + err := es.Load(ctx, auth) + if err != nil { + return err + } + } + + // fall back to use auth from submitted claims + if auth == nil { + auth = c.JRD + } + } + + if auth == nil { + return fmt.Errorf("auth not found") + } + + err = a.OnAuth(c.JRD, auth) + if err != nil { + return err + } + + return a.OnClaims(c.JRD) }) is.NoErr(err) diff --git a/app/webfinger/webfinger.go b/app/webfinger/webfinger.go index bd22273..80ba478 100644 --- a/app/webfinger/webfinger.go +++ b/app/webfinger/webfinger.go @@ -9,19 +9,32 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "github.com/golang-jwt/jwt/v4" "github.com/sour-is/ev" "github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/pkg/es/event" + "github.com/sour-is/ev/pkg/set" ) type service struct { - es *ev.EventStore + es *ev.EventStore + self set.Set[string] } -func New(ctx context.Context, es *ev.EventStore) (*service, error) { +type Option interface { + ApplyWebfinger(s *service) +} + +type WithHostnames []string + +func (o WithHostnames) ApplyWebfinger(s *service) { + s.self = set.New(o...) +} + +func New(ctx context.Context, es *ev.EventStore, opts ...Option) (*service, error) { ctx, span := lg.Span(ctx) defer span.End() @@ -36,6 +49,10 @@ func New(ctx context.Context, es *ev.EventStore) (*service, error) { } svc := &service{es: es} + for _, o := range opts { + o.ApplyWebfinger(svc) + } + return svc, nil } @@ -52,7 +69,6 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) fmt.Fprint(w, http.StatusText(http.StatusNotFound)) return - } switch r.Method { @@ -93,6 +109,8 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.JRD.Subject = c.Subject c.StandardClaims.Subject = c.Subject + c.SetProperty(NSpubkey, &c.PubKey) + pub, err := dec(c.PubKey) return ed25519.PublicKey(pub), err }, @@ -117,10 +135,52 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { } a, err := ev.Upsert(ctx, s.es, StreamID(c.Subject), func(ctx context.Context, a *JRD) error { - if r.Method == http.MethodDelete { - return a.OnDelete(c.PubKey, c.JRD) + var auth *JRD + + // does the target have a pubkey for self auth? + if _, ok := a.Properties[NSpubkey]; ok { + auth = a } - return a.OnClaims(c.PubKey, c.JRD) + + // Check current version for auth. + if authID, ok := a.Properties[NSauth]; ok && authID != nil && auth == nil { + auth = &JRD{} + auth.SetStreamID(StreamID(*authID)) + err := s.es.Load(ctx, auth) + if err != nil { + return err + } + } + if a.Version() == 0 || a.IsDeleted() { + // else does the new object claim auth from another object? + if authID, ok := c.Properties[NSauth]; ok && authID != nil && auth == nil { + auth = &JRD{} + auth.SetStreamID(StreamID(*authID)) + err := s.es.Load(ctx, auth) + if err != nil { + return err + } + } + + // fall back to use auth from submitted claims + if auth == nil { + auth = c.JRD + } + } + + if auth == nil { + return fmt.Errorf("auth not found") + } + + err = a.OnAuth(c.JRD, auth) + if err != nil { + return err + } + + if r.Method == http.MethodDelete { + return a.OnDelete(c.JRD) + } + return a.OnClaims(c.JRD) }) if err != nil { @@ -131,9 +191,17 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/jrd+json") - w.WriteHeader(http.StatusCreated) + if version := a.Version(); r.Method == http.MethodDelete && version > 0 { + err = s.es.Truncate(ctx, a.StreamID(), int64(version)) + span.RecordError(err) + } + w.Header().Set("Content-Type", "application/jrd+json") + if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusCreated) + } j := json.NewEncoder(w) j.SetIndent("", " ") err = j.Encode(a) @@ -141,6 +209,18 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodGet: resource := r.URL.Query().Get("resource") + rels := r.URL.Query()["rel"] + + if u := Parse(resource); u != nil && !s.self.Has(u.URL.Hostname()) { + redirect := &url.URL{} + redirect.Scheme = "https" + redirect.Host = u.URL.Host + redirect.RawQuery = r.URL.RawQuery + redirect.Path = "/.well-known/webfinger" + w.Header().Set("location", redirect.String()) + w.WriteHeader(http.StatusSeeOther) + return + } a := &JRD{} a.SetStreamID(StreamID(resource)) @@ -167,6 +247,18 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if len(rels) > 0 { + a.Links = a.GetLinksByRel(rels...) + } + + if a.Properties != nil { + if redirect, ok := a.Properties[NSredirect]; ok && redirect != nil { + w.Header().Set("location", *redirect) + w.WriteHeader(http.StatusSeeOther) + return + } + } + w.Header().Set("Content-Type", "application/jrd+json") w.WriteHeader(http.StatusOK) diff --git a/cmd/ev/app.webfinger.go b/cmd/ev/app.webfinger.go index a84cfba..236cdc8 100644 --- a/cmd/ev/app.webfinger.go +++ b/cmd/ev/app.webfinger.go @@ -3,10 +3,12 @@ package main import ( "context" "fmt" + "strings" "github.com/sour-is/ev" "github.com/sour-is/ev/app/webfinger" "github.com/sour-is/ev/internal/lg" + "github.com/sour-is/ev/pkg/env" "github.com/sour-is/ev/pkg/service" "github.com/sour-is/ev/pkg/slice" ) @@ -21,7 +23,12 @@ var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error return fmt.Errorf("*es.EventStore not found in services") } - wf, err := webfinger.New(ctx, eventstore) + wf, err := webfinger.New( + ctx, + eventstore, + webfinger.WithHostnames( + strings.Fields(env.Default("WEBFINGER_DOMAINS", "sour.is")), + )) if err != nil { span.RecordError(err) return err diff --git a/cmd/ev/svc.http.go b/cmd/ev/svc.http.go index b350f99..6c81ff8 100644 --- a/cmd/ev/svc.http.go +++ b/cmd/ev/svc.http.go @@ -15,7 +15,9 @@ import ( ) var _ = apps.Register(20, func(ctx context.Context, svc *service.Harness) error { - s := &http.Server{} + s := &http.Server{ + + } svc.Add(s) mux := mux.New() diff --git a/cmd/webfinger-cli/main.go b/cmd/webfinger-cli/main.go new file mode 100644 index 0000000..d6a97dd --- /dev/null +++ b/cmd/webfinger-cli/main.go @@ -0,0 +1,320 @@ +package main + +import ( + "bufio" + "context" + "crypto/ed25519" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path/filepath" + "strings" + "time" + + "github.com/docopt/docopt-go" + "github.com/golang-jwt/jwt" + "gopkg.in/yaml.v3" + + "github.com/sour-is/ev/app/webfinger" + "github.com/sour-is/ev/cmd/webfinger-cli/xdg" +) + +var usage = `Webfinger CLI. +usage: + webfinger-cli gen [--key KEY] [--force] + webfinger-cli get [--host HOST] [...] + webfinger-cli put [--host HOST] [--key KEY] + webfinger-cli rm [--host HOST] [--key KEY] + +Options: + --key From key [default: ` + xdg.Get(xdg.EnvConfigHome, "webfinger/$USER.key") + `] + --host Hostname to use [default: https://ev.sour.is] + --force, -f Force recreate key for gen +` + +type opts struct { + Gen bool `docopt:"gen"` + Get bool `docopt:"get"` + Put bool `docopt:"put"` + Remove bool `docopt:"rm"` + + Key string `docopt:"--key"` + Host string `docopt:"--host"` + File string `docopt:""` + Subject string `docopt:""` + Rel []string `docopt:""` + + Force bool `docopt:"--force"` +} + +func main() { + o, err := docopt.ParseDoc(usage) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + + var opts opts + o.Bind(&opts) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + go func() { + <-ctx.Done() + defer cancel() // restore interrupt function + }() + + if err := run(opts); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func run(opts opts) error { + // fmt.Fprintf(os.Stderr, "%#v\n", opts) + + switch { + case opts.Gen: + err := mkKeyfile(opts.Key, opts.Force) + if err != nil { + return err + } + fmt.Println("wrote keyfile to", opts.Key) + case opts.Get: + url, err := url.Parse(opts.Host) + if err != nil { + return err + } + + url.Path = "/.well-known/webfinger" + query := url.Query() + query.Set("resource", opts.Subject) + for _, rel := range opts.Rel { + query.Add("rel", rel) + } + url.RawQuery = query.Encode() + + req, err := http.NewRequest(http.MethodGet, url.String(), nil) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + s, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + fmt.Println(string(s)) + case opts.Remove: + url, err := url.Parse(opts.Host) + if err != nil { + return err + } + + url.Path = "/.well-known/webfinger" + + key, err := readKeyfile(opts.Key) + if err != nil { + return err + } + bkey := []byte(key.Public().(ed25519.PublicKey)) + + token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{ + "sub": opts.Subject, + "subject": opts.Subject, + "pub": enc(bkey), + "exp": time.Now().Add(90 * time.Minute).Unix(), + "iat": time.Now().Unix(), + "aud": "webfinger", + "iss": "sour.is-webfingerCLI", + }) + aToken, err := token.SignedString(key) + if err != nil { + return err + } + + body := strings.NewReader(aToken) + req, err := http.NewRequest(http.MethodDelete, url.String(), body) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + s, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + fmt.Println(res.Status, string(s)) + case opts.Put: + url, err := url.Parse(opts.Host) + if err != nil { + return err + } + + url.Path = "/.well-known/webfinger" + + key, err := readKeyfile(opts.Key) + if err != nil { + return err + } + bkey := []byte(key.Public().(ed25519.PublicKey)) + + fmt.Fprintln(os.Stderr, opts.File) + fp, err := os.Open(opts.File) + if err != nil { + return err + } + y := yaml.NewDecoder(fp) + + type claims struct { + Subject string `json:"sub"` + PubKey string `json:"pub"` + *webfinger.JRD + jwt.StandardClaims + } + + for err == nil { + j := claims{ + PubKey: enc(bkey), + JRD: &webfinger.JRD{}, + StandardClaims: jwt.StandardClaims{ + Audience: "sour.is-webfinger", + ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), + IssuedAt: time.Now().Unix(), + Issuer: "sour.is-webfingerCLI", + }, + } + + err = y.Decode(j.JRD) + if err != nil { + break + } + + j.Subject = j.JRD.Subject + j.StandardClaims.Subject = j.JRD.Subject + + token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, &j) + aToken, err := token.SignedString(key) + if err != nil { + return err + } + + body := strings.NewReader(aToken) + + req, err := http.NewRequest(http.MethodPut, url.String(), body) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + s, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + fmt.Println(res.Status, string(s)) + + } + if err != nil && !errors.Is(err, io.EOF) { + return err + } + } + + return nil +} + +func enc(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} +func dec(s string) ([]byte, error) { + s = strings.TrimSpace(s) + return base64.RawURLEncoding.DecodeString(s) +} + +func mkKeyfile(keyfile string, force bool) error { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + return err + } + + err = os.MkdirAll(filepath.Dir(keyfile), 0700) + if err != nil { + return err + } + + _, err = os.Stat(keyfile) + if !os.IsNotExist(err) { + if force { + fmt.Println("removing keyfile", keyfile) + err = os.Remove(keyfile) + if err != nil { + return err + } + } else { + return fmt.Errorf("the keyfile %s exists. use --force", keyfile) + } + } + + fp, err := os.OpenFile(keyfile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + return err + } + fmt.Fprint(fp, "# pub: ", enc(pub), "\n", enc(priv)) + + return fp.Close() +} + +func readKeyfile(keyfile string) (ed25519.PrivateKey, error) { + fd, err := os.Stat(keyfile) + if err != nil { + return nil, err + } + + if fd.Mode()&0066 != 0 { + return nil, fmt.Errorf("permissions are too weak") + } + + f, err := os.Open(keyfile) + scan := bufio.NewScanner(f) + + var key ed25519.PrivateKey + for scan.Scan() { + txt := scan.Text() + if strings.HasPrefix(txt, "#") { + continue + } + if strings.TrimSpace(txt) == "" { + continue + } + + txt = strings.TrimPrefix(txt, "# priv: ") + b, err := dec(txt) + if err != nil { + return nil, err + } + key = b + } + + return key, err +} diff --git a/cmd/webfinger-cli/xdg/path_darwin.go b/cmd/webfinger-cli/xdg/path_darwin.go new file mode 100644 index 0000000..10c2ba1 --- /dev/null +++ b/cmd/webfinger-cli/xdg/path_darwin.go @@ -0,0 +1,30 @@ +//go:build darwin +// +build darwin + +package xdg + +func literal(name string) string { + return "$" + name +} + +const ( + defaultDataHome = "~/Library/Application Support" + defaultDataDirs = "/Library/Application Support" + defaultConfigHome = "~/Library/Preferences" + defaultConfigDirs = "/Library/Preferences" + defaultCacheHome = "~/Library/Caches" + defaultStateHome = "~/Library/Caches" + defaultRuntime = "~/Library/Application Support" + + defaultDesktop = "~/Desktop" + defaultDownload = "~/Downloads" + defaultDocuments = "~/Documents" + defaultMusic = "~/Music" + defaultPictures = "~/Pictures" + defaultVideos = "~/Videos" + defaultTemplates = "~/Templates" + defaultPublic = "~/Public" + + defaultApplicationDirs = "~/Applications:/Applications" + defaultFontDirs = "~/Library/Fonts:/Library/Fonts:/System/Library/Fonts:/Network/Library/Fonts" +) diff --git a/cmd/webfinger-cli/xdg/path_linux.go b/cmd/webfinger-cli/xdg/path_linux.go new file mode 100644 index 0000000..ca8cffe --- /dev/null +++ b/cmd/webfinger-cli/xdg/path_linux.go @@ -0,0 +1,30 @@ +//go:build linux +// +build linux + +package xdg + +func literal(name string) string { + return "$" + name +} + +const ( + defaultDataHome = "~/.local/share" + defaultDataDirs = "/usr/local/share:/usr/share" + defaultConfigHome = "~/.config" + defaultConfigDirs = "/etc/xdg" + defaultCacheHome = "~/.local/cache" + defaultStateHome = "~/.local/state" + defaultRuntime = "/run/user/$UID" + + defaultDesktop = "~/Desktop" + defaultDownload = "~/Downloads" + defaultDocuments = "~/Documents" + defaultMusic = "~/Music" + defaultPictures = "~/Pictures" + defaultVideos = "~/Videos" + defaultTemplates = "~/Templates" + defaultPublic = "~/Public" + + defaultApplicationDirs = "~/Applications:/Applications" + defaultFontDirs = "~/.local/share/fonts:/usr/local/share/fonts:/usr/share/fonts:~/.fonts" +) diff --git a/cmd/webfinger-cli/xdg/path_windows.go b/cmd/webfinger-cli/xdg/path_windows.go new file mode 100644 index 0000000..6886bf0 --- /dev/null +++ b/cmd/webfinger-cli/xdg/path_windows.go @@ -0,0 +1,30 @@ +//go:build windows +// +build windows + +package xdg + +func literal(name string) string { + return "%" + name + "%" +} + +const ( + defaultDataHome = `%LOCALAPPDATA%` + defaultDataDirs = `%APPDATA%\Roaming;%ProgramData%` + defaultConfigHome = `%LOCALAPPDATA%` + defaultConfigDirs = `%ProgramData%` + defaultCacheHome = `%LOCALAPPDATA%\cache` + defaultStateHome = `%LOCALAPPDATA%\state` + defaultRuntime = `%LOCALAPPDATA%` + + defaultDesktop = `%USERPROFILE%\Desktop` + defaultDownload = `%USERPROFILE%\Downloads` + defaultDocuments = `%USERPROFILE%\Documents` + defaultMusic = `%USERPROFILE%\Music` + defaultPictures = `%USERPROFILE%\Pictures` + defaultVideos = `%USERPROFILE%\Videos` + defaultTemplates = `%USERPROFILE%\Templates` + defaultPublic = `%USERPROFILE%\Public` + + defaultApplicationDirs = `%APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs` + defaultFontDirs = `%windir%\Fonts;%LOCALAPPDATA%\Microsoft\Windows\Fonts` +) diff --git a/cmd/webfinger-cli/xdg/xdg.go b/cmd/webfinger-cli/xdg/xdg.go new file mode 100644 index 0000000..8f2f709 --- /dev/null +++ b/cmd/webfinger-cli/xdg/xdg.go @@ -0,0 +1,52 @@ +package xdg + +import ( + "os" + "path/filepath" + "strings" +) + +var ( + EnvDataHome = setENV("XDG_DATA_HOME", defaultDataHome) + EnvDataDirs = setENV("XDG_DATA_DIRS", defaultDataDirs) + EnvConfigHome = setENV("XDG_CONFIG_HOME", defaultConfigHome) + EnvConfigDirs = setENV("XDG_CONFIG_DIRS", defaultConfigDirs) + EnvCacheHome = setENV("XDG_CACHE_HOME", defaultCacheHome) + EnvStateHome = setENV("XDG_STATE_HOME", defaultStateHome) + EnvRuntime = setENV("XDG_RUNTIME_DIR", defaultRuntime) + EnvDesktopDir = setENV("XDG_DESKTOP_DIR", defaultDesktop) + EnvDownloadDir = setENV("XDG_DOWNLOAD_DIR", defaultDownload) + EnvDocumentsDir = setENV("XDG_DOCUMENTS_DIR", defaultDocuments) + EnvMusicDir = setENV("XDG_MUSIC_DIR", defaultMusic) + EnvPicturesDir = setENV("XDG_PICTURES_DIR", defaultPictures) + EnvVideosDir = setENV("XDG_VIDEOS_DIR", defaultVideos) + EnvTemplatesDir = setENV("XDG_TEMPLATES_DIR", defaultTemplates) + EnvPublicShareDir = setENV("XDG_PUBLICSHARE_DIR", defaultPublic) + EnvApplicationsDir = setENV("XDG_APPLICATIONS_DIR", defaultApplicationDirs) + EnvFontsDir = setENV("XDG_FONTS_DIR", defaultFontDirs) +) + +func setENV(name, value string) string { + if _, ok := os.LookupEnv(name); !ok { + os.Setenv(name, value) + } + return literal(name) +} +func Get(base, suffix string) string { + paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator)) + for i, path := range paths { + if strings.HasPrefix(path, "~") { + path = strings.Replace(path, "~", getHome(), 1) + } + paths[i] = os.ExpandEnv(filepath.Join(path, suffix)) + } + return strings.Join(paths, string(os.PathListSeparator)) +} + +func getHome() string { + home, err := os.UserHomeDir() + if err != nil { + return "." + } + return home +} diff --git a/ev.go b/ev.go index ff419c2..cde5295 100644 --- a/ev.go +++ b/ev.go @@ -154,12 +154,16 @@ func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error { attribute.String("agg.type", event.TypeOf(agg)), attribute.String("agg.streamID", agg.StreamID()), ) - l, err := es.Driver.EventLog(ctx, agg.StreamID()) if err != nil { return err } + first, err := l.FirstIndex(ctx) + if err != nil { + return err + } + events, err := l.Read(ctx, 0, AllEvents) if err != nil { return err @@ -169,6 +173,7 @@ func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error { } Mes_load.Add(ctx, events.Count()) + event.Start(agg, first-1) event.Append(agg, events...) span.SetAttributes( @@ -335,7 +340,7 @@ func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string attribute.String("agg.streamID", streamID), ) - if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound){ + if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound) { return } diff --git a/go.mod b/go.mod index c54985c..9b58aaa 100644 --- a/go.mod +++ b/go.mod @@ -100,6 +100,8 @@ require ( ) require ( + github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v4 v4.4.3 github.com/keys-pub/keys v0.1.22 github.com/matryer/is v1.4.0 @@ -112,4 +114,5 @@ require ( go.mills.io/saltyim v0.0.0-20220925030055-7c149128b431 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0 go.uber.org/multierr v1.8.0 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 835e00b..cad5ae1 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s= github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -153,6 +155,8 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -834,6 +838,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/es/event/aggregate.go b/pkg/es/event/aggregate.go index 40806ee..4f28cf9 100644 --- a/pkg/es/event/aggregate.go +++ b/pkg/es/event/aggregate.go @@ -15,6 +15,10 @@ type Aggregate interface { AggregateRootInterface } +func Start(a Aggregate, i uint64) { + a.start(i) +} + // Raise adds new uncommitted events func Raise(a Aggregate, lis ...Event) { lis = NewEvents(lis...) @@ -58,6 +62,7 @@ type AggregateRootInterface interface { // Version returns the current aggrigate version. (committed + uncommitted) Version() uint64 + start(uint64) raise(lis ...Event) append(lis ...Event) Commit() @@ -66,25 +71,26 @@ type AggregateRootInterface interface { var _ AggregateRootInterface = &AggregateRoot{} type AggregateRoot struct { - events Events - streamID string - streamVersion uint64 + events Events + streamID string + firstIndex uint64 + lastIndex uint64 mu sync.RWMutex } -func (a *AggregateRoot) Commit() { a.streamVersion = uint64(len(a.events)) } -func (a *AggregateRoot) StreamID() string { return a.streamID } -func (a *AggregateRoot) SetStreamID(streamID string) { a.streamID = streamID } -func (a *AggregateRoot) StreamVersion() uint64 { return a.streamVersion } -func (a *AggregateRoot) Version() uint64 { return uint64(len(a.events)) } +func (a *AggregateRoot) Commit() { a.lastIndex = uint64(len(a.events)) } +func (a *AggregateRoot) StreamID() string { return a.streamID } +func (a *AggregateRoot) SetStreamID(streamID string) { a.streamID = streamID } +func (a *AggregateRoot) StreamVersion() uint64 { return a.lastIndex } +func (a *AggregateRoot) Version() uint64 { return a.firstIndex + uint64(len(a.events)) } func (a *AggregateRoot) Events(new bool) Events { a.mu.RLock() defer a.mu.RUnlock() events := a.events if new { - events = events[a.streamVersion:] + events = events[a.lastIndex-a.firstIndex:] } lis := make(Events, len(events)) @@ -93,6 +99,11 @@ func (a *AggregateRoot) Events(new bool) Events { return lis } +func (a *AggregateRoot) start(i uint64) { + a.firstIndex = i + a.lastIndex = i +} + //lint:ignore U1000 is called by embeded interface func (a *AggregateRoot) raise(lis ...Event) { //nolint a.mu.Lock() @@ -111,13 +122,13 @@ func (a *AggregateRoot) append(lis ...Event) { a.posStartAt(lis...) a.events = append(a.events, lis...) - a.streamVersion += uint64(len(lis)) + a.lastIndex += uint64(len(lis)) } func (a *AggregateRoot) posStartAt(lis ...Event) { for i, e := range lis { m := e.EventMeta() - m.Position = a.streamVersion + uint64(i) + 1 + m.Position = a.lastIndex + uint64(i) + 1 e.SetEventMeta(m) } }