...
  View open merge request
Commits (2)
package controllers
//
// GangGo API Library
// Copyright (C) 2019 Lukas Matt <lukas@zauberstuhl.de>
//
// 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 <http://www.gnu.org/licenses/>.
//
import (
"net/http"
// run "github.com/revel/modules/jobs/app/jobs"
// "git.feneas.org/ganggo/ganggo/app/jobs"
"git.feneas.org/ganggo/federation"
"github.com/revel/revel"
"net/url"
"github.com/go-fed/activity/pub"
//"github.com/go-fed/activity/deliverer"
"context"
"time"
"errors"
//"golang.org/x/time/rate"
"fmt"
"regexp"
)
var pubber pub.Pubber
var catchAllHandler pub.HandlerFunc
type ApiAP struct {
ApiHelper
}
type ApiAPResult struct{
ApiHelper
Type string
}
type Clock struct{}
func (c *Clock) Now() time.Time {
return time.Now()
}
type NaiveDeliverer struct {}
func (d *NaiveDeliverer) Do(a []byte, b *url.URL, toDo func(aa []byte, bb *url.URL) error) {
err := toDo(a, b)
if err != nil {
revel.AppLog.Error("NaiveDeliverer", "err", err)
}
}
type ApiAPFederationApp struct {
ApiAPApplication
ApiAPFederation
ApiAPSocialAPI
}
func init() {
var cb ApiAPCallbacker
ap := ApiAPFederationApp{
ApiAPApplication{},
ApiAPFederation{},
ApiAPSocialAPI{},
}
//opts := deliverer.DeliveryOptions{}
//opts.InitialRetryTime = 60 * time.Second
//opts.MaximumRetryTime = time.Hour
//opts.BackoffFactor = 1.5
//opts.MaxRetries = 30
//opts.RateLimit = rate.NewLimiter(0.5, 30)
//dpool := deliverer.NewDelivererPool(opts)
deliverer := &NaiveDeliverer{}
clock := &Clock{}
client := &http.Client{}
pubber = pub.NewPubber(
clock,
ap,
cb,
cb,
deliverer,
client,
"GGG/v0 UA",
5,
5,
)
catchAllHandler = pub.ServeActivityPubObject(ap, clock)
federation.SetPubber(pubber)
}
func (a ApiAPResult) Apply(req *revel.Request, resp *revel.Response) {
c := context.Background()
request := req.In.GetRaw().(*http.Request)
response := resp.Out.Server.GetRaw().(http.ResponseWriter)
var err error
var handled bool
switch a.Type {
case "inbox":
revel.AppLog.Error("ApiAPResult inbox", "method", req.Method)
if req.Method == "POST" {
handled, err = pubber.PostInbox(c, response, request)
} else if req.Method == "GET" {
handled, err = pubber.GetInbox(c, response, request)
}
case "outbox":
revel.AppLog.Error("ApiAPResult outbox", "method", req.Method)
if req.Method == "POST" {
handled, err = pubber.PostOutbox(c, response, request)
} else if req.Method == "GET" {
handled, err = pubber.GetOutbox(c, response, request)
}
default:
revel.AppLog.Error("ApiAPResult catchAllHandler", "method", req.Method)
handled, err = catchAllHandler(c, response, request)
}
// render error message if unhandled
if !handled {
revel.AppLog.Debug("ApiAPResult", "handled", handled)
a.ApiHelper.CatchAll().Apply(req, resp)
}
if err != nil {
revel.AppLog.Error("ApiAPResult", "err", err)
}
}
func (a ApiAP) Inbox(username string) revel.Result {
return ApiAPResult{ApiHelper: a.ApiHelper, Type: "inbox"}
}
func (a ApiAP) Outbox(username string) revel.Result {
return ApiAPResult{ApiHelper: a.ApiHelper, Type: "outbox"}
}
func (a ApiAP) CatchAll() revel.Result {
return ApiAPResult{ApiHelper: a.ApiHelper}
}
func extractUsernameFromURL(u *url.URL) (string, error) {
usernameRegex := regexp.MustCompile(`/person/([^/]+?)(/.*){0,1}$`)
regexParts := usernameRegex.FindAllStringSubmatch(u.String(), 1)
if len(regexParts) <= 0 || len(regexParts[0]) <= 1 {
return "", errors.New("cannot extract username from URL")
}
return regexParts[0][1], nil
}
func copyAndSetURLPageParam(url *url.URL, page uint64) *url.URL {
// create a copy of the struct
copyUrl := *url
query := copyUrl.Query()
query.Set("page", fmt.Sprintf("%d", page))
copyUrl.RawQuery = query.Encode()
return &copyUrl
}
This diff is collapsed.
package controllers
//
// GangGo API Library
// Copyright (C) 2019 Lukas Matt <lukas@zauberstuhl.de>
//
// 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 <http://www.gnu.org/licenses/>.
//
import (
api "git.feneas.org/ganggo/api/app"
run "github.com/revel/modules/jobs/app/jobs"
"git.feneas.org/ganggo/ganggo/app/jobs"
"git.feneas.org/ganggo/ganggo/app/models"
federation "git.feneas.org/ganggo/federation"
"github.com/revel/revel"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/vocab"
"context"
"regexp"
"strings"
"fmt"
)
type ApiAPCallbacker struct {}
func (ApiAPCallbacker) Create(c context.Context, s *streams.Create) error {
m, _ := s.Raw().Serialize()
revel.AppLog.Debug("ApiAPCallbacker Create",
"objLen", s.Raw().ObjectLen(), "m", m)
for i := 0; i < s.Raw().ObjectLen(); i += 1 {
usernameRegex := regexp.MustCompile(`/person/([^/]+?)(/.*){0,1}$`)
obj := s.Raw().GetObject(i)
switch {
case vocab.HasTypeNote(obj):
post := &federation.ActivityPubPost{
CreateType: s.Raw(),
NoteType: obj.(vocab.NoteType),
}
msg := federation.ActivityPubMessage{}
msg.SetEntity(post)
if post.Public() {
run.Now(jobs.Receiver{Message: msg})
} else {
for i := 0; i < post.NoteType.ToLen(); i += 1 {
to := post.NoteType.GetToIRI(i).String()
if !strings.HasPrefix(to, fmt.Sprintf("%s%s", api.PROTO, api.ADDRESS)) {
continue
}
regexParts := usernameRegex.FindAllStringSubmatch(to, 1)
if len(regexParts) <= 0 || len(regexParts[0]) <= 1 {
continue
}
username := regexParts[0][1]
var user models.User
err := user.FindByUsername(username)
if err != nil {
continue
}
run.Now(jobs.Receiver{Guid:user.Person.Guid, Message: msg})
}
}
}
}
return nil
}
func (ApiAPCallbacker) Update(c context.Context, s *streams.Update) error {
revel.AppLog.Error("ApiAPCallbacker Update")
return nil
}
func (ApiAPCallbacker) Delete(c context.Context, s *streams.Delete) error {
revel.AppLog.Error("ApiAPCallbacker Delete")
return nil
}
func (ApiAPCallbacker) Add(c context.Context, s *streams.Add) error {
revel.AppLog.Error("ApiAPCallbacker Add")
return nil
}
func (ApiAPCallbacker) Remove(c context.Context, s *streams.Remove) error {
revel.AppLog.Error("ApiAPCallbacker Remove")
return nil
}
func (ApiAPCallbacker) Like(c context.Context, s *streams.Like) error {
revel.AppLog.Debug("ApiAPCallbacker Like")
//
return nil
}
func (ApiAPCallbacker) Block(c context.Context, s *streams.Block) error {
revel.AppLog.Error("ApiAPCallbacker Block")
return nil
}
func (a ApiAPCallbacker) Follow(c context.Context, s *streams.Follow) error {
revel.AppLog.Debug("ApiAPCallbacker Follow", "ActorLen", s.Raw().ActorLen())
return a.follow(c, true, s)
}
func (ApiAPCallbacker) follow(c context.Context, f bool, s *streams.Follow) error {
for i := 0; i < s.Raw().ActorLen(); i+= 1 {
follow := &federation.ActivityPubFollow{FollowType: s.Raw()}
follow.SetSharing(f)
msg := federation.ActivityPubMessage{}
msg.SetEntity(follow)
run.Now(jobs.Receiver{Message: msg})
}
return nil
}
func (a ApiAPCallbacker) Undo(c context.Context, s *streams.Undo) error {
revel.AppLog.Debug("ApiAPCallbacker Undo")
for i := 0; i < s.Raw().ObjectLen(); i+= 1 {
if !s.Raw().IsObject(i) {
continue
}
m, err := s.Raw().GetObject(i).Serialize()
if err != nil {
revel.AppLog.Error("ApiAPCallbacker Undo", "err", err)
continue
}
resolver := &streams.Resolver{
FollowCallback: func(follow *streams.Follow) error {
return a.follow(c, false, follow)
},
}
err = resolver.Deserialize(m)
if err != nil {
revel.AppLog.Error("ApiAPCallbacker Undo", "err", err)
continue
}
}
return nil
}
func (ApiAPCallbacker) Accept(c context.Context, s *streams.Accept) error {
revel.AppLog.Error("ApiAPCallbacker Accept")
return nil
}
func (ApiAPCallbacker) Reject(c context.Context, s *streams.Reject) error {
revel.AppLog.Error("ApiAPCallbacker Reject")
return nil
}
package controllers
//
// GangGo API Library
// Copyright (C) 2019 Lukas Matt <lukas@zauberstuhl.de>
//
// 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 <http://www.gnu.org/licenses/>.
//
import (
api "git.feneas.org/ganggo/api/app"
// run "github.com/revel/modules/jobs/app/jobs"
// "git.feneas.org/ganggo/ganggo/app/jobs"
fhelpers "git.feneas.org/ganggo/federation/helpers"
"git.feneas.org/ganggo/ganggo/app/models"
"github.com/revel/revel"
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/vocab"
//"github.com/go-fed/activity/deliverer"
"github.com/go-fed/activity/streams"
"context"
"errors"
"crypto"
"github.com/go-fed/httpsig"
//"golang.org/x/time/rate"
"fmt"
"regexp"
)
type ApiAPFederation struct {}
func (ApiAPFederation) OnFollow(c context.Context, s *streams.Follow) pub.FollowResponse {
return pub.AutomaticAccept
}
func (ApiAPFederation) Unblocked(c context.Context, actorIRIs []*url.URL) error {
revel.AppLog.Error("ApiAPFederation Unblocked")
return nil
}
func (ApiAPFederation) FilterForwarding(c context.Context, activity vocab.ActivityType, iris []*url.URL) ([]*url.URL, error) {
revel.AppLog.Error("ApiAPFederation FilterForwarding")
return nil, errors.New("na2")
}
func (ApiAPFederation) NewSigner() (httpsig.Signer, error) {
revel.AppLog.Error("ApiAPFederation NewSigner")
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
headersToSign := []string{"User-Agent"}
signer, _, err := httpsig.NewSigner(prefs, headersToSign, httpsig.Signature)
if err != nil {
return nil, err
}
return signer, nil
}
func (ApiAPFederation) PrivateKey(boxIRI *url.URL) (crypto.PrivateKey, string, error) {
usernameRegex := regexp.MustCompile(`/person/([^/]+?)(/.*){0,1}$`)
regexParts := usernameRegex.FindAllStringSubmatch(boxIRI.String(), 1)
if len(regexParts) <= 0 || len(regexParts[0]) <= 1 {
revel.AppLog.Error("ApiAPApplication PrivateKey", "url", boxIRI.String())
return nil, "", errors.New(ERR_USER_NOT_FOUND)
}
username := regexParts[0][1]
host := fmt.Sprintf("%s%s", api.PROTO, api.ADDRESS)
keyId := fmt.Sprintf("%s/api/v1/ap/person/%s/%s",
host, username, "actor#main-key")
var user models.User
err := user.FindByUsername(username)
if err != nil {
return nil, keyId, err
}
privKey, err := fhelpers.ParseRSAPrivateKey([]byte(user.SerializedPrivateKey))
if err != nil {
return nil, keyId, err
}
return privKey, keyId, nil
}
package controllers
//
// GangGo API Library
// Copyright (C) 2019 Lukas Matt <lukas@zauberstuhl.de>
//
// 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 <http://www.gnu.org/licenses/>.
//
import (
"net/http"
api "git.feneas.org/ganggo/api/app"
// run "github.com/revel/modules/jobs/app/jobs"
// "git.feneas.org/ganggo/ganggo/app/jobs"
fhelpers "git.feneas.org/ganggo/federation/helpers"
"git.feneas.org/ganggo/ganggo/app/models"
"github.com/revel/revel"
"net/url"
"github.com/go-fed/activity/pub"
//"github.com/go-fed/activity/deliverer"
"context"
"errors"
"crypto"
"github.com/go-fed/httpsig"
//"golang.org/x/time/rate"
)
type ApiAPSocialAPI struct {}
type ApiAPSocialAPIVerifier struct {}
func (ApiAPSocialAPIVerifier) Verify(r *http.Request) (authenticatedUser *url.URL, authn, authz bool, err error) {
return nil, false, false, errors.New(ERR_NOT_IMPLEMENTED)
}
func (ApiAPSocialAPIVerifier) VerifyForOutbox(r *http.Request, outbox *url.URL) (authn, authz bool, err error) {
// XXX think of something better :\
if r.Proto == "INTERNAL" {
return true, true, nil
}
accessToken := r.Header.Get("access_token")
if accessToken != "" {
var token models.OAuthToken
err := token.FindByToken(accessToken)
if err != nil {
return false, false, err
}
username, err := extractUsernameFromURL(outbox)
if err != nil {
return false, false, err
}
if token.User.Username != username {
return false, false, errors.New("user does not own outbox")
}
if !token.User.ActiveLastDay() {
err = token.User.UpdateLastSeen()
if err != nil {
return false, false, err
}
}
return true, true, nil
}
return false, false, nil
}
func (ApiAPSocialAPI) ActorIRI(c context.Context, r *http.Request) (*url.URL, error) {
revel.AppLog.Error("ApiAPSocialAPI ActorIRI")
return nil, errors.New("na1")
}
func (ApiAPSocialAPI) GetSocialAPIVerifier(c context.Context) pub.SocialAPIVerifier {
revel.AppLog.Error("ApiAPSocialAPI GetSocialAPIVerifier")
return &ApiAPSocialAPIVerifier{}
}
func (ApiAPSocialAPI) GetPublicKeyForOutbox(c context.Context, publicKeyId string, boxIRI *url.URL) (crypto.PublicKey, httpsig.Algorithm, error) {
revel.AppLog.Error("ApiAPSocialAPI GetPublicKeyForOutbox")
u, err := url.Parse(publicKeyId)
if err != nil {
return nil, httpsig.RSA_SHA256, err
}
username, err := extractUsernameFromURL(u)
if err != nil {
return nil, httpsig.RSA_SHA256, err
}
var person models.Person
err = person.FindByAuthor(username + "@" + api.ADDRESS)
if err != nil {
return nil, httpsig.RSA_SHA256, err
}
pubKey, err := fhelpers.ParseRSAPublicKey([]byte(person.SerializedPublicKey))
if err != nil {
return nil, httpsig.RSA_SHA256, err
}
return pubKey, httpsig.RSA_SHA256, nil
}
This diff is collapsed.
......@@ -264,6 +264,7 @@ func (a ApiAspect) DeletePerson() revel.Result {
run.Now(jobs.Dispatcher{
User: a.CurrentUser,
Message: membership,
Retract: true,
})
}
return a.RenderJSON(membership)
......
# Restful API routes
## ActivityPub
POST /api/v0/ap/inbox ApiApUser.Inbox
* /api/v0/ap/user/:username/outbox ApiApUser.Outbox
POST /api/v0/ap/user/:username/inbox ApiApUser.Inbox
* /api/v0/ap/user/:username/following ApiApUser.Following
* /api/v0/ap/user/:username/followers ApiApUser.Followers
* /api/v0/ap/user/:username/actor ApiApUser.Actor
## ActivityPub GoFed Lib
* /api/v1/ap/inbox ApiAP.Inbox
* /api/v1/ap/person/:username/inbox ApiAP.Inbox
* /api/v1/ap/person/:username/outbox ApiAP.Outbox
* /api/v1/ap/*catchall ApiAP.CatchAll
## GangGo API
POST /api/v0/oauth/tokens ApiOAuth.Create
......@@ -58,4 +56,4 @@ DELETE /api/v0/people/:id/aspects/:aspect_id ApiAspect.DeletePerson
POST /api/v0/search ApiSearch.Create
# API catch-all-rule will result in "not implemented" error
* /api/v0/*catchall ApiHelper.CatchAll
* /api/*catchall ApiHelper.CatchAll