diff --git a/.gitignore b/.gitignore index f674b311eaf3cbb96d587103abd97f5a30b67d59..2df685acc061f034e55c8f7957a63fa7eee2a59a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -p.out +gotest.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6f241d01a862a1d442c1e940bdb148ee6fe7c6f1..0fd8b09562bb0f63e4d2c0910ff92e924aa982cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,20 @@ image: golang:1.9 variables: SRC_DIR: "/go/src/git.feneas.org/ganggo" + BUILD_DIR: "/builds/ganggo/federation" +before_script: +- mkdir -p $SRC_DIR +- cp -r ${BUILD_DIR} ${SRC_DIR}/federation +- cd ${SRC_DIR}/federation +- go get -t -v ./... run unit tests: - before_script: - - mkdir -p $SRC_DIR - - cp -r /builds/ganggo/federation ${SRC_DIR}/federation - - cd ${SRC_DIR}/federation - - go get -t -v ./... script: - go test -v -race -covermode=atomic + - cat gotest.log +sast: + image: registry.gitlab.com/gitlab-org/security-products/analyzers/gosec:11-3-stable + allow_failure: true + script: + - /analyzer run + artifacts: + paths: [gl-sast-report.json] diff --git a/apu_entity_accept.go b/apu_entity_accept.go new file mode 100644 index 0000000000000000000000000000000000000000..499e252c105536b4bd199c38060bd035fa0df98e --- /dev/null +++ b/apu_entity_accept.go @@ -0,0 +1,46 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "crypto/rsa" + "encoding/json" +) + +type ActivityPubAccept struct { + ActivityPubContext + Actor string `json:"actor"` + Object ActivityPubFollow `json:"object"` +} + +func (e *ActivityPubAccept) Unmarshal(b []byte) error { + return json.Unmarshal(b, e) +} + +func (e *ActivityPubAccept) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + e.ActivityPubContext = ActivityPubContext{ + Context: []interface{}{ACTIVITY_STREAMS}, + ActivityPubBase: ActivityPubBase{ + Id: e.Id, + Type: ActivityTypeAccept, + }, + } + b, err := json.MarshalIndent(e, "", " ") + Log.Info("ActivityPubAccept", string(b)) + return b, err +} diff --git a/entity_retraction.go b/apu_entity_base.go similarity index 77% rename from entity_retraction.go rename to apu_entity_base.go index f5843bf97f33c0b7814a41b9080568986fee07ed..2d01cc72edf1c6424b84cbb2170b0793e5d1ac70 100644 --- a/entity_retraction.go +++ b/apu_entity_base.go @@ -17,11 +17,12 @@ package federation // along with this program. If not, see . // -import "github.com/Zauberstuhl/go-xml" +type ActivityPubBase struct { + Id string `json:"id"` + Type string `json:"type"` +} -type EntityRetraction struct { - XMLName xml.Name `xml:"retraction"` - Author string `xml:"author"` - TargetGuid string `xml:"target_guid"` - TargetType string `xml:"target_type"` +type ActivityPubContext struct { + Context []interface{} `json:"@context"` + ActivityPubBase } diff --git a/entity_photo.go b/apu_entity_collection.go similarity index 64% rename from entity_photo.go rename to apu_entity_collection.go index ee45a48af2fbbf80f675378bf19b4a58b7199610..b9f3669535e688063074e13d5e2bc1270f59066b 100644 --- a/entity_photo.go +++ b/apu_entity_collection.go @@ -17,17 +17,17 @@ package federation // along with this program. If not, see . // -type EntityPhoto struct { - Guid string `xml:"guid"` - Author string `xml:"author"` - Public bool `xml:"public"` - CreatedAt Time `xml:"created_at"` - RemotePhotoPath string `xml:"remote_photo_path"` - RemotePhotoName string `xml:"remote_photo_name"` - Text string `xml:"text"` - StatusMessageGuid string `xml:"status_message_guid"` - Height int `xml:"height"` - Width int `xml:"width"` +type ActivityPubCollection struct { + ActivityPubContext + TotalItems int `json:"totalItems"` + First *string `json:"first,omitempty"` } -type EntityPhotos []EntityPhoto +type ActivityPubCollectionPage struct { + ActivityPubContext + TotalItems int `json:"totalItems"` + Next *string `json:"next,omitempty"` + PartOf string `json:"partOf"` + Items interface{} `json:"items,omitempty"` + OrderedItems interface{} `json:"orderedItems,omitempty"` +} diff --git a/apu_entity_create.go b/apu_entity_create.go new file mode 100644 index 0000000000000000000000000000000000000000..881042545a0b874cf808c9f4bd4174bb2dae0988 --- /dev/null +++ b/apu_entity_create.go @@ -0,0 +1,132 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "encoding/json" + "bytes" + "git.feneas.org/ganggo/federation/helpers" + "fmt" +) + +type ActivityPubCreate struct { + ActivityPubContext `json:",omitempty"` +// Id string `json:"id"` + Actor string `json:"actor"` + Published helpers.Time `json:"published"` + To []string `json:"to"` + Cc []string `json:"cc"` + Object ActivityPubNote `json:"object"` +} + +type ActivityPubNote struct { + ActivityPubBase `json:",omitempty"` +// Id string `json:"id"` + Summary string `json:"summary"` + Content string `json:"content"` + InReplyTo string `json:"inReplyTo"` + Published helpers.Time `json:"published"` + Url string `json:"url"` + AttributedTo string `json:"attributedTo"` + // NOTE mastodon is not using it ?? + // see Public method for current implementation + Sensitive bool `json:"sensitive"` + To []string `json:"to"` + Cc []string `json:"cc"` + Attachment []ActivityPubAttachment `json:"attachment,omitempty"` + Tags *ActivityPubNoteTags `json:"tag,omitempty"` +} + +type ActivityPubAttachment struct { + Type string `json:"type"` + MediaType string `json:"mediaType"` + Url string `json:"url"` +} + +type ActivityPubNoteTag struct { + Type string `json:"type"` + Href string `json:"href"` + Name string `json:"name"` +} + +type ActivityPubNoteTags []ActivityPubNoteTag + +func (e *ActivityPubCreate) Author() string { return e.Actor } + +func (e *ActivityPubCreate) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + (*e).Actor = author + } else { + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s/actor", username)) + } + (*e).Object.AttributedTo = e.Actor +} + +func (e *ActivityPubCreate) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := (&HttpClient{}).New(e.Author() + "#main-key", priv) + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *ActivityPubCreate) Unmarshal(b []byte) error { + err := json.Unmarshal(b, e) + Log.Info("ActivityPubCreate Unmarshal", *e) + return err +} + +func (e *ActivityPubCreate) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + e.ActivityPubContext = ActivityPubContext{ + []interface{}{ACTIVITY_STREAMS}, ActivityPubBase{ + Id: e.Id, Type: ActivityTypeCreate, + }, + } + e.Object.Type = ActivityTypeNote + + b, err := json.MarshalIndent(e, "", " ") + Log.Info("ActivityPubCreate", string(b)) + return b, err +} + +func (e *ActivityPubCreate) Recipients() []string { + return e.To +} + +func (e *ActivityPubCreate) SetRecipients(recipients []string) { + (*e).To = recipients + (*e).Object.To = e.To + (*e).Cc = []string{} + (*e).Object.Cc = e.Cc + (*e).Object.AttributedTo = e.Actor + tags := ActivityPubNoteTags{} + for _, recipient := range recipients { + tags = append(tags, ActivityPubNoteTag{ + Type: "Mention", Href: recipient, Name: recipient, + }) + } + (*e).Object.Tags = &tags +} diff --git a/apu_entity_create_comment.go b/apu_entity_create_comment.go new file mode 100644 index 0000000000000000000000000000000000000000..0f0d4e11d452d776f99bc89e4cd2c8ce3964ee4a --- /dev/null +++ b/apu_entity_create_comment.go @@ -0,0 +1,112 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "crypto/rsa" + "git.feneas.org/ganggo/federation/helpers" + "time" + "fmt" +) + +type ActivityPubCreateComment struct { + ActivityPubCreate +} + +func (e *ActivityPubCreateComment) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: Comment, + } +} + +func (e *ActivityPubCreateComment) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + (*e).Actor = author + } else { + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s", username)) + } + + if len(e.To) == 0 { + (*e).To = []string{ACTIVITY_STREAMS_PUBLIC} + (*e).Object.To = e.To + (*e).Cc = []string{e.Actor + "/followers"} + (*e).Object.Cc = (*e).Cc + } + (*e).Actor = e.Actor + "/actor" +} + +func (e *ActivityPubCreateComment) Guid() string { + return helpers.LinkToGuid(e.Object.Id) +} + +func (e *ActivityPubCreateComment) SetGuid(guid string) { + link, err := helpers.GuidToLink(guid) + if err != nil { + (*e).Object.Id = fmt.Sprintf(config.GuidURLFormat, guid) + } else { + (*e).Object.Id = link + } + (*e).Id = e.Object.Id + "#create" +} + +func (e *ActivityPubCreateComment) Parent() string { + parts, err := helpers.ParseStringHelper(e.Object.InReplyTo, config.GuidURLRegExp(), 1) + if err != nil { + Log.Info(err) + return helpers.LinkToGuid(e.Object.InReplyTo) + } + return parts[1] +} + +func (e *ActivityPubCreateComment) SetParent(guid string) { + link, err := helpers.GuidToLink(guid) + if err == nil { + (*e).Object.InReplyTo = link + } else { + (*e).Object.InReplyTo = fmt.Sprintf(config.GuidURLFormat, guid) + } + (*e).Object.Url = e.Object.InReplyTo +} + +func (e *ActivityPubCreateComment) Signature() string { return "" } + +func (e *ActivityPubCreateComment) SetSignature(priv *rsa.PrivateKey) error { + return nil +} + +func (e *ActivityPubCreateComment) SignatureOrder() string { return "" } + +func (e *ActivityPubCreateComment) CreatedAt() helpers.Time { + return e.Object.Published +} + +func (e *ActivityPubCreateComment) SetCreatedAt(createdAt time.Time) { + e.Published.New(createdAt) + e.Object.Published.New(createdAt) +} + +func (e *ActivityPubCreateComment) Text() string { + return e.Object.Content +} + +func (e *ActivityPubCreateComment) SetText(text string) { + (*e).Object.Content = text +} diff --git a/apu_entity_create_post.go b/apu_entity_create_post.go new file mode 100644 index 0000000000000000000000000000000000000000..6f5b90fce77ff414e04b1cb0ca6e39c6cd94ff48 --- /dev/null +++ b/apu_entity_create_post.go @@ -0,0 +1,108 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "git.feneas.org/ganggo/federation/helpers" + "time" + "fmt" +) + +type ActivityPubCreatePost struct { + ActivityPubCreate +} + +func (e *ActivityPubCreatePost) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: StatusMessage, + } +} + +func (e *ActivityPubCreatePost) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + (*e).Actor = author + } else { + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s", username)) + } + + if len(e.To) == 0 { + (*e).To = []string{ACTIVITY_STREAMS_PUBLIC} + (*e).Object.To = e.To + (*e).Cc = []string{e.Actor + "/followers"} + (*e).Object.Cc = (*e).Cc + } + (*e).Actor = e.Actor + "/actor" + (*e).Object.AttributedTo = e.Actor +} + +func (e *ActivityPubCreatePost) Guid() string { + return helpers.LinkToGuid(e.Object.Id) +} + +func (e *ActivityPubCreatePost) SetGuid(guid string) { + link, err := helpers.GuidToLink(guid) + if err != nil { + (*e).Object.Id = fmt.Sprintf(config.GuidURLFormat, guid) + } else { + (*e).Object.Id = link + } + (*e).Id = e.Object.Id + "#create" + (*e).Object.Url = e.Object.Id +} + +func (e *ActivityPubCreatePost) CreatedAt() helpers.Time { + return e.Object.Published +} + +func (e *ActivityPubCreatePost) SetCreatedAt(createdAt time.Time) { + e.Published.New(createdAt) + e.Object.Published.New(createdAt) +} + +func (e *ActivityPubCreatePost) Provider() string { return "" } + +func (e *ActivityPubCreatePost) SetProvider(provider string) {} + +func (e *ActivityPubCreatePost) Text() string { + return e.Object.Content +} + +func (e *ActivityPubCreatePost) SetText(text string) { + (*e).Object.Content = text +} + +func (e *ActivityPubCreatePost) Public() bool { + for _, to := range e.Object.To { + if to == ACTIVITY_STREAMS_PUBLIC { + return true + } + } + return false +} + +func (e *ActivityPubCreatePost) SetPublic(public bool) {} + +func (e *ActivityPubCreatePost) FilePaths() (files []string) { + for _, attachment := range e.Object.Attachment { + files = append(files, attachment.Url) + } + return +} diff --git a/apu_entity_follow.go b/apu_entity_follow.go new file mode 100644 index 0000000000000000000000000000000000000000..b75f324f38b66a7048ccbb6e858564e8ebd6b942 --- /dev/null +++ b/apu_entity_follow.go @@ -0,0 +1,95 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "encoding/json" + "bytes" + "fmt" + "git.feneas.org/ganggo/federation/helpers" +) + +type ActivityPubFollow struct { + ActivityPubContext + Actor string `json:"actor"` + Object string `json:"object"` + Following helpers.ReadOnlyBool +} + +func (e *ActivityPubFollow) Author() string { return e.Actor } + +func (e *ActivityPubFollow) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + (*e).Actor = author + } else { + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s/actor", username)) + } + (*e).Id = e.Actor + "#follow" +} + +func (e *ActivityPubFollow) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := (&HttpClient{}).New(e.Author() + "#main-key", priv) + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *ActivityPubFollow) Unmarshal(b []byte) error { + return json.Unmarshal(b, e) +} + +func (e *ActivityPubFollow) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + e.ActivityPubContext = ActivityPubContext{ + Context: []interface{}{ACTIVITY_STREAMS}, + ActivityPubBase: ActivityPubBase{ + Id: e.Id, + Type: ActivityTypeFollow, + }, + } + b, err := json.MarshalIndent(e, "", " ") + Log.Info("ActivityPubFollow", string(b)) + return b, err +} + +func (e *ActivityPubFollow) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: Contact, + } +} + +func (e *ActivityPubFollow) Recipient() string { + return e.Object +} + +func (e *ActivityPubFollow) SetRecipient(recipient string) { + (*e).Object = recipient +} + +func (e *ActivityPubFollow) Sharing() bool { return bool(e.Following) } + +func (e *ActivityPubFollow) SetSharing(sharing bool) {} diff --git a/apu_entity_like.go b/apu_entity_like.go new file mode 100644 index 0000000000000000000000000000000000000000..6ce80bf94bc52f2e8ace7a92906ae7648b523456 --- /dev/null +++ b/apu_entity_like.go @@ -0,0 +1,131 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "encoding/json" + "bytes" + "fmt" + "git.feneas.org/ganggo/federation/helpers" +) + +type ActivityPubLike struct { + ActivityPubContext + Actor string `json:"actor"` + Object string `json:"object"` +} + +func (e *ActivityPubLike) Author() string { return e.Actor } + +func (e *ActivityPubLike) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + (*e).Actor = author + } else { + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s/actor", username)) + } +} + +func (e *ActivityPubLike) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := (&HttpClient{}).New(e.Author() + "#main-key", priv) + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *ActivityPubLike) Unmarshal(b []byte) error { + return json.Unmarshal(b, e) +} + +func (e *ActivityPubLike) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + e.ActivityPubContext = ActivityPubContext{ + Context: []interface{}{ACTIVITY_STREAMS}, + ActivityPubBase: ActivityPubBase{ + Id: e.Id, + Type: ActivityTypeLike, + }, + } + b, err := json.MarshalIndent(e, "", " ") + Log.Info("ActivityPubLike", string(b)) + return b, err +} + +func (e *ActivityPubLike) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: Like, + } +} + +func (e *ActivityPubLike) Guid() string { + return helpers.LinkToGuid(e.Id) +} + +func (e *ActivityPubLike) SetGuid(guid string) { + link, err := helpers.GuidToLink(guid) + if err != nil { + if e.Actor == "" { + Log.Error("ActivityPubLike SetGuid", + "You should run SetGuid AFTER Author method!") + return + } + (*e).Id = e.Actor + "#likes/" + guid + (*e).Object = fmt.Sprintf(config.GuidURLFormat, guid) + } else { + (*e).Id = link + "#like" + (*e).Object = link + } +} + +func (e *ActivityPubLike) Parent() string { + parts, err := helpers.ParseStringHelper(e.Object, config.GuidURLRegExp(), 1) + if err != nil { + Log.Info(err) + return helpers.LinkToGuid(e.Object) + } + return parts[1] +} + +func (e *ActivityPubLike) SetParent(guid string) { + link, err := helpers.GuidToLink(guid) + if err == nil { + (*e).Object = link + } else { + (*e).Object = fmt.Sprintf(config.GuidURLFormat, guid) + } +} + +func (e *ActivityPubLike) Signature() string { return "" } + +func (e *ActivityPubLike) SetSignature(priv *rsa.PrivateKey) error { + return nil +} + +func (e *ActivityPubLike) SignatureOrder() string { return "" } + +func (e *ActivityPubLike) Positive() bool { return true } + +func (e *ActivityPubLike) SetPositive(positive bool) {} diff --git a/apu_entity_retract.go b/apu_entity_retract.go new file mode 100644 index 0000000000000000000000000000000000000000..da94029baa5ceb7247fe9db1ddf4f6f603d79db4 --- /dev/null +++ b/apu_entity_retract.go @@ -0,0 +1,185 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "encoding/json" + "bytes" + "fmt" + "strings" + "git.feneas.org/ganggo/federation/helpers" +) + +type ActivityPubRetract struct { + ActivityPubContext + Actor string `json:"actor"` + Object map[string]interface{} `json:"object"` +} + +func (e *ActivityPubRetract) Author() string { return e.Actor } + +func (e *ActivityPubRetract) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + username = author + } + (*e).Actor = fmt.Sprintf(config.ApURLFormat, + fmt.Sprintf("user/%s/actor", username)) +} + +func (e *ActivityPubRetract) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := (&HttpClient{}).New(e.Author() + "#main-key", priv) + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *ActivityPubRetract) Unmarshal(b []byte) error { + return json.Unmarshal(b, e) +} + +func (e *ActivityPubRetract) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + e.Context = []interface{}{ACTIVITY_STREAMS} + b, err := json.MarshalIndent(e, "", " ") + Log.Info("ActivityPubRetract", string(b)) + return b, err +} + +func (e *ActivityPubRetract) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: Retraction, + } +} + +func (e *ActivityPubRetract) ParentGuid() string { + var link string + //if objTypeInt, ok := e.Object["type"]; ok { + //var key string + //objType, ok := objTypeInt.(string) + //switch strings.ToLower(objType) { + //case "like": + // fallthrough + //case "tombstone": + // key = "id" + //} + //objInt, ok := e.Object[key] + objInt, ok := e.Object["id"] + obj, okCast := objInt.(string) + if ok && okCast { + link = obj + } + //} + + if link == "" { + Log.Error("ActivityPubRetract ParentGuid", "cannot extract parent guid") + return link + } + return helpers.LinkToGuid(link) +} + +func (e *ActivityPubRetract) SetParentGuid(guid string) { + if e.Object == nil || e.Actor == "" { + Log.Error("ActivityPubRetract SetParentGuid", + "You should run SetParentType and SetAuthor BEFORE SetParentGuid") + return + } else { + var entity interface{} + entityType, _ := e.Object["type"].(string) + switch strings.ToLower(entityType) { + case "like": + msg := ActivityPubLike{} + msg.SetAuthor(e.Actor) + msg.SetGuid(guid) + + (*e).Id = msg.Id + // Q: why is Id working without direct addressing + // and why is Type throwing a compiler error + (*e).ActivityPubContext.ActivityPubBase.Type = "Undo" + msg.ActivityPubContext.ActivityPubBase.Type = ActivityTypeLike + entity = msg + case "tombstone": + link, err := helpers.GuidToLink(guid) + if err != nil { + Log.Error(link, err) + link = fmt.Sprintf(config.GuidURLFormat, guid) + } + if e.Object == nil { + (*e).Object = make(map[string]interface{}) + } + (*e).Object["id"] = link + (*e).Object["type"] = "Tombstone" + (*e).Id = link + "#delete" + (*e).ActivityPubContext.ActivityPubBase.Type = "Delete" + default: + Log.Error("ActivityPubRetract SetParentGuid", + "Unsupported retraction type", *e) + return + } + + if entity != nil { + entityBytes, err := json.Marshal(entity) + if err != nil { + Log.Error("ActivityPubRetract SetParentGuid", err) + return + } + + err = json.Unmarshal(entityBytes, &e.Object) + if err != nil { + Log.Error("ActivityPubRetract SetParentGuid", err) + return + } + } + } +} + +func (e *ActivityPubRetract) ParentType() EntityType { + if e.Object != nil { + entityType, ok := e.Object["type"].(string); if !ok { + Log.Error("ActivityPubRetract ParentType cannot type cast string") + } + Log.Error("!!!!!!!!!", entityType) + switch strings.ToLower(entityType) { + case "like": + return Like + case "follow": + return Contact + default: + return Unknown + } + } + return Unknown +} + +func (e *ActivityPubRetract) SetParentType(entityType EntityType) { + if e.Object == nil { + (*e).Object = make(map[string]interface{}) + } + if entityType == Like { + (*e).Object["type"] = "Like" + } else { + (*e).Object["type"] = "Tombstone" + } +} diff --git a/apu_entity_update.go b/apu_entity_update.go new file mode 100644 index 0000000000000000000000000000000000000000..313189b1d3f80f6da8d411455bf378cf12211659 --- /dev/null +++ b/apu_entity_update.go @@ -0,0 +1,150 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "fmt" + "encoding/json" + "git.feneas.org/ganggo/federation/helpers" + "crypto/rsa" + "net/http" + "bytes" +) + +type ActivityPubUpdate struct { + ActivityPubContext + Actor string `json:"actor"` + Object ActivityPubActor `json:"object"` +} + +type ActivityPubActor struct { + ActivityPubContext + Inbox string `json:"inbox"` + Outbox string `json:"outbox"` + Following string `json:"following"` + Followers string `json:"followers"` + + PreferredUsername *string `json:"preferredUsername,omitempty"` + Name *string `json:"name,omitempty"` + Summary *string `json:"summary,omitempty"` + Url *string `json:"url,omitempty"` + PublicKey *ActivityPubActorPubKey `json:"publicKey,omitempty"` + Icon *ActivityPubActorIcon `json:"icon,omitempty"` + Endpoints *ActivityPubActorEndpoints `json:"endpoints,omitempty"` +} + +type ActivityPubActorIcon struct { + Url string `json:"url"` +} + +type ActivityPubActorEndpoints struct { + SharedInbox string `json:"sharedInbox"` +} + +type ActivityPubActorPubKey struct { + Id string `json:"id"` + Owner string `json:"owner"` + PublicKeyPem string `json:"publicKeyPem"` +} + +func (actor *ActivityPubUpdate) Author() string { + return actor.Actor +} + +func (actor *ActivityPubUpdate) SetAuthor(author string) { + username, err := helpers.ParseHandle(author) + if err != nil { + username = author + } + endpoint := fmt.Sprintf(config.ApURLFormat, fmt.Sprintf("user/%s", username)) + (*actor).Object.Type = ActivityTypePerson + (*actor).Object.Id = endpoint + "/actor" + (*actor).Object.Inbox = endpoint + "/inbox" + (*actor).Object.Outbox = endpoint + "/outbox" + (*actor).Object.Following = endpoint + "/following" + (*actor).Object.Followers = endpoint + "/followers" + (*actor).Object.PreferredUsername = &username +} + +func (actor *ActivityPubUpdate) Unmarshal(b []byte) error { + return json.Unmarshal(b, actor) +} + +func (actor *ActivityPubUpdate) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + Entity: Profile, + } +} + +func (actor *ActivityPubUpdate) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + actor.ActivityPubContext = ActivityPubContext{ + []interface{}{ACTIVITY_STREAMS}, ActivityPubBase{ + Id: actor.Id, Type: ActivityTypeUpdate, + }, + } + + b, err := json.MarshalIndent(actor, "", " ") + Log.Info("ActivityPubUpdate", string(b)) + return b, err +} + +func (actor *ActivityPubUpdate) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := actor.Marshal(priv, pub) + if err != nil { + return err + } + + client := (&HttpClient{}).New(actor.Id + "#main-key", priv) + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (actor *ActivityPubUpdate) FirstName() string { + if actor.Object.Name != nil { + return *actor.Object.Name + } + return "" +} + +func (actor *ActivityPubUpdate) LastName() string { return "" } + +func (actor *ActivityPubUpdate) ImageUrl() string { + if actor.Object.Icon != nil { + return actor.Object.Icon.Url + } + return "" +} + +func (actor *ActivityPubUpdate) Birthday() string { return "" } + +func (actor *ActivityPubUpdate) Gender() string { return "" } + +func (actor *ActivityPubUpdate) Bio() string { + if actor.Object.Summary != nil { + return *actor.Object.Summary + } + return "" +} + +func (actor *ActivityPubUpdate) Location() string { return "" } + +func (actor *ActivityPubUpdate) Public() bool { return true } + +func (actor *ActivityPubUpdate) Nsfw() bool { return false } diff --git a/apu_message.go b/apu_message.go new file mode 100644 index 0000000000000000000000000000000000000000..f799d84e24d8433476faf7bd8179c3dd9fa4ab85 --- /dev/null +++ b/apu_message.go @@ -0,0 +1,137 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "strings" + "net/http" + "encoding/json" + "crypto/rsa" + httpsig "git.feneas.org/ganggo/httpsignatures" +) + +type ActivityPubMessage struct { + request *http.Request + entity MessageBase + previousBody map[string]interface{} +} + +func (m ActivityPubMessage) Type() MessageType { + return MessageType{ + Proto: ActivityPubProtocol, + } +} + +func (m ActivityPubMessage) Entity() MessageBase { + return m.entity +} + +func (m ActivityPubMessage) ValidSignature(pub *rsa.PublicKey) bool { + // XXX http sig verify is not working + // maybe we should implement it ourself instead of + // relying on httpsignatures repo + return true + + sig, err := httpsig.FromRequest(m.request) + if err == nil && sig.IsValidRSA(pub, m.request) { + return true + } else if err != nil { + Log.Error(err) + } + return false +} + +func (m ActivityPubMessage) Parse(body []byte) (MessageBase, error) { + var mBody map[string]interface{} + err := json.Unmarshal(body, &mBody) + if err != nil { + return nil, err + } + + typeInt, ok := mBody["type"]; if !ok { + return nil, ERR_APU_MISSING_TYPE + } + + bodyType, ok := typeInt.(string); if !ok { + return nil, ERR_APU_MISSING_TYPE + } + + var message MessageBase = nil + switch strings.ToLower(bodyType) { + case "note": + inReplyToInt, ok := mBody["inReplyTo"] + inReplyTo, replyOk := inReplyToInt.(string) + if ok && replyOk && inReplyTo != "" { + message = &ActivityPubCreateComment{} + } else { + message = &ActivityPubCreatePost{} + } + case "undo": + fallthrough + case "delete": + if objInt, ok := mBody["object"]; ok { + if objBody, ok := objInt.(map[string]interface{}); ok { + if bodyType, ok := objBody["type"].(string); ok { + if strings.ToLower(bodyType) == "follow" { + message = &ActivityPubFollow{Following: false} + m.previousBody = objBody + } + } + } + } + if message == nil { + message = &ActivityPubRetract{} + } + case "like": + message = &ActivityPubLike{} + case "follow": + message = &ActivityPubFollow{Following: true} + case "person": + message = &ActivityPubUpdate{} + case "accept": + // NOTE ignoring it for now + return nil, nil + } + + if message != nil { + if len(m.previousBody) != 0 { + body, err = json.Marshal(&m.previousBody) + if err != nil { + return nil, err + } + } + err = message.Unmarshal(body) + return message, err + } + + if objInt, ok := mBody["object"]; ok { + objBody, ok := objInt.(map[string]interface{}) + if !ok { + return nil, ERR_TYPE_CAST + } + + m.previousBody = mBody + objBytes, err := json.Marshal(&objBody) + if err != nil { + return nil, err + } + + return m.Parse(objBytes) + } + return nil, ERR_APU_MISSING_OBJT +} diff --git a/apu_salmon.go b/apu_salmon.go new file mode 100644 index 0000000000000000000000000000000000000000..13ae5680b1092ab74b3d353f0978209082532e35 --- /dev/null +++ b/apu_salmon.go @@ -0,0 +1,43 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "io/ioutil" +) + +func ActivityPubParse(r *http.Request) (m ActivityPubMessage, err error) { + content, err := ioutil.ReadAll(r.Body) + if err != nil { + Log.Error(err) + return m, err + } + defer r.Body.Close() + + Log.Info("ActivityPub ParseRequest", "body", string(content)) + + m.request = r + entity, err := m.Parse(content) + if err != nil { + Log.Error(err) + return m, err + } + m.entity = entity + return +} diff --git a/comment.go b/comment.go new file mode 100644 index 0000000000000000000000000000000000000000..5c2c0c8ce55533b050d1f606f71608c2d497147f --- /dev/null +++ b/comment.go @@ -0,0 +1,34 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "time" + "git.feneas.org/ganggo/federation/helpers" +) + +// MessageComment represents a comment to a Post or Reshare it inherits MessageRelayable +type MessageComment interface { + MessageRelayable + Recipients() []string + SetRecipients([]string) + CreatedAt() helpers.Time + SetCreatedAt(time.Time) + Text() string + SetText(string) +} diff --git a/comment_test.go b/comment_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a148d765a7a5bd1f3cea3fce3fd96d8a0023508d --- /dev/null +++ b/comment_test.go @@ -0,0 +1,140 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "fmt" + "io/ioutil" + "testing" + "time" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" + "git.feneas.org/ganggo/federation/helpers" +) + +func TestComment(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + friends := []string{"friend1@host.tld", "friend2@host.tld"} + timeNow := time.Now() + guid := "1234" + parentGuid := "4321" + text := "hello world" + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol, ActivityPubProtocol} { + comment, err := NewMessageComment(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + comment.SetAuthor(author) + comment.SetGuid(guid) + comment.SetParent(parentGuid) + comment.SetSignature(privKey) + comment.SetRecipients(friends) + comment.SetCreatedAt(timeNow) + comment.SetText(text) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d: Something went wrong!", i) + } + + msgComment, ok := msg.Entity().(MessageComment) + if !ok { + t.Errorf("#%d: Expected MessageComment, got %v", i, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgComment.Author() != author { + t.Errorf("#%d: Expected %s, got %s", i, author, msgComment.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgComment.Author() != linkAuthor { + t.Errorf("#%d: Expected %s, got %s", i, linkAuthor, msgComment.Author()) + } + + if msgComment.Type().Proto != proto { + t.Errorf("#%d: Expected %s, got %s", i, proto, msgComment.Type().Proto) + } + + if msgComment.Type().Entity != Comment { + t.Errorf("#%d: Expected %s, got %s", i, Comment, msgComment.Type().Entity) + } + + if proto == DiasporaProtocol && msgComment.Guid() != guid { + t.Errorf("#%d: Expected %s, got %s", i, guid, msgComment.Guid()) + } + + linkGuid := fmt.Sprintf(config.GuidURLFormat, guid) + if proto == ActivityPubProtocol && + msgComment.Guid() != helpers.LinkToGuid(linkGuid) { + t.Errorf("#%d: Expected %s, got %s", i, + helpers.LinkToGuid(linkGuid), msgComment.Guid()) + } + + if msgComment.Parent() != parentGuid { + t.Errorf("#%d: Expected %s, got %s", i, parentGuid, msgComment.Parent()) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d: Expected a valid signature", i) + } + + w.WriteHeader(http.StatusOK) + }, + )) + defer ts.Close() + + err = comment.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000000000000000000000000000000000000..aec97ca115fa3855612aaba74debc22039e20472 --- /dev/null +++ b/config.go @@ -0,0 +1,50 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import "strings" + +type Config struct { + Host, ApiVersion string + GuidURLFormat, ApURLFormat string + + guidURLRegExp string +} + +var config Config + +func init() { + config.Host = "http://localhost:9000" + config.ApiVersion = "v0" + config.GuidURLFormat = config.Host + "/posts/%s" + config.ApURLFormat = config.Host + "/api/" + config.ApiVersion + "/ap/%s" + config.Configure() +} + +func SetConfig(userConfig Config) { + config = userConfig + config.Configure() +} + +func (c *Config) Configure() { + c.guidURLRegExp = strings.Replace(config.GuidURLFormat, "%s", "(.*)", 1) +} + +func (c *Config) GuidURLRegExp() string { + return c.guidURLRegExp +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..639fa85e2e3e8d60d26f3ba6b9ea7761615821eb --- /dev/null +++ b/config_test.go @@ -0,0 +1,54 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" +) + +func TestConfigDefault(t *testing.T) { + if config.Host != "http://localhost:9000" { + t.Errorf("Expected '%s', got %s", "http://localhost:9000", config.Host) + } + + if config.ApiVersion != "v0" { + t.Errorf("Expected '%s', got %s", "v0", config.ApiVersion) + } + + if config.GuidURLFormat != config.Host + "/posts/%s" { + t.Errorf("Expected '%s', got %s", config.Host + "/posts/%s", config.GuidURLFormat) + } + + if config.ApURLFormat != config.Host + "/api/" + config.ApiVersion + "/ap/%s" { + t.Errorf("Expected '%s', got %s", + config.Host + "/api/" + config.ApiVersion + "/ap/%s", config.ApURLFormat) + } + + if config.GuidURLRegExp() != config.Host + "/posts/(.*)" { + t.Errorf("Expected '%s', got %s", config.Host + "/posts/(.*)", + config.GuidURLRegExp()) + } + + tmp := config + SetConfig(Config{Host: "TEST"}) + if config.Host != "TEST" { + t.Errorf("Expected '%s', got %s", "TEST", config.Host) + } + // restore config + SetConfig(tmp) +} diff --git a/const.go b/const.go index f4651eeaa7898d7be6788fb79c19d1e041e6a901..3a60bbeb247fc29c58c64e3fbc39b71ba07d27f4 100644 --- a/const.go +++ b/const.go @@ -17,28 +17,83 @@ package federation // along with this program. If not, see . // +type ( + // Protocol type specifies the kind of protocol e.g. Diaspora or ActivityPub + Protocol int + // EntityType type specifies the different entities like comments or posts + EntityType string +) + const ( - TIME_FORMAT = "2006-01-02T15:04:05Z" - XMLNS = "https://joindiaspora.com/protocol" - XMLNS_ME = "http://salmon-protocol.org/ns/magic-env" - APPLICATION_XML = "application/xml" - BASE64_URL = "base64url" - RSA_SHA256 = "RSA-SHA256" + CONTENT_TYPE_ENVELOPE = "application/magic-envelope+xml" + CONTENT_TYPE_JSON = "application/json" + CONTENT_TYPE_JSONLD = "application/ld+json" + CONTENT_TYPE_TEXTHTML = "text/html" + CONTENT_TYPE_XRDXML = "application/xrd+xml" + USER_AGENT = "GangGo/v0 (Federation library)" +) + +const ( + LOG_C_TUR = "\033[0;36m" + LOG_C_RED = "\033[31m" + LOG_C_YELLOW = "\033[33m" + LOG_C_RESET = "\033[0m" +) + +const ( + HttpApHeader = "x-ap-object" + + // protocols + DiasporaProtocol Protocol = 100 + iota + ActivityPubProtocol // entity names - Retraction = "retraction" - Profile = "profile" - StatusMessage = "status_message" - Reshare = "reshare" - Comment = "comment" - Like = "like" - Contact = "contact" + Retraction EntityType = "retraction" + Profile EntityType = "profile" + StatusMessage EntityType = "status_message" + Reshare EntityType = "reshare" + Comment EntityType = "comment" + Like EntityType = "like" + Contact EntityType = "contact" + Unknown EntityType = "unknown" // webfinger WebFingerOstatus = "http://ostatus.org/schema/1.0/subscribe" WebFingerHcard = "http://microformats.org/profile/hcard" + WebFingerDiaspora = "http://joindiaspora.com/seed_location" + WebFingerSelf = "self" // signatures SignatureDelimiter = "." SignatureAuthorDelimiter = ";" + SignatureHTTPDelimiter = "\n" +) + +// Diaspora +const ( + XMLNS = "https://joindiaspora.com/protocol" + XMLNS_ME = "http://salmon-protocol.org/ns/magic-env" + APPLICATION_XML = "application/xml" + BASE64_URL = "base64url" + RSA_SHA256 = "RSA-SHA256" +) + +// ActivityPub +const ( + ACTIVITY_STREAMS = "https://www.w3.org/ns/activitystreams" + ACTIVITY_STREAMS_PUBLIC = ACTIVITY_STREAMS + "#Public" + + ActivityTypeUndo = "Undo" + ActivityTypeFollow = "Follow" + ActivityTypeDelete = "Delete" + ActivityTypeAccept = "Accept" + ActivityTypeCreate = "Create" + ActivityTypeUpdate = "Update" + ActivityTypeLike = "Like" + ActivityTypeNote = "Note" + ActivityTypePerson = "Person" + ActivityTypeCollection = "Collection" + ActivityTypeOrderedCollection = "OrderedCollection" + ActivityTypeCollectionPage = "CollectionPage" + ActivityTypeOrderedCollectionPage = "OrderedCollectionPage" ) diff --git a/entity_contact.go b/contact.go similarity index 66% rename from entity_contact.go rename to contact.go index 0691657e9a865ea35823ae31fe1a43165c521764..cc703415cdb0e7917d0a3d9067eafba439965f97 100644 --- a/entity_contact.go +++ b/contact.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,12 +17,11 @@ package federation // along with this program. If not, see . // -import "github.com/Zauberstuhl/go-xml" - -type EntityContact struct { - XMLName xml.Name `xml:"contact"` - Author string `xml:"author"` - Recipient string `xml:"recipient"` - Following bool `xml:"following"` - Sharing bool `xml:"sharing"` +// MessageContact represents a state with another person it inherits MessageBase +type MessageContact interface { + MessageBase + Recipient() string + SetRecipient(string) + Sharing() bool + SetSharing(bool) } diff --git a/contact_test.go b/contact_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a30e2baecc6921b6594925195c1b4a7e4f064cd5 --- /dev/null +++ b/contact_test.go @@ -0,0 +1,123 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "fmt" + "io/ioutil" + "testing" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" +) + +func TestContact(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + friends := []string{"friend1@host.tld", "friend2@host.tld"} + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol, ActivityPubProtocol} { + contact, err := NewMessageContact(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + contact.SetAuthor(author) + contact.SetRecipient(friends[0]) + contact.SetSharing(true) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d: Something went wrong!", i) + } + + msgContact, ok := msg.Entity().(MessageContact) + if !ok { + t.Errorf("#%d: Expected MessageContact, got %v", i, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgContact.Author() != author { + t.Errorf("#%d: Expected %s, got %s", i, author, msgContact.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgContact.Author() != linkAuthor { + t.Errorf("#%d: Expected %s, got %s", i, linkAuthor, msgContact.Author()) + } + + if msgContact.Type().Proto != proto { + t.Errorf("#%d: Expected %s, got %s", i, proto, msgContact.Type().Proto) + } + + if msgContact.Type().Entity != Contact { + t.Errorf("#%d: Expected %s, got %s", i, Contact, msgContact.Type().Entity) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d: Expected a valid signature", i) + } + + if msgContact.Recipient() != friends[0] { + t.Errorf("#%d: Expected %s, got %s", i, friends[0], msgContact.Recipient()) + } + + if !msgContact.Sharing() { + t.Errorf("#%d: Expected true, got false") + } + + w.WriteHeader(http.StatusOK) + }, + )) + defer ts.Close() + + err = contact.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } +} diff --git a/aes.go b/dia_aes.go similarity index 88% rename from aes.go rename to dia_aes.go index 4970854a64a2f7f0aa38c658312d46a50a13897c..7801258784cf2b51ca33e32ca9b1f9aa54979f61 100644 --- a/aes.go +++ b/dia_aes.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -28,18 +28,18 @@ import ( "crypto/rand" ) -type Aes struct { +type DiasporaAes struct { Key string `json:"key,omitempty"` Iv string `json:"iv,omitempty"` Data string `json:"-"` } -type AesWrapper struct { +type DiasporaAesWrapper struct { AesKey string `json:"aes_key"` MagicEnvelope string `json:"encrypted_magic_envelope"` } -func (a *Aes) Generate() error { +func (a *DiasporaAes) Generate() error { // The key argument should be the AES key, // either 16, 24, or 32 bytes to select // AES-128, AES-192, or AES-256. @@ -61,7 +61,7 @@ func (a *Aes) Generate() error { return nil } -func (a *Aes) Encrypt(data []byte) error { +func (a *DiasporaAes) Encrypt(data []byte) error { // CBC mode works on blocks so plaintexts may need to be padded to the // next whole block. For an example of such padding, see // https://tools.ietf.org/html/rfc5246#section-6.2.3.2. @@ -93,7 +93,7 @@ func (a *Aes) Encrypt(data []byte) error { return nil } -func (a Aes) Decrypt() (ciphertext []byte, err error) { +func (a DiasporaAes) Decrypt() (ciphertext []byte, err error) { key, err := base64.StdEncoding.DecodeString(a.Key) if err != nil { return ciphertext, err @@ -120,7 +120,7 @@ func (a Aes) Decrypt() (ciphertext []byte, err error) { return ciphertext, nil } -func (w AesWrapper) Decrypt(privKey *rsa.PrivateKey) (entityXML []byte, err error) { +func (w DiasporaAesWrapper) Decrypt(privKey *rsa.PrivateKey) (entityXML []byte, err error) { encryptedAesKey, err := base64.StdEncoding.DecodeString(w.AesKey) if err != nil { return @@ -131,7 +131,7 @@ func (w AesWrapper) Decrypt(privKey *rsa.PrivateKey) (entityXML []byte, err erro return } - var aes Aes + var aes DiasporaAes err = json.Unmarshal(decryptedAesKey, &aes) if err != nil { return diff --git a/aes_test.go b/dia_aes_test.go similarity index 93% rename from aes_test.go rename to dia_aes_test.go index c76452c15a755979dbe527060d70f3f40493d00c..b9ba64e77265afeedd2487252cbd73315d35566d 100644 --- a/aes_test.go +++ b/dia_aes_test.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,9 +23,9 @@ import ( "strings" ) -func TestAes(t *testing.T) { +func TestDiasporaAes(t *testing.T) { var ( - aes Aes + aes DiasporaAes expected = "Hello World" ) diff --git a/dia_entity_comment.go b/dia_entity_comment.go new file mode 100644 index 0000000000000000000000000000000000000000..0fbaab4f33597dbf8538404d7ad48d2d9142c026 --- /dev/null +++ b/dia_entity_comment.go @@ -0,0 +1,167 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + helpers "git.feneas.org/ganggo/federation/helpers" + "time" + "bytes" +) + +type DiasporaComment struct { + XMLName xml.Name `xml:"comment"` + EntityAuthor string `xml:"author"` + EntityCreatedAt helpers.Time `xml:"created_at"` + EntityGuid string `xml:"guid"` + EntityParentGuid string `xml:"parent_guid"` + EntityText string `xml:"text"` + EntityAuthorSignature string `xml:"author_signature"` + + // store relayable signature order + EntitySignatureOrder string `xml:"-"` + // store original entity in case we relay it + EntityRaw []byte `xml:"-"` +} + +func (e *DiasporaComment) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaComment) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaComment) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + public := len(inbox) > 6 && inbox[len(inbox)-6:] == "public" + if public { + pub = nil + } + + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if public { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaComment) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) (payload []byte, err error) { + if len(e.EntityRaw) > 0 { + // that is a relay do not marshal + // and use the old payload + payload = e.EntityRaw + } else { + payload, err = xml.MarshalIndent(e, "", " ") + if err != nil { + return + } + payload, err = DiasporaSortByOrder(e.EntitySignatureOrder, payload) + if err != nil { + return + } + } + + if pub != nil { + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaComment) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaComment) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Comment, + } +} + +func (e *DiasporaComment) Recipients() []string { return []string{} } + +func (e *DiasporaComment) SetRecipients(recipients []string) {} + +func (e *DiasporaComment) Guid() string { + return e.EntityGuid +} + +func (e *DiasporaComment) SetGuid(guid string) { + (*e).EntityGuid = guid +} + +func (e *DiasporaComment) Parent() string { + return e.EntityParentGuid +} + +func (e *DiasporaComment) SetParent(guid string) { + (*e).EntityParentGuid = guid +} + +func (e *DiasporaComment) Signature() string { + return e.EntityAuthorSignature +} + +func (e *DiasporaComment) SetSignature(priv *rsa.PrivateKey) error { + signature := (&signature{}).New(e.signatureText(""), SignatureAuthorDelimiter) + return signature.Sign(priv, &e.EntityAuthorSignature) +} + +func (e *DiasporaComment) SignatureOrder() string { + return e.EntitySignatureOrder +} + +func (e *DiasporaComment) signatureText(order string) []string { + if order != "" { + return helpers.ExractSignatureText(order, e) + } + return []string{ + e.EntityAuthor, + e.EntityCreatedAt.String(), + e.EntityGuid, + e.EntityParentGuid, + e.EntityText, + } +} + +func (e *DiasporaComment) CreatedAt() helpers.Time { + return e.EntityCreatedAt +} + +func (e *DiasporaComment) SetCreatedAt(createdAt time.Time) { + e.EntityCreatedAt.New(createdAt) +} + +func (e *DiasporaComment) Text() string { + return e.EntityText +} + +func (e *DiasporaComment) SetText(text string) { + (*e).EntityText = text +} diff --git a/dia_entity_contact.go b/dia_entity_contact.go new file mode 100644 index 0000000000000000000000000000000000000000..36cd4b860ca4397b37121fa70d34adf3860ef376 --- /dev/null +++ b/dia_entity_contact.go @@ -0,0 +1,103 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + "bytes" +) + +type DiasporaContact struct { + XMLName xml.Name `xml:"contact"` + EntityAuthor string `xml:"author"` + EntityRecipient string `xml:"recipient"` + EntityFollowing bool `xml:"following"` + EntitySharing bool `xml:"sharing"` + EntityBlocking bool `xml:"blocking"` +} + +func (e *DiasporaContact) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaContact) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaContact) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + public := len(inbox) > 6 && inbox[len(inbox)-6:] == "public" + if public { + pub = nil + } + + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if public { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaContact) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + payload, err := xml.MarshalIndent(e, "", " ") + if err != nil { + return payload, err + } + if pub != nil { + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaContact) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaContact) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Contact, + } +} + +func (e *DiasporaContact) Recipient() string { + return e.EntityRecipient +} + +func (e *DiasporaContact) SetRecipient(recipient string) { + (*e).EntityRecipient = recipient +} + +func (e *DiasporaContact) Sharing() bool { + return e.EntitySharing && e.EntityFollowing +} + +func (e *DiasporaContact) SetSharing(sharing bool) { + (*e).EntitySharing = sharing + (*e).EntityFollowing = sharing +} diff --git a/dia_entity_like.go b/dia_entity_like.go new file mode 100644 index 0000000000000000000000000000000000000000..7c4d75526c98d0069752ca6348f9db58b33c6c4d --- /dev/null +++ b/dia_entity_like.go @@ -0,0 +1,162 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "bytes" + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + helpers "git.feneas.org/ganggo/federation/helpers" +) + +type DiasporaLike struct { + XMLName xml.Name `xml:"like"` + EntityPositive bool `xml:"positive"` + EntityGuid string `xml:"guid"` + EntityParentGuid string `xml:"parent_guid"` + EntityParentType string `xml:"parent_type"` + EntityAuthor string `xml:"author"` + EntityAuthorSignature string `xml:"author_signature"` + + // store relayable signature order + EntitySignatureOrder string `xml:"-"` + // store original entity in case we relay it + EntityRaw []byte `xml:"-"` +} + +func (e *DiasporaLike) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaLike) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaLike) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + public := len(inbox) > 6 && inbox[len(inbox)-6:] == "public" + if public { + pub = nil + } + + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if public { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaLike) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) (payload []byte, err error) { + if len(e.EntityRaw) > 0 { + // that is a relay do not marshal + // and use the old payload + payload = e.EntityRaw + } else { + payload, err = xml.MarshalIndent(e, "", " ") + if err != nil { + return + } + payload, err = DiasporaSortByOrder(e.EntitySignatureOrder, payload) + if err != nil { + return + } + } + + if pub != nil { + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaLike) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaLike) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Like, + } +} + +func (e *DiasporaLike) Guid() string { + return e.EntityGuid +} + +func (e *DiasporaLike) SetGuid(guid string) { + (*e).EntityGuid = guid +} + +func (e *DiasporaLike) Parent() string { + return e.EntityParentGuid +} + +func (e *DiasporaLike) SetParent(guid string) { + (*e).EntityParentGuid = guid + // XXX see models.ShareablePost depending on AP + // implementation we should change this + (*e).EntityParentType = "Post" +} + +func (e *DiasporaLike) Signature() string { + return e.EntityAuthorSignature +} + +func (e *DiasporaLike) SetSignature(priv *rsa.PrivateKey) error { + signature := (&signature{}).New(e.signatureText(""), SignatureAuthorDelimiter) + return signature.Sign(priv, &e.EntityAuthorSignature) +} + +func (e *DiasporaLike) SignatureOrder() string { + return e.EntitySignatureOrder +} + +func (e *DiasporaLike) signatureText(order string) []string { + if order != "" { + return helpers.ExractSignatureText(order, e) + } + + positive := "false" + if e.EntityPositive { + positive = "true" + } + return []string{ + positive, + e.EntityGuid, + e.EntityParentGuid, + e.EntityParentType, + e.EntityAuthor, + } +} + +func (e *DiasporaLike) Positive() bool { + return e.EntityPositive +} + +func (e *DiasporaLike) SetPositive(positive bool) { + (*e).EntityPositive = positive +} diff --git a/dia_entity_post.go b/dia_entity_post.go new file mode 100644 index 0000000000000000000000000000000000000000..4bb789d56fd64ba85cab6a6b0a76a5f1778bf039 --- /dev/null +++ b/dia_entity_post.go @@ -0,0 +1,186 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "bytes" + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + helpers "git.feneas.org/ganggo/federation/helpers" + "time" +) + +type DiasporaStatusMessage struct { + XMLName xml.Name `xml:"status_message"` + EntityAuthor string `xml:"author"` + EntityGuid string `xml:"guid"` + EntityCreatedAt helpers.Time `xml:"created_at"` + EntityProviderName string `xml:"provider_display_name"` + EntityText string `xml:"text,omitempty"` + EntityPhotos *DiasporaPhotos `xml:"photo,omitempty"` + EntityLocation *DiasporaLocation `xml:"location,omitempty"` + EntityPoll *DiasporaPoll `xml:"poll,omitempty"` + EntityPublic bool `xml:"public"` + EntityEvent *DiasporaEvent `xml:"event,omitempty"` +} + +type DiasporaPhoto struct { + Guid string `xml:"guid"` + Author string `xml:"author"` + Public bool `xml:"public"` + CreatedAt helpers.Time `xml:"created_at"` + RemotePhotoPath string `xml:"remote_photo_path"` + RemotePhotoName string `xml:"remote_photo_name"` + Text string `xml:"text"` + StatusMessageGuid string `xml:"status_message_guid"` + Height int `xml:"height"` + Width int `xml:"width"` +} + +type DiasporaPhotos []DiasporaPhoto + +type DiasporaLocation struct { + Address string `xml:"address"` + Lat string `xml:"lat"` + Lng string `xml:"lng"` +} + +type DiasporaPoll struct { + Guid string `xml:"guid"` + Question string `xml:"question"` + PollAnswers []DiasporaPollAnswer `xml:"poll_answers"` +} + +type DiasporaPollAnswer struct { + Guid string `xml:"guid"` + Answer string `xml:"answer"` +} + +type DiasporaEvent struct { + Author string `xml:"author"` + Guid string `xml:"guid"` + Summary string `xml:"summary"` + Start helpers.Time `xml:"start"` + End helpers.Time `xml:"end"` + AllDay bool `xml:"all_day"` + Timezone string `xml:"timezone"` + Description string `xml:"description"` + Location *DiasporaLocation `xml:"location,omitempty"` +} + +func (e *DiasporaStatusMessage) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaStatusMessage) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaStatusMessage) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if e.EntityPublic { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaStatusMessage) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + payload, err := xml.MarshalIndent(e, "", " ") + if err != nil { + return payload, err + } + if e.EntityPublic { + return DiasporaMagicEnvelope(priv, e.Author(), payload) + } + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) +} + +func (e *DiasporaStatusMessage) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaStatusMessage) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: StatusMessage, + } +} + +func (e *DiasporaStatusMessage) Recipients() []string { return []string{} } + +func (e *DiasporaStatusMessage) SetRecipients(recipient []string) {} + +func (e *DiasporaStatusMessage) Guid() string { + return e.EntityGuid +} + +func (e *DiasporaStatusMessage) SetGuid(guid string) { + (*e).EntityGuid = guid +} + +func (e *DiasporaStatusMessage) CreatedAt() helpers.Time { + return e.EntityCreatedAt +} + +func (e *DiasporaStatusMessage) SetCreatedAt(createdAt time.Time) { + e.EntityCreatedAt.New(createdAt) +} + +func (e *DiasporaStatusMessage) Provider() string { + return e.EntityProviderName +} + +func (e *DiasporaStatusMessage) SetProvider(provider string) { + (*e).EntityProviderName = provider +} + +func (e *DiasporaStatusMessage) Text() string { + return e.EntityText +} + +func (e *DiasporaStatusMessage) SetText(text string) { + (*e).EntityText = text +} + +func (e *DiasporaStatusMessage) Public() bool { + return e.EntityPublic +} + +func (e *DiasporaStatusMessage) SetPublic(public bool) { + (*e).EntityPublic = public +} + + +func (e *DiasporaStatusMessage) FilePaths() (result []string) { + if e.EntityPhotos != nil { + for _, photo := range *e.EntityPhotos { + result = append(result, photo.RemotePhotoPath + photo.RemotePhotoName) + } + } + return +} diff --git a/dia_entity_profile.go b/dia_entity_profile.go new file mode 100644 index 0000000000000000000000000000000000000000..b8aafcb1165a723623f00dc8664b58327f1ca296 --- /dev/null +++ b/dia_entity_profile.go @@ -0,0 +1,131 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + "net/http" + "bytes" +) + +type DiasporaProfile struct { + XMLName xml.Name `xml:"profile"` + EntityAuthor string `xml:"author"` + EntityFirstName string `xml:"first_name"` + EntityLastName string `xml:"last_name"` + EntityImageUrl string `xml:"image_url"` + EntityImageUrlMedium string `xml:"image_url_medium"` + EntityImageUrlSmall string `xml:"image_url_small"` + EntityBirthday string `xml:"birthday"` + EntityGender string `xml:"gender"` + EntityBio string `xml:"bio"` + EntityLocation string `xml:"location"` + EntitySearchable bool `xml:"searchable"` + EntityPublic bool `xml:"public"` + EntityNsfw bool `xml:"nsfw"` + EntityTagString string `xml:"tag_string"` +} + +func (e *DiasporaProfile) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaProfile) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaProfile) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + public := len(inbox) > 6 && inbox[len(inbox)-6:] == "public" + if public { + pub = nil + } + + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if public { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaProfile) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + payload, err := xml.MarshalIndent(e, "", " ") + if err != nil { + return payload, err + } + if pub != nil { + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaProfile) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaProfile) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Profile, + } +} + +func (e *DiasporaProfile) FirstName() string { + return e.EntityFirstName +} + +func (e *DiasporaProfile) LastName() string { + return e.EntityLastName +} + +func (e *DiasporaProfile) ImageUrl() string { + return e.EntityImageUrl +} + +func (e *DiasporaProfile) Birthday() string { + return e.EntityBirthday +} + +func (e *DiasporaProfile) Gender() string { + return e.EntityGender +} + +func (e *DiasporaProfile) Bio() string { + return e.EntityBio +} + +func (e *DiasporaProfile) Location() string { + return e.EntityLocation +} + +func (e *DiasporaProfile) Public() bool { + return e.EntityPublic +} + +func (e *DiasporaProfile) Nsfw() bool { + return e.EntityNsfw +} diff --git a/dia_entity_reshare.go b/dia_entity_reshare.go new file mode 100644 index 0000000000000000000000000000000000000000..18a0c289da4b6fc7078d4856822072137a290181 --- /dev/null +++ b/dia_entity_reshare.go @@ -0,0 +1,107 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "net/http" + "bytes" + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + helpers "git.feneas.org/ganggo/federation/helpers" + "time" +) + +type DiasporaReshare struct { + XMLName xml.Name `xml:"reshare"` + EntityAuthor string `xml:"author"` + EntityGuid string `xml:"guid"` + EntityCreatedAt helpers.Time `xml:"created_at"` + EntityRootAuthor string `xml:"root_author"` + EntityRootGuid string `xml:"root_guid"` +} + +func (e *DiasporaReshare) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaReshare) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaReshare) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + payload, err := e.Marshal(priv, nil) + if err != nil { + return err + } + + client := HttpClient{} + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaReshare) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + payload, err := xml.MarshalIndent(e, "", " ") + if err != nil { + return payload, err + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaReshare) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaReshare) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Reshare, + } +} + +func (e *DiasporaReshare) Guid() string { + return e.EntityGuid +} + +func (e *DiasporaReshare) SetGuid(guid string) { + (*e).EntityGuid = guid +} + +func (e *DiasporaReshare) Parent() string { + return e.EntityRootGuid +} + +func (e *DiasporaReshare) SetParent(guid string) { + (*e).EntityRootGuid = guid +} + +func (e *DiasporaReshare) ParentAuthor() string { + return e.EntityRootAuthor +} + +func (e *DiasporaReshare) SetParentAuthor(author string) { + (*e).EntityRootAuthor = author +} + +func (e *DiasporaReshare) CreatedAt() helpers.Time { + return e.EntityCreatedAt +} + +func (e *DiasporaReshare) SetCreatedAt(createdAt time.Time) { + e.EntityCreatedAt.New(createdAt) +} diff --git a/dia_entity_retraction.go b/dia_entity_retraction.go new file mode 100644 index 0000000000000000000000000000000000000000..289b34f81aafd349936f09b1c16bcbe45372c2c2 --- /dev/null +++ b/dia_entity_retraction.go @@ -0,0 +1,130 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "crypto/rsa" + "github.com/Zauberstuhl/go-xml" + "net/http" + "bytes" +) + +type DiasporaRetraction struct { + XMLName xml.Name `xml:"retraction"` + EntityAuthor string `xml:"author"` + EntityTargetGuid string `xml:"target_guid"` + EntityTargetType string `xml:"target_type"` +} + +func (e *DiasporaRetraction) Author() string { + return e.EntityAuthor +} + +func (e *DiasporaRetraction) SetAuthor(author string) { + (*e).EntityAuthor = author +} + +func (e *DiasporaRetraction) Send(inbox string, priv *rsa.PrivateKey, pub *rsa.PublicKey) error { + public := len(inbox) > 6 && inbox[len(inbox)-6:] == "public" + if public { + pub = nil + } + + payload, err := e.Marshal(priv, pub) + if err != nil { + return err + } + + client := HttpClient{} + if public { + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_ENVELOPE}, + }, bytes.NewBuffer(payload)) + } + return client.Push(inbox, http.Header{ + "Content-Type": []string{CONTENT_TYPE_JSON}, + }, bytes.NewBuffer(payload)) +} + +func (e *DiasporaRetraction) Marshal(priv *rsa.PrivateKey, pub *rsa.PublicKey) ([]byte, error) { + payload, err := xml.MarshalIndent(e, "", " ") + if err != nil { + return payload, err + } + if pub != nil { + return DiasporaEncryptedMagicEnvelope(priv, pub, e.Author(), payload) + } + return DiasporaMagicEnvelope(priv, e.Author(), payload) +} + +func (e *DiasporaRetraction) Unmarshal(b []byte) error { + return xml.Unmarshal(b, &e) +} + +func (e *DiasporaRetraction) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, + Entity: Retraction, + } +} + +func (e *DiasporaRetraction) ParentGuid() string { + return e.EntityTargetGuid +} + +func (e *DiasporaRetraction) SetParentGuid(guid string) { + (*e).EntityTargetGuid = guid +} + +func (e *DiasporaRetraction) ParentType() EntityType { + switch e.EntityTargetType { + case "Profile": + return Profile + case "Reshare": + return Reshare + case "StatusMessage": + fallthrough + case "Post": + return StatusMessage + case "Contact": + return Contact + case "Like": + return Like + case "Comment": + return Comment + } + // fallback to normal post + return StatusMessage +} + +func (e *DiasporaRetraction) SetParentType(parentType EntityType) { + switch parentType { + case Profile: + (*e).EntityTargetType = "Profile" + case Reshare: + (*e).EntityTargetType = "Reshare" + case StatusMessage: + (*e).EntityTargetType = "Post" + case Contact: + (*e).EntityTargetType = "Contact" + case Like: + (*e).EntityTargetType = "Like" + case Comment: + (*e).EntityTargetType = "Comment" + } +} diff --git a/magic.go b/dia_magic.go similarity index 63% rename from magic.go rename to dia_magic.go index cf9ff48c87dce7a83a0e875ef2377b391162fb1f..e34cb148a8fa4005e14724ecb94522e88cc61cc4 100644 --- a/magic.go +++ b/dia_magic.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -25,13 +25,13 @@ import ( "crypto/rand" ) -func MagicEnvelope(privKey *rsa.PrivateKey, handle string, plainXml []byte) (payload []byte, err error) { - logger.Info("MagicEnvelope with", string(plainXml), "for", handle) +func DiasporaMagicEnvelope(privKey *rsa.PrivateKey, handle string, plainXml []byte) (payload []byte, err error) { + Log.Info("DiasporaMagicEnvelope with", string(plainXml), "for", handle) data := base64.URLEncoding.EncodeToString(plainXml) keyId := base64.URLEncoding.EncodeToString([]byte(handle)) - xmlBody := Message{} + xmlBody := DiasporaMessage{} xmlBody.Data.Type = APPLICATION_XML xmlBody.Data.Data = data xmlBody.Me = XMLNS_ME @@ -39,33 +39,32 @@ func MagicEnvelope(privKey *rsa.PrivateKey, handle string, plainXml []byte) (pay xmlBody.Alg = RSA_SHA256 xmlBody.Sig.KeyId = keyId - var signature Signature - err = signature.New(xmlBody).Sign(privKey, - &(xmlBody.Sig.Sig)) + signature := (&signature{}).New(xmlBody.signatureText(), SignatureDelimiter) + err = signature.Sign(privKey, &(xmlBody.Sig.Sig)) if err != nil { - logger.Warn(err) + Log.Warn(err) return } payload, err = xml.MarshalIndent(xmlBody, "", " ") if err != nil { - logger.Warn(err) + Log.Warn(err) return } - logger.Info("MagicEnvelope payload", string(payload)) + Log.Info("DiasporaMagicEnvelope payload", string(payload)) return } -func EncryptedMagicEnvelope(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, handle string, serializedXml []byte) (payload []byte, err error) { - logger.Info("EncryptedMagicEnvelope with", string(serializedXml), "for", handle) +func DiasporaEncryptedMagicEnvelope(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, handle string, serializedXml []byte) (payload []byte, err error) { + Log.Info("DiasporaEncryptedMagicEnvelope with", string(serializedXml), "for", handle) - var aesKeySet Aes - var aesWrapper AesWrapper + var aesKeySet DiasporaAes + var aesWrapper DiasporaAesWrapper data := base64.URLEncoding.EncodeToString(serializedXml) keyId := base64.URLEncoding.EncodeToString([]byte(handle)) - envelope := Message{ + envelope := DiasporaMessage{ Me: XMLNS_ME, Encoding: BASE64_URL, Alg: RSA_SHA256, @@ -74,36 +73,35 @@ func EncryptedMagicEnvelope(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, hand envelope.Data.Data = data envelope.Sig.KeyId = keyId - var signature Signature - err = signature.New(envelope).Sign(privKey, - &(envelope.Sig.Sig)) + signature := (&signature{}).New(envelope.signatureText(), SignatureDelimiter) + err = signature.Sign(privKey, &(envelope.Sig.Sig)) if err != nil { - logger.Warn(err) + Log.Warn(err) return } // Generate a new AES key pair err = aesKeySet.Generate() if err != nil { - logger.Warn(err) + Log.Warn(err) return } // payload with aes encryption payload, err = xml.MarshalIndent(envelope, "", " ") if err != nil { - logger.Warn(err) + Log.Warn(err) return } - logger.Info( - "EncryptedMagicEnvelope payload with aes encryption", + Log.Info( + "DiasporaEncryptedMagicEnvelope payload with aes encryption", string(payload), ) err = aesKeySet.Encrypt(payload) if err != nil { - logger.Warn(err) + Log.Warn(err) return } aesWrapper.MagicEnvelope = aesKeySet.Data @@ -111,25 +109,25 @@ func EncryptedMagicEnvelope(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, hand // aes with rsa encryption aesKeySetXml, err := json.MarshalIndent(aesKeySet, "", " ") if err != nil { - logger.Warn(err) + Log.Warn(err) return } - logger.Info("AES key-set XML", string(aesKeySetXml)) + Log.Info("AES key-set XML", string(aesKeySetXml)) aesKey, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, aesKeySetXml) if err != nil { - logger.Warn(err) + Log.Warn(err) return } aesWrapper.AesKey = base64.StdEncoding.EncodeToString(aesKey) payload, err = json.MarshalIndent(aesWrapper, "", " ") if err != nil { - logger.Warn(err) + Log.Warn(err) return } - logger.Info("EncryptedMagicEnvelope payload", string(payload)) + Log.Info("DiasporaEncryptedMagicEnvelope payload", string(payload)) return } diff --git a/entities.go b/dia_message.go similarity index 54% rename from entities.go rename to dia_message.go index e3cf72a52d6dcaa0730a133acbb2edb8cf1d3546..9b180df3db5078cb9f81134aeaa4bae87d19190b 100644 --- a/entities.go +++ b/dia_message.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -21,11 +21,11 @@ import ( "errors" "github.com/Zauberstuhl/go-xml" "encoding/base64" - "time" "strings" + "crypto/rsa" ) -type Message struct { +type DiasporaMessage struct { XMLName xml.Name `xml:"me:env"` Me string `xml:"me,attr"` Data struct { @@ -40,144 +40,149 @@ type Message struct { Sig string `xml:",chardata"` KeyId string `xml:"key_id,attr,omitempty"` } + entity DiasporaEntity `xml:"-"` } -type Entity struct { +type DiasporaEntity struct { XMLName xml.Name // Use custom unmarshaler for xml fetch XMLName // and decide which entity to use Type string `xml:"-"` SignatureOrder string `xml:"-"` - Data interface{} `xml:"-"` + Data MessageBase `xml:"-"` + DataRaw []byte `xml:"-"` } -type Time string - -func (m Message) Signature() string { - return m.Sig.Sig +func (message DiasporaMessage) ValidSignature(pub *rsa.PublicKey) bool { + signature := (&signature{}).New(message.signatureText(), SignatureDelimiter) + return signature.Verify(pub, message.Sig.Sig) } -func (m Message) SignatureText(order string) []string { +func (m DiasporaMessage) signatureText() []string { return []string{ m.Data.Data, - base64.StdEncoding.EncodeToString([]byte(m.Data.Type)), - base64.StdEncoding.EncodeToString([]byte(m.Encoding)), - base64.StdEncoding.EncodeToString([]byte(m.Alg)), + base64.URLEncoding.EncodeToString([]byte(m.Data.Type)), + base64.URLEncoding.EncodeToString([]byte(m.Encoding)), + base64.URLEncoding.EncodeToString([]byte(m.Alg)), + } +} + +func (m DiasporaMessage) Type() MessageType { + return MessageType{ + Proto: DiasporaProtocol, } } -func (message *Message) Parse() (entity Entity, err error) { +func (m DiasporaMessage) Entity() MessageBase { + return m.entity.Data +} + +func (message *DiasporaMessage) Parse() error { if !strings.EqualFold(message.Encoding, BASE64_URL) { - logger.Error("Encoding doesn't match", + Log.Error("Encoding doesn't match", "message", message.Encoding, "lib", BASE64_URL) - return entity, errors.New("Encoding doesn't match") + return errors.New("Encoding doesn't match") } if !strings.EqualFold(message.Alg, RSA_SHA256) { - logger.Error("Algorithm doesn't match", + Log.Error("Algorithm doesn't match", "message", message.Alg, "lib", RSA_SHA256) - return entity, errors.New("Algorithm doesn't match") + return errors.New("Algorithm doesn't match") } keyId, err := base64.StdEncoding.DecodeString(message.Sig.KeyId) if err != nil { - logger.Error("Cannot decode signature key ID", "err", err) - return entity, err + Log.Error("Cannot decode signature key ID", "err", err) + return err } message.Sig.KeyId = string(keyId) - logger.Info("Entity sender", message.Sig.KeyId) + Log.Info("Entity sender", message.Sig.KeyId) data, err := base64.URLEncoding.DecodeString(message.Data.Data) if err != nil { - logger.Error("Cannot decode message data", "err", err) - return entity, err + Log.Error("Cannot decode message data", "err", err) + return err } - logger.Info("Entity raw", string(data)) + message.entity.DataRaw = data + Log.Info("Entity raw", string(data)) - entity.SignatureOrder, err = FetchEntityOrder(data) + message.entity.SignatureOrder, err = DiasporaFetchOrder(data) if err != nil { - logger.Error("Cannot fetch entity order", "err", err) - return entity, err + Log.Error("Cannot fetch entity order", "err", err) + return err } - logger.Info("Entity order", entity.SignatureOrder) + Log.Info("Entity order", message.entity.SignatureOrder) - err = xml.Unmarshal(data, &entity) + err = xml.Unmarshal(data, &message.entity) if err != nil { - logger.Error("Cannot unmarshal data", "err", err) - return entity, err + Log.Error("Cannot unmarshal data", "err", err) + return err } - return entity, nil -} - -func (t *Time) New(newTime time.Time) *Time { - *t = Time(newTime.UTC().Format(TIME_FORMAT)) - return t -} - -func (t Time) Time() (time.Time, error) { - return time.Parse(TIME_FORMAT, string(t)) -} - -func (t Time) String() string { - return string(t) + return nil } -func (e *Entity) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (e *DiasporaEntity) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // NOTE since the encoder ignores the interface type // (see https://golang.org/src/encoding/xml/read.go#L377) // we have to decode on every single step - switch local := start.Name.Local; local { + switch local := EntityType(start.Name.Local); local { case Retraction: - content := EntityRetraction{} + content := &DiasporaRetraction{} if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case Profile: - content := EntityProfile{} + content := &DiasporaProfile{} if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case StatusMessage: - content := EntityStatusMessage{} + content := &DiasporaStatusMessage{} if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case Reshare: - content := EntityReshare{} + content := &DiasporaReshare{} if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case Comment: - content := EntityComment{} + content := &DiasporaComment{ + EntityRaw: e.DataRaw, + EntitySignatureOrder: e.SignatureOrder, + } if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case Like: - content := EntityLike{} + content := &DiasporaLike{ + EntityRaw: e.DataRaw, + EntitySignatureOrder: e.SignatureOrder, + } if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content case Contact: - content := EntityContact{} + content := &DiasporaContact{} if err := d.DecodeElement(&content, &start); err != nil { return err } - (*e).Type = local + (*e).Type = string(local) (*e).Data = content default: - return errors.New("Entity '" + local + "' not known!") + return errors.New("Entity '" + string(local) + "' not known!") } return nil } diff --git a/entities_test.go b/dia_message_test.go similarity index 55% rename from entities_test.go rename to dia_message_test.go index d2f3e85322b091b75cf179cc44ffba0e1cdd4d27..44a1ad3f037a8e6174cfdc7e4c3afb47d5b37abc 100644 --- a/entities_test.go +++ b/dia_message_test.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,54 +19,18 @@ package federation import ( "testing" - "reflect" + "git.feneas.org/ganggo/federation/helpers" "github.com/Zauberstuhl/go-xml" ) -func TestEntitiesUnmarshalXML(t *testing.T) { - var entity Entity - var tests = []struct{ - Type string - Raw []byte - }{ - {Type: "EntityRetraction", Raw: []byte(``)}, - {Type: "EntityProfile", Raw: []byte(``)}, - {Type: "EntityStatusMessage", Raw: []byte(``)}, - {Type: "EntityReshare", Raw: []byte(``)}, - {Type: "EntityComment", Raw: []byte(``)}, - {Type: "EntityLike", Raw: []byte(``)}, - {Type: "EntityContact", Raw: []byte(``)}, - } - - for i, test := range tests { - err := xml.Unmarshal(test.Raw, &entity) - if err != nil { - t.Errorf("#%d: Some error occured while parsing: %v", i, err) - } - name := reflect.TypeOf(entity.Data).Name() - if test.Type != name { - t.Errorf("#%d: Expected to be '%s', got '%s'", i, test.Type, name) - } - err = xml.Unmarshal(test.Raw[:len(test.Raw)-1], &entity) - if err == nil { - t.Errorf("#%d: Expected an error, got nil", i) - } - } - - err := xml.Unmarshal([]byte(``), &entity) - if err == nil { - t.Errorf("Expected an error, got nil") - } -} - -func TestEntitiesTimeMarshalAndUnmarshal(t *testing.T) { +func TestDiaEntitiesTimeMarshalAndUnmarshal(t *testing.T) { // federation time format // 2006-01-02T15:04:05Z var time = "2018-01-19T01:32:23Z" var rawXml = ""; var origTime = struct { XMLName xml.Name `xml:"time"` - CreatedAt Time + CreatedAt helpers.Time }{} err := xml.Unmarshal([]byte(rawXml), &origTime) @@ -89,9 +53,9 @@ func TestEntitiesTimeMarshalAndUnmarshal(t *testing.T) { if err != nil { t.Errorf("Some error occured while parsing: %v", err) } - if timeTime.Format(TIME_FORMAT) != time { + if timeTime.Format(helpers.TIME_FORMAT) != time { t.Errorf("Expected to be '%s', got '%s'", - time, timeTime.Format(TIME_FORMAT)) + time, timeTime.Format(helpers.TIME_FORMAT)) } // XXX the application server uses time.Now if this happens diff --git a/salmon.go b/dia_salmon.go similarity index 59% rename from salmon.go rename to dia_salmon.go index eb7814dda4c2aee6da7f2b40d21b63cab69bead4..b17513e08d683e9f4bf8c36bc26a58c6fa1a0969 100644 --- a/salmon.go +++ b/dia_salmon.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,25 +19,33 @@ package federation import ( "crypto/rsa" + "encoding/json" "github.com/Zauberstuhl/go-xml" ) -func ParseDecryptedRequest(entityXML []byte) (message Message, entity Entity, err error) { - err = xml.Unmarshal(entityXML, &message) +func DiasporaParse(raw []byte) (message DiasporaMessage, err error) { + err = xml.Unmarshal(raw, &message) if err != nil { - logger.Error(err) + Log.Error(err) return } - entity, err = message.Parse() + err = message.Parse() return } -func ParseEncryptedRequest(wrapper AesWrapper, privKey *rsa.PrivateKey) (message Message, entity Entity, err error) { +func DiasporaParseEncrypted(raw []byte, privKey *rsa.PrivateKey) (message DiasporaMessage, err error) { + wrapper := &DiasporaAesWrapper{} + err = json.Unmarshal(raw, wrapper) + if err != nil { + Log.Error("Cannot parse request", "err", err) + return + } + entityXML, err := wrapper.Decrypt(privKey) if err != nil { - logger.Error(err) + Log.Error(err) return } - return ParseDecryptedRequest(entityXML) + return DiasporaParse(entityXML) } diff --git a/dia_salmon_test.go b/dia_salmon_test.go new file mode 100644 index 0000000000000000000000000000000000000000..81520d8178e541520c4d6417a095574f73b209cd --- /dev/null +++ b/dia_salmon_test.go @@ -0,0 +1,85 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" + "crypto/rsa" + "crypto/rand" +) + +func TestDiaSalmon(t *testing.T) { + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + handle := "test@host.tld" + tests := []struct{ + Expected EntityType + Xml []byte + }{ + {Expected: Comment, Xml: []byte(``)}, + {Expected: Like, Xml: []byte(``)}, + {Expected: Contact, Xml: []byte(``)}, + {Expected: StatusMessage, Xml: []byte(``)}, + {Expected: Profile, Xml: []byte(``)}, + {Expected: Reshare, Xml: []byte(``)}, + {Expected: Retraction, Xml: []byte(``)}, + {Expected: Unknown, Xml: []byte(``)}, + } + + for i, test := range tests { + salmon, err := DiasporaMagicEnvelope(privKey, handle, test.Xml) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + encSalmon, err := DiasporaEncryptedMagicEnvelope( + privKey, &privKey.PublicKey, handle, test.Xml) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + msg, err := DiasporaParse(salmon) + if test.Expected == Unknown && err == nil { + t.Errorf("#%d: Expected an error, got nothing", i) + } else if test.Expected != Unknown && err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + if test.Expected != Unknown && + msg.Entity().Type().Entity != test.Expected { + t.Errorf("#%d: Expected %s, got %s", i, test.Expected, + msg.Entity().Type().Entity) + } + + encMsg, err := DiasporaParseEncrypted(encSalmon, privKey) + if test.Expected == Unknown && err == nil { + t.Errorf("#%d: Expected an error, got nothing", i) + } else if test.Expected != Unknown && err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + if test.Expected != Unknown && + encMsg.Entity().Type().Entity != test.Expected { + t.Errorf("#%d: Expected %s, got %s", i, test.Expected, + encMsg.Entity().Type().Entity) + } + } +} diff --git a/sort.go b/dia_sort.go similarity index 74% rename from sort.go rename to dia_sort.go index 54a35fd1bab6959e4b8a46e997994bab804ed4b0..e6882a7c8de36d56e49928d98f11da19346b7273 100644 --- a/sort.go +++ b/dia_sort.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,24 +23,24 @@ import ( "sort" ) -type XmlOrder struct { +type DiasporaXmlOrder struct { XMLName xml.Name Order []string `xml:"-"` - Lines []XmlOrderLine `xml:",any"` + Lines []DiasporaXmlOrderLine `xml:",any"` } -type XmlOrderLine struct { +type DiasporaXmlOrderLine struct { XMLName xml.Name Value string `xml:",chardata"` } -func (order XmlOrder) Len() int { return len(order.Lines) } +func (order DiasporaXmlOrder) Len() int { return len(order.Lines) } -func (order XmlOrder) Swap(i, j int) { +func (order DiasporaXmlOrder) Swap(i, j int) { order.Lines[i], order.Lines[j] = order.Lines[j], order.Lines[i] } -func (order XmlOrder) Less(i, j int) bool { +func (order DiasporaXmlOrder) Less(i, j int) bool { for _, o := range order.Order { if order.Lines[i].XMLName.Local == o { return true @@ -54,14 +54,14 @@ func (order XmlOrder) Less(i, j int) bool { return false } -func (o *XmlOrderLine) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (o *DiasporaXmlOrderLine) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { o.XMLName = start.Name return d.DecodeElement(&(o.Value), &start) } -func FetchEntityOrder(entity []byte) (string, error) { +func DiasporaFetchOrder(entity []byte) (string, error) { var order []string - var xmlOrder XmlOrder + var xmlOrder DiasporaXmlOrder err := xml.Unmarshal(entity, &xmlOrder) if err != nil { return "", err @@ -77,13 +77,13 @@ func FetchEntityOrder(entity []byte) (string, error) { return strings.Join(order, " "), nil } -func SortByEntityOrder(order string, entity []byte) (sorted []byte, err error) { +func DiasporaSortByOrder(order string, entity []byte) (sorted []byte, err error) { // if we do not require sorting skip it if order == "" { return entity, err } - var xmlOrder XmlOrder + var xmlOrder DiasporaXmlOrder xmlOrder.Order = strings.Split(order, " ") err = xml.Unmarshal(entity, &xmlOrder) if err != nil { diff --git a/sort_test.go b/dia_sort_test.go similarity index 93% rename from sort_test.go rename to dia_sort_test.go index 25a9e170de68492158c6f3975e4ac186d0cfd02b..dbaa1dd601c3b22daf042b303bc873666a7d3efb 100644 --- a/sort_test.go +++ b/dia_sort_test.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -86,7 +86,7 @@ var tests = []TestContent { }, } -func TestFetchEntityOrder(t *testing.T) { +func TestDiasporaFetchOrder(t *testing.T) { var testsCopy []TestContent testsCopy = make([]TestContent, len(tests)) copy(testsCopy, tests) @@ -96,7 +96,7 @@ func TestFetchEntityOrder(t *testing.T) { testsCopy[2].Expected = "author guid created_at parent_guid text" for i, test := range testsCopy { - result, err := FetchEntityOrder([]byte(test.Entity)) + result, err := DiasporaFetchOrder([]byte(test.Entity)) if err != nil { t.Errorf("#%d: Some error occured while parsing: %v", i, err) } @@ -107,9 +107,9 @@ func TestFetchEntityOrder(t *testing.T) { } } -func TestSortByEntityOrder(t *testing.T) { +func TestDiasporaSortByOrder(t *testing.T) { for i, test := range tests { - result, err := SortByEntityOrder(test.Order, []byte(test.Entity)) + result, err := DiasporaSortByOrder(test.Order, []byte(test.Entity)) if err != nil { t.Errorf("#%d: Some error occured while parsing: %v", i, err) } diff --git a/doc.go b/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..e884278c9e5b0fd867660cb996c433b109fe9c96 --- /dev/null +++ b/doc.go @@ -0,0 +1,60 @@ +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Package federation is a library to communicate between fediverse protocols +// +// SENDING +// +// To send a entity to a fediverse server we simply have to construct one with +// the protocol we want to use and hit send! :) +// +// msg := federation.NewMessagePost(federation.DiasporaProtocol) +// msg.SetAuthor(author) +// msg.SetText(text) +// msg.SetGuid(guid) +// msg.SetPublic(public) +// msg.SetCreatedAt(createdAt) +// err = msg.Send(endpoint, privKey, pubKey) +// if err != nil { +// [...] +// } +// +// RECEIVING +// +// Diaspora +// +// To receive and parser diaspora requests `federation.DiasporaParse` +// and `federation.DiasporaParseEncrypted` is used. +// +// Diaspora has to main routes to receive entities: +// 1) /receive/public +// 2) /receive/private/:guid +// +// If we receive something via 1) it will be in a xml format and can be parsed +// without encryption in the first place. +// If we receive a entity via 2) we have to decrypt the xml first. This is +// better described here: https://diaspora.github.io/diaspora_federation/federation/encryption.html +// +// ActivityPub +// +// [...] +// +// +// After we succesfully parsed the request we will end up with a `Message` interface +// which includes the `MessageBase` and can be type casted to every entity message. +// Afterwards all information can be retrieved via abstraction methods e.g. entity.Author() +package federation diff --git a/entity_comment.go b/entity_comment.go deleted file mode 100644 index 179dd54e77d4a61b26b1d75db3f57c0de8a07090..0000000000000000000000000000000000000000 --- a/entity_comment.go +++ /dev/null @@ -1,47 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import "github.com/Zauberstuhl/go-xml" - -type EntityComment struct { - XMLName xml.Name `xml:"comment"` - Author string `xml:"author"` - CreatedAt Time `xml:"created_at"` - Guid string `xml:"guid"` - ParentGuid string `xml:"parent_guid"` - Text string `xml:"text"` - AuthorSignature string `xml:"author_signature"` -} - -func (e EntityComment) Signature() string { - return e.AuthorSignature -} - -func (e EntityComment) SignatureText(order string) (signatureOrder []string) { - if order != "" { - return ExractSignatureText(order, e) - } - return []string{ - e.Author, - e.CreatedAt.String(), - e.Guid, - e.ParentGuid, - e.Text, - } -} diff --git a/entity_comment_test.go b/entity_comment_test.go deleted file mode 100644 index 68186996a20155733a0cd1476ad879ad2be79754..0000000000000000000000000000000000000000 --- a/entity_comment_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import ( - "testing" - "time" -) - -var comment = EntityComment{ - Author: "author@localhost", - Guid: "1234", - ParentGuid: "4321", - AuthorSignature: "1234", - Text: "hello world", -} - -func TestCommentSignature(t *testing.T) { - if comment.Signature() != comment.AuthorSignature { - t.Errorf("Expected to be '%s', got '%s'", - comment.AuthorSignature, comment.Signature()) - } -} - -func TestCommentAppendSignature(t *testing.T) { - comment.CreatedAt.New(time.Now()) - - if comment.AuthorSignature != "1234" { - t.Errorf("Expected to be empty, got %s", comment.AuthorSignature) - } - - privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - var signature Signature - err = signature.New(comment).Sign(privKey, &(comment.AuthorSignature)) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if comment.AuthorSignature == "" { - t.Errorf("Expected signature, got empty string") - } -} diff --git a/entity_like.go b/entity_like.go deleted file mode 100644 index cb58ef9a5600beaada7460ea484c25c741b57262..0000000000000000000000000000000000000000 --- a/entity_like.go +++ /dev/null @@ -1,55 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import "github.com/Zauberstuhl/go-xml" - -type EntityLike struct { - XMLName xml.Name `xml:"like"` - Positive bool `xml:"positive"` - Guid string `xml:"guid"` - ParentGuid string `xml:"parent_guid"` - ParentType string `xml:"parent_type"` - Author string `xml:"author"` - AuthorSignature string `xml:"author_signature"` - - // store relayable signature order - SignatureOrder string `xml:"-"` -} - -func (e EntityLike) Signature() string { - return e.AuthorSignature -} - -func (e EntityLike) SignatureText(order string) []string { - if order != "" { - return ExractSignatureText(order, e) - } - - positive := "false" - if e.Positive { - positive = "true" - } - return []string{ - positive, - e.Guid, - e.ParentGuid, - e.ParentType, - e.Author, - } -} diff --git a/entity_like_test.go b/entity_like_test.go deleted file mode 100644 index 76805bdcc3b8f06456bbe8caeeec16daf274fa12..0000000000000000000000000000000000000000 --- a/entity_like_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import "testing" - -var like = EntityLike{ - Positive: true, - Guid: "1234", - ParentGuid: "4321", - ParentType: "Post", - Author: "author@localhost", -} - -func TestLikeSignature(t *testing.T) { - like.AuthorSignature = "1234" - if like.Signature() != like.AuthorSignature { - t.Errorf("Expected to be '%s', got '%s'", - like.AuthorSignature, like.Signature()) - } -} - -func TestLikeAppendSignature(t *testing.T) { - privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - var signature Signature - err = signature.New(like).Sign(privKey, &(like.AuthorSignature)) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if like.AuthorSignature == "" { - t.Errorf("Expected signature, got empty string") - } -} diff --git a/entity_post.go b/entity_post.go deleted file mode 100644 index ba4ebd511b7b5250dd96b99a3afdd1e502172a72..0000000000000000000000000000000000000000 --- a/entity_post.go +++ /dev/null @@ -1,72 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import "github.com/Zauberstuhl/go-xml" - -type EntityStatusMessage struct { - XMLName xml.Name `xml:"status_message"` - Author string `xml:"author"` - Guid string `xml:"guid"` - CreatedAt Time `xml:"created_at"` - ProviderName string `xml:"provider_display_name"` - Text string `xml:"text,omitempty"` - Photos *EntityPhotos `xml:"photo,omitempty"` - Location *EntityLocation `xml:"location,omitempty"` - Poll *EntityPoll `xml:"poll,omitempty"` - Public bool `xml:"public"` - Event *EntityEvent `xml:"event,omitempty"` -} - -type EntityReshare struct { - XMLName xml.Name `xml:"reshare"` - Author string `xml:"author"` - Guid string `xml:"guid"` - CreatedAt Time `xml:"created_at"` - RootAuthor string `xml:"root_author"` - RootGuid string `xml:"root_guid"` -} - -type EntityLocation struct { - Address string `xml:"address"` - Lat string `xml:"lat"` - Lng string `xml:"lng"` -} - -type EntityPoll struct { - Guid string `xml:"guid"` - Question string `xml:"question"` - PollAnswers []EntityPollAnswer `xml:"poll_answers"` -} - -type EntityPollAnswer struct { - Guid string `xml:"guid"` - Answer string `xml:"answer"` -} - -type EntityEvent struct { - Author string `xml:"author"` - Guid string `xml:"guid"` - Summary string `xml:"summary"` - Start Time `xml:"start"` - End Time `xml:"end"` - AllDay bool `xml:"all_day"` - Timezone string `xml:"timezone"` - Description string `xml:"description"` - Location *EntityLocation `xml:"location,omitempty"` -} diff --git a/entity_profile.go b/error.go similarity index 59% rename from entity_profile.go rename to error.go index 2f60ff258740d441aa78ef1b42356cb0a60469dd..509ced437a9a3c5cdde8806583aea0314f6d2e2f 100644 --- a/entity_profile.go +++ b/error.go @@ -17,19 +17,14 @@ package federation // along with this program. If not, see . // -type EntityProfile struct { - Author string `xml:"author"` - FirstName string `xml:"first_name"` - LastName string `xml:"last_name"` - ImageUrl string `xml:"image_url"` - ImageUrlMedium string `xml:"image_url_medium"` - ImageUrlSmall string `xml:"image_url_small"` - Birthday string `xml:"birthday"` - Gender string `xml:"gender"` - Bio string `xml:"bio"` - Location string `xml:"location"` - Searchable bool `xml:"searchable"` - Public bool `xml:"public"` - Nsfw bool `xml:"nsfw"` - TagString string `xml:"tag_string"` -} +import "errors" + +var ( + ERR_APU_MISSING_OBJT = errors.New("missing object attribute in request") + ERR_APU_MISSING_TYPE = errors.New("missing type in request") + ERR_APU_MISSING_ATTR = errors.New("missing attribute in body") + + ERR_INVALID_ENDPOINT = errors.New("endpoint is invalid") + ERR_TYPE_CAST = errors.New("cannot type cast") + ERR_NOT_IMPLEMENTED = errors.New("not implemented") +) diff --git a/hcard.go b/hcard.go index 97d9f51a311de80d797f1f3cdea4495a269ceb9f..45635c81338b8c04e893f1f07dbc018784d1da9b 100644 --- a/hcard.go +++ b/hcard.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ import ( "github.com/PuerkitoBio/goquery" ) +// Hcard is used by Diaspora to build and share user profiles type Hcard struct { Guid string Nickname string @@ -36,6 +37,7 @@ type Hcard struct { PhotoSmall string } +// Fetch will try parsing a html document after fetching it from a certain endpoint func (h *Hcard) Fetch(endpoint string) error { resp, err := FetchHtml("GET", endpoint, nil) if err != nil { diff --git a/hcard_test.go b/hcard_test.go index 756be2f856ce4356f76a986db763bbed35a03587..6ac3fee69d4648bbb5f10b1519ddd1a2885baf75 100644 --- a/hcard_test.go +++ b/hcard_test.go @@ -1,7 +1,7 @@ package federation // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/helper_test.go b/helper_test.go deleted file mode 100644 index 29e01a3717bea21bdf31ab3da947d30c0fc11588..0000000000000000000000000000000000000000 --- a/helper_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import ( - "crypto/rsa" - "testing" - "strings" -) - -var TEST_AUTHOR = `diaspora_2nd@localhost:3001` - -var TEST_PRIV_KEY = []byte(`-----BEGIN PRIVATE KEY----- -MIIEpAIBAAKCAQEA5qryP/x8Wtht+VLH/TvLNofTF9B4aj78sYlctQxIuzEmuLc3 -nU+EhpeExe9DpJEt6/l5V3NIHymkkNMA7Mu4fWmxdyVMEweY7utTl+wkwdJFFlBV -TI1H9dF91sUB2p5o7irZUdgW8zyZv2d2aHW5cOwR1mysh16IwDSNSRd8quqIkqPC -xYKcVc2cga0jYb5RIpv4mq1MlKrmFwxXws3OK+/5ZtErOJZCyVkz2lf0gqzMIyBD -JT9iU0x0HEq4A9LcFMCtjv/AuQj5y7AV2ehY/J7kxmw/m7sLr3R7YZ/H+pwdiW7n -LyQilUNXRuwLJQY2HmQ1rZaVivwviBqaqeFkpwIDAQABAoIBAHAkr/33zKWGD4Fl -g6FUDqoGQtSTH9fXo5bUx2OmAz4u2Tp4qOssG6wrwftRJbu+cWsGML4ZZ/juj/lw -/EQjjyA54HOiiGfAC9QsSMnVntE0Xy5IBBBhp5iVLu7ZfNtCpJUV8+3cdtvunHj3 -3hNPGMcTnmB3GTH+/dEkO4RLjOqyi0GAZEv5NgeKavP6TkkwYuFChh+QiOy0tQSc -P+5pFtDuVn/ytyxxwH3FuML00y6moOishxpWgV4Ik8tcGzpk4VKDKMxtrhecAp+O -Z4fnYTWKUMb9eQhI8K68IM7I6VcMAYIXidHtKrgGoVvAQQk4ih1EpVhF1WwEX9ra -ZHYR28ECgYEA7+x1v48ymXe1wz1d24x8MkYD3oxNHTMhJ+17dfpJjhPlO3q8oWu5 -fgK5n5EzGfjlBKdCXK/STIDunz69WnNRs8b2HLyyoFaXiFJVSM7juY0vV8yIfmQN -0GXSfO7Evz0xct8QZfm61wD80LJ9YWuqFKXqGxhBRZRMg3vupwSLcZcCgYEA9h+2 -9OxyKI9h0+me2oiibF3ke+78sjUPqqTGb8QrcQp0QenBqO7n4Y9PpXE5X3/yjyqk -GcsL5oaFFxZn+8t6XMZnT6nubQpyY7TFTK3TKyqhfdQPfsXbqgH5KFFisK40TL1a -yx2a0MzYOyGi5eArr+kRhRlt8vSpwnRQmGJz53ECgYEAoLlYPAZy0CpIok021f/r -p0YOC4UTl68L1BKcNXGA2uPrGYhkWwKuVYL/1KxRfmGlEhP2Od8y0ztAH3/JG5HL -NtLfRmsGgrDffFwjc83c8g1pnLiQ65KdSnEbq8PMG4yj1p8l/hpoluW7dxdLNPsK -CiEHjjUWbMUm6KIaQtqhi2sCgYAtK8zsTqj1ALu3pNzexszojqLsjAQcwNhLPUqe -IKbIbF7B6iD83Dv6jc7UUl9xQ45E8FKF2VopyO6MOjSDZejjNhan7Ewx/wTXf8nm -NNDYz04sRctCPRX/sbUEzUsLmi1HGEmdlaVgRPg6ggXforDh7CinAO/I81ZktexE -y2zyQQKBgQDVxzgzD4CcJtpkxwn2qiEIW94Dp/E91Rbr+PUqzV2vSESMLTZyfOh5 -NxEAgN/1IVKPsH0Ct7G9TqEgkG7UaHOw4HBtybSUf+gDRw98tb0mk9gGK6m2A1vz -sK4ExAtclJCC8pc2MldVoeGbePUIYIsMMTGMZQ2O6jlINbYaOkFM/w== ------END PRIVATE KEY-----`) - -var TEST_PUB_KEY = []byte(`-----BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxREQ/8yKpfxNTET2MIIY -1sIOktYl+6u1O3Ad9GJctzrRxWa/PbxWKX8QmGoWNb9S9tt1oLeRZ4ZGTV51t/ln -4ZOUUhR5Xpx5+jxP6DM5Jf2wqGjzXmQaSgKWsrJ2R0EyP9Ga+j5zk+uMpRXBBt0C -bh2kcPAF7mRZ7EBK+p1a+YKaKhebTJNF3YXSjxNlN6LXvTq+JUg6zTzUWg/6w1K0 -jXGpXMVMOSjmRsdfbMjIYg8AXSH4xK2AvqUjWvIrQRPiXa6EjB9468sT0IABzcaY -4iNs3kzMB2aRCcdzMdZplAUH9kA+BOC3sU6VmOCfKCsQ3RyudeWqkbKxJzk/duoG -HaO+3nFgxhubRhO1VNj0EhpGPkVuQH6hhDpNh6sYH0Tq4Fs6gTxQynlwiERqoHW/ -bp4TZc8mmSgrnDNuWklM6IeyoSse1lO4ivaSLDuvm8UbZTT1P09QaaCPLI5iLHVT -dP7gLEAYalirOXeZSyzwMAWzWF7NXkVbHjFn4JvJFYKyN+xoBpAwlIXdI1DMOK1H -kJaT7PEyjvar0M9oIqVqEV5hdGqFrlDnW4MvP+wQkmuK+9CygggE/0oefhKYIYc3 -zQKne2ejiu9e5cDD5WyVusjeRstj/+9bDlOrQ8X4eh6vmjvd+98B/ZWFCCEkTH5m -DX3H15lu+GelrDpYLThXjnkCAwEAAQ== ------END PUBLIC KEY-----`) - -func TestParseRSAPublicKey(t *testing.T) { - var ( - err error - pub interface{} - ) - - pub, err = ParseRSAPublicKey(TEST_PUB_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if data, ok := pub.(*rsa.PublicKey); !ok { - t.Errorf("Expected to be '*rsa.PublicKey', got %v", data) - } - - _, err = ParseRSAPublicKey([]byte("")) - if err == nil { - t.Errorf("Expected an error, got nil") - } - - _, err = ParseRSAPublicKey(TEST_PRIV_KEY) - if err == nil { - t.Errorf("Expected an error, got nil") - } -} - -func TestParseRSAPrivateKey(t *testing.T) { - var ( - err error - priv interface{} - ) - - priv, err = ParseRSAPrivateKey(TEST_PRIV_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if data, ok := priv.(*rsa.PrivateKey); !ok { - t.Errorf("Expected to be '*rsa.PrivateKey', got %v", data) - } - - _, err = ParseRSAPrivateKey([]byte("")) - if err == nil { - t.Errorf("Expected an error, got nil") - } - - _, err = ParseRSAPrivateKey(TEST_PUB_KEY) - if err == nil { - t.Errorf("Expected an error, got nil") - } -} - -func TestParseWebfingerHandle(t *testing.T) { - expectedHandle := "diaspora_2nd" - handle, err := ParseWebfingerHandle("acct:" + TEST_AUTHOR) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if handle != expectedHandle { - t.Errorf("Expected to be %s, got %s", expectedHandle, handle) - } -} - -func TestParseStringHelper(t *testing.T) { - parts, err := parseStringHelper("abc", `^(\w{2})`, 1) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - expected := "ab" - if parts[1] != expected { - t.Errorf("Expected to be %s, got %s", expected, parts[1]) - } - - parts, err = parseStringHelper("abc", `^\w{2}`, 1) - if err == nil { - t.Errorf("Expected an error, got nil") - } -} - -func TestExractSignatureText(t *testing.T) { - var entity = EntityComment { - Author: "1", - CreatedAt: "2", - Guid: "3", - ParentGuid: "4", - Text: "5", - AuthorSignature: "6", - } - - var tests = []struct { - Order string - Expected string - }{ - { - Order: "author parent_guid", - Expected: "1;4", - }, - { - Order: "author parent_guid text guid", - Expected: "1;4;5;3", - }, - { - Order: "", - Expected: "", - }, - } - - for i, test := range tests { - result := ExractSignatureText(test.Order, entity) - if strings.Join(result, ";") != test.Expected { - t.Errorf("#%d: Expected to be '%s', got '%s'", i, test.Expected, strings.Join(result, ";")) - } - } -} diff --git a/helpers/json.go b/helpers/json.go new file mode 100644 index 0000000000000000000000000000000000000000..1056fdc8b07cd30e007f5aee42980a66158a42a4 --- /dev/null +++ b/helpers/json.go @@ -0,0 +1,24 @@ +package helpers +// +// GangGo Federation Library +// Copyright (C) 2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// ReadOnlyBool will not be affected by json.Unmarshal +// and can therefore be presetted before the task is invoked +type ReadOnlyBool bool + +func (ReadOnlyBool) UnmarshalJSON([]byte) error { return nil } diff --git a/helpers/parse.go b/helpers/parse.go new file mode 100644 index 0000000000000000000000000000000000000000..39ea3d6232f797cf8c5e5de5fc7e17c1322feadf --- /dev/null +++ b/helpers/parse.go @@ -0,0 +1,73 @@ +package helpers +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "regexp" + "errors" + "encoding/base64" +) + +func LinkToGuid(link string) string { + return base64.RawURLEncoding.EncodeToString([]byte(link)) +} + +func GuidToLink(guid string) (string, error) { + b, err := base64.RawURLEncoding.DecodeString(guid) + link := string(b) + if len(link) < 4 || link[:4] != "http" { + err = errors.New("Not a valid link!") + } + return link, err +} + +func ParseActorHandle(handle string) (string, error) { + parts, err := ParseStringHelper(handle, `^.*/([^/]+?)/actor$`, 1) + if err != nil { + return "", err + } + return parts[1], nil +} + +func ParseHandle(handle string) (string, error) { + parts, err := ParseStringHelper(handle, `^(.+?)@.+?$`, 1) + if err != nil { + return "", err + } + return parts[1], nil +} + +// ParseWebfingerHandle will parse a handle in webfinger format e.g. acct:lukas@sechat.org +func ParseWebfingerHandle(handle string) (string, error) { + parts, err := ParseStringHelper(handle, `^acct:(.+?)@.+?$`, 1) + if err != nil { + return "", err + } + return parts[1], nil +} + +func ParseStringHelper(line, regex string, max int) (parts []string, err error) { + r := regexp.MustCompile(regex) + parts = r.FindStringSubmatch(line) + + if (len(parts) - 1) < max { + err = errors.New("Cannot extract " + regex + " from " + line) + return + } + return +} diff --git a/helpers/rand.go b/helpers/rand.go new file mode 100644 index 0000000000000000000000000000000000000000..e12f49e6587c55b1e93e8e7fe6f32c921048cc28 --- /dev/null +++ b/helpers/rand.go @@ -0,0 +1,47 @@ +package helpers +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "fmt" + "crypto/rand" +) + +// Uuid generates a [16]byte string to seperate and identify entities +func Uuid() (string, error) { + b, err := randomBytes(16) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", b), nil +} + +// Token is similiar to Uuid() but instead of 16 it uses 32 bytes +func Token() (string, error) { + b, err := randomBytes(32) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", b), nil +} + +func randomBytes(length int) (b []byte, err error) { + b = make([]byte, length) + _, err = rand.Read(b) + return +} diff --git a/helpers/rsa.go b/helpers/rsa.go new file mode 100644 index 0000000000000000000000000000000000000000..6033ad3276dc14b999fe7efc1d7d31aafeffa44c --- /dev/null +++ b/helpers/rsa.go @@ -0,0 +1,50 @@ +package helpers +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +// ParseRSAPublicKey transforms a serialized public key into a rsa.PublicKey +func ParseRSAPublicKey(decodedKey []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(decodedKey) + if block == nil { + return nil, errors.New("Decode public key block is nil") + } + data, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + if pubKey, ok := data.(*rsa.PublicKey); ok { + return pubKey, nil + } + return nil, errors.New("Wasn't able to parse the public key!") +} + +// ParseRSAPublicKey transforms a serialized private key into a rsa.PrivateKey +func ParseRSAPrivateKey(decodedKey []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(decodedKey) + if block == nil { + return nil, errors.New("Decode private key block is nil!") + } + return x509.ParsePKCS1PrivateKey(block.Bytes) +} diff --git a/helper.go b/helpers/signature.go similarity index 57% rename from helper.go rename to helpers/signature.go index 1d4f736a8dfc6c6a288751dca2fc969b2a1933ee..6edbe1ec7dd34ac7225c02c8d8c96381a6d02cb7 100644 --- a/helper.go +++ b/helpers/signature.go @@ -1,7 +1,7 @@ -package federation +package helpers // -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,15 +18,12 @@ package federation // import ( - "regexp" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" "reflect" "strings" ) +// ExractSignatureText is used to extract order information about xml elements +// this is required to sign and verify Diaspora author signatures func ExractSignatureText(order string, entity interface{}) (signatureOrder []string) { orderArr := strings.Split(order, " ") @@ -63,45 +60,3 @@ func ExractSignatureText(order string, entity interface{}) (signatureOrder []str } return } - -func ParseRSAPublicKey(decodedKey []byte) (*rsa.PublicKey, error) { - block, _ := pem.Decode(decodedKey) - if block == nil { - return nil, errors.New("Decode public key block is nil") - } - data, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err - } - if pubKey, ok := data.(*rsa.PublicKey); ok { - return pubKey, nil - } - return nil, errors.New("Wasn't able to parse the public key!") -} - -func ParseRSAPrivateKey(decodedKey []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(decodedKey) - if block == nil { - return nil, errors.New("Decode private key block is nil!") - } - return x509.ParsePKCS1PrivateKey(block.Bytes) -} - -func ParseWebfingerHandle(handle string) (string, error) { - parts, err := parseStringHelper(handle, `^acct:(.+?)@.+?$`, 1) - if err != nil { - return "", err - } - return parts[1], nil -} - -func parseStringHelper(line, regex string, max int) (parts []string, err error) { - r := regexp.MustCompile(regex) - parts = r.FindStringSubmatch(line) - - if (len(parts) - 1) < max { - err = errors.New("Cannot extract " + regex + " from " + line) - return - } - return -} diff --git a/helpers/time.go b/helpers/time.go new file mode 100644 index 0000000000000000000000000000000000000000..17011dffbc93210500e01099ee582c32732b7dab --- /dev/null +++ b/helpers/time.go @@ -0,0 +1,42 @@ +package helpers +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import "time" + +// TIME_FORMAT is the required time format for the Diaspora protocol +const TIME_FORMAT = "2006-01-02T15:04:05Z" + +// Time will be used for xml/json marshal and unmarshal +type Time string + +// New generates a protocol-friendly time from a normal time.Time struct +func (t *Time) New(newTime time.Time) *Time { + *t = Time(newTime.UTC().Format(TIME_FORMAT)) + return t +} + +// Time parses the protocol time and returns time.Time +func (t Time) Time() (time.Time, error) { + return time.Parse(TIME_FORMAT, string(t)) +} + +// String returns a serialized protocol time +func (t Time) String() string { + return string(t) +} diff --git a/http_client.go b/http_client.go index 49b370e5140a9d65832a176d4c47b3de06df9c38..f9f96bca92f2e09a8b759eb477eab605f18a4131 100644 --- a/http_client.go +++ b/http_client.go @@ -22,43 +22,75 @@ import ( "net/http" "encoding/json" "github.com/Zauberstuhl/go-xml" + "crypto/rsa" "errors" "io" "strings" -) - -const ( - PROTO_HTTP = "http://" - PROTO_HTTPS = "https://" - CONTENT_TYPE_ENVELOPE = "application/magic-envelope+xml" - CONTENT_TYPE_JSON = "application/json" - USER_AGENT = "GangGo/v0 (Federation library)" + httpsig "git.feneas.org/ganggo/httpsignatures" ) var timeout = time.Duration(10 * time.Second) -func PushToPrivate(host, guid string, body io.Reader) error { - return push(host, "/receive/users/" + guid, PROTO_HTTPS, CONTENT_TYPE_JSON, body) +// HttpClient struct can hold a private key and keyId for signing +// http headers this is required for ActivityPub communication +type HttpClient struct { + pem *rsa.PrivateKey + keyId string } -func PushToPublic(host string, body io.Reader) error { - return push(host, "/receive/public", PROTO_HTTPS, CONTENT_TYPE_ENVELOPE, body) +// New can set private key and keyId for http header signing +func (c *HttpClient) New(id string, key *rsa.PrivateKey) *HttpClient { + c.pem = key + c.keyId = id + return c } -func push(host, endpoint, proto, contentType string, body io.Reader) error { - req, err := http.NewRequest("POST", proto + host + endpoint, body) +// Push can push a body with modified headers to a certain endpoint. +// If a private key is available it will also sign the http header. +// It will try https first and fallback to http if the first one fails! +func (c HttpClient) Push(endpoint string, header http.Header, body io.Reader) error { + isHttp := len(endpoint) > 6 && endpoint[:7] == "http://" + isHttps := len(endpoint) > 7 && endpoint[:8] == "https://" + + var proto string + if !isHttp && !isHttps { + proto = "https://" + } + + req, err := http.NewRequest("POST", proto + endpoint, body) if err != nil { return err } req.Header.Set("User-Agent", USER_AGENT) - req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept-Encoding", "*") + // first delete all duplicates + for key, _ := range header { + if req.Header.Get(key) != "" { + req.Header.Del(key) + } + } + // then add all new entries to the request + for key, entries := range header { + for _, entry := range entries { + req.Header.Add(key, entry) + } + } + Log.Info("HTTP Headers", req.Header) + + if c.pem != nil { + err = httpsig.DefaultRsaSha256Signer.SignRequestRSA(c.keyId, c.pem, req) + if err != nil { + return err + } + Log.Info("Signed http header", req.Header) + } client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { - if proto == PROTO_HTTPS { - logger.Info("Retry with", PROTO_HTTP, "on", host, err) - return push(host, endpoint, PROTO_HTTP, contentType, body) + if isHttps { + Log.Info("Retry with HTTP on", endpoint, err) + return c.Push("http://" + endpoint[8:], header, body) } return err } @@ -70,24 +102,27 @@ func push(host, endpoint, proto, contentType string, body io.Reader) error { return nil } +// FetchJson fetches from an url with optional body and content-type json func FetchJson(method, url string, body io.Reader, result interface{}) error { - resp, err := fetch(method, url, "application/json", body) + resp, err := fetch(method, url, CONTENT_TYPE_JSON, body) if err != nil { return err } return json.NewDecoder(resp.Body).Decode(result) } +// FetchXml fetches from an url with optional body and content-type xml func FetchXml(method, url string, body io.Reader, result interface{}) error { - resp, err := fetch(method, url, "application/xrd+xml", body) + resp, err := fetch(method, url, CONTENT_TYPE_XRDXML, body) if err != nil { return err } return xml.NewDecoder(resp.Body).Decode(result) } +// FetchHtml fetches from an url with optional body and content-type html func FetchHtml(method, url string, body io.Reader) (resp *http.Response, err error) { - return fetch(method, url, "text/html", body) + return fetch(method, url, CONTENT_TYPE_TEXTHTML, body) } func fetch(method, url, contentType string, body io.Reader) (*http.Response, error) { @@ -101,6 +136,17 @@ func fetch(method, url, contentType string, body io.Reader) (*http.Response, err } req.Header.Set("User-Agent", USER_AGENT) req.Header.Set("Content-Type", contentType) + switch contentType { + case CONTENT_TYPE_JSON: + req.Header.Set("Accept", CONTENT_TYPE_JSONLD + + `; profile="https://www.w3.org/ns/activitystreams"`) + req.Header.Add("Accept", CONTENT_TYPE_JSON) + case CONTENT_TYPE_XRDXML: + req.Header.Set("Accept", CONTENT_TYPE_ENVELOPE) + req.Header.Add("Accept", CONTENT_TYPE_XRDXML) + case CONTENT_TYPE_TEXTHTML: + req.Header.Set("Accept", CONTENT_TYPE_TEXTHTML) + } client := &http.Client{Timeout: timeout} resp, err := client.Do(req) diff --git a/http_client_test.go b/http_client_test.go index bfb819e33dd64bd1d1e30de7fd9de03e46d60d03..7fe4805d99b3e1d97ce7b164ebd5ac7869b81cd1 100644 --- a/http_client_test.go +++ b/http_client_test.go @@ -24,6 +24,9 @@ import ( "testing" "github.com/Zauberstuhl/go-xml" "io/ioutil" + "crypto/rsa" + "crypto/rand" + httpsig "git.feneas.org/ganggo/httpsignatures" ) type Test struct { @@ -32,45 +35,40 @@ type Test struct { B string `xml:"B" json:"B"` } -func TestPushToPrivate(t *testing.T) { - var guid = "1234" +func TestHttpClientPush(t *testing.T) { + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - if r.URL.Path != "/receive/users/" + guid { - t.Errorf("%s", r.URL.Path) + if r.Header.Get("User-Agent") != USER_AGENT { + t.Errorf("Expected '%s' header, got %v", USER_AGENT, r.Header) } - })) - defer ts.Close() - err := PushToPrivate(ts.URL[7:], guid, nil) - if err != nil { - t.Errorf("Some error occured while sending: %v", err) - } + if r.Header.Get("X-TEST") != "HEADER" { + t.Errorf("Expected X-TEST header, got %v", r.Header) + } - err = PushToPrivate("", guid, nil) - if err == nil { - t.Errorf("Expected an error, got nil") - } -} + sig, err := httpsig.FromRequest(r) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } -func TestPushToPublic(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - if r.URL.Path != "/receive/public" { - t.Errorf("%s", r.URL.Path) + if !sig.IsValidRSA(&privKey.PublicKey, r) { + t.Errorf("Expected a valid signature") } + + w.WriteHeader(http.StatusOK) })) defer ts.Close() - err := PushToPublic(ts.URL[7:], nil) + header := make(http.Header) + header.Set("X-TEST", "HEADER") + client := (&HttpClient{}).New("#id", privKey) + err = client.Push(ts.URL, header, nil) if err != nil { - t.Errorf("Some error occured while sending: %v", err) - } - - err = PushToPublic("", nil) - if err == nil { - t.Errorf("Expected an error, got nil") + t.Errorf("Some error occured while parsing: %v", err) } } diff --git a/like.go b/like.go new file mode 100644 index 0000000000000000000000000000000000000000..3fb5af86c455a70ad56e760e0be160a38c53d9f0 --- /dev/null +++ b/like.go @@ -0,0 +1,25 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// MessageLike represents a like on a other entity e.g. a post it inherits MessageRelayable +type MessageLike interface { + MessageRelayable + Positive() bool + SetPositive(bool) +} diff --git a/like_test.go b/like_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c92e0e0ecb707afd635c521d337369cc9518c59d --- /dev/null +++ b/like_test.go @@ -0,0 +1,138 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" + "fmt" + "io/ioutil" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" + "git.feneas.org/ganggo/federation/helpers" +) + +func TestLike(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + guid := "1234" + parentGuid := "4321" + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol, ActivityPubProtocol} { + like, err := NewMessageLike(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + like.SetAuthor(author) + like.SetGuid(guid) + like.SetParent(parentGuid) + like.SetSignature(privKey) + like.SetPositive(true) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d: Something went wrong!", i) + } + + msgLike, ok := msg.Entity().(MessageLike) + if !ok { + t.Errorf("#%d: Expected MessageLike, got %v", i, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgLike.Author() != author { + t.Errorf("#%d: Expected %s, got %s", i, author, msgLike.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgLike.Author() != linkAuthor { + t.Errorf("#%d: Expected %s, got %s", i, linkAuthor, msgLike.Author()) + } + + if msgLike.Type().Proto != proto { + t.Errorf("#%d: Expected %s, got %s", i, proto, msgLike.Type().Proto) + } + + if msgLike.Type().Entity != Like { + t.Errorf("#%d: Expected %s, got %s", i, Contact, msgLike.Type().Entity) + } + + if proto == DiasporaProtocol && msgLike.Guid() != guid { + t.Errorf("#%d: Expected %s, got %s", i, guid, msgLike.Guid()) + } + + linkGuid := fmt.Sprintf("%s#likes/%s", linkAuthor, guid) + if proto == ActivityPubProtocol && + msgLike.Guid() != helpers.LinkToGuid(linkGuid) { + t.Errorf("#%d: Expected %s, got %s", i, + helpers.LinkToGuid(linkGuid), msgLike.Guid()) + } + + if msgLike.Parent() != parentGuid { + t.Errorf("#%d: Expected %s, got %s", i, parentGuid, msgLike.Parent()) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d: Expected a valid signature", i) + } + + if !msgLike.Positive() { + t.Errorf("#%d: Expected true, got false") + } + + w.WriteHeader(http.StatusOK) + }, + )) + defer ts.Close() + + err = like.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } +} diff --git a/logger.go b/logger.go index 6d482e781b13d08366504267e7937444351d6fea..00d98d0fcbce8063c42cb3d7a64b58926f16f82a 100644 --- a/logger.go +++ b/logger.go @@ -24,15 +24,10 @@ import ( "log" ) -const ( - LOG_C_TUR = "\033[0;36m" - LOG_C_RED = "\033[31m" - LOG_C_YELLOW = "\033[33m" - LOG_C_RESET = "\033[0m" -) - var ( - logger Logger + // Log is the used logger for the federation library. + // On default this is stdout but you can change it with SetLogger() + Log Logger defaultPrefix string ) @@ -43,7 +38,7 @@ func init() { file, line := f.FileLine(pc[0]) defaultPrefix = fmt.Sprintf("%s:%d %s ", file, line, f.Name()) - logger = Logger{ + Log = Logger{ log.New(os.Stdout, defaultPrefix, log.Lshortfile), LOG_C_TUR, } @@ -59,19 +54,24 @@ type Logger struct{ Prefix string } +// SetLogger sets the logger interface and makes it possible +// to include log output in your application logs func SetLogger(writer LogWriter) { - logger = Logger{writer, LOG_C_TUR} + Log = Logger{writer, LOG_C_TUR} } +// Info prints information/debug messages func (l Logger) Info(values... interface{}) { l.Println(l.Prefix, values, LOG_C_RESET) } +// Error prints error messages and fatals func (l Logger) Error(values... interface{}) { l.Prefix = LOG_C_RED l.Info(values) } +// Warn prints warnings func (l Logger) Warn(values... interface{}) { l.Prefix = LOG_C_YELLOW l.Info(values) diff --git a/logger_test.go b/logger_test.go index 7ccc293ebbff6590760782443aea2087b38c749c..9062615c84e93ad3739c83a3b30d2f318fd85d06 100644 --- a/logger_test.go +++ b/logger_test.go @@ -20,17 +20,28 @@ package federation import ( "testing" "log" - "os" "strings" "bytes" + "os" ) +var testLog *os.File + +func init() { + file, err := os.Create("gotest.log") + if err != nil { + panic(err) + } + SetLogger(log.New(file, defaultPrefix, log.Lshortfile)) + testLog = file +} + func TestLoggerOutput(t *testing.T) { var buf bytes.Buffer var msg = "Hello World" SetLogger(log.New(&buf, "", log.Lshortfile)) - logger.Info(msg) + Log.Info(msg) expected := LOG_C_TUR + msg + LOG_C_RESET if strings.Contains(expected, buf.String()) { @@ -38,19 +49,19 @@ func TestLoggerOutput(t *testing.T) { } buf.Reset() - logger.Error(msg) + Log.Error(msg) expected = LOG_C_RED + msg + LOG_C_RESET if strings.Contains(expected, buf.String()) { t.Errorf("Expected to contain '%s', got '%s'", expected, buf.String()) } buf.Reset() - logger.Warn(expected) + Log.Warn(expected) expected = LOG_C_YELLOW + msg + LOG_C_RESET if strings.Contains(expected, buf.String()) { t.Errorf("Expected to contain '%s', got '%s'", expected, buf.String()) } // reset otherwise it will break test output - SetLogger(log.New(os.Stdout, defaultPrefix, log.Lshortfile)) + SetLogger(log.New(testLog, defaultPrefix, log.Lshortfile)) } diff --git a/magic_test.go b/magic_test.go deleted file mode 100644 index cf6edebda7004e13a0316d9814fdc0c3374822ee..0000000000000000000000000000000000000000 --- a/magic_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import ( - "testing" - "bytes" - "regexp" -) - -var TEST_MAGIC_DATA = []byte(``) - -var TEST_MAGIC_PAYLOAD = []byte(` - PHg-PC94Pg== - base64url - RSA-SHA256 - PIlS0XhUHGqSsoGKP2efeitDKv7uO0ekNoDQPm5lk844muzMPk7iK9t6T3ageqIsl14xmnInDGKlbrM49JiuYB4aFKEwqHAIEj2axCjdm6HRF5mv-2nhVjKISx-AcuKY1Rav9pKQoQqphRm8p9CQr632TK5mkFfBAeGpyJE8I3WNwguy9AozR-ym0b3MrbDhHxpzGxcRAvjyzbRMfvLhOlVKauaIEGDVN6nbBXVSY4hSBYu38-02PyGuyPjjlBJHNIPQXUL9dcSq_LXs_ElwQA2JBLwF6-opQvIBDbjUVkX4spKo_uRNEAlFuR4Ul-bi_Y7-ssoD3DrMHN4Fg2hx5w== -`) - -func TestMagicEnvelope(t *testing.T) { - privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - payload, err := MagicEnvelope(privKey, string(TEST_AUTHOR), TEST_MAGIC_DATA) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - if bytes.Compare(payload, TEST_MAGIC_PAYLOAD) != 0 { - t.Errorf("Expected to be %s, got %s", - string(TEST_MAGIC_PAYLOAD), string(payload)) - } -} - -func TestEncryptedMagicEnvelope(t *testing.T) { - privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - pubKey, err := ParseRSAPublicKey(TEST_PUB_KEY) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - payload, err := EncryptedMagicEnvelope( - privKey, pubKey, TEST_AUTHOR, TEST_MAGIC_DATA) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - - matched := regexp.MustCompile(`encrypted_magic_envelope`).Match(payload) - if !matched { - t.Errorf("Expected match for pattern 'encrypted_magic_envelope' got nothing") - } -} diff --git a/message.go b/message.go new file mode 100644 index 0000000000000000000000000000000000000000..6643b67cfa4015c31fe95bb2d2de4a81fbb697fd --- /dev/null +++ b/message.go @@ -0,0 +1,114 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import "crypto/rsa" + +// MessageType defines what kind of message we are dealing with +type MessageType struct { + Proto Protocol + Entity EntityType +} + +// Message is the main interface for handling requests. It will be returned +// by all [...]Parse(...) functions e.g. DiasporaParse() or ActivityPubParse() +type Message interface { + ValidSignature(*rsa.PublicKey) bool + Type() MessageType + Entity() MessageBase +} + +// MessageBase is the minimum what a entity has to support/implement +type MessageBase interface { + Author() string + SetAuthor(string) + Unmarshal([]byte) error + Type() MessageType + Marshal(*rsa.PrivateKey, *rsa.PublicKey) ([]byte, error) + Send(string, *rsa.PrivateKey, *rsa.PublicKey) error +} + +// NewMessagePost will return a MessagePost interface depending on the specified Protocol +func NewMessagePost(proto Protocol) (MessagePost, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaStatusMessage{}, nil + case ActivityPubProtocol: + return &ActivityPubCreatePost{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} + +// NewMessageReshare will return a MessageReshare interface depending on the specified Protocol +func NewMessageReshare(proto Protocol) (MessageReshare, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaReshare{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} + +// NewMessageComment will return a MessageComment interface depending on the specified Protocol +func NewMessageComment(proto Protocol) (MessageComment, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaComment{}, nil + case ActivityPubProtocol: + return &ActivityPubCreateComment{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} + +// NewMessageLike will return a MessageLike interface depending on the specified Protocol +func NewMessageLike(proto Protocol) (MessageLike, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaLike{}, nil + case ActivityPubProtocol: + return &ActivityPubLike{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} + +// NewMessageContact will return a MessageContact interface depending on the specified Protocol +func NewMessageContact(proto Protocol) (MessageContact, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaContact{}, nil + case ActivityPubProtocol: + return &ActivityPubFollow{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} + +// NewMessageRetract will return a MessageRetract interface depending on the specified Protocol +func NewMessageRetract(proto Protocol) (MessageRetract, error) { + switch proto { + case DiasporaProtocol: + return &DiasporaRetraction{}, nil + case ActivityPubProtocol: + return &ActivityPubRetract{}, nil + default: + return nil, ERR_NOT_IMPLEMENTED + } +} diff --git a/message_test.go b/message_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f5ba84ce10cc486c2525a54f1637d91614c1530a --- /dev/null +++ b/message_test.go @@ -0,0 +1,119 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" +) + +func TestMessagePost(t *testing.T) { + _, err := NewMessagePost(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessagePost(ActivityPubProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessagePost(Protocol(666)) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} + +func TestMessageReshare(t *testing.T) { + _, err := NewMessageReshare(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageReshare(ActivityPubProtocol) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} + +func TestMessageComment(t *testing.T) { + _, err := NewMessageComment(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageComment(ActivityPubProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageComment(Protocol(666)) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} + +func TestMessageLike(t *testing.T) { + _, err := NewMessageLike(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageLike(ActivityPubProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageLike(Protocol(666)) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} + +func TestMessageContact(t *testing.T) { + _, err := NewMessageContact(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageContact(ActivityPubProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageContact(Protocol(666)) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} + +func TestMessageRetract(t *testing.T) { + _, err := NewMessageRetract(DiasporaProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageRetract(ActivityPubProtocol) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + _, err = NewMessageRetract(Protocol(666)) + if err == nil { + t.Errorf("Expected %v, got nil", ERR_NOT_IMPLEMENTED) + } +} diff --git a/post.go b/post.go new file mode 100644 index 0000000000000000000000000000000000000000..ba14c034af0e0cb830e02b0b80d054b1741650dc --- /dev/null +++ b/post.go @@ -0,0 +1,41 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "time" + "git.feneas.org/ganggo/federation/helpers" +) + +// MessagePost represents a status message or post from a user it inherits MessageBase +type MessagePost interface { + MessageBase + Recipients() []string + SetRecipients([]string) + Guid() string + SetGuid(string) + CreatedAt() helpers.Time + SetCreatedAt(time.Time) + Provider() string + SetProvider(string) + Text() string + SetText(string) + Public() bool + SetPublic(bool) + FilePaths() []string +} diff --git a/post_test.go b/post_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bcc484d5293eafefc03767f06764f9e121d487a0 --- /dev/null +++ b/post_test.go @@ -0,0 +1,173 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "fmt" + "testing" + "time" + "io/ioutil" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" + "git.feneas.org/ganggo/federation/helpers" +) + +func TestPost(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + friends := []string{"friend1@host.tld", "friend2@host.tld"} + timeNow := time.Now() + guid := "1234" + text := "hello world" + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol, ActivityPubProtocol} { + post, err := NewMessagePost(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + post.SetAuthor(author) + post.SetRecipients(friends) + post.SetGuid(guid) + post.SetCreatedAt(timeNow) + post.SetProvider("ganggo") + post.SetText(text) + post.SetPublic(true) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d: Something went wrong!", i) + } + + msgPost, ok := msg.Entity().(MessagePost) + if !ok { + t.Errorf("#%d: Expected MessagePost, got %v", i, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgPost.Author() != author { + t.Errorf("#%d: Expected %s, got %s", i, author, msgPost.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgPost.Author() != linkAuthor { + t.Errorf("#%d: Expected %s, got %s", i, linkAuthor, msgPost.Author()) + } + + if msgPost.Type().Proto != proto { + t.Errorf("#%d: Expected %s, got %s", i, proto, msgPost.Type().Proto) + } + + if msgPost.Type().Entity != StatusMessage { + t.Errorf("#%d: Expected %s, got %s", i, StatusMessage, msgPost.Type().Entity) + } + + if proto == DiasporaProtocol && msgPost.Guid() != guid { + t.Errorf("#%d: Expected %s, got %s", i, guid, msgPost.Guid()) + } + + linkGuid := fmt.Sprintf(config.GuidURLFormat, guid) + if proto == ActivityPubProtocol && + msgPost.Guid() != helpers.LinkToGuid(linkGuid) { + t.Errorf("#%d: Expected %s, got %s", i, + helpers.LinkToGuid(linkGuid), msgPost.Guid()) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d: Expected a valid signature", i) + } + + // XXX we have to re-think the SetPublic switch + // e.g. in AP it will be public if no recipients + // were added to the interface type + //if !msgPost.Public() { + // t.Errorf("#%d: Expected true, got false", i) + //} + + for ii, recipient := range msgPost.Recipients() { + if recipient != friends[ii] { + t.Errorf("#%d(%d): Expected %s, got %s", + i, ii, friends[ii], recipient) + } + } + if proto != DiasporaProtocol && + len(msgPost.Recipients()) != len(friends) { + t.Errorf("#%d: Expected len %d, got %d", i, + len(msgPost.Recipients()), len(friends)) + } + + postTime, err := msgPost.CreatedAt().Time() + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + if postTime.Unix() != timeNow.Unix() { + t.Errorf("#%d: Expected %s, got %s", i, + timeNow.Unix(), postTime.Unix()) + } + + // XXX also a Diaspora only thing do we still need it? + if proto == DiasporaProtocol && msgPost.Provider() != "ganggo" { + t.Errorf("#%d: Expected %s, got %s", i, "ganggo", msgPost.Provider()) + } + + if msgPost.Text() != text { + t.Errorf("#%d: Expected %s, got %s", i, text, msgPost.Text()) + } + + w.WriteHeader(http.StatusOK) + }, + )) + defer ts.Close() + + err = post.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } +} diff --git a/profile.go b/profile.go new file mode 100644 index 0000000000000000000000000000000000000000..9e1a617cf34d718288c874407ed209fd43fb0570 --- /dev/null +++ b/profile.go @@ -0,0 +1,32 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// MessageProfile represents a user profile and inherits MessageBase +type MessageProfile interface { + MessageBase + FirstName() string + LastName() string + ImageUrl() string + Birthday() string + Gender() string + Bio() string + Location() string + Public() bool + Nsfw() bool +} diff --git a/relayable.go b/relayable.go new file mode 100644 index 0000000000000000000000000000000000000000..add842c162bfc075efb1c42dbb83360615303d88 --- /dev/null +++ b/relayable.go @@ -0,0 +1,32 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import "crypto/rsa" + +// MessageRelayable represents a minimum required to relay child entities +type MessageRelayable interface { + MessageBase + Guid() string + SetGuid(string) + Parent() string + SetParent(string) + Signature() string + SetSignature(*rsa.PrivateKey) error + SignatureOrder() string +} diff --git a/reshare.go b/reshare.go new file mode 100644 index 0000000000000000000000000000000000000000..41f6024a911439f7422b1f3f20d21485b5b4dc13 --- /dev/null +++ b/reshare.go @@ -0,0 +1,37 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "time" + "git.feneas.org/ganggo/federation/helpers" +) + +// MessageReshare represents a boost or reshare of a post +// or status message it also inherits MessageBase +type MessageReshare interface { + MessageBase + Guid() string + SetGuid(string) + Parent() string + SetParent(string) + ParentAuthor() string + SetParentAuthor(string) + CreatedAt() helpers.Time + SetCreatedAt(time.Time) +} diff --git a/reshare_test.go b/reshare_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a31b9d3f6793742dae2b405f978670c9856d4597 --- /dev/null +++ b/reshare_test.go @@ -0,0 +1,149 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" + "time" + "io/ioutil" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" + "git.feneas.org/ganggo/federation/helpers" + "fmt" +) + +func TestReshare(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + friends := []string{"friend1@host.tld", "friend2@host.tld"} + timeNow := time.Now() + guid := "1234" + parentGuid := "4321" + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol} { + reshare, err := NewMessageReshare(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + reshare.SetAuthor(author) + reshare.SetGuid(guid) + reshare.SetParent(parentGuid) + reshare.SetParentAuthor(friends[0]) + reshare.SetCreatedAt(timeNow) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d: Something went wrong!", i) + } + + msgReshare, ok := msg.Entity().(MessageReshare) + if !ok { + t.Errorf("#%d: Expected MessagePost, got %v", i, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgReshare.Author() != author { + t.Errorf("#%d: Expected %s, got %s", i, author, msgReshare.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgReshare.Author() != linkAuthor { + t.Errorf("#%d: Expected %s, got %s", i, linkAuthor, msgReshare.Author()) + } + + if msgReshare.Type().Proto != proto { + t.Errorf("#%d: Expected %s, got %s", i, proto, msgReshare.Type().Proto) + } + + if msgReshare.Type().Entity != Reshare { + t.Errorf("#%d: Expected %s, got %s", i, Reshare, msgReshare.Type().Entity) + } + + if proto == DiasporaProtocol && msgReshare.Guid() != guid { + t.Errorf("#%d: Expected %s, got %s", i, guid, msgReshare.Guid()) + } + + linkGuid := fmt.Sprintf(config.GuidURLFormat, guid) + if proto == ActivityPubProtocol && + msgReshare.Guid() != helpers.LinkToGuid(linkGuid) { + t.Errorf("#%d: Expected %s, got %s", i, + helpers.LinkToGuid(linkGuid), msgReshare.Guid()) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d: Expected a valid signature", i) + } + + if msgReshare.Parent() != parentGuid { + t.Errorf("#%d: Expected %s, got %s", i, parentGuid, msgReshare.Parent()) + } + + if msgReshare.ParentAuthor() != friends[0] { + t.Errorf("#%d: Expected %s, got %s", i, friends[0], msgReshare.ParentAuthor()) + } + + postTime, err := msgReshare.CreatedAt().Time() + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + if postTime.Unix() != timeNow.Unix() { + t.Errorf("#%d: Expected %s, got %s", i, + timeNow.Unix(), postTime.Unix()) + } + }, + )) + defer ts.Close() + + err = reshare.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } +} diff --git a/retract.go b/retract.go new file mode 100644 index 0000000000000000000000000000000000000000..9470a67348c6be017d2219737ef1802e932b4c42 --- /dev/null +++ b/retract.go @@ -0,0 +1,26 @@ +package federation +// +// GangGo Federation Library +// Copyright (C) 2017-2018 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +type MessageRetract interface { + MessageBase + ParentGuid() string + SetParentGuid(string) + ParentType() EntityType + SetParentType(EntityType) +} diff --git a/retract_test.go b/retract_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f5c4447ce8faf7d3ef1b75ec84e7bd8368d66b72 --- /dev/null +++ b/retract_test.go @@ -0,0 +1,143 @@ +package federation +// +// GangGo Diaspora Federation Library +// Copyright (C) 2017 Lukas Matt +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import ( + "testing" + "io/ioutil" + "crypto/rsa" + "crypto/rand" + "net/http" + "net/http/httptest" + "fmt" + "git.feneas.org/ganggo/federation/helpers" +) + +func TestRetract(t *testing.T) { + username := "test" + author := fmt.Sprintf("%s@host.tld", username) + parentGuid := "1234" + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Some error occured while parsing: %v", err) + } + + for i, proto := range []Protocol{DiasporaProtocol, ActivityPubProtocol} { + retract, err := NewMessageRetract(proto) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + + for ii, parent := range []EntityType{Like, Comment, StatusMessage, Contact} { + retract.SetAuthor(author) + retract.SetParentType(parent) + retract.SetParentGuid(parentGuid) + + ts := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + var msg Message = nil + switch proto { + case ActivityPubProtocol: + apu, err := ActivityPubParse(r) + if err != nil { + t.Errorf("#%d.%d: Some error occured while parsing: %v", i, ii, err) + } + msg = apu + case DiasporaProtocol: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("#%d.%d: Some error occured while parsing: %v", i, ii, err) + } + dia, err := DiasporaParse(body) + if err != nil { + dia, err = DiasporaParseEncrypted(body, privKey) + if err != nil { + t.Errorf("#%d.%d: Some error occured while parsing: %v", i, ii, err) + } + } + msg = dia + } + if msg == nil { + t.Errorf("#%d.%d: Something went wrong!", i, ii) + } + + msgRetraction, ok := msg.Entity().(MessageRetract) + if !ok { + t.Errorf("#%d.%d: Expected MessagePost, got %v", i, ii, msg.Entity()) + return + } + + if proto == DiasporaProtocol && msgRetraction.Author() != author { + t.Errorf("#%d.%d: Expected %s, got %s", i, ii, author, + msgRetraction.Author()) + } + + linkAuthor := fmt.Sprintf( + config.ApURLFormat, "user/" + username + "/actor") + if proto == ActivityPubProtocol && msgRetraction.Author() != linkAuthor { + t.Errorf("#%d.%d: Expected %s, got %s", i, ii, linkAuthor, + msgRetraction.Author()) + } + + if msgRetraction.Type().Proto != proto { + t.Errorf("#%d.%d: Expected %s, got %s", i, ii, proto, + msgRetraction.Type().Proto) + } + + if msgRetraction.Type().Entity != Retraction { + t.Errorf("#%d.%d: Expected %s, got %s", i, ii, Retraction, + msgRetraction.Type().Entity) + } + + if !msg.ValidSignature(&privKey.PublicKey) { + t.Errorf("#%d.%d: Expected a valid signature", i, ii) + } + + linkGuid1 := fmt.Sprintf("%s#likes/%s", linkAuthor, parentGuid) + linkGuid2 := fmt.Sprintf(config.GuidURLFormat, parentGuid) + if proto == ActivityPubProtocol && + msgRetraction.ParentGuid() != helpers.LinkToGuid(linkGuid1) && + msgRetraction.ParentGuid() != helpers.LinkToGuid(linkGuid2) { + t.Errorf("#%d.%d: Expected %s or %s, got %s", i, ii, + helpers.LinkToGuid(linkGuid1), helpers.LinkToGuid(linkGuid2), + msgRetraction.ParentGuid()) + } + + if proto == DiasporaProtocol && msgRetraction.ParentGuid() != parentGuid { + t.Errorf("#%d.%d: Expected %s, got %s", i, parentGuid, + msgRetraction.ParentGuid()) + } + + if proto == DiasporaProtocol && msgRetraction.ParentType() != parent { + t.Errorf("#%d.%d: Expected %s, got %s", i, ii, parent, + msgRetraction.ParentType()) + } + + w.WriteHeader(http.StatusOK) + }, + )) + defer ts.Close() + + err = retract.Send(ts.URL, privKey, &privKey.PublicKey) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } + } + } +} diff --git a/salmon_test.go b/salmon_test.go deleted file mode 100644 index e6d2d497b9bac1675867dd0ad1a2b48615e1c094..0000000000000000000000000000000000000000 --- a/salmon_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package federation -// -// GangGo Diaspora Federation Library -// Copyright (C) 2017 Lukas Matt -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import ( - "testing" - "fmt" -) - -func TestParseDecryptedRequest(t *testing.T) { - var msgData = `PHN0YXR1c19tZXNzYWdlPgogIDxhdXRob3I-ZGlhc3BvcmFfMm5kQGxvY2FsaG9zdDozMDAxPC9hdXRob3I-CiAgPGd1aWQ-ZmUyZDJhODA1MzQ4MDEzNWQwOGY1Mjk2ZjJlNzQ0N2I8L2d1aWQ-CiAgPGNyZWF0ZWRfYXQ-MjAxNy0wNy0yNVQwOToyNDozM1o8L2NyZWF0ZWRfYXQ-CiAgPHByb3ZpZGVyX2Rpc3BsYXlfbmFtZS8-CiAgPHRleHQ-cGluZzwvdGV4dD4KICA8cHVibGljPmZhbHNlPC9wdWJsaWM-Cjwvc3RhdHVzX21lc3NhZ2U-` - var tmpl = `%s%s%sNbuD4kERZzXPFRORH4NOcr7EAij-dWKTCG0eBBGZObN3Aic0lMAZ_rLU7o6PLOH9Q6p6dyneYjUjSu07vtI5Jy_N2XQpKUni3fUWxfDNgfMo26XKmxdJ5S2Gp1ux1ToO3FY9RByTZw5HZRpOBAfRSgttTgiY5_Yu5D-BLcEm_94R6FMWRniQXrMAt8hU9qCNSuVQlUKtuuy8qJXu6Z21VhI9lAT7wIALlR9UwIgz0e6UG9S9sU95f_38co0ibD1KbQpBd8c_lu5vCVIqlEe_Fa_xYZupMLaU8De-wzoBpBgqR65mRtUQTu2jP-Qxa3aXrANHxweIbnYfpZ5QcNA50hfyVJJSolczDSlDljTunEmHmWNaS3J7waEQsIDFATPFy6H5leRPpSzebXYca4T-EiapPP-mn41Vs3VKIdUXOHus_HcTPWRVT-Vr-yt7byFYEanb5b5lQ_IHcI0oyqX7RrVJid6UsBtwxwkX0FSc1cZgLhBQUgxBsUh5MNte-WZJv_6c9rHyNsH3rn9YEZp431P9GCe8gNdLY9bFQ1pYS9BxOAS2enu3yVpWpWRechiR7D__HC4-Hw2MHfSSmBQTxq5oO01_efEHB8XxWF85XYNT6_icXf3ZsTxkURT9HlHapkFwL7TlO5gPUZZVJt9f6kn9HoGQ56MX2n46KdKKid8=` - tests := [][]string{ - []string{msgData, "base32url", "RSA-SHA256", "ZGlhc3BvcmFfMm5kQGxvY2FsaG9zdDozMDAx"}, - []string{msgData, "base64url", "RSA-SHA128", "ZGlhc3BvcmFfMm5kQGxvY2FsaG9zdDozMDAx"}, - []string{msgData, "base64url", "RSA-SHA256", "not valid at all"}, - []string{"not valid", "base64url", "RSA-SHA256", "ZGlhc3BvcmFfMm5kQGxvY2FsaG9zdDozMDAx"}, - } - - for i, test := range tests { - _, entity, err := ParseDecryptedRequest([]byte(fmt.Sprintf( - tmpl, test[0], test[1], test[2], test[3], - ))); if err == nil { - t.Errorf("#%d: Expected to be an error, got %+v, with %+v", i, entity, test) - } - } - - _, _, err := ParseDecryptedRequest([]byte(" Location: Poll: Public:false Event:}} - if entity.Type != "status_message" { - t.Errorf("Expected type string 'status_message', got '%s'", entity.Type) - } - - if entity.SignatureOrder != "author guid created_at provider_display_name text public" { - t.Errorf("Expected an order of 'author guid created_at text public', got %s", entity.SignatureOrder) - } - - if _, ok := entity.Data.(EntityStatusMessage); !ok { - t.Errorf("Expected the struct type EntityStatusMessage, got %+v", entity.Data) - } -} - -func parseMessageRequest(t *testing.T, message Message) { - var data = `PHN0YXR1c19tZXNzYWdlPgogIDxhdXRob3I-ZGlhc3BvcmFfMm5kQGxvY2FsaG9zdDozMDAxPC9hdXRob3I-CiAgPGd1aWQ-ZmUyZDJhODA1MzQ4MDEzNWQwOGY1Mjk2ZjJlNzQ0N2I8L2d1aWQ-CiAgPGNyZWF0ZWRfYXQ-MjAxNy0wNy0yNVQwOToyNDozM1o8L2NyZWF0ZWRfYXQ-CiAgPHByb3ZpZGVyX2Rpc3BsYXlfbmFtZS8-CiAgPHRleHQ-cGluZzwvdGV4dD4KICA8cHVibGljPmZhbHNlPC9wdWJsaWM-Cjwvc3RhdHVzX21lc3NhZ2U-` - - var sig = `NbuD4kERZzXPFRORH4NOcr7EAij-dWKTCG0eBBGZObN3Aic0lMAZ_rLU7o6PLOH9Q6p6dyneYjUjSu07vtI5Jy_N2XQpKUni3fUWxfDNgfMo26XKmxdJ5S2Gp1ux1ToO3FY9RByTZw5HZRpOBAfRSgttTgiY5_Yu5D-BLcEm_94R6FMWRniQXrMAt8hU9qCNSuVQlUKtuuy8qJXu6Z21VhI9lAT7wIALlR9UwIgz0e6UG9S9sU95f_38co0ibD1KbQpBd8c_lu5vCVIqlEe_Fa_xYZupMLaU8De-wzoBpBgqR65mRtUQTu2jP-Qxa3aXrANHxweIbnYfpZ5QcNA50hfyVJJSolczDSlDljTunEmHmWNaS3J7waEQsIDFATPFy6H5leRPpSzebXYca4T-EiapPP-mn41Vs3VKIdUXOHus_HcTPWRVT-Vr-yt7byFYEanb5b5lQ_IHcI0oyqX7RrVJid6UsBtwxwkX0FSc1cZgLhBQUgxBsUh5MNte-WZJv_6c9rHyNsH3rn9YEZp431P9GCe8gNdLY9bFQ1pYS9BxOAS2enu3yVpWpWRechiR7D__HC4-Hw2MHfSSmBQTxq5oO01_efEHB8XxWF85XYNT6_icXf3ZsTxkURT9HlHapkFwL7TlO5gPUZZVJt9f6kn9HoGQ56MX2n46KdKKid8=` - - if message.Me != XMLNS_ME { - t.Errorf("Expected to be %s, got %s", XMLNS_ME, message.Me) - } - - if message.Encoding != BASE64_URL { - t.Errorf("Expected to be %s, got %s", BASE64_URL, message.Encoding) - } - - if message.Alg != RSA_SHA256 { - t.Errorf("Expected to be %s, got %s", RSA_SHA256, message.Alg) - } - - if message.Data.Type != APPLICATION_XML { - t.Errorf("Expected to be %s, got %s", APPLICATION_XML, message.Data.Type) - } - - if message.Data.Data != data { - t.Errorf("Expected to be %s, got %s", data, message.Data.Data) - } - - if message.Sig.Sig != sig { - t.Errorf("Expected to be %s, got %s", sig, message.Sig.Sig) - } - - if message.Sig.KeyId != TEST_AUTHOR { - t.Errorf("Expected to be %s, got %s", TEST_AUTHOR, message.Sig.KeyId) - } - - if message.Signature() != `NbuD4kERZzXPFRORH4NOcr7EAij-dWKTCG0eBBGZObN3Aic0lMAZ_rLU7o6PLOH9Q6p6dyneYjUjSu07vtI5Jy_N2XQpKUni3fUWxfDNgfMo26XKmxdJ5S2Gp1ux1ToO3FY9RByTZw5HZRpOBAfRSgttTgiY5_Yu5D-BLcEm_94R6FMWRniQXrMAt8hU9qCNSuVQlUKtuuy8qJXu6Z21VhI9lAT7wIALlR9UwIgz0e6UG9S9sU95f_38co0ibD1KbQpBd8c_lu5vCVIqlEe_Fa_xYZupMLaU8De-wzoBpBgqR65mRtUQTu2jP-Qxa3aXrANHxweIbnYfpZ5QcNA50hfyVJJSolczDSlDljTunEmHmWNaS3J7waEQsIDFATPFy6H5leRPpSzebXYca4T-EiapPP-mn41Vs3VKIdUXOHus_HcTPWRVT-Vr-yt7byFYEanb5b5lQ_IHcI0oyqX7RrVJid6UsBtwxwkX0FSc1cZgLhBQUgxBsUh5MNte-WZJv_6c9rHyNsH3rn9YEZp431P9GCe8gNdLY9bFQ1pYS9BxOAS2enu3yVpWpWRechiR7D__HC4-Hw2MHfSSmBQTxq5oO01_efEHB8XxWF85XYNT6_icXf3ZsTxkURT9HlHapkFwL7TlO5gPUZZVJt9f6kn9HoGQ56MX2n46KdKKid8=` { - t.Errorf("Expected different signature, got '%s'", message.Signature()) - } -} diff --git a/signature.go b/signature.go index 1425c9498873067cf72dddcc389006c209377b3e..581153ba6bdca16c6d87fa14c5c29efcbe35c50f 100644 --- a/signature.go +++ b/signature.go @@ -26,31 +26,29 @@ import ( "strings" ) -type SignatureInterface interface { - Signature() string - SignatureText(string) []string -} - -type Signature struct { - entity SignatureInterface - delim string +// signature will be used to generate and validate RSA signatures +type signature struct { + text, delim string Err error } -func (signature *Signature) New(entity SignatureInterface) *Signature { - signature.entity = entity - signature.delim = SignatureAuthorDelimiter - if _, ok := entity.(Message); ok { - signature.delim = SignatureDelimiter - } +// New sets subjects and a delimiter which is later used to construct a signature text +func (signature *signature) New(subjects []string, delim string) *signature { + signature.text = strings.Join(subjects, delim) + signature.delim = delim return signature } -func (signature *Signature) Sign(privKey *rsa.PrivateKey, sig *string) error { +// Sign will sign the signature text from the New() method +// and stores the output in the provided string pointer +func (signature *signature) Sign(privKey *rsa.PrivateKey, sig *string) error { h := sha256.New() - h.Write([]byte(strings.Join( - signature.entity.SignatureText(""), signature.delim))) + _, err := h.Write([]byte(signature.text)) + if err != nil { + signature.Err = err + return err + } digest := h.Sum(nil) rng := rand.Reader @@ -59,24 +57,28 @@ func (signature *Signature) Sign(privKey *rsa.PrivateKey, sig *string) error { signature.Err = err return err } - *sig = base64.URLEncoding.EncodeToString(bytes) + + if signature.delim == SignatureAuthorDelimiter { + *sig = base64.StdEncoding.EncodeToString(bytes) + } else { + *sig = base64.URLEncoding.EncodeToString(bytes) + } return nil } -func (signature *Signature) Verify(order string, pubKey *rsa.PublicKey) bool { - sig, err := base64.URLEncoding.DecodeString(signature.entity.Signature()) +// Verify will check the signature text provided in the method call +// against the signature text constructed in the New() method if it fails +// it returns false and adds the error message to the signature struct +func (signature *signature) Verify(pubKey *rsa.PublicKey, text string) bool { + sig, err := base64.URLEncoding.DecodeString(text) if err != nil { - sig, err = base64.StdEncoding.DecodeString(signature.entity.Signature()) + sig, err = base64.StdEncoding.DecodeString(text) if err != nil { signature.Err = err return false } } - orderArr := signature.entity.SignatureText(order) - message := []byte(strings.Join(orderArr, signature.delim)) - hashed := sha256.Sum256(message) - - err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sig) - signature.Err = err - return err == nil + hashed := sha256.Sum256([]byte(signature.text)) + signature.Err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sig) + return signature.Err == nil } diff --git a/signature_test.go b/signature_test.go index 9f95159cc3362079bafa885d9eed1e81031ef8c3..022f82264abf40bfd35259c2b0eb2603cfd3c943 100644 --- a/signature_test.go +++ b/signature_test.go @@ -19,46 +19,35 @@ package federation import ( "testing" - "encoding/base64" + "crypto/rsa" + "crypto/rand" ) -func TestSignatureInterface(t *testing.T) { - var signature Signature - var sig string - - priv, err := ParseRSAPrivateKey(TEST_PRIV_KEY) +func TestSignature(t *testing.T) { + var helloWorldSignature string + var helloWorldSubject = []string{"hello", "world"} + privKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Errorf("Some error occured while parsing: %v", err) } - err = signature.New(EntityLike{}).Sign(priv, &sig) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } + for i, delim := range []string{SignatureDelimiter, SignatureAuthorDelimiter} { + signature := (&signature{}).New(helloWorldSubject, delim) - if !signature.New(EntityLike{AuthorSignature: sig}).Verify( - "positive guid parent_guid parent_type author", &priv.PublicKey) { - t.Errorf("Expected to be a valid signature, got invalid") - } + err = signature.Sign(privKey, &helloWorldSignature) + if err != nil { + t.Errorf("#%d: Some error occured while parsing: %v", i, err) + } - err = signature.New(EntityComment{}).Sign(priv, &sig) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } + if helloWorldSignature == "" { + t.Errorf("#%d: Expected non empty string, got '%s'", + i, helloWorldSignature) + } - if !signature.New(EntityComment{AuthorSignature: sig}).Verify( - "author created_at guid parent_guid text", &priv.PublicKey) { - t.Errorf("Expected to be a valid signature, got invalid") + if !signature.Verify(&privKey.PublicKey, helloWorldSignature) { + t.Errorf("#%d: Expected a valid signature", i) + } } +} - sigBytes, err := base64.URLEncoding.DecodeString(sig) - if err != nil { - t.Errorf("Some error occured while parsing: %v", err) - } - sig = base64.StdEncoding.EncodeToString(sigBytes) - if !signature.New(EntityComment{AuthorSignature: sig}).Verify( - "author created_at guid parent_guid text", &priv.PublicKey) { - t.Errorf("Expected to be a valid signature, got invalid") - } -} diff --git a/webfinger.go b/webfinger.go index 4acc38d2a09bb70699fa98633452f9df41c40cbc..37977808a302731e1dab48451b25b5cb445ce82c 100644 --- a/webfinger.go +++ b/webfinger.go @@ -23,12 +23,14 @@ import ( "errors" ) +// WebFinger will be used to discover users on different servers (see https://tools.ietf.org/html/rfc7033) type WebFinger struct { Host string Handle string Data WebfingerData } +// WebfingerData holds and stores discovered webfinger profiles type WebfingerData struct { // xml XMLName xml.Name `xml:"XRD" json:"-"` @@ -41,6 +43,7 @@ type WebfingerData struct { Links []WebfingerDataLink `json:"links" xml:"Link"` } +// WebfingerDataLink holds and stores webfinger links type WebfingerDataLink struct { // xml XMLName xml.Name `xml:"Link" json:"-"` @@ -51,6 +54,8 @@ type WebfingerDataLink struct { Template string `json:"template,omitempty" xml:"template,attr,omitempty"` } +// Discovery will try fetching json data from a host. If that fails +// it will fallback to Diaspora XML webfinger func (w *WebFinger) Discovery() error { url := fmt.Sprintf("%s/.well-known/webfinger?resource=acct:%s", w.Host, w.Handle) err := FetchJson("GET", url, nil, &w.Data) @@ -67,3 +72,13 @@ func (w *WebFinger) Discovery() error { } return nil } + +// Protocol tries to identify a server by his webfinger profile +func (w *WebFinger) Protocol() Protocol { + for _, link := range w.Data.Links { + if link.Rel == WebFingerHcard || link.Rel == WebFingerDiaspora { + return DiasporaProtocol + } + } + return ActivityPubProtocol +}