diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d344ba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json diff --git a/finger.go b/finger.go new file mode 100644 index 0000000..66abbca --- /dev/null +++ b/finger.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "strings" +) + +type Resource struct { + Subject string `json:"subject"` + Aliases []string `json:"aliases,omitempty"` + Properties map[string]string `json:"properties,omitempty"` + Links []Links `json:"links,omitempty"` +} + +type Links struct { + Rel string `json:"rel"` + Type string `json:"type,omitempty"` + Href string `json:"href,omitempty"` +} + +type Webfinger struct { + domain string + username string + accountName string +} + +func NewWebfinger(username string, domain string, aliases ...string) (*Webfinger, error) { + return &Webfinger{ + username: username, + domain: domain, + accountName: fmt.Sprintf("acct:%s@%s", username, domain), + }, nil +} + +func (w *Webfinger) FingerResource(name string) (Resource, error) { + if !strings.EqualFold(w.accountName, name) { + return Resource{}, fmt.Errorf("account not found") + } + return Resource{ + Subject: w.accountName, + Links: []Links{ + { + Rel: "self", + Type: "application/activity+json", + Href: fmt.Sprintf("https://%s/users/%s", w.domain, w.username), + }, + }, + }, nil +} diff --git a/friend b/friend new file mode 100755 index 0000000..aaf8f43 Binary files /dev/null and b/friend differ diff --git a/go.mod b/go.mod index db58468..6337c51 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.yetaga.in/alazyreader/friend -go 1.18 +go 1.19 + +require github.com/go-chi/chi/v5 v5.0.7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..433d671 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..cad9e47 --- /dev/null +++ b/handlers.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" +) + +func WebFingerHandler(wf *Webfinger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + requestedResource := r.URL.Query().Get("resource") + res, err := wf.FingerResource(requestedResource) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error": "account not found"}`)) + return + } + b, err := json.Marshal(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(b) + } +} + +func ProfileHandler(pp *ProfileProvider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "user") + res, err := pp.Get(userID) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error": "account not found"}`)) + return + } + b, err := json.Marshal(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(b) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..822d832 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + "net/http" + + "github.com/go-chi/chi/v5" +) + +func main() { + wf, err := NewWebfinger("dma", "hello.yetaga.in") + if err != nil { + log.Fatalln(err) + } + pp, err := NewProfileProvider("hello.yetaga.in") + if err != nil { + log.Fatalln(err) + } + + r := chi.NewRouter() + r.Get("/.well-known/webfinger", WebFingerHandler(wf)) + r.Get("/users/{user}", ProfileHandler(pp)) + + log.Println("listening on http://0.0.0.0:8008") + err = http.ListenAndServe(":8008", r) + if err != nil { + log.Fatalln(err) + } +} diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..ef3d013 --- /dev/null +++ b/profile.go @@ -0,0 +1,68 @@ +package main + +import "fmt" + +type Actor struct { + ID string `json:"id"` + Type string `json:"type"` + URL string `json:"url"` + Name string `json:"name"` + PreferredUsername string `json:"preferredUsername,omitempty"` + Summary string `json:"summary,omitempty"` + + PublicKey PublicKey `json:"publicKey,omitempty"` + + Inbox string `json:"inbox,omitempty"` + Outbox string `json:"outbo,omitempty"` + Followers string `json:"followers,omitempty"` + Following string `json:"following,omitempty"` + + Icon *Link `json:"icon,omitempty"` + Image *Link `json:"image,omitempty"` + + Endpoints map[string]string `json:"endpoints,omitempty"` +} + +type Link struct { + Type string `json:"type"` + MediaType string `json:"mediaType"` + URL string `json:"url"` +} + +type PublicKey struct { + ID string `json:"id"` + Owner string `json:"owner"` + PublicKeyPem string `json:"publicKeyPem"` +} + +type ProfileProvider struct { + domain string +} + +func NewProfileProvider(domain string) (*ProfileProvider, error) { + return &ProfileProvider{ + domain: domain, + }, nil +} + +func (pp *ProfileProvider) Get(userID string) (*Actor, error) { + userRoot := fmt.Sprintf("https://%s/users/%s", pp.domain, userID) + return &Actor{ + ID: userRoot, + Type: "Person", + URL: fmt.Sprintf("https://%s/@%s", pp.domain, userID), + Name: userID, + Inbox: userRoot + "/inbox", + Outbox: userRoot + "/outbox", + Followers: userRoot + "/followers", + Following: userRoot + "/following", + Endpoints: map[string]string{ + "sharedInbox": fmt.Sprintf("https://%s/inbox", pp.domain), + }, + PublicKey: PublicKey{ + ID: userRoot + "#main-key", + Owner: userRoot, + PublicKeyPem: "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + }, + }, nil +} diff --git a/signature.go b/signature.go new file mode 100644 index 0000000..20fcc00 --- /dev/null +++ b/signature.go @@ -0,0 +1,30 @@ +package main + +import ( + "crypto" + "crypto/rand" + "fmt" + "net/http" + "strings" + "time" +) + +type Signer struct { + key crypto.Signer + keyID string +} + +// Sign modifies the request, computing and setting the Signature header, and setting the Date header to the provided time. +func (s Signer) Sign(r *http.Request, date time.Time) error { + host := r.URL.Host + path := r.URL.Path + method := strings.ToLower(r.Method) + stringToSign := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", method, path, host, date.Format(time.RFC1123)) + sig, err := s.key.Sign(rand.Reader, []byte(stringToSign), nil) + if err != nil { + return err + } + r.Header.Set("Date", date.Format(time.RFC1123)) + r.Header.Set("Signature", fmt.Sprintf(`keyId="%s",headers="(request-target) host date",signature="%s"`, s.keyID, sig)) + return nil +} diff --git a/streams.go b/streams.go new file mode 100644 index 0000000..582bea7 --- /dev/null +++ b/streams.go @@ -0,0 +1,38 @@ +package main + +import "time" + +type Stream struct { + Context string `json:"@context"` + ID string `json:"id"` + Type string `json:"type"` + TotalItems int `json:"totalItems"` + Items []struct{} `json:"items"` +} + +type Activity struct { + Context string `json:"@context"` + Summary string `json:"summary"` + Type string `json:"type"` + Published time.Time `json:"published"` + Actor Actor `json:"actor"` + Object *Object `json:"object,omitempty"` + Target *Object `json:"target,omitempty"` +} + +type Object struct { + ID string `json:"id"` + Type string `json:"type"` + URL string `json:"url"` + Name string `json:"name"` +} + +type Streams struct{} + +func (s *Streams) Inbox() + +func (s *Streams) Outbox() + +func (s *Streams) Followers() + +func (s *Streams) Followed()