├── .travis.yml ├── LICENSE ├── README.md ├── actions.go ├── ap ├── actor.go ├── actor_map.go ├── apdb.go ├── c2s.go ├── clock.go ├── common.go ├── database.go ├── instance_actor_common.go ├── instance_actor_s2s.go ├── s2s.go └── util.go ├── app ├── application.go ├── database.go ├── framework.go ├── funcs.go ├── paths.go ├── router.go └── software.go ├── cmdline.go ├── dep_inj.go ├── example ├── README.md ├── app.go ├── main.go ├── services.go └── templates │ ├── auth.tmpl │ ├── bad_request.tmpl │ ├── create_note.tmpl │ ├── followers.tmpl │ ├── followers_request.tmpl │ ├── following.tmpl │ ├── following_create.tmpl │ ├── footer.tmpl │ ├── header.tmpl │ ├── home.tmpl │ ├── inbox.tmpl │ ├── inline_css.tmpl │ ├── internal_error.tmpl │ ├── list_notes.tmpl │ ├── list_users.tmpl │ ├── login.tmpl │ ├── nav.tmpl │ ├── not_allowed.tmpl │ ├── not_found.tmpl │ ├── note.tmpl │ ├── outbox.tmpl │ └── user.tmpl ├── framework ├── clarke.go ├── client.go ├── config.go ├── config │ ├── config.go │ ├── core_config.go │ └── verify.go ├── conn │ ├── host_limiter.go │ ├── retrier.go │ └── transport.go ├── db │ ├── db.go │ └── postgres.go ├── framework.go ├── handler.go ├── mux_wrapper.go ├── nodeinfo │ ├── nodeinfo.go │ ├── nodeinfo2.go │ └── pkg.go ├── oauth2 │ ├── oauth.go │ └── proxy.go ├── prompt.go ├── prompt_config.go ├── router.go ├── server.go ├── web │ ├── sessions.go │ └── user_agent.go └── webfinger │ └── webfinger.go ├── go.mod ├── go.sum ├── models ├── client_infos.go ├── credentials.go ├── delivery_attempts.go ├── fed_data.go ├── followers.go ├── following.go ├── inboxes.go ├── liked.go ├── local_data.go ├── model.go ├── outboxes.go ├── policies.go ├── private_keys.go ├── resolutions.go ├── serialization.go ├── sql_dialect.go ├── test │ ├── main.go │ └── testdata.go ├── token_infos.go └── users.go ├── paths ├── iri.go └── query.go ├── pkg.go ├── run.go ├── services ├── activitystreams.go ├── any.go ├── crypto.go ├── data.go ├── delivery_attempts.go ├── followers.go ├── following.go ├── inboxes.go ├── liked.go ├── nodeinfo.go ├── oauth2.go ├── outboxes.go ├── pagination.go ├── policies.go ├── private_keys.go ├── tx.go └── users.go └── util ├── context.go ├── log.go ├── resolvers.go └── safe_start_stop.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.15 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apcore 2 | 3 | > Server framework for quickly building ActivityPub applications 4 | 5 | *Under Construction* 6 | 7 | [![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url] 8 | [![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url] 9 | [![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url] 10 | 11 | `go get github.com/go-fed/apcore` 12 | 13 | apcore is a powerful single server 14 | [ActivityPub](https://www.w3.org/TR/activitypub) 15 | framework for performant Fediverse applications. 16 | 17 | It is built on top of the 18 | [go-fed/activity](https://github.com/go-fed/activity) 19 | suite of libraries, which means it can readily allow application developers to 20 | iterate and leverage new 21 | [ActivityStreams](https://www.w3.org/TR/activitystreams-core) 22 | or RDF vocabularies. 23 | 24 | ## Features 25 | 26 | *This list is a work in progress.* 27 | 28 | * Uses `go-fed/activity` 29 | * ActivityPub S2S (Server-to-Server) Protocol supported 30 | * ActivityPub C2S (Client-to-Server) Protocol supported 31 | * Both S2S and C2S can be used at the same time 32 | * Comes with the Core & Extended ActivityStreams types 33 | * Readily expands to support new ActivityStreams types and/or RDF vocabularies 34 | * Federation & Moderation Policy System 35 | * Administrators and/or users can create policies to customize their federation experience 36 | * Auditable results of applying policies on incoming federated data 37 | * Supports common out-of-the-box command-line commands for: 38 | * Initializing a database with the appropriate `apcore` tables as well as your application-specific tables 39 | * Initializing a new administrator account 40 | * Creating a server configuration file in a guided flow 41 | * Comprehensive help command 42 | * Guided command line flow for administrators for all the above tasks, featuring Clarke the Cow 43 | * Configuration file support 44 | * Add your configuration options to the existing `apcore` configuration options 45 | * Administrators can customize their ActivityPub and your app's experience 46 | * Database support 47 | * Currently, only PostgreSQL supported 48 | * Others can be added with a some SQL work, in the future 49 | * No ORM overhead 50 | * Your custom application has access to `apcore` tables, and more 51 | * OAuth2 support 52 | * Easy API to build authorization grant and validation flows 53 | * Handles server side state for you 54 | * Webfinger & Host-Meta support 55 | 56 | ## How To Use This Framework 57 | 58 | *This guide is a work in progress.* 59 | 60 | Building an application is not an easy thing to do, but following these steps 61 | reduces the cost of building a *federated* application: 62 | 63 | 0. Implement the `apcore.Application` interface. 64 | 0. Call `apcore.Run` with your implementation in `main`. 65 | 66 | The most work is in the first step, as your application logic is able to live as 67 | functional closures as the `Application` is used within the `apcore` framework. 68 | See the documentation on the `Application` interface for specific details. 69 | 70 | [Build-Status-Image]: https://travis-ci.org/go-fed/apcore.svg?branch=master 71 | [Build-Status-Url]: https://travis-ci.org/go-fed/apcore 72 | [Go-Reference-Image]: https://pkg.go.dev/badge/github.com/go-fed/apcore.svg 73 | [Go-Reference-Url]: https://pkg.go.dev/github.com/go-fed/apcore 74 | [Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/go-fed/apcore 75 | [Go-Report-Card-Url]: https://goreportcard.com/report/github.com/go-fed/apcore 76 | [License-Image]: https://img.shields.io/github/license/go-fed/apcore?color=blue 77 | [License-Url]: https://www.gnu.org/licenses/agpl-3.0.en.html 78 | [Chat-Image]: https://img.shields.io/matrix/go-fed:feneas.org?server_fqdn=matrix.org 79 | [Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org 80 | [OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs 81 | [OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs 82 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package apcore 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-fed/apcore/app" 23 | "github.com/go-fed/apcore/framework" 24 | "github.com/go-fed/apcore/services" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | func doCreateTables(configFilePath string, a app.Application, debug bool, scheme string) error { 29 | db, d, ms, cfg, err := newModels(configFilePath, a, debug, scheme) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | tx, err := db.BeginTx(context.Background(), nil) 35 | if err != nil { 36 | return err 37 | } 38 | defer tx.Rollback() 39 | for _, m := range ms { 40 | if err := m.CreateTable(tx, d); err != nil { 41 | return err 42 | } 43 | } 44 | if err = tx.Commit(); err != nil { 45 | return err 46 | } 47 | return a.CreateTables(context.Background(), &services.Any{db}, cfg, debug) 48 | } 49 | 50 | func doInitAdmin(configFilePath string, a app.Application, debug bool, scheme string) error { 51 | db, users, c, err := newUserService(configFilePath, a, debug, scheme) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // Prompt for admin information 57 | p := services.CreateUserParameters{ 58 | Scheme: scheme, 59 | Host: c.ServerConfig.Host, 60 | RSAKeySize: c.ServerConfig.RSAKeySize, 61 | HashParams: services.HashPasswordParameters{ 62 | SaltSize: c.ServerConfig.SaltSize, 63 | BCryptStrength: c.ServerConfig.BCryptStrength, 64 | }, 65 | } 66 | var password string 67 | p.Username, p.Email, password, err = framework.PromptAdminUser() 68 | 69 | // Create the user in the database 70 | defer db.Close() 71 | tx, err := db.BeginTx(context.Background(), nil) 72 | if err != nil { 73 | return err 74 | } 75 | defer tx.Rollback() 76 | userID, err := users.CreateAdminUser(util.Context{context.Background()}, p, password) 77 | if err != nil { 78 | return err 79 | } 80 | if err = tx.Commit(); err != nil { 81 | return err 82 | } 83 | return a.OnCreateAdminUser(context.Background(), userID, &services.Any{db}, c) 84 | } 85 | 86 | func doInitData(configFilePath string, a app.Application, debug bool, scheme string) error { 87 | db, users, c, err := newUserService(configFilePath, a, debug, scheme) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // Create the server actor in the database 93 | defer db.Close() 94 | tx, err := db.BeginTx(context.Background(), nil) 95 | if err != nil { 96 | return err 97 | } 98 | defer tx.Rollback() 99 | _, err = users.CreateInstanceActorSingleton(util.Context{context.Background()}, scheme, c.ServerConfig.Host, c.ServerConfig.RSAKeySize) 100 | if err != nil { 101 | return err 102 | } 103 | return tx.Commit() 104 | } 105 | 106 | func doInitServerProfile(configFilePath string, a app.Application, debug bool, scheme string) error { 107 | db, users, c, err := newUserService(configFilePath, a, debug, scheme) 108 | if err != nil { 109 | return err 110 | } 111 | defer db.Close() 112 | 113 | sp, err := framework.PromptServerProfile(scheme, c.ServerConfig.Host) 114 | if err != nil { 115 | return err 116 | } 117 | tx, err := db.BeginTx(context.Background(), nil) 118 | if err != nil { 119 | return err 120 | } 121 | defer tx.Rollback() 122 | err = users.SetServerPreferences(util.Context{context.Background()}, sp) 123 | if err != nil { 124 | return err 125 | } 126 | return tx.Commit() 127 | } 128 | -------------------------------------------------------------------------------- /ap/actor.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-fed/activity/pub" 23 | "github.com/go-fed/apcore/app" 24 | "github.com/go-fed/apcore/framework/config" 25 | "github.com/go-fed/apcore/framework/conn" 26 | "github.com/go-fed/apcore/framework/oauth2" 27 | "github.com/go-fed/apcore/services" 28 | ) 29 | 30 | func NewActor(c *config.Config, 31 | a app.Application, 32 | clock *Clock, 33 | db *Database, 34 | apdb *APDB, 35 | o *oauth2.Server, 36 | pk *services.PrivateKeys, 37 | po *services.Policies, 38 | f *services.Followers, 39 | u *services.Users, 40 | tc *conn.Controller) (actor pub.Actor, err error) { 41 | 42 | common := NewCommonBehavior(a, db, tc, o, pk) 43 | ca, isC2S := a.(app.C2SApplication) 44 | sa, isS2S := a.(app.S2SApplication) 45 | if !isC2S && !isS2S { 46 | err = fmt.Errorf("the Application is neither a C2SApplication nor a S2SApplication") 47 | } else if isC2S && isS2S { 48 | c2s := NewSocialBehavior(ca, o) 49 | s2s := NewFederatingBehavior(c, sa, db, po, pk, f, u, tc) 50 | actor = pub.NewActor( 51 | common, 52 | c2s, 53 | s2s, 54 | apdb, 55 | clock) 56 | } else if isC2S { 57 | c2s := NewSocialBehavior(ca, o) 58 | actor = pub.NewSocialActor( 59 | common, 60 | c2s, 61 | apdb, 62 | clock) 63 | } else { 64 | s2s := NewFederatingBehavior(c, sa, db, po, pk, f, u, tc) 65 | actor = pub.NewFederatingActor( 66 | common, 67 | s2s, 68 | apdb, 69 | clock) 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /ap/actor_map.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "github.com/go-fed/activity/pub" 21 | "github.com/go-fed/apcore/framework/config" 22 | "github.com/go-fed/apcore/framework/conn" 23 | "github.com/go-fed/apcore/paths" 24 | "github.com/go-fed/apcore/services" 25 | ) 26 | 27 | func NewActorMap(c *config.Config, 28 | clock *Clock, 29 | db *Database, 30 | apdb *APDB, 31 | pk *services.PrivateKeys, 32 | f *services.Followers, 33 | tc *conn.Controller) (actorMap map[paths.Actor]pub.Actor) { 34 | actorMap = make(map[paths.Actor]pub.Actor, 1) 35 | actorMap[paths.InstanceActor] = newInstanceActor(c, clock, db, apdb, pk, f, tc) 36 | return 37 | } 38 | 39 | func newInstanceActor(c *config.Config, 40 | clock *Clock, 41 | db *Database, 42 | apdb *APDB, 43 | pk *services.PrivateKeys, 44 | f *services.Followers, 45 | tc *conn.Controller) (actor pub.Actor) { 46 | common := newInstanceActorCommonBehavior(db, tc, pk) 47 | s2s := newInstanceActorFederatingBehavior(c, db, pk, f, tc) 48 | actor = pub.NewFederatingActor(common, s2s, apdb, clock) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /ap/apdb.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/url" 23 | "sync" 24 | 25 | "github.com/go-fed/activity/pub" 26 | "github.com/go-fed/activity/streams/vocab" 27 | "github.com/go-fed/apcore/app" 28 | ) 29 | 30 | var _ pub.Database = &APDB{} 31 | 32 | type APDB struct { 33 | *Database 34 | // Use sync.Map, which is specially optimized: 35 | // 36 | // "The Map type is optimized [...] when the entry for a given key is 37 | // only ever written once but read many times, as in caches that only 38 | // grow" 39 | // 40 | // This means we only ever append to the map during the lifetime of the 41 | // running application. This may become a scaling bottleneck in the 42 | // future, but unsure how the performance will look in practice. 43 | // 44 | // This map will only store *sync.Mutex, each is 4 bytes. Assuming that 45 | // conservatively the average key is a string of 124 bytes, this means 46 | // each entry is 128 bytes of memory. 47 | // 48 | // If this map holds 2,000,000 entries then it would take 256 MB of 49 | // memory. To take up 1 GB, 7,812,500 entries are needed. If one entry 50 | // is added per second, then in 90 days it will take up 1 GB of memory. 51 | // 52 | // TODO: Address this unbounded growth for memory-constrained or very 53 | // long running applications. 54 | locks *sync.Map 55 | app app.Application 56 | } 57 | 58 | func NewAPDB(db *Database, a app.Application) *APDB { 59 | return &APDB{ 60 | Database: db, 61 | locks: &sync.Map{}, 62 | app: a, 63 | } 64 | } 65 | 66 | func (a *APDB) Lock(c context.Context, id *url.URL) error { 67 | mui, _ := a.locks.LoadOrStore(id.String(), &sync.Mutex{}) 68 | if mu, ok := mui.(*sync.Mutex); !ok { 69 | return fmt.Errorf("lock for Lock is not a *sync.Mutex") 70 | } else { 71 | mu.Lock() 72 | return nil 73 | } 74 | } 75 | 76 | func (a *APDB) Unlock(c context.Context, id *url.URL) error { 77 | mui, _ := a.locks.Load(id.String()) 78 | if mu, ok := mui.(*sync.Mutex); !ok { 79 | return fmt.Errorf("lock for Unlock is not a *sync.Mutex") 80 | } else { 81 | mu.Unlock() 82 | return nil 83 | } 84 | } 85 | 86 | func (a *APDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { 87 | var path string 88 | path, err = a.app.NewIDPath(c, t) 89 | if err != nil { 90 | return 91 | } 92 | id = &url.URL{ 93 | Scheme: a.scheme, 94 | Host: a.host, 95 | Path: path, 96 | } 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /ap/c2s.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | 24 | "github.com/go-fed/activity/pub" 25 | "github.com/go-fed/activity/streams/vocab" 26 | "github.com/go-fed/apcore/app" 27 | "github.com/go-fed/apcore/framework/oauth2" 28 | "github.com/go-fed/apcore/util" 29 | oa2 "github.com/go-fed/oauth2" 30 | ) 31 | 32 | var _ pub.SocialProtocol = &SocialBehavior{} 33 | 34 | type SocialBehavior struct { 35 | app app.C2SApplication 36 | o *oauth2.Server 37 | } 38 | 39 | func NewSocialBehavior(app app.C2SApplication, o *oauth2.Server) *SocialBehavior { 40 | return &SocialBehavior{ 41 | app: app, 42 | o: o, 43 | } 44 | } 45 | 46 | func (s *SocialBehavior) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (out context.Context, err error) { 47 | ctx := util.Context{c} 48 | ctx.WithActivityStream(data) 49 | out = ctx.Context 50 | return 51 | } 52 | 53 | func (s *SocialBehavior) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 54 | out = c 55 | var t oa2.TokenInfo 56 | t, authenticated, err = s.o.ValidateOAuth2AccessToken(w, r) 57 | if err != nil || !authenticated { 58 | return 59 | } 60 | // Authenticated, but must determine if permitted by the granted scope. 61 | authenticated, err = s.app.ScopePermitsPostOutbox(t.GetScope()) 62 | return 63 | } 64 | 65 | func (s *SocialBehavior) SocialCallbacks(c context.Context) (wrapped pub.SocialWrappedCallbacks, other []interface{}, err error) { 66 | wrapped = pub.SocialWrappedCallbacks{} 67 | other = s.app.ApplySocialCallbacks(&wrapped) 68 | return 69 | } 70 | 71 | func (s *SocialBehavior) DefaultCallback(c context.Context, activity pub.Activity) error { 72 | return fmt.Errorf("Unhandled client Activity of type: %s", activity.GetTypeName()) 73 | } 74 | -------------------------------------------------------------------------------- /ap/clock.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/go-fed/activity/pub" 23 | ) 24 | 25 | var _ pub.Clock = &Clock{} 26 | 27 | type Clock struct { 28 | loc *time.Location 29 | } 30 | 31 | // Creates new clock with IANA Time Zone database string 32 | func NewClock(location string) (c *Clock, err error) { 33 | c = &Clock{} 34 | c.loc, err = time.LoadLocation(location) 35 | return 36 | } 37 | 38 | func (c *Clock) Now() time.Time { 39 | return time.Now().In(c.loc) 40 | } 41 | -------------------------------------------------------------------------------- /ap/common.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "crypto/rsa" 22 | "net/http" 23 | "net/url" 24 | 25 | "github.com/go-fed/activity/pub" 26 | "github.com/go-fed/activity/streams/vocab" 27 | "github.com/go-fed/apcore/app" 28 | "github.com/go-fed/apcore/framework/conn" 29 | "github.com/go-fed/apcore/framework/oauth2" 30 | "github.com/go-fed/apcore/paths" 31 | "github.com/go-fed/apcore/services" 32 | "github.com/go-fed/apcore/util" 33 | oa2 "github.com/go-fed/oauth2" 34 | ) 35 | 36 | var _ pub.CommonBehavior = &CommonBehavior{} 37 | 38 | type CommonBehavior struct { 39 | app app.Application 40 | tc *conn.Controller 41 | o *oauth2.Server 42 | db *Database 43 | pk *services.PrivateKeys 44 | } 45 | 46 | func NewCommonBehavior( 47 | app app.Application, 48 | db *Database, 49 | tc *conn.Controller, 50 | o *oauth2.Server, 51 | pk *services.PrivateKeys) *CommonBehavior { 52 | return &CommonBehavior{ 53 | app: app, 54 | tc: tc, 55 | o: o, 56 | db: db, 57 | pk: pk, 58 | } 59 | } 60 | 61 | func (a *CommonBehavior) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (newCtx context.Context, authenticated bool, err error) { 62 | return a.authenticateGetRequest(util.Context{c}, w, r) 63 | } 64 | 65 | func (a *CommonBehavior) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (newCtx context.Context, authenticated bool, err error) { 66 | return a.authenticateGetRequest(util.Context{c}, w, r) 67 | } 68 | 69 | func (a *CommonBehavior) GetOutbox(c context.Context, r *http.Request) (ocp vocab.ActivityStreamsOrderedCollectionPage, err error) { 70 | ctx := util.Context{c} 71 | // IfChange 72 | var outboxIRI *url.URL 73 | if outboxIRI, err = ctx.CompleteRequestURL(); err != nil { 74 | return 75 | } 76 | if ctx.HasPrivateScope() { 77 | ocp, err = a.db.GetOutbox(c, outboxIRI) 78 | } else { 79 | ocp, err = a.db.GetPublicOutbox(c, outboxIRI) 80 | } 81 | // ThenChange(router.go) 82 | return 83 | } 84 | 85 | func (a *CommonBehavior) NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t pub.Transport, err error) { 86 | ctx := util.Context{c} 87 | var userUUID paths.UUID 88 | userUUID, err = ctx.UserPathUUID() 89 | if err != nil { 90 | return 91 | } 92 | var privKey *rsa.PrivateKey 93 | var pubKeyURL *url.URL 94 | privKey, pubKeyURL, err = a.pk.GetUserHTTPSignatureKey(util.Context{c}, userUUID) 95 | if err != nil { 96 | return 97 | } 98 | return a.tc.Get(privKey, pubKeyURL.String()) 99 | } 100 | 101 | func (a *CommonBehavior) authenticateGetRequest(c util.Context, w http.ResponseWriter, r *http.Request) (newCtx context.Context, authenticated bool, err error) { 102 | newCtx = c 103 | var t oa2.TokenInfo 104 | var oAuthAuthenticated bool 105 | t, oAuthAuthenticated, err = a.o.ValidateOAuth2AccessToken(w, r) 106 | if err != nil { 107 | return 108 | } else { 109 | // With or without OAuth, permit public access 110 | authenticated = true 111 | } 112 | // No OAuth2 means guaranteed denial of private access 113 | if !oAuthAuthenticated { 114 | return 115 | } 116 | // Determine if private access permitted by the granted scope. 117 | var ok bool 118 | ok, err = a.app.ScopePermitsPrivateGetInbox(t.GetScope()) 119 | if err != nil { 120 | return 121 | } else { 122 | ctx := &util.Context{c} 123 | ctx.WithPrivateScope(ok) 124 | newCtx = ctx.Context 125 | } 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /ap/instance_actor_common.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "crypto/rsa" 22 | "net/http" 23 | "net/url" 24 | 25 | "github.com/go-fed/activity/pub" 26 | "github.com/go-fed/activity/streams/vocab" 27 | "github.com/go-fed/apcore/framework/conn" 28 | "github.com/go-fed/apcore/services" 29 | "github.com/go-fed/apcore/util" 30 | ) 31 | 32 | var _ pub.CommonBehavior = &instanceActorCommonBehavior{} 33 | 34 | type instanceActorCommonBehavior struct { 35 | tc *conn.Controller 36 | db *Database 37 | pk *services.PrivateKeys 38 | } 39 | 40 | func newInstanceActorCommonBehavior( 41 | db *Database, 42 | tc *conn.Controller, 43 | pk *services.PrivateKeys) *instanceActorCommonBehavior { 44 | return &instanceActorCommonBehavior{ 45 | tc: tc, 46 | db: db, 47 | pk: pk, 48 | } 49 | } 50 | 51 | func (a *instanceActorCommonBehavior) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (newCtx context.Context, authenticated bool, err error) { 52 | authenticated = true 53 | newCtx = c 54 | return 55 | } 56 | 57 | func (a *instanceActorCommonBehavior) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (newCtx context.Context, authenticated bool, err error) { 58 | authenticated = true 59 | newCtx = c 60 | return 61 | } 62 | 63 | func (a *instanceActorCommonBehavior) GetOutbox(c context.Context, r *http.Request) (ocp vocab.ActivityStreamsOrderedCollectionPage, err error) { 64 | ctx := util.Context{c} 65 | // IfChange 66 | var outboxIRI *url.URL 67 | if outboxIRI, err = ctx.CompleteRequestURL(); err != nil { 68 | return 69 | } 70 | ocp, err = a.db.GetPublicOutbox(c, outboxIRI) 71 | // ThenChange(router.go) 72 | return 73 | } 74 | 75 | func (a *instanceActorCommonBehavior) NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t pub.Transport, err error) { 76 | var privKey *rsa.PrivateKey 77 | var pubKeyURL *url.URL 78 | privKey, pubKeyURL, err = a.pk.GetUserHTTPSignatureKeyForInstanceActor(util.Context{c}) 79 | if err != nil { 80 | return 81 | } 82 | return a.tc.Get(privKey, pubKeyURL.String()) 83 | } 84 | -------------------------------------------------------------------------------- /ap/instance_actor_s2s.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | "net/url" 23 | 24 | "github.com/go-fed/activity/pub" 25 | "github.com/go-fed/activity/streams/vocab" 26 | "github.com/go-fed/apcore/framework/config" 27 | "github.com/go-fed/apcore/framework/conn" 28 | "github.com/go-fed/apcore/services" 29 | "github.com/go-fed/apcore/util" 30 | ) 31 | 32 | var _ pub.FederatingProtocol = &instanceActorFederatingBehavior{} 33 | 34 | type instanceActorFederatingBehavior struct { 35 | maxInboxForwardingDepth int 36 | maxDeliveryDepth int 37 | db *Database 38 | pk *services.PrivateKeys 39 | f *services.Followers 40 | tc *conn.Controller 41 | } 42 | 43 | func newInstanceActorFederatingBehavior(c *config.Config, 44 | db *Database, 45 | pk *services.PrivateKeys, 46 | f *services.Followers, 47 | tc *conn.Controller) *instanceActorFederatingBehavior { 48 | return &instanceActorFederatingBehavior{ 49 | maxInboxForwardingDepth: c.ActivityPubConfig.MaxInboxForwardingRecursionDepth, 50 | maxDeliveryDepth: c.ActivityPubConfig.MaxDeliveryRecursionDepth, 51 | db: db, 52 | pk: pk, 53 | f: f, 54 | tc: tc, 55 | } 56 | } 57 | 58 | func (f *instanceActorFederatingBehavior) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) (out context.Context, err error) { 59 | ctx := &util.Context{c} 60 | ctx.WithActivity(activity) 61 | out = ctx.Context 62 | return 63 | } 64 | 65 | func (f *instanceActorFederatingBehavior) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 66 | authenticated, err = verifyHttpSignatures(c, r, f.db, f.pk, f.tc) 67 | out = c 68 | return 69 | } 70 | 71 | func (f *instanceActorFederatingBehavior) Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error) { 72 | return 73 | } 74 | 75 | func (f *instanceActorFederatingBehavior) FederatingCallbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { 76 | wrapped = pub.FederatingWrappedCallbacks{ 77 | OnFollow: pub.OnFollowDoNothing, 78 | } 79 | return 80 | } 81 | 82 | func (f *instanceActorFederatingBehavior) DefaultCallback(c context.Context, activity pub.Activity) error { 83 | activityIRI, err := pub.GetId(activity) 84 | if err != nil { 85 | return err 86 | } 87 | util.InfoLogger.Infof("Nothing to do for federated Activity of type %q: %s", activity.GetTypeName(), activityIRI) 88 | return nil 89 | } 90 | 91 | func (f *instanceActorFederatingBehavior) MaxInboxForwardingRecursionDepth(c context.Context) int { 92 | return f.maxInboxForwardingDepth 93 | } 94 | 95 | func (f *instanceActorFederatingBehavior) MaxDeliveryRecursionDepth(c context.Context) int { 96 | return f.maxDeliveryDepth 97 | } 98 | 99 | func (f *instanceActorFederatingBehavior) FilterForwarding(c context.Context, potentialRecipients []*url.URL, a pub.Activity) (filteredRecipients []*url.URL, err error) { 100 | ctx := util.Context{c} 101 | var actorIRI *url.URL 102 | actorIRI, err = ctx.ActorIRI() 103 | if err != nil { 104 | return 105 | } 106 | // Here we limit to only allow forwarding to the target user's 107 | // followers. 108 | var fc vocab.ActivityStreamsCollection 109 | fc, err = f.f.GetAllForActor(ctx, actorIRI) 110 | if err != nil { 111 | return 112 | } 113 | allowedRecipients := make(map[*url.URL]bool, 0) 114 | items := fc.GetActivityStreamsItems() 115 | if items != nil { 116 | for iter := items.Begin(); iter != items.End(); iter = iter.Next() { 117 | var id *url.URL 118 | id, err = pub.ToId(iter) 119 | if err != nil { 120 | return 121 | } 122 | allowedRecipients[id] = true 123 | } 124 | } 125 | for _, elem := range potentialRecipients { 126 | if has, ok := allowedRecipients[elem]; ok && has { 127 | filteredRecipients = append(filteredRecipients, elem) 128 | } 129 | } 130 | return 131 | } 132 | 133 | func (f *instanceActorFederatingBehavior) GetInbox(c context.Context, r *http.Request) (ocp vocab.ActivityStreamsOrderedCollectionPage, err error) { 134 | ctx := util.Context{c} 135 | // IfChange 136 | var inboxIRI *url.URL 137 | if inboxIRI, err = ctx.CompleteRequestURL(); err != nil { 138 | return 139 | } 140 | ocp, err = f.db.GetPublicInbox(c, inboxIRI) 141 | // ThenChange(router.go) 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /ap/util.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package ap 18 | 19 | import ( 20 | "context" 21 | "crypto" 22 | "crypto/rsa" 23 | "crypto/x509" 24 | "encoding/json" 25 | "encoding/pem" 26 | "fmt" 27 | "net/http" 28 | "net/url" 29 | 30 | "github.com/go-fed/activity/pub" 31 | "github.com/go-fed/activity/streams" 32 | "github.com/go-fed/activity/streams/vocab" 33 | "github.com/go-fed/apcore/framework/conn" 34 | "github.com/go-fed/apcore/paths" 35 | "github.com/go-fed/apcore/services" 36 | "github.com/go-fed/apcore/util" 37 | "github.com/go-fed/httpsig" 38 | ) 39 | 40 | type publicKeyer interface { 41 | GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty 42 | } 43 | 44 | func getPublicKeyFromResponse(c context.Context, b []byte, keyId *url.URL) (p crypto.PublicKey, err error) { 45 | m := make(map[string]interface{}, 0) 46 | err = json.Unmarshal(b, &m) 47 | if err != nil { 48 | return 49 | } 50 | var t vocab.Type 51 | t, err = streams.ToType(c, m) 52 | if err != nil { 53 | return 54 | } 55 | pker, ok := t.(publicKeyer) 56 | if !ok { 57 | err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t) 58 | return 59 | } 60 | pkp := pker.GetW3IDSecurityV1PublicKey() 61 | if pkp == nil { 62 | err = fmt.Errorf("publicKey property is not provided") 63 | return 64 | } 65 | var pkpFound vocab.W3IDSecurityV1PublicKey 66 | for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() { 67 | if !pkpIter.IsW3IDSecurityV1PublicKey() { 68 | continue 69 | } 70 | pkValue := pkpIter.Get() 71 | var pkId *url.URL 72 | pkId, err = pub.GetId(pkValue) 73 | if err != nil { 74 | return 75 | } 76 | if pkId.String() != keyId.String() { 77 | continue 78 | } 79 | pkpFound = pkValue 80 | break 81 | } 82 | if pkpFound == nil { 83 | err = fmt.Errorf("cannot find publicKey with id: %s", keyId) 84 | return 85 | } 86 | pkPemProp := pkpFound.GetW3IDSecurityV1PublicKeyPem() 87 | if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { 88 | err = fmt.Errorf("publicKeyPem property is not provided or it is not embedded as a value") 89 | return 90 | } 91 | pubKeyPem := pkPemProp.Get() 92 | var block *pem.Block 93 | block, _ = pem.Decode([]byte(pubKeyPem)) 94 | if block == nil || block.Type != "PUBLIC KEY" { 95 | err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") 96 | return 97 | } 98 | p, err = x509.ParsePKIXPublicKey(block.Bytes) 99 | return 100 | } 101 | 102 | func verifyHttpSignatures(c context.Context, 103 | r *http.Request, 104 | db *Database, 105 | pk *services.PrivateKeys, 106 | tc *conn.Controller) (authenticated bool, err error) { 107 | // 1. Figure out what key we need to verify 108 | ctx := util.Context{c} 109 | var v httpsig.Verifier 110 | v, err = httpsig.NewVerifier(r) 111 | if err != nil { 112 | return 113 | } 114 | kId := v.KeyId() 115 | var kIdIRI *url.URL 116 | kIdIRI, err = url.Parse(kId) 117 | if err != nil { 118 | return 119 | } 120 | // 2. Get our user's credentials 121 | var userUUID paths.UUID 122 | userUUID, err = ctx.UserPathUUID() 123 | if err != nil { 124 | return 125 | } 126 | var privKey *rsa.PrivateKey 127 | var pubKeyURL *url.URL 128 | privKey, pubKeyURL, err = pk.GetUserHTTPSignatureKey(ctx, userUUID) 129 | if err != nil { 130 | return 131 | } 132 | pubKeyId := pubKeyURL.String() 133 | // 3. Fetch the public key of the other actor using our credentials 134 | tp, err := tc.Get(privKey, pubKeyId) 135 | if err != nil { 136 | return 137 | } 138 | var b []byte 139 | b, err = tp.Dereference(c, kIdIRI) 140 | if err != nil { 141 | return 142 | } 143 | pKey, err := getPublicKeyFromResponse(c, b, kIdIRI) 144 | if err != nil { 145 | return 146 | } 147 | // 4. Verify the other actor's key 148 | algo := tc.GetFirstAlgorithm() 149 | authenticated = nil == v.Verify(pKey, algo) 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /app/database.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | type Database interface { 24 | Begin() TxBuilder 25 | } 26 | 27 | type TxBuilder interface { 28 | QueryOneRow(sql string, cb func(r SingleRow) error, args ...interface{}) 29 | Query(sql string, cb func(r SingleRow) error, args ...interface{}) 30 | ExecOneRow(sql string, args ...interface{}) 31 | Exec(sql string, args ...interface{}) 32 | Do(c context.Context) error 33 | } 34 | 35 | type SingleRow interface { 36 | Scan(dest ...interface{}) error 37 | } 38 | -------------------------------------------------------------------------------- /app/framework.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | "net/url" 23 | 24 | "github.com/go-fed/activity/streams/vocab" 25 | "github.com/go-fed/apcore/paths" 26 | ) 27 | 28 | // Framework provides request-time hooks for use in handlers. 29 | type Framework interface { 30 | Context(r *http.Request) context.Context 31 | 32 | UserIRI(userUUID paths.UUID) *url.URL 33 | 34 | // CreateUser creates a new unprivileged user with the given username, 35 | // email, and password. 36 | // 37 | // If an error is returned, it can be checked using IsNotUniqueUsername 38 | // and IsNotUniqueEmail to show the error to the user. 39 | CreateUser(c context.Context, username, email, password string) (userID string, err error) 40 | 41 | // IsNotUniqueUsername returns true if the error returned from 42 | // CreateUser is due to the username not being unique. 43 | IsNotUniqueUsername(error) bool 44 | 45 | // IsNotUniqueEmail returns true if the error returned from CreateUser 46 | // is due to the email not being unique. 47 | IsNotUniqueEmail(error) bool 48 | 49 | // Validate attempts to obtain and validate the OAuth token or first 50 | // party credential in the request. This can be called in your handlers 51 | // at request-handing time. 52 | // 53 | // If an error is returned, both the token and authentication values 54 | // should be ignored. 55 | // 56 | // TODO: Scopes 57 | Validate(w http.ResponseWriter, r *http.Request) (userID paths.UUID, authenticated bool, err error) 58 | 59 | // Send will send an Activity or Object on behalf of the user. 60 | // 61 | // Note that a new ID is not needed on the activity and/or objects that 62 | // are being sent; they will be generated as needed. 63 | // 64 | // Calling Send when federation is disabled results in an error. 65 | Send(c context.Context, userID paths.UUID, toSend vocab.Type) error 66 | 67 | // SendAcceptFollow accepts the provided Follow on behalf of the user. 68 | // 69 | // Calling SendAcceptFollow when federation is disabled results in an 70 | // error. 71 | SendAcceptFollow(c context.Context, userID paths.UUID, followIRI *url.URL) error 72 | 73 | // SendRejectFollow rejects the provided Follow on behalf of the user. 74 | // 75 | // Calling SendRejectFollow when federation is disabled results in an 76 | // error. 77 | SendRejectFollow(c context.Context, userID paths.UUID, followIRI *url.URL) error 78 | 79 | Session(r *http.Request) (Session, error) 80 | 81 | // TODO: Determine if we need this. 82 | GetByIRI(c context.Context, id *url.URL) (vocab.Type, error) 83 | 84 | // Given a user ID, retrieves all follow requests that have not yet been 85 | // Accepted nor Rejected. 86 | OpenFollowRequests(c context.Context, userID paths.UUID) ([]vocab.ActivityStreamsFollow, error) 87 | 88 | // GetPrivileges accepts a pointer to an appPrivileges struct to read 89 | // from the database for the given user, and also returns whether that 90 | // user is an admin. 91 | GetPrivileges(c context.Context, userID paths.UUID, appPrivileges interface{}) (admin bool, err error) 92 | // SetPrivileges sets the given application privileges and admin status 93 | // for the given user. 94 | SetPrivileges(c context.Context, userID paths.UUID, admin bool, appPrivileges interface{}) error 95 | } 96 | 97 | type Session interface { 98 | UserID() (string, error) 99 | Set(string, interface{}) 100 | Get(string) (interface{}, bool) 101 | Has(string) bool 102 | Delete(string) 103 | Save(*http.Request, http.ResponseWriter) error 104 | } 105 | -------------------------------------------------------------------------------- /app/funcs.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | ) 25 | 26 | type AuthorizeFunc func(c context.Context, w http.ResponseWriter, r *http.Request, db Database) (permit bool, err error) 27 | 28 | type CollectionPageHandlerFunc func(http.ResponseWriter, *http.Request, vocab.ActivityStreamsCollectionPage) 29 | 30 | type VocabHandlerFunc func(http.ResponseWriter, *http.Request, vocab.Type) 31 | -------------------------------------------------------------------------------- /app/paths.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | // Paths is a set of endpoints for apcore handlers provided out of the box. It 20 | // allows applications to override defaults, for example in case localization 21 | // where "/login" can instead be "/{locale}/login". 22 | // 23 | // The Redirect fields are functions. They accept the current path and can 24 | // return a corresponding path. For example, when redirecting to the homepage 25 | // in a locale usecase, it may be passed "/de-DE/login" which means the function 26 | // then returns "/de-DE". 27 | // 28 | // A zero-value struct is valid and uses apcore defaults. 29 | type Paths struct { 30 | GetLogin string 31 | PostLogin string 32 | GetLogout string 33 | GetOAuth2Authorize string 34 | PostOAuth2Authorize string 35 | RedirectToHomepage func(string) string 36 | RedirectToLogin func(string) string 37 | } 38 | 39 | func (p Paths) getOrDefault(s, d string) string { 40 | if s == "" { 41 | return d 42 | } 43 | return s 44 | } 45 | 46 | func (p Paths) GetLoginPath() string { 47 | return p.getOrDefault(p.GetLogin, "/login") 48 | } 49 | 50 | func (p Paths) PostLoginPath() string { 51 | return p.getOrDefault(p.PostLogin, "/login") 52 | } 53 | 54 | func (p Paths) GetLogoutPath() string { 55 | return p.getOrDefault(p.GetLogout, "/logout") 56 | } 57 | 58 | func (p Paths) GetOAuth2AuthorizePath() string { 59 | return p.getOrDefault(p.GetOAuth2Authorize, "/oauth2/authorize") 60 | } 61 | 62 | func (p Paths) PostOAuth2AuthorizePath() string { 63 | return p.getOrDefault(p.PostOAuth2Authorize, "/oauth2/authorize") 64 | } 65 | 66 | func (p Paths) RedirectToHomepagePath(currentPath string) string { 67 | if p.RedirectToHomepage == nil { 68 | return "/" 69 | } 70 | return p.getOrDefault(p.RedirectToHomepage(currentPath), "/") 71 | } 72 | 73 | func (p Paths) RedirectToLoginPath(currentPath string) string { 74 | if p.RedirectToLogin == nil { 75 | return "/login" 76 | } 77 | return p.getOrDefault(p.RedirectToLogin(currentPath), "/") 78 | } 79 | -------------------------------------------------------------------------------- /app/router.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | import ( 20 | "net/http" 21 | 22 | "github.com/gorilla/mux" 23 | ) 24 | 25 | type Router interface { 26 | ActivityPubOnlyHandleFunc(path string, authFn AuthorizeFunc) Route 27 | ActivityPubAndWebHandleFunc(path string, authFn AuthorizeFunc, f func(http.ResponseWriter, *http.Request)) Route 28 | HandleAuthorizationRequest(path string) Route 29 | HandleAccessTokenRequest(path string) Route 30 | Get(name string) Route 31 | WebOnlyHandle(path string, handler http.Handler) Route 32 | WebOnlyHandleFunc(path string, f func(http.ResponseWriter, *http.Request)) Route 33 | Handle(path string, handler http.Handler) Route 34 | HandleFunc(path string, f func(http.ResponseWriter, *http.Request)) Route 35 | Headers(pairs ...string) Route 36 | Host(tpl string) Route 37 | Methods(methods ...string) Route 38 | Name(name string) Route 39 | NewRoute() Route 40 | Path(tpl string) Route 41 | PathPrefix(tpl string) Route 42 | Queries(pairs ...string) Route 43 | Schemes(schemes ...string) Route 44 | Use(mwf ...mux.MiddlewareFunc) 45 | Walk(walkFn mux.WalkFunc) error 46 | } 47 | 48 | type Route interface { 49 | ActivityPubOnlyHandleFunc(path string, authFn AuthorizeFunc) Route 50 | ActivityPubAndWebHandleFunc(path string, authFn AuthorizeFunc, f func(http.ResponseWriter, *http.Request)) Route 51 | HandleAuthorizationRequest(path string) Route 52 | HandleAccessTokenRequest(path string) Route 53 | WebOnlyHandler(path string, handler http.Handler) Route 54 | WebOnlyHandlerFunc(path string, f func(http.ResponseWriter, *http.Request)) Route 55 | Handler(handler http.Handler) Route 56 | HandlerFunc(f func(http.ResponseWriter, *http.Request)) Route 57 | Headers(pairs ...string) Route 58 | Host(tpl string) Route 59 | Methods(methods ...string) Route 60 | Name(name string) Route 61 | Path(tpl string) Route 62 | PathPrefix(tpl string) Route 63 | Queries(pairs ...string) Route 64 | Schemes(schemes ...string) Route 65 | Subrouter() Router 66 | } 67 | -------------------------------------------------------------------------------- /app/software.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package app 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | type Software struct { 24 | Name string 25 | UserAgent string 26 | MajorVersion int 27 | MinorVersion int 28 | PatchVersion int 29 | Repository string 30 | } 31 | 32 | func (s Software) String() string { 33 | return fmt.Sprintf( 34 | "%s (%s)", 35 | s.Name, 36 | s.Version()) 37 | } 38 | 39 | func (s Software) Version() string { 40 | return fmt.Sprintf("%d.%d.%d", 41 | s.MajorVersion, 42 | s.MinorVersion, 43 | s.PatchVersion) 44 | } 45 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # apcore example 2 | 3 | This shows a very bare-bones server that has both S2S (Federation API) and C2S 4 | (Social API) enabled using the `apcore` framework. 5 | 6 | ## Requirements 7 | 8 | * A platform that can build `go-fed/activity` and `go-fed/apcore`. 9 | * A local Postgres server. 10 | 11 | ## Guide 12 | 13 | Once the example has been obtained, you will go through setting up the example 14 | like any administrator would when using the apcore framework. This lets all 15 | administrators benefit from a common workflow; improvements to this workflow are 16 | then shared across all applications, including this example one. 17 | 18 | The steps are: 19 | 20 | 0. Configuring the software (`configure`) 21 | 0. Initializing the database tables (`init-db`) 22 | 0. Creating an administrator's account (`init-admin`) 23 | 0. Launching the server (`serve`) 24 | 25 | ### Building the Binary 26 | 27 | Build the example's binary: 28 | 29 | `go install github.com/go-fed/apcore/example` 30 | 31 | This binary will be referred to as `example`. 32 | 33 | ### Preparing 34 | 35 | Note: These steps may require you to have a certificate (and key) to run a local 36 | server that serves `https` connections. A self-signed certificate is sufficient 37 | for development and non-public purposes. It can be generated using `openssl`: 38 | 39 | `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365` 40 | 41 | If you have a domain name and certificate for that domain name, you can run this 42 | example application using that domain name to test federation with other 43 | instances. 44 | 45 | This application is meant to be a proof-of-concept, example, or a base for other 46 | applications, and *not* a permanent production server. **To enforce this, it will 47 | terminate itself after being up for one hour.** 48 | 49 | #### Configuring 50 | 51 | First, we must configure the software. This configuration can be changed later, 52 | but to take effect requires restarting the server. It mainly sets parameters 53 | for tuning resource usage, but applications can include other specific 54 | configuration parameters. 55 | 56 | To launch the guided flow, run: 57 | 58 | `example configure` 59 | 60 | which will write a `config.ini` file. 61 | 62 | #### Database Initialization 63 | 64 | Second, the configuration will be used to create the tables in a database. It 65 | requires connecting to the database. 66 | 67 | To do so, run: 68 | 69 | `example init-db` 70 | 71 | which will run `CREATE TABLE IF NOT EXISTS` commands. 72 | 73 | #### Administrator Account Creation 74 | 75 | Third, the first account needs to be created: an administrator account! It will 76 | be done in a guided flow, and requires connecting to the database. 77 | 78 | To launch the guided flow, run: 79 | 80 | `example init-admin` 81 | 82 | which will run several queries to create the administrator user. 83 | 84 | ### Launching The Example App Server 85 | 86 | The time has come! To launch the server, run the following command: 87 | 88 | `example serve` 89 | 90 | which can be exited with `ctl+c`. 91 | 92 | Once the server is up, it can be visited by going to `https://localhost/`. 93 | 94 | ## Features 95 | 96 | TODO 97 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/go-fed/apcore" 21 | "github.com/go-fed/apcore/app" 22 | ) 23 | 24 | func main() { 25 | // Build an instance of our struct that satisfies the Application 26 | // interface. 27 | // 28 | // Implementing the Application interface is where most of the work 29 | // to use the framework lies. 30 | // 31 | // go-fed/apcore provides a very quick-to-implement but vanilla 32 | // ActivityPub framework. It is a convenience layer on top of 33 | // go-fed/activity, which has the opposite philosophy: assume as little 34 | // as possible, provide more powerful but time-consuming interfaces to 35 | // satisfy. 36 | var a app.Application 37 | var e error 38 | a, e = newApplication("templates/*.tmpl") 39 | if e != nil { 40 | panic(e) 41 | } 42 | // Run the apcore framework. 43 | // 44 | // Depending on the command line flags chosen, an action can occur: 45 | // - Configuring the application and generating a config file 46 | // - Initializing a database 47 | // - Initializing an admin account in the database 48 | // - Launching the example App to serve live web & ActivityPub traffic 49 | // 50 | // All of these capabilities are supported by the framework out of the 51 | // box. Refer to the command line help for more details. 52 | apcore.Run(a) 53 | } 54 | -------------------------------------------------------------------------------- /example/templates/auth.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Authorize

3 | {{template "footer.tmpl" .}} 4 | -------------------------------------------------------------------------------- /example/templates/bad_request.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

400 Bad Request

3 |

Sorry, a bad request was made!

4 | {{template "footer.tmpl" .}} 5 | -------------------------------------------------------------------------------- /example/templates/create_note.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Create A Note

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
To:
Summary:
Content:
Public:
11 |
12 | {{template "footer.tmpl" .}} 13 | -------------------------------------------------------------------------------- /example/templates/followers.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Followers

3 |
    4 | {{if isString .Other.items}} 5 | 6 | {{.Other.items}} 7 | 8 | {{else}} 9 | {{range $i := seq (len .Other.items)}} 10 | {{with (index .Other.items $i)}} 11 |
  • {{.}}
  • 12 | {{end}} 13 | {{end}} 14 | {{end}} 15 |
16 | {{template "footer.tmpl" .}} 17 | -------------------------------------------------------------------------------- /example/templates/followers_request.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Follow Requests

3 | {{if eq (len .Other) 0}} 4 |

There are no pending follow requests.

5 | {{else}} 6 |
7 | 8 | 9 | {{range .Other}} 10 | 11 | 12 | 14 | 16 | 18 | 19 | {{end}} 20 | 21 |
UserAccept?Reject?Do Nothing
{{.Actor}} 13 | 15 | 17 |
22 |
23 | {{end}} 24 | {{template "footer.tmpl" .}} 25 | -------------------------------------------------------------------------------- /example/templates/following.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Following

3 |
    4 | {{if isString .Other.items}} 5 | 6 | {{.Other.items}} 7 | 8 | {{else}} 9 | {{range $i := seq (len .Other.items)}} 10 | {{with (index .Other.items $i)}} 11 |
  • {{.}}
  • 12 | {{end}} 13 | {{end}} 14 | {{end}} 15 |
16 | {{template "footer.tmpl" .}} 17 | -------------------------------------------------------------------------------- /example/templates/following_create.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Request To Follow Someone

3 |
4 | 5 | 6 |
7 | {{template "footer.tmpl" .}} 8 | -------------------------------------------------------------------------------- /example/templates/footer.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/templates/header.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BLand 6 | {{template "inline_css.tmpl" .}} 7 | 8 | 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /example/templates/home.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

BLand Home Page

3 |

Welcome to the

apcore
example application, BLand!

4 | {{if gt (len .Other) 0}} 5 |

Here are the latest public notes:

6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | {{range $i, $n := .Other}} 14 | 15 | 16 | 17 | 18 | 19 | {{end}} 20 |
IDSummaryContent
{{$i}} {{$n.id}}{{$n.summary}}{{$n.content}}
21 |
22 | {{else}} 23 |

There are no public notes to see

24 | {{end}} 25 | {{template "footer.tmpl" .}} 26 | -------------------------------------------------------------------------------- /example/templates/inbox.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Inbox

3 |

These activity links should 404 because they are accessible as 4 | ActivityStreams content but not as web page content. This is to 5 | showcase this framework feature. It is not a limitation, as a 6 | developer could instead decide to support rendering it as a webpage. 7 | I just choose not to do so for activities in this demo app. 8 | For example, to verify the data is available to federated peers, you 9 | can run: 10 |

curl $ID -H "Accept: application/activity+json"
11 | Which will fetch the content as ActivityStreams data.

12 | 13 | 14 | 15 | 16 | {{$root := .}} 17 | {{if isString .Other.orderedItems}} 18 | 19 | 20 | 21 | {{else}} 22 | {{range $i, $_ := seq (len .Other.orderedItems)}} 23 | {{with (index $root.Other.orderedItems $i)}} 24 | 25 | 26 | 27 | {{end}} 28 | {{end}} 29 | {{end}} 30 |
Activity
{{.Other.orderedItems}}
{{.}}
31 | {{if .Other.prev}} 32 |

Prev

33 | {{else}} 34 |

Prev

35 | {{end}} 36 | {{if .Other.next}} 37 |

Next

38 | {{else}} 39 |

Next

40 | {{end}} 41 | {{template "footer.tmpl" .}} 42 | -------------------------------------------------------------------------------- /example/templates/inline_css.tmpl: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /example/templates/internal_error.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

500 Internal Error

3 |

Sorry, an internal error has occurred in the server!

4 | {{template "footer.tmpl" .}} 5 | -------------------------------------------------------------------------------- /example/templates/list_notes.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Notes

3 | {{if gt (len .Other) 0}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{range $i, $n := .Other}} 11 | 12 | 13 | 14 | 15 | 16 | {{end}} 17 |
IDSummaryContent
{{$i}} {{$n.id}}{{$n.summary}}{{$n.content}}
18 | {{else}} 19 |

No notes to see

20 | {{end}} 21 | {{template "footer.tmpl" .}} 22 | -------------------------------------------------------------------------------- /example/templates/list_users.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

All Users

3 | 4 | 5 | 6 | 7 | 8 | 9 | {{range $i, $u := .Other}} 10 | 11 | 12 | 13 | 14 | 15 | {{end}} 16 |
NamePreferred UsernameActor IRI
{{$u.name}}{{$u.preferredUsername}}{{$u.id}}
17 | {{template "footer.tmpl" .}} 18 | -------------------------------------------------------------------------------- /example/templates/login.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Login

3 | {{if .Error}} 4 |

Error logging in

5 | {{end}} 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
email
password
17 | 18 |
19 | {{template "footer.tmpl" .}} 20 | -------------------------------------------------------------------------------- /example/templates/nav.tmpl: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /example/templates/not_allowed.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

405 Method Not Allowed

3 |

Sorry, that HTTP method is not allowed at this endpoint!

4 | {{template "footer.tmpl" .}} 5 | -------------------------------------------------------------------------------- /example/templates/not_found.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

404 Not Found

3 |

Sorry, we could not find the page you were looking for!

4 | {{template "footer.tmpl" .}} 5 | -------------------------------------------------------------------------------- /example/templates/note.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Note

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
From:{{.Other.attributedTo}}
To: 11 | {{if isString .Other.to}} 12 | {{.Other.to}} 13 | {{else}} 14 | {{range $i, $v := .Other.to}} 15 | {{$v}} 16 | {{end}} 17 | {{end}} 18 |
CC: 23 | {{if isString .Other.cc}} 24 | {{.Other.cc}} 25 | {{else}} 26 | {{range $i, $v := .Other.cc}} 27 | {{$v}} 28 | {{end}} 29 | {{end}} 30 |
Summary:{{.Other.summary}}
Content:{{.Other.content}}
41 | {{template "footer.tmpl" .}} 42 | -------------------------------------------------------------------------------- /example/templates/outbox.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

Outbox

3 |

These activity links should 404 because they are accessible as 4 | ActivityStreams content but not as web page content. This is to 5 | showcase this framework feature. It is not a limitation, as a 6 | developer could instead decide to support rendering it as a webpage. 7 | I just choose not to do so for activities in this demo app. 8 | For example, to verify the data is available to federated peers, you 9 | can run: 10 |

curl $ID -H "Accept: application/activity+json"
11 | Which will fetch the content as ActivityStreams data.

12 | 13 | 14 | 15 | 16 | {{$root := .}} 17 | {{if isString .Other.orderedItems}} 18 | 19 | 20 | 21 | {{else}} 22 | {{range $i, $_ := seq (len .Other.orderedItems)}} 23 | {{with (index $root.Other.orderedItems $i)}} 24 | 25 | 26 | 27 | {{end}} 28 | {{end}} 29 | {{end}} 30 |
Activity
{{.Other.orderedItems}}
{{.}}
31 | {{if .Other.prev}} 32 |

Prev

33 | {{else}} 34 |

Prev

35 | {{end}} 36 | {{if .Other.next}} 37 |

Next

38 | {{else}} 39 |

Next

40 | {{end}} 41 | {{template "footer.tmpl" .}} 42 | -------------------------------------------------------------------------------- /example/templates/user.tmpl: -------------------------------------------------------------------------------- 1 | {{template "header.tmpl" .}} 2 |

{{.Other.name}}

3 |

Links:

4 | 10 | {{template "footer.tmpl" .}} 11 | -------------------------------------------------------------------------------- /framework/clarke.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package framework 18 | 19 | import ( 20 | "strings" 21 | ) 22 | 23 | const ( 24 | // Clarke says something shorter than usual. 25 | // Line 1 usable range is [86:138] (len=52) 26 | // Line 2 usable range is [156:209] (len=53) 27 | // Line 3 usable range is [227:280] (len=53) 28 | clarkeShort = ` ______________________________________________________ 29 | \__/ / \ 30 | \------(oo) / | 31 | || (__) < | 32 | ||w--|| \-------------------------------------------------------/` 33 | // Clarke says something longer. 34 | // Lines 1 - 3 are the same as above 35 | // Line 4 usable range is [298:351] (len=53) 36 | clarkeLongBegin = ` ______________________________________________________ 37 | \__/ / \ 38 | \------(oo) / | 39 | || (__) < | 40 | ||w--|| | |` 41 | // Line usable range is [15:68] (len=53) 42 | clarkeLongMiddle = ` | |` 43 | clarkeLongEnd = ` \-------------------------------------------------------/` 44 | ) 45 | 46 | func replace(input, replace string, offset int) string { 47 | in := []byte(input) 48 | repl := []byte(replace) 49 | return string(append(in[:offset], 50 | append(repl, 51 | in[offset+len(repl):]...)...)) 52 | } 53 | 54 | func ClarkeSays(moo string) string { 55 | moo = strings.TrimSpace(strings.ReplaceAll(moo, "\n", " ")) 56 | words := strings.Split(moo, " ") 57 | lines := make([][]string, 0, 1) 58 | var line []string 59 | var length int 60 | for _, word := range words { 61 | maxlen := 53 62 | if len(lines) == 0 { 63 | maxlen = 52 64 | } 65 | sl := 0 66 | if len(line) > 0 { 67 | sl = 1 68 | } 69 | if length+len(word)+sl > maxlen { 70 | lines = append(lines, line) 71 | line = []string{word} 72 | length = len(word) 73 | } else { 74 | line = append(line, word) 75 | length += len(word) + sl 76 | } 77 | } 78 | lines = append(lines, line) 79 | 80 | var s string 81 | switch len(lines) { 82 | case 1: 83 | // Middle line 84 | s = clarkeShort 85 | s = replace(s, strings.Join(lines[0], " "), 156) 86 | case 2: 87 | // Middle and bottom line 88 | s = clarkeShort 89 | s = replace(s, strings.Join(lines[0], " "), 156) 90 | s = replace(s, strings.Join(lines[1], " "), 226) 91 | case 3: 92 | // Top, middle and bottom line 93 | s = clarkeShort 94 | s = replace(s, strings.Join(lines[0], " "), 86) 95 | s = replace(s, strings.Join(lines[1], " "), 156) 96 | s = replace(s, strings.Join(lines[2], " "), 227) 97 | default: 98 | // Long paragraph. 99 | s = clarkeLongBegin 100 | s = replace(s, strings.Join(lines[0], " "), 86) 101 | s = replace(s, strings.Join(lines[1], " "), 156) 102 | s = replace(s, strings.Join(lines[2], " "), 227) 103 | s = replace(s, strings.Join(lines[3], " "), 298) 104 | if len(lines) > 4 { 105 | for i := 4; i < len(lines); i++ { 106 | m := clarkeLongMiddle 107 | m = replace(m, strings.Join(lines[i], " "), 15) 108 | s += "\n" 109 | s += m 110 | } 111 | } 112 | s += "\n" 113 | s += clarkeLongEnd 114 | } 115 | s += "\n" 116 | return s 117 | } 118 | -------------------------------------------------------------------------------- /framework/client.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package framework 18 | 19 | import ( 20 | "net/http" 21 | "time" 22 | 23 | "github.com/go-fed/apcore/framework/config" 24 | ) 25 | 26 | func NewHTTPClient(c *config.Config) *http.Client { 27 | return &http.Client{ 28 | Timeout: time.Duration(c.ServerConfig.HttpClientTimeoutSeconds) * time.Second, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /framework/config.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package framework 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-fed/apcore/app" 23 | "github.com/go-fed/apcore/framework/config" 24 | "github.com/go-fed/apcore/util" 25 | "golang.org/x/crypto/bcrypt" 26 | "gopkg.in/ini.v1" 27 | ) 28 | 29 | const ( 30 | postgresDB = "postgres" 31 | ) 32 | 33 | func defaultConfig(dbkind string) (c *config.Config, err error) { 34 | var dbc config.DatabaseConfig 35 | dbc, err = defaultDatabaseConfig(dbkind) 36 | if err != nil { 37 | return 38 | } 39 | c = &config.Config{ 40 | ServerConfig: defaultServerConfig(), 41 | OAuthConfig: defaultOAuth2Config(), 42 | DatabaseConfig: dbc, 43 | ActivityPubConfig: defaultActivityPubConfig(), 44 | NodeInfoConfig: defaultNodeInfoConfig(), 45 | } 46 | return 47 | } 48 | 49 | func defaultServerConfig() config.ServerConfig { 50 | return config.ServerConfig{ 51 | HttpsPort: 443, 52 | CookieMaxAge: 86400, 53 | SaltSize: 32, 54 | BCryptStrength: bcrypt.DefaultCost, 55 | RSAKeySize: 1024, 56 | } 57 | } 58 | 59 | func defaultOAuth2Config() config.OAuth2Config { 60 | return config.OAuth2Config{ 61 | AccessTokenExpiry: 3600, 62 | RefreshTokenExpiry: 7200, 63 | } 64 | } 65 | 66 | func defaultDatabaseConfig(dbkind string) (d config.DatabaseConfig, err error) { 67 | d = config.DatabaseConfig{ 68 | DatabaseKind: dbkind, 69 | // This default is implicit in Go but could change, so here we 70 | // make it explicit instead 71 | MaxIdleConns: 2, 72 | // This default is arbitrarily chosen 73 | DefaultCollectionPageSize: 10, 74 | // This default is arbitrarily chosen 75 | MaxCollectionPageSize: 200, 76 | } 77 | if dbkind != postgresDB { 78 | err = fmt.Errorf("unsupported database kind: %s", dbkind) 79 | return 80 | } 81 | d.PostgresConfig = defaultPostgresConfig() 82 | return 83 | } 84 | 85 | func defaultActivityPubConfig() config.ActivityPubConfig { 86 | return config.ActivityPubConfig{ 87 | ClockTimezone: "UTC", 88 | OutboundRateLimitQPS: 2, 89 | OutboundRateLimitBurst: 5, 90 | HttpSignaturesConfig: defaultHttpSignaturesConfig(), 91 | MaxInboxForwardingRecursionDepth: 50, 92 | MaxDeliveryRecursionDepth: 50, 93 | RetryPageSize: 25, 94 | RetryAbandonLimit: 10, 95 | RetrySleepPeriod: 300, 96 | OutboundRateLimitPrunePeriodSeconds: 60, 97 | OutboundRateLimitPruneAgeSeconds: 30, 98 | } 99 | } 100 | 101 | func defaultHttpSignaturesConfig() config.HttpSignaturesConfig { 102 | return config.HttpSignaturesConfig{ 103 | Algorithms: []string{"rsa-sha256", "rsa-sha512"}, 104 | DigestAlgorithm: "SHA-256", 105 | GetHeaders: []string{"(request-target)", "Date"}, 106 | PostHeaders: []string{"(request-target)", "Date", "Digest"}, 107 | } 108 | } 109 | 110 | func defaultPostgresConfig() config.PostgresConfig { 111 | return config.PostgresConfig{} 112 | } 113 | 114 | func defaultNodeInfoConfig() config.NodeInfoConfig { 115 | return config.NodeInfoConfig{ 116 | EnableNodeInfo: true, 117 | EnableNodeInfo2: true, 118 | EnableAnonymousStatsSharing: true, 119 | AnonymizedStatsCacheInvalidatedSeconds: 86400, 120 | } 121 | } 122 | 123 | func LoadConfigFile(filename string, a app.Application, debug bool) (c *config.Config, err error) { 124 | util.InfoLogger.Infof("Loading config file: %s", filename) 125 | var cfg *ini.File 126 | cfg, err = ini.Load(filename) 127 | if err != nil { 128 | return 129 | } 130 | c = &config.Config{} 131 | err = cfg.MapTo(c) 132 | if err != nil { 133 | return 134 | } 135 | appCfg := a.NewConfiguration() 136 | if appCfg != nil { 137 | err = cfg.MapTo(appCfg) 138 | if err != nil { 139 | return 140 | } 141 | } 142 | err = c.Verify() 143 | if err != nil { 144 | return 145 | } 146 | err = a.SetConfiguration(appCfg, c, debug) 147 | if err != nil { 148 | return 149 | } 150 | if debug { 151 | c.ServerConfig.Host = "localhost" 152 | } 153 | return 154 | } 155 | 156 | func SaveConfigFile(filename string, c *config.Config, others ...interface{}) error { 157 | util.InfoLogger.Infof("Saving config file: %s", filename) 158 | cfg := ini.Empty() 159 | err := ini.ReflectFrom(cfg, c) 160 | if err != nil { 161 | return err 162 | } 163 | for _, o := range others { 164 | if o == nil { 165 | continue 166 | } 167 | err = ini.ReflectFrom(cfg, o) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | return cfg.SaveTo(filename) 173 | } 174 | -------------------------------------------------------------------------------- /framework/config/core_config.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2021 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package config 18 | 19 | func (c *Config) Host() string { 20 | return c.ServerConfig.Host 21 | } 22 | 23 | func (c *Config) ClockTimezone() string { 24 | return c.ActivityPubConfig.ClockTimezone 25 | } 26 | 27 | func (c *Config) Schema() string { 28 | return c.DatabaseConfig.PostgresConfig.Schema 29 | } 30 | -------------------------------------------------------------------------------- /framework/config/verify.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package config 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | ) 23 | 24 | func (c *Config) Verify() error { 25 | if err := c.ServerConfig.Verify(); err != nil { 26 | return err 27 | } 28 | if err := c.OAuthConfig.Verify(); err != nil { 29 | return err 30 | } 31 | if err := c.DatabaseConfig.Verify(); err != nil { 32 | return err 33 | } 34 | if err := c.ActivityPubConfig.Verify(); err != nil { 35 | return err 36 | } 37 | if err := c.NodeInfoConfig.Verify(); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (c *ServerConfig) Verify() error { 44 | if len(c.Host) == 0 { 45 | return errors.New("sr_host is empty, but it is required") 46 | } 47 | if c.HttpsPort == 0 { 48 | return errors.New("sr_https_port is empty, but it is required") 49 | } 50 | if len(c.CertFile) == 0 { 51 | return errors.New("sr_cert_file is empty, but it is required") 52 | } 53 | if len(c.KeyFile) == 0 { 54 | return errors.New("sr_key_file is empty, but it is required") 55 | } 56 | if len(c.CookieAuthKeyFile) == 0 { 57 | return errors.New("sr_cookie_auth_key_file is empty, but it is required") 58 | } 59 | if len(c.CookieSessionName) == 0 { 60 | return errors.New("sr_cookie_session_name is empty, but it is required") 61 | } 62 | if len(c.StaticRootDirectory) == 0 { 63 | return errors.New("sr_static_root_directory is empty, but it is required") 64 | } 65 | const minKeySize = 1024 66 | if c.RSAKeySize < minKeySize { 67 | return fmt.Errorf("sr_rsa_private_key_size is configured to be < %d, which is forbidden: %d", minKeySize, c.RSAKeySize) 68 | } 69 | return nil 70 | } 71 | 72 | func (c *OAuth2Config) Verify() error { 73 | if c.AccessTokenExpiry <= 0 { 74 | return fmt.Errorf("oauth_access_token_expiry is zero or negative, which is forbidden: %d", c.AccessTokenExpiry) 75 | } 76 | if c.RefreshTokenExpiry <= 0 { 77 | return fmt.Errorf("oauth_refresh_token_expiry is zero or negative, which is forbidden: %d", c.RefreshTokenExpiry) 78 | } 79 | return nil 80 | } 81 | 82 | func (c *DatabaseConfig) Verify() error { 83 | if len(c.DatabaseKind) == 0 { 84 | return errors.New("db_database_kind is empty, but it is required") 85 | } 86 | if c.DatabaseKind == "postgres" { 87 | if err := c.PostgresConfig.Verify(); err != nil { 88 | return err 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | func (c *ActivityPubConfig) Verify() error { 95 | if c.OutboundRateLimitQPS <= 0 { 96 | return fmt.Errorf("ap_outbound_rate_limit_qps is zero or negative, which is forbidden: %g", c.OutboundRateLimitQPS) 97 | } 98 | if c.OutboundRateLimitBurst <= 0 { 99 | return fmt.Errorf("ap_outbound_rate_limit_burst is zero or negative, which is forbidden: %d", c.OutboundRateLimitBurst) 100 | } 101 | if c.OutboundRateLimitPrunePeriodSeconds <= 0 { 102 | return fmt.Errorf("ap_outbound_rate_limit_prune_period_seconds is zero or negative, which is forbidden: %d", c.OutboundRateLimitPrunePeriodSeconds) 103 | } 104 | if c.OutboundRateLimitPruneAgeSeconds < 0 { 105 | return fmt.Errorf("ap_outbound_rate_limit_prune_age_seconds is negative, which is forbidden: %d", c.OutboundRateLimitPruneAgeSeconds) 106 | } 107 | if c.MaxInboxForwardingRecursionDepth < 0 { 108 | return fmt.Errorf("ap_max_inbox_forwarding_recursion_depth is negative, which is forbidden: %d", c.MaxInboxForwardingRecursionDepth) 109 | } 110 | if c.MaxDeliveryRecursionDepth < 0 { 111 | return fmt.Errorf("ap_max_delivery_recursion_depth is negative, which is forbidden: %d", c.MaxDeliveryRecursionDepth) 112 | } 113 | if c.RetryPageSize <= 0 { 114 | return fmt.Errorf("ap_retry_page_size is zero or negative, which is forbidden: %d", c.RetryPageSize) 115 | } 116 | if c.RetryAbandonLimit <= 0 { 117 | return fmt.Errorf("ap_retry_abandon_limit is zero or negative, which is forbidden: %d", c.RetryAbandonLimit) 118 | } 119 | if c.RetrySleepPeriod <= 0 { 120 | return fmt.Errorf("ap_retry_sleep_period_seconds is zero or negative, which is forbidden: %d", c.RetrySleepPeriod) 121 | } 122 | if err := c.HttpSignaturesConfig.Verify(); err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | func (c *HttpSignaturesConfig) Verify() error { 129 | return nil 130 | } 131 | 132 | func (c *PostgresConfig) Verify() error { 133 | return nil 134 | } 135 | 136 | func (c *NodeInfoConfig) Verify() error { 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /framework/conn/host_limiter.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package conn 18 | 19 | import ( 20 | "context" 21 | "sync" 22 | "time" 23 | 24 | "github.com/go-fed/apcore/framework/config" 25 | "golang.org/x/time/rate" 26 | ) 27 | 28 | type entry struct { 29 | L *rate.Limiter 30 | LastUsed time.Time 31 | } 32 | 33 | type hostLimiter struct { 34 | // Immutable 35 | limit rate.Limit 36 | burst int 37 | prunePeriod time.Duration 38 | pruneAge time.Duration 39 | wg sync.WaitGroup 40 | // Mutable 41 | pruneTicker *time.Ticker 42 | pruneCtx context.Context 43 | pruneCancel context.CancelFunc 44 | pMu sync.Mutex 45 | m map[string]entry 46 | mu sync.Mutex 47 | } 48 | 49 | func newHostLimiter(c *config.Config) *hostLimiter { 50 | return &hostLimiter{ 51 | limit: rate.Limit(c.ActivityPubConfig.OutboundRateLimitQPS), 52 | burst: c.ActivityPubConfig.OutboundRateLimitBurst, 53 | prunePeriod: time.Duration(c.ActivityPubConfig.OutboundRateLimitPrunePeriodSeconds) * time.Second, 54 | pruneAge: time.Duration(c.ActivityPubConfig.OutboundRateLimitPruneAgeSeconds) * time.Second, 55 | m: make(map[string]entry), 56 | } 57 | } 58 | 59 | func (h *hostLimiter) Start() { 60 | h.resetMap() 61 | h.goPrune() 62 | } 63 | 64 | func (h *hostLimiter) Stop() { 65 | h.stopPrune() 66 | } 67 | 68 | func (h *hostLimiter) Get(host string) *rate.Limiter { 69 | h.mu.Lock() 70 | defer h.mu.Unlock() 71 | e, ok := h.m[host] 72 | if ok { 73 | e.LastUsed = time.Now() 74 | h.m[host] = e 75 | return e.L 76 | } else { 77 | e = entry{ 78 | L: rate.NewLimiter(h.limit, h.burst), 79 | LastUsed: time.Now(), 80 | } 81 | h.m[host] = e 82 | return e.L 83 | } 84 | } 85 | 86 | func (h *hostLimiter) resetMap() { 87 | h.mu.Lock() 88 | defer h.mu.Unlock() 89 | h.m = make(map[string]entry) 90 | } 91 | 92 | func (h *hostLimiter) stopPrune() { 93 | h.pMu.Lock() // WARNING: NO DEFER UNLOCK 94 | if h.pruneCancel == nil { 95 | h.pMu.Unlock() 96 | return 97 | } 98 | h.pruneCancel() 99 | h.pMu.Unlock() 100 | h.wg.Wait() 101 | } 102 | 103 | func (h *hostLimiter) goPrune() { 104 | h.pMu.Lock() 105 | defer h.pMu.Unlock() 106 | if h.pruneTicker != nil { 107 | return 108 | } 109 | h.pruneTicker = time.NewTicker(h.prunePeriod) 110 | h.pruneCtx, h.pruneCancel = context.WithCancel(context.Background()) 111 | h.wg.Add(1) 112 | go func() { 113 | defer func() { 114 | h.pMu.Lock() 115 | defer h.pMu.Unlock() 116 | h.pruneTicker.Stop() 117 | h.pruneTicker = nil 118 | h.pruneCtx = nil 119 | h.pruneCancel = nil 120 | h.wg.Done() 121 | }() 122 | for { 123 | select { 124 | case <-h.pruneTicker.C: 125 | h.prune() 126 | case <-h.pruneCtx.Done(): 127 | return 128 | } 129 | } 130 | }() 131 | } 132 | 133 | func (h *hostLimiter) prune() { 134 | h.mu.Lock() 135 | defer h.mu.Unlock() 136 | now := time.Now() 137 | for k, v := range h.m { 138 | if v.LastUsed.Sub(now) > h.pruneAge { 139 | delete(h.m, k) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /framework/conn/retrier.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package conn 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "github.com/go-fed/apcore/framework/config" 24 | "github.com/go-fed/apcore/paths" 25 | "github.com/go-fed/apcore/services" 26 | "github.com/go-fed/apcore/util" 27 | ) 28 | 29 | type retrier struct { 30 | // Immutable 31 | da *services.DeliveryAttempts 32 | pk *services.PrivateKeys 33 | tc *Controller 34 | pageSize int 35 | abandonLimit int 36 | reattemptBackoff func(n int) time.Duration 37 | retrierFn *util.SafeStartStop 38 | } 39 | 40 | func newRetrier(da *services.DeliveryAttempts, pk *services.PrivateKeys, tc *Controller, c *config.Config) *retrier { 41 | r := &retrier{ 42 | da: da, 43 | pk: pk, 44 | tc: tc, 45 | pageSize: c.ActivityPubConfig.RetryPageSize, 46 | abandonLimit: c.ActivityPubConfig.RetryAbandonLimit, 47 | reattemptBackoff: func(n int) time.Duration { 48 | z := time.Duration(c.ActivityPubConfig.RetrySleepPeriod) * time.Second 49 | // Exponential backoff 50 | for i := 0; i < n; i++ { 51 | z += z 52 | } 53 | // If larger than a day, cap at one attempt per day 54 | if z > time.Hour*24 { 55 | z = time.Hour * 24 56 | } 57 | return z 58 | }, 59 | } 60 | r.retrierFn = util.NewSafeStartStop(r.retry, time.Duration(c.ActivityPubConfig.RetrySleepPeriod)*time.Second) 61 | return r 62 | } 63 | 64 | func (r *retrier) Start() { 65 | r.retrierFn.Start() 66 | } 67 | 68 | func (r *retrier) Stop() { 69 | r.retrierFn.Stop() 70 | } 71 | 72 | func (r *retrier) retry(ctx context.Context) { 73 | c := util.Context{ctx} 74 | now := time.Now() 75 | failures, err := r.da.FirstPageRetryableFailures(c, r.pageSize) 76 | if err != nil { 77 | util.ErrorLogger.Errorf("retrier failed to obtain first page: %s", err) 78 | return 79 | } 80 | for len(failures) > 0 { 81 | for _, failure := range failures { 82 | // Skip this if the retry attempt would be too soon; 83 | // this applies a backoff function. 84 | if failure.LastAttempt.Sub(now) < r.reattemptBackoff(failure.NAttempts) { 85 | continue 86 | } 87 | privKey, pubKeyID, err := r.pk.GetUserHTTPSignatureKey(c, paths.UUID(failure.UserID)) 88 | if err != nil { 89 | util.ErrorLogger.Errorf("retrier failed to obtain user's HTTP Signature key: %s", err) 90 | continue 91 | } 92 | tp, err := r.tc.Get(privKey, pubKeyID.String()) 93 | if err != nil { 94 | util.ErrorLogger.Errorf("retrier failed to obtain a transport for delivery: %s", err) 95 | continue 96 | } 97 | // Attempt delivery and update its associated record. 98 | err = tp.Deliver(ctx, failure.Payload, failure.DeliverTo) 99 | if err != nil { 100 | util.ErrorLogger.Errorf("retrier failed in an attempt to retry delivery: %s", err) 101 | if failure.NAttempts >= r.abandonLimit { 102 | err = r.da.MarkAbandonedAttempt(c, failure.ID) 103 | if err != nil { 104 | util.ErrorLogger.Errorf("retrier failed to mark attempt as abandoned: %s", err) 105 | } 106 | } else { 107 | err = r.da.MarkRetryFailureAttempt(c, failure.ID) 108 | if err != nil { 109 | util.ErrorLogger.Errorf("retrier failed to mark attempt as failed: %s", err) 110 | } 111 | } 112 | } else { 113 | err = r.da.MarkSuccessfulAttempt(c, failure.ID) 114 | if err != nil { 115 | util.ErrorLogger.Errorf("retrier failed to mark attempt as successful: %s", err) 116 | } 117 | } 118 | } 119 | last := failures[len(failures)-1] 120 | failures, err = r.da.NextPageRetryableFailures(c, last.ID, last.FetchTime, r.pageSize) 121 | if err != nil { 122 | util.ErrorLogger.Errorf("retrier failed to obtain the next page of retriable failures: %s", err) 123 | return 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /framework/db/db.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package db 18 | 19 | import ( 20 | "database/sql" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/go-fed/apcore/framework/config" 25 | "github.com/go-fed/apcore/models" 26 | "github.com/go-fed/apcore/util" 27 | _ "github.com/jackc/pgx/v4/stdlib" 28 | ) 29 | 30 | func NewDB(c *config.Config) (sqldb *sql.DB, d models.SqlDialect, err error) { 31 | kind := c.DatabaseConfig.DatabaseKind 32 | var conn string 33 | var driver string 34 | switch kind { 35 | case "postgres": 36 | conn, err = postgresConn(c.DatabaseConfig.PostgresConfig) 37 | d = NewPgV0(c.DatabaseConfig.PostgresConfig.Schema) 38 | driver = "pgx" 39 | default: 40 | err = fmt.Errorf("unhandled database_kind in config: %s", kind) 41 | } 42 | if err != nil { 43 | return 44 | } 45 | 46 | util.InfoLogger.Infof("Calling sql.Open...") 47 | sqldb, err = sql.Open(driver, conn) 48 | if err != nil { 49 | return 50 | } 51 | util.InfoLogger.Infof("Calling sql.Open complete") 52 | 53 | // Apply general database configurations 54 | if c.DatabaseConfig.ConnMaxLifetimeSeconds > 0 { 55 | sqldb.SetConnMaxLifetime( 56 | time.Duration(c.DatabaseConfig.ConnMaxLifetimeSeconds) * 57 | time.Second) 58 | } 59 | if c.DatabaseConfig.MaxOpenConns > 0 { 60 | sqldb.SetMaxOpenConns(c.DatabaseConfig.MaxOpenConns) 61 | } 62 | if c.DatabaseConfig.MaxIdleConns >= 0 { 63 | sqldb.SetMaxIdleConns(c.DatabaseConfig.MaxIdleConns) 64 | } 65 | util.InfoLogger.Infof("Database connections configured successfully") 66 | util.InfoLogger.Infof("NOTE: No underlying database connections may have happened yet!") 67 | return 68 | } 69 | 70 | func MustPing(db *sql.DB) (err error) { 71 | util.InfoLogger.Infof("Opening connection to database by pinging, which will create a connection...") 72 | start := time.Now() 73 | err = db.Ping() 74 | if err != nil { 75 | util.ErrorLogger.Errorf("Unsuccessfully pinged database: %s", err) 76 | return 77 | } 78 | end := time.Now() 79 | util.InfoLogger.Infof("Successfully pinged database with latency: %s", end.Sub(start)) 80 | return 81 | } 82 | 83 | func postgresConn(pg config.PostgresConfig) (s string, err error) { 84 | util.InfoLogger.Info("Postgres database configuration") 85 | if len(pg.DatabaseName) == 0 { 86 | err = fmt.Errorf("postgres config missing db_name") 87 | return 88 | } else if len(pg.UserName) == 0 { 89 | err = fmt.Errorf("postgres config missing user") 90 | return 91 | } 92 | s = fmt.Sprintf("dbname=%s user=%s", pg.DatabaseName, pg.UserName) 93 | if len(pg.Password) > 0 { 94 | s = fmt.Sprintf("%s password=%s", s, pg.Password) 95 | } 96 | if len(pg.Host) > 0 { 97 | s = fmt.Sprintf("%s host=%s", s, pg.Host) 98 | } 99 | if pg.Port > 0 { 100 | s = fmt.Sprintf("%s port=%d", s, pg.Port) 101 | } 102 | if len(pg.SSLMode) > 0 { 103 | s = fmt.Sprintf("%s sslmode=%s", s, pg.SSLMode) 104 | } 105 | if len(pg.FallbackApplicationName) > 0 { 106 | s = fmt.Sprintf("%s fallback_application_name=%s", s, pg.FallbackApplicationName) 107 | } 108 | if pg.ConnectTimeout > 0 { 109 | s = fmt.Sprintf("%s connect_timeout=%d", s, pg.ConnectTimeout) 110 | } 111 | if len(pg.SSLCert) > 0 { 112 | s = fmt.Sprintf("%s sslcert=%s", s, pg.SSLCert) 113 | } 114 | if len(pg.SSLKey) > 0 { 115 | s = fmt.Sprintf("%s sslkey=%s", s, pg.SSLKey) 116 | } 117 | if len(pg.SSLRootCert) > 0 { 118 | s = fmt.Sprintf("%s sslrootcert=%s", s, pg.SSLRootCert) 119 | } 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /framework/mux_wrapper.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package framework 18 | 19 | import ( 20 | "net/http" 21 | 22 | "github.com/gorilla/mux" 23 | ) 24 | 25 | // Vars returns the route variables for the current request, if any. 26 | func Vars(r *http.Request) map[string]string { 27 | return mux.Vars(r) 28 | } 29 | -------------------------------------------------------------------------------- /framework/nodeinfo/nodeinfo2.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package nodeinfo 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | 24 | "github.com/go-fed/apcore/app" 25 | srv "github.com/go-fed/apcore/services" 26 | "github.com/go-fed/apcore/util" 27 | ) 28 | 29 | // This file contains the NodeInfo2 v1 implementation. 30 | 31 | const ( 32 | nodeInfo2Version = "1.0" 33 | nodeInfo2WellKnownPath = "/.well-known/x-nodeinfo2" 34 | ) 35 | 36 | type nodeInfo2 struct { 37 | Version string `json:"version"` 38 | Server server2 `json:"server"` 39 | Organization organization2 `json:"organization"` 40 | Protocols []string `json:"protocols"` 41 | Services services2 `json:"services"` 42 | OpenRegistrations bool `json:"openRegistrations"` 43 | Usage usage2 `json:"usage"` 44 | Relay string `json:"relay"` 45 | OtherFeatures []feature2 `json:"otherFeatures"` 46 | } 47 | 48 | type server2 struct { 49 | BaseURL string `json:"baseUrl"` 50 | Name string `json:"name"` 51 | Software string `json:"software"` 52 | Version string `json:"version"` 53 | } 54 | 55 | type organization2 struct { 56 | Name string `json:"name"` 57 | Contact string `json:"contact"` 58 | Account string `json:"account"` 59 | } 60 | 61 | type services2 struct { 62 | Inbound []string `json:"inbound"` 63 | Outbound []string `json:"outbound"` 64 | } 65 | 66 | type usage2 struct { 67 | Users users2 `json:"users"` 68 | LocalPosts int `json:"localPosts"` 69 | LocalComments int `json:"localComments"` 70 | } 71 | 72 | type users2 struct { 73 | Total int `json:"total"` 74 | ActiveHalfYear int `json:"activeHalfyear"` 75 | ActiveMonth int `json:"activeMonth"` 76 | ActiveWeek int `json:"activeWeek"` 77 | } 78 | 79 | type feature2 struct { 80 | Name string `json:"name"` 81 | Version string `json:"version"` 82 | } 83 | 84 | func toNodeInfo2(s, apcore app.Software, t *srv.NodeInfoStats, p srv.ServerPreferences) nodeInfo2 { 85 | n := nodeInfo2{ 86 | Version: nodeInfo2Version, 87 | Server: server2{ 88 | BaseURL: p.ServerBaseURL, 89 | Name: p.ServerName, 90 | Software: s.Name, 91 | Version: s.Version(), 92 | }, 93 | Organization: organization2{ 94 | Name: p.OrgName, 95 | Contact: p.OrgContact, 96 | Account: p.OrgAccount, 97 | }, 98 | Protocols: []string{"activitypub"}, 99 | Services: services2{ 100 | Inbound: []string{}, 101 | Outbound: []string{}, 102 | }, 103 | OpenRegistrations: p.OpenRegistrations, 104 | Relay: "", 105 | OtherFeatures: []feature2{ 106 | { 107 | Name: apcore.Name, 108 | Version: apcore.Version(), 109 | }, 110 | }, 111 | } 112 | if t != nil { 113 | n.Usage = usage2{ 114 | Users: users2{ 115 | Total: t.TotalUsers, 116 | ActiveHalfYear: t.ActiveHalfYear, 117 | ActiveMonth: t.ActiveMonth, 118 | ActiveWeek: t.ActiveWeek, 119 | }, 120 | LocalPosts: t.NLocalPosts, 121 | LocalComments: t.NLocalComments, 122 | } 123 | } 124 | return n 125 | } 126 | 127 | func nodeInfo2WellKnownHandler(ni *srv.NodeInfo, u *srv.Users, s, apcore app.Software, useStats bool) http.HandlerFunc { 128 | return func(w http.ResponseWriter, r *http.Request) { 129 | w.Header().Set("Content-Type", `application/json`) 130 | 131 | ctx := util.Context{r.Context()} 132 | var t *srv.NodeInfoStats 133 | if useStats { 134 | st, err := ni.GetAnonymizedStats(ctx) 135 | if err != nil { 136 | http.Error(w, fmt.Sprintf("error serving nodeinfo2 response"), http.StatusInternalServerError) 137 | util.ErrorLogger.Errorf("error in getting anonymized stats for nodeinfo2 response: %s", err) 138 | return 139 | } 140 | t = &st 141 | } 142 | 143 | p, err := u.GetServerPreferences(ctx) 144 | if err != nil { 145 | http.Error(w, fmt.Sprintf("error serving nodeinfo2 response"), http.StatusInternalServerError) 146 | util.ErrorLogger.Errorf("error in getting server profile for nodeinfo2 response: %s", err) 147 | return 148 | } 149 | 150 | ni := toNodeInfo2(s, apcore, t, p) 151 | b, err := json.Marshal(ni) 152 | if err != nil { 153 | http.Error(w, fmt.Sprintf("error serving nodeinfo2 response"), http.StatusInternalServerError) 154 | util.ErrorLogger.Errorf("error marshalling nodeinfo2 response to JSON: %s", err) 155 | return 156 | } 157 | 158 | n, err := w.Write(b) 159 | if err != nil { 160 | util.ErrorLogger.Errorf("error writing nodeinfo2 response: %s", err) 161 | } else if n != len(b) { 162 | util.ErrorLogger.Errorf("error writing nodeinfo2 response: wrote %d of %d bytes", n, len(b)) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /framework/nodeinfo/pkg.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | // There are two competing standards: NodeInfo and NodeInfo2. Confusingly, 18 | // NodeInfo is at version 2, so it is NodeInfo 2 vs NodeInfo2. 19 | // 20 | // I have no idea about the background of either. But it is late, and I find 21 | // myself tired and, having thought this would be a quick serialize-to-JSON 22 | // implementation, massively disappointed at what I've found instead. 23 | // 24 | // So, support both, I guess. Thank God, I was worried I would one day find 25 | // myself upon my deathbed and staring into the eyes of my loved ones and 26 | // quietly whisper in the soft exhale of my last breath "if only I could have 27 | // implemented another NodeInfo standard, I would have had a fulfilling life". 28 | // So my survivors would all turn to each other teary-eyed and put on my 29 | // gravestone "NodeInfo 2" with an ambiguously-sized space in-between. 30 | // 31 | // If at least this means one other person doesn't have to deal with this 32 | // dual headache, then I guess it was indeed worth it. 33 | // 34 | // I guess this is what happens when people can't get along and at least one 35 | // person is being uncompromising. A lesson for me to avoid this at all costs. 36 | package nodeinfo 37 | 38 | import ( 39 | "net/http" 40 | 41 | "github.com/go-fed/apcore/app" 42 | "github.com/go-fed/apcore/framework/config" 43 | srv "github.com/go-fed/apcore/services" 44 | ) 45 | 46 | type PathHandler struct { 47 | Path string 48 | Handler http.HandlerFunc 49 | } 50 | 51 | func GetNodeInfoHandlers(c config.NodeInfoConfig, scheme, host string, ni *srv.NodeInfo, u *srv.Users, s, apcore app.Software) []PathHandler { 52 | var ph []PathHandler 53 | if c.EnableNodeInfo { 54 | ph = append(ph, PathHandler{ 55 | Path: nodeInfoWellKnownPath, 56 | Handler: nodeInfoWellKnownHandler(scheme, host), 57 | }) 58 | ph = append(ph, PathHandler{ 59 | Path: nodeInfoPath, 60 | Handler: nodeInfoHandler(ni, u, s, apcore, c.EnableAnonymousStatsSharing), 61 | }) 62 | } 63 | if c.EnableNodeInfo2 { 64 | ph = append(ph, PathHandler{ 65 | Path: nodeInfo2WellKnownPath, 66 | Handler: nodeInfo2WellKnownHandler(ni, u, s, apcore, c.EnableAnonymousStatsSharing), 67 | }) 68 | } 69 | return ph 70 | } 71 | -------------------------------------------------------------------------------- /framework/oauth2/proxy.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package oauth2 18 | 19 | import ( 20 | "net/url" 21 | ) 22 | 23 | const ( 24 | redirQueryKey = "redir" 25 | redirQueryQueryKey = "q" 26 | loginErrorQueryKey = "login_error" 27 | authErrorQueryKey = "auth_error" 28 | ) 29 | 30 | func loginWithFirstPartyRedirPath(u *url.URL) string { 31 | var v url.Values 32 | v.Add(redirQueryKey, u.Path) 33 | v.Add(redirQueryQueryKey, u.RawQuery) 34 | n := url.URL{ 35 | Path: "/login", 36 | RawQuery: v.Encode(), 37 | } 38 | return n.String() 39 | } 40 | 41 | func isFirstPartyOAuth2Request(u *url.URL) bool { 42 | return u.Query().Get(redirQueryKey) != "" 43 | } 44 | 45 | func FirstPartyOAuth2LoginRedirPath(u *url.URL) (string, error) { 46 | v := u.Query() 47 | p, err := url.QueryUnescape(v.Get(redirQueryKey)) 48 | if err != nil { 49 | return "", err 50 | } 51 | rq, err := url.QueryUnescape(v.Get(redirQueryQueryKey)) 52 | if err != nil { 53 | return "", err 54 | } 55 | n := url.URL{ 56 | Path: p, 57 | RawQuery: rq, 58 | } 59 | return n.String(), nil 60 | } 61 | 62 | func AddLoginError(u *url.URL) *url.URL { 63 | return addKV(u, loginErrorQueryKey, "true") 64 | } 65 | 66 | func AddAuthError(u *url.URL) *url.URL { 67 | return addKV(u, authErrorQueryKey, "true") 68 | } 69 | 70 | func addKV(u *url.URL, key, value string) *url.URL { 71 | v := u.Query() 72 | v.Add(key, value) 73 | out := &url.URL{ 74 | Path: u.Path, 75 | RawQuery: v.Encode(), 76 | } 77 | return out 78 | } 79 | -------------------------------------------------------------------------------- /framework/web/sessions.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package web 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | 24 | "github.com/go-fed/apcore/framework/config" 25 | "github.com/go-fed/apcore/util" 26 | gs "github.com/gorilla/sessions" 27 | ) 28 | 29 | type Sessions struct { 30 | name string 31 | cookies *gs.CookieStore 32 | } 33 | 34 | func NewSessions(c *config.Config, scheme string) (s *Sessions, err error) { 35 | var authKey, encKey []byte 36 | var keys [][]byte 37 | authKey, err = ioutil.ReadFile(c.ServerConfig.CookieAuthKeyFile) 38 | if err != nil { 39 | return 40 | } 41 | if len(c.ServerConfig.CookieEncryptionKeyFile) > 0 { 42 | util.InfoLogger.Info("Cookie encryption key file detected") 43 | encKey, err = ioutil.ReadFile(c.ServerConfig.CookieEncryptionKeyFile) 44 | if err != nil { 45 | return 46 | } 47 | keys = [][]byte{authKey, encKey} 48 | } else { 49 | util.InfoLogger.Info("No cookie encryption key file detected") 50 | keys = [][]byte{authKey} 51 | } 52 | if len(c.ServerConfig.CookieSessionName) <= 0 { 53 | err = fmt.Errorf("no cookie session name provided") 54 | return 55 | } 56 | s = &Sessions{ 57 | name: c.ServerConfig.CookieSessionName, 58 | cookies: gs.NewCookieStore(keys...), 59 | } 60 | opt := &gs.Options{ 61 | Path: "/", 62 | Domain: c.ServerConfig.Host, 63 | MaxAge: c.ServerConfig.CookieMaxAge, 64 | Secure: scheme != "http", 65 | HttpOnly: true, 66 | } 67 | s.cookies.Options = opt 68 | s.cookies.MaxAge(opt.MaxAge) 69 | return 70 | } 71 | 72 | func (s *Sessions) Get(r *http.Request) (ses *Session, err error) { 73 | var gs *gs.Session 74 | gs, err = s.cookies.Get(r, s.name) 75 | ses = &Session{ 76 | gs: gs, 77 | } 78 | return 79 | } 80 | 81 | type Session struct { 82 | gs *gs.Session 83 | } 84 | 85 | const ( 86 | userIDSessionKey = "userid" 87 | ) 88 | 89 | func (s *Session) SetUserID(uuid string) { 90 | s.gs.Values[userIDSessionKey] = uuid 91 | return 92 | } 93 | 94 | func (s *Session) UserID() (uuid string, err error) { 95 | if v, ok := s.gs.Values[userIDSessionKey]; !ok { 96 | err = fmt.Errorf("no user id in session") 97 | return 98 | } else if uuid, ok = v.(string); !ok { 99 | err = fmt.Errorf("user id in session is not a string") 100 | return 101 | } 102 | return 103 | } 104 | 105 | func (s *Session) DeleteUserID() { 106 | delete(s.gs.Values, userIDSessionKey) 107 | } 108 | 109 | const ( 110 | firstPartyCredentialKey = "fpckey" 111 | ) 112 | 113 | func (s *Session) SetFirstPartyCredentialID(id string) { 114 | s.gs.Values[firstPartyCredentialKey] = id 115 | return 116 | } 117 | 118 | func (s *Session) HasFirstPartyCredentialID() bool { 119 | _, ok := s.gs.Values[firstPartyCredentialKey] 120 | return ok 121 | } 122 | 123 | func (s *Session) FirstPartyCredentialID() (id string, err error) { 124 | if v, ok := s.gs.Values[firstPartyCredentialKey]; !ok { 125 | err = fmt.Errorf("no first party credential in session") 126 | return 127 | } else if id, ok = v.(string); !ok { 128 | err = fmt.Errorf("first party credential in session is not a string") 129 | return 130 | } 131 | return 132 | } 133 | 134 | func (s *Session) DeleteFirstPartyCredentialID() { 135 | delete(s.gs.Values, firstPartyCredentialKey) 136 | } 137 | 138 | func (s *Session) Clear() { 139 | s.DeleteUserID() 140 | s.DeleteFirstPartyCredentialID() 141 | } 142 | 143 | func (s *Session) Set(k string, i interface{}) { 144 | s.gs.Values[k] = i 145 | } 146 | 147 | func (s *Session) Get(k string) (interface{}, bool) { 148 | v, ok := s.gs.Values[k] 149 | return v, ok 150 | } 151 | 152 | func (s *Session) Has(k string) bool { 153 | _, ok := s.gs.Values[k] 154 | return ok 155 | } 156 | 157 | func (s *Session) Delete(k string) { 158 | delete(s.gs.Values, k) 159 | } 160 | 161 | func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { 162 | return s.gs.Save(r, w) 163 | } 164 | -------------------------------------------------------------------------------- /framework/web/user_agent.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package web 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-fed/apcore/app" 23 | ) 24 | 25 | func UserAgent(s app.Software) string { 26 | return fmt.Sprintf("%s (go-fed/activity go-fed/apcore)", s.UserAgent) 27 | } 28 | -------------------------------------------------------------------------------- /framework/webfinger/webfinger.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package webfinger 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | type Link struct { 24 | Rel string `json:"rel,omitempty"` 25 | Type string `json:"type,omitempty"` 26 | Href string `json:"href,omitempty"` 27 | Template string `json:"template,omitempty"` 28 | } 29 | 30 | type Webfinger struct { 31 | Subject string `json:"subject,omitempty"` 32 | Aliases []string `json:"aliases,omitempty"` 33 | Links []Link `json:"links,omitempty"` 34 | } 35 | 36 | func ToWebfinger(scheme, host, username, idPath string) (w Webfinger, err error) { 37 | w = Webfinger{ 38 | Subject: fmt.Sprintf("acct:%s@%s", username, host), 39 | Aliases: []string{ 40 | fmt.Sprintf("%s://%s%s", scheme, host, idPath), 41 | }, 42 | Links: []Link{ 43 | { 44 | Rel: "self", 45 | Type: "application/activity+json", 46 | Href: fmt.Sprintf("%s://%s%s", scheme, host, idPath), 47 | }, 48 | }, 49 | } 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fed/apcore 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 7 | github.com/go-fed/activity v1.0.1-0.20201213224552-472d90163f3a 8 | github.com/go-fed/httpsig v1.1.1-0.20201221212626-dda7895774cb 9 | github.com/go-fed/oauth2 v1.2.1-0.20201216115557-3ad6eea84720 10 | github.com/google/logger v1.0.1 11 | github.com/google/uuid v1.1.2 12 | github.com/gorilla/mux v1.8.0 13 | github.com/gorilla/sessions v1.2.0 14 | github.com/jackc/pgx/v4 v4.9.0 15 | github.com/manifoldco/promptui v0.3.2 16 | github.com/microcosm-cc/bluemonday v1.0.7 17 | github.com/nicksnyder/go-i18n v1.10.1 // indirect 18 | github.com/tidwall/gjson v1.8.1 // indirect 19 | github.com/tidwall/pretty v1.2.0 // indirect 20 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad 21 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 22 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 // indirect 23 | gopkg.in/ini.v1 v1.44.0 24 | ) 25 | -------------------------------------------------------------------------------- /models/client_infos.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | 22 | "github.com/go-fed/apcore/util" 23 | "github.com/go-fed/oauth2" 24 | ) 25 | 26 | var _ oauth2.ClientInfo = &ClientInfo{} 27 | 28 | type ClientInfo struct { 29 | ID string 30 | Secret sql.NullString 31 | Domain string 32 | UserID string 33 | } 34 | 35 | func (c *ClientInfo) GetID() string { 36 | return c.ID 37 | } 38 | 39 | func (c *ClientInfo) GetSecret() string { 40 | if c.Secret.Valid { 41 | return c.Secret.String 42 | } 43 | return "" 44 | } 45 | 46 | func (c *ClientInfo) GetDomain() string { 47 | return c.Domain 48 | } 49 | 50 | func (c *ClientInfo) GetUserID() string { 51 | return c.UserID 52 | } 53 | 54 | // ClientInfos is a Model that provides additional database methods for OAuth2 55 | // client information. 56 | type ClientInfos struct { 57 | create *sql.Stmt 58 | getByID *sql.Stmt 59 | } 60 | 61 | func (c *ClientInfos) Prepare(db *sql.DB, s SqlDialect) error { 62 | return prepareStmtPairs(db, 63 | stmtPairs{ 64 | {&(c.create), s.CreateClientInfo()}, 65 | {&(c.getByID), s.GetClientInfoByID()}, 66 | }) 67 | } 68 | 69 | func (c *ClientInfos) CreateTable(t *sql.Tx, s SqlDialect) error { 70 | _, err := t.Exec(s.CreateClientInfosTable()) 71 | return err 72 | } 73 | 74 | func (c *ClientInfos) Close() { 75 | c.create.Close() 76 | c.getByID.Close() 77 | } 78 | 79 | // Create adds a ClientInfo into the database. 80 | func (c *ClientInfos) Create(ctx util.Context, tx *sql.Tx, info oauth2.ClientInfo) (id string, err error) { 81 | var rows *sql.Rows 82 | rows, err = tx.Stmt(c.create).QueryContext(ctx, 83 | info.GetID(), 84 | info.GetSecret(), 85 | info.GetDomain(), 86 | info.GetUserID()) 87 | if err != nil { 88 | return 89 | } 90 | defer rows.Close() 91 | return id, enforceOneRow(rows, "ClientInfos.Create", func(r SingleRow) error { 92 | return r.Scan(&(id)) 93 | }) 94 | } 95 | 96 | // GetByID fetches ClientInfo based on its id. 97 | func (c *ClientInfos) GetByID(ctx util.Context, tx *sql.Tx, id string) (oauth2.ClientInfo, error) { 98 | rows, err := tx.Stmt(c.getByID).QueryContext(ctx, id) 99 | if err != nil { 100 | return nil, err 101 | } 102 | defer rows.Close() 103 | ci := &ClientInfo{} 104 | return ci, enforceOneRow(rows, "ClientInfos.GetByID", func(r SingleRow) error { 105 | return r.Scan(&(ci.ID), &(ci.Secret), &(ci.Domain), &(ci.UserID)) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /models/credentials.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "time" 22 | 23 | "github.com/go-fed/apcore/util" 24 | "github.com/go-fed/oauth2" 25 | ) 26 | 27 | // Credentials is a Model that provides a first-party proxy to OAuth2 tokens for 28 | // cookies and other first-party storage. 29 | type Credentials struct { 30 | createCred *sql.Stmt 31 | updateCred *sql.Stmt 32 | updateCredExpires *sql.Stmt 33 | removeCred *sql.Stmt 34 | removeExpiredCreds *sql.Stmt 35 | getTokenInfoByCredID *sql.Stmt 36 | } 37 | 38 | func (c *Credentials) Prepare(db *sql.DB, s SqlDialect) error { 39 | return prepareStmtPairs(db, 40 | stmtPairs{ 41 | {&(c.createCred), s.CreateFirstPartyCredential()}, 42 | {&(c.updateCred), s.UpdateFirstPartyCredential()}, 43 | {&(c.updateCredExpires), s.UpdateFirstPartyCredentialExpires()}, 44 | {&(c.removeCred), s.RemoveFirstPartyCredential()}, 45 | {&(c.removeExpiredCreds), s.RemoveExpiredFirstPartyCredentials()}, 46 | {&(c.getTokenInfoByCredID), s.GetTokenInfoForCredentialID()}, 47 | }) 48 | } 49 | 50 | func (c *Credentials) CreateTable(tx *sql.Tx, s SqlDialect) error { 51 | _, err := tx.Exec(s.CreateFirstPartyCredentialsTable()) 52 | return err 53 | } 54 | 55 | func (c *Credentials) Close() { 56 | c.createCred.Close() 57 | c.updateCred.Close() 58 | c.updateCredExpires.Close() 59 | c.removeCred.Close() 60 | c.removeExpiredCreds.Close() 61 | c.getTokenInfoByCredID.Close() 62 | } 63 | 64 | // Create saves the new first party credential. 65 | func (c *Credentials) Create(ctx util.Context, tx *sql.Tx, userID, tokenID string, expires time.Time) (id string, err error) { 66 | var rows *sql.Rows 67 | rows, err = tx.Stmt(c.createCred).QueryContext(ctx, userID, tokenID, expires) 68 | if err != nil { 69 | return 70 | } 71 | defer rows.Close() 72 | return id, enforceOneRow(rows, "Credentials.Create", func(r SingleRow) error { 73 | return r.Scan(&(id)) 74 | }) 75 | } 76 | 77 | func (c *Credentials) Update(ctx util.Context, tx *sql.Tx, id string, info oauth2.TokenInfo) error { 78 | r, err := tx.Stmt(c.updateCred).ExecContext(ctx, 79 | id, 80 | info.GetClientID(), 81 | info.GetUserID(), 82 | info.GetRedirectURI(), 83 | info.GetScope(), 84 | info.GetCode(), 85 | info.GetCodeCreateAt(), 86 | info.GetCodeExpiresIn(), 87 | info.GetCodeChallenge(), 88 | info.GetCodeChallengeMethod(), 89 | info.GetAccess(), 90 | info.GetAccessCreateAt(), 91 | info.GetAccessExpiresIn(), 92 | info.GetRefresh(), 93 | info.GetRefreshCreateAt(), 94 | info.GetRefreshExpiresIn(), 95 | ) 96 | return mustChangeOneRow(r, err, "Credentials.Update") 97 | } 98 | 99 | func (c *Credentials) UpdateExpires(ctx util.Context, tx *sql.Tx, id string, expires time.Time) error { 100 | r, err := tx.Stmt(c.updateCredExpires).ExecContext(ctx, id, expires) 101 | return mustChangeOneRow(r, err, "Credentials.UpdateExpires") 102 | } 103 | 104 | func (c *Credentials) Delete(ctx util.Context, tx *sql.Tx, id string) error { 105 | r, err := tx.Stmt(c.removeCred).ExecContext(ctx, id) 106 | return mustChangeOneRow(r, err, "Credentials.Delete") 107 | } 108 | 109 | func (c *Credentials) GetTokenInfo(ctx util.Context, tx *sql.Tx, id string) (oauth2.TokenInfo, error) { 110 | rows, err := tx.Stmt(c.getTokenInfoByCredID).QueryContext(ctx, id) 111 | if err != nil { 112 | return nil, err 113 | } 114 | defer rows.Close() 115 | ti := &TokenInfo{} 116 | return ti, enforceOneRow(rows, "Credentials.GetTokenInfo", func(r SingleRow) error { 117 | return ti.scanFromSingleRow(r) 118 | }) 119 | } 120 | 121 | func (c *Credentials) DeleteExpired(ctx util.Context, tx *sql.Tx) error { 122 | _, err := tx.Stmt(c.removeExpiredCreds).ExecContext(ctx) 123 | return err 124 | } 125 | -------------------------------------------------------------------------------- /models/delivery_attempts.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | "time" 23 | 24 | "github.com/go-fed/apcore/util" 25 | ) 26 | 27 | // These constants are used to mark the simple state of the delivery attempt. 28 | const ( 29 | newDeliveryAttempt = "new" 30 | successDeliveryAttempt = "success" 31 | failedDeliveryAttempt = "failed" 32 | abandonedDeliveryAttempt = "abandoned" 33 | ) 34 | 35 | var _ Model = &DeliveryAttempts{} 36 | 37 | // DeliveryAttempts is a Model that provides additional database methods for 38 | // delivery attempts. 39 | type DeliveryAttempts struct { 40 | insertDeliveryAttempt *sql.Stmt 41 | markDeliveryAttemptSuccessful *sql.Stmt 42 | markDeliveryAttemptFailed *sql.Stmt 43 | markDeliveryAttemptAbandoned *sql.Stmt 44 | firstRetryablePage *sql.Stmt 45 | nextRetryablePage *sql.Stmt 46 | } 47 | 48 | func (d *DeliveryAttempts) Prepare(db *sql.DB, s SqlDialect) error { 49 | return prepareStmtPairs(db, 50 | stmtPairs{ 51 | {&(d.insertDeliveryAttempt), s.InsertAttempt()}, 52 | {&(d.markDeliveryAttemptSuccessful), s.MarkSuccessfulAttempt()}, 53 | {&(d.markDeliveryAttemptFailed), s.MarkFailedAttempt()}, 54 | {&(d.markDeliveryAttemptAbandoned), s.MarkAbandonedAttempt()}, 55 | {&(d.firstRetryablePage), s.FirstPageRetryableFailures()}, 56 | {&(d.nextRetryablePage), s.NextPageRetryableFailures()}, 57 | }) 58 | } 59 | 60 | func (d *DeliveryAttempts) CreateTable(t *sql.Tx, s SqlDialect) error { 61 | _, err := t.Exec(s.CreateDeliveryAttemptsTable()) 62 | return err 63 | } 64 | 65 | func (d *DeliveryAttempts) Close() { 66 | d.insertDeliveryAttempt.Close() 67 | d.markDeliveryAttemptSuccessful.Close() 68 | d.markDeliveryAttemptFailed.Close() 69 | } 70 | 71 | // Create a new delivery attempt. 72 | func (d *DeliveryAttempts) Create(c util.Context, tx *sql.Tx, from string, toActor *url.URL, payload []byte) (id string, err error) { 73 | var rows *sql.Rows 74 | rows, err = tx.Stmt(d.insertDeliveryAttempt).QueryContext(c, 75 | from, 76 | toActor.String(), 77 | payload, 78 | newDeliveryAttempt) 79 | if err != nil { 80 | return 81 | } 82 | defer rows.Close() 83 | return id, enforceOneRow(rows, "DeliveryAttempts.Create", func(r SingleRow) error { 84 | return r.Scan(&(id)) 85 | }) 86 | } 87 | 88 | // MarkSuccessful marks a delivery attempt as successful. 89 | func (d *DeliveryAttempts) MarkSuccessful(c util.Context, tx *sql.Tx, id string) error { 90 | r, err := tx.Stmt(d.markDeliveryAttemptSuccessful).ExecContext(c, 91 | id, 92 | successDeliveryAttempt) 93 | return mustChangeOneRow(r, err, "DeliveryAttempts.MarkSuccessful") 94 | } 95 | 96 | // MarkFailed marks a delivery attempt as failed. 97 | func (d *DeliveryAttempts) MarkFailed(c util.Context, tx *sql.Tx, id string) error { 98 | r, err := tx.Stmt(d.markDeliveryAttemptFailed).ExecContext(c, 99 | id, 100 | failedDeliveryAttempt) 101 | return mustChangeOneRow(r, err, "DeliveryAttempts.MarkFailed") 102 | } 103 | 104 | // MarkAbandoned marks a delivery attempt as abandoned. 105 | func (d *DeliveryAttempts) MarkAbandoned(c util.Context, tx *sql.Tx, id string) error { 106 | r, err := tx.Stmt(d.markDeliveryAttemptAbandoned).ExecContext(c, 107 | id, 108 | abandonedDeliveryAttempt) 109 | return mustChangeOneRow(r, err, "DeliveryAttempts.Abandoned") 110 | } 111 | 112 | type RetryableFailure struct { 113 | ID string 114 | UserID string 115 | DeliverTo URL 116 | Payload []byte 117 | NAttempts int 118 | LastAttempt time.Time 119 | } 120 | 121 | // FirstPageFailures obtains the first page of retryable failures. 122 | func (d *DeliveryAttempts) FirstPageFailures(c util.Context, tx *sql.Tx, fetchTime time.Time, n int) (rf []RetryableFailure, err error) { 123 | var rows *sql.Rows 124 | rows, err = tx.Stmt(d.firstRetryablePage).QueryContext(c, failedDeliveryAttempt, fetchTime, n) 125 | if err != nil { 126 | return 127 | } 128 | defer rows.Close() 129 | return rf, doForRows(rows, "DeliveryAttempts.FirstPageFailures", func(r SingleRow) error { 130 | var rt RetryableFailure 131 | if err := r.Scan(&(rt.ID), &(rt.UserID), &(rt.DeliverTo), &(rt.Payload), &(rt.NAttempts), &(rt.LastAttempt)); err != nil { 132 | return err 133 | } 134 | rf = append(rf, rt) 135 | return nil 136 | }) 137 | } 138 | 139 | // NextPageFailures obtains the next page of retryable failures. 140 | func (d *DeliveryAttempts) NextPageFailures(c util.Context, tx *sql.Tx, prevID string, fetchTime time.Time, n int) (rf []RetryableFailure, err error) { 141 | var rows *sql.Rows 142 | rows, err = tx.Stmt(d.nextRetryablePage).QueryContext(c, failedDeliveryAttempt, fetchTime, n, prevID) 143 | if err != nil { 144 | return 145 | } 146 | defer rows.Close() 147 | return rf, doForRows(rows, "DeliveryAttempts.NextPageFailures", func(r SingleRow) error { 148 | var rt RetryableFailure 149 | if err := r.Scan(&(rt.ID), &(rt.UserID), &(rt.DeliverTo), &(rt.Payload), &(rt.NAttempts), &(rt.LastAttempt)); err != nil { 150 | return err 151 | } 152 | rf = append(rf, rt) 153 | return nil 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /models/fed_data.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/apcore/util" 24 | ) 25 | 26 | var _ Model = &FedData{} 27 | 28 | // FedData is a Model that provides additional database methods for 29 | // ActivityStreams data received from federated peers. 30 | type FedData struct { 31 | exists *sql.Stmt 32 | get *sql.Stmt 33 | fedCreate *sql.Stmt 34 | fedUpdate *sql.Stmt 35 | fedDelete *sql.Stmt 36 | } 37 | 38 | func (f *FedData) Prepare(db *sql.DB, s SqlDialect) error { 39 | return prepareStmtPairs(db, 40 | stmtPairs{ 41 | {&(f.exists), s.FedExists()}, 42 | {&(f.get), s.FedGet()}, 43 | {&(f.fedCreate), s.FedCreate()}, 44 | {&(f.fedUpdate), s.FedUpdate()}, 45 | {&(f.fedDelete), s.FedDelete()}, 46 | }) 47 | } 48 | 49 | func (f *FedData) CreateTable(t *sql.Tx, s SqlDialect) error { 50 | if _, err := t.Exec(s.CreateFedDataTable()); err != nil { 51 | return err 52 | } 53 | _, err := t.Exec(s.CreateIndexIDFedDataTable()) 54 | return err 55 | } 56 | 57 | func (f *FedData) Close() { 58 | f.exists.Close() 59 | f.get.Close() 60 | f.fedCreate.Close() 61 | f.fedUpdate.Close() 62 | f.fedDelete.Close() 63 | } 64 | 65 | // Exists determines if the ID is stored in the federated table. 66 | func (f *FedData) Exists(c util.Context, tx *sql.Tx, id *url.URL) (exists bool, err error) { 67 | var rows *sql.Rows 68 | rows, err = tx.Stmt(f.exists).QueryContext(c, id.String()) 69 | if err != nil { 70 | return 71 | } 72 | defer rows.Close() 73 | err = enforceOneRow(rows, "FedData.Exists", func(r SingleRow) error { 74 | return r.Scan(&exists) 75 | }) 76 | return 77 | } 78 | 79 | // Get retrieves the ID from the federated table. 80 | func (f *FedData) Get(c util.Context, tx *sql.Tx, id *url.URL) (v ActivityStreams, err error) { 81 | var rows *sql.Rows 82 | rows, err = tx.Stmt(f.get).QueryContext(c, id.String()) 83 | if err != nil { 84 | return 85 | } 86 | defer rows.Close() 87 | err = enforceOneRow(rows, "FedData.Get", func(r SingleRow) error { 88 | return r.Scan(&v) 89 | }) 90 | return 91 | } 92 | 93 | // Create inserts the federated data into the table. 94 | func (f *FedData) Create(c util.Context, tx *sql.Tx, v ActivityStreams) error { 95 | v.SanitizeContentSummaryHTML() 96 | r, err := tx.Stmt(f.fedCreate).ExecContext(c, v) 97 | return mustChangeOneRow(r, err, "FedData.Create") 98 | } 99 | 100 | // Update replaces the federated data for the specified IRI. 101 | func (f *FedData) Update(c util.Context, tx *sql.Tx, fedIDIRI *url.URL, v ActivityStreams) error { 102 | v.SanitizeContentSummaryHTML() 103 | r, err := tx.Stmt(f.fedUpdate).ExecContext(c, fedIDIRI.String(), v) 104 | return mustChangeOneRow(r, err, "FedData.Update") 105 | } 106 | 107 | // Delete removes the federated data with the specified IRI. 108 | func (f *FedData) Delete(c util.Context, tx *sql.Tx, fedIDIRI *url.URL) error { 109 | r, err := tx.Stmt(f.fedDelete).ExecContext(c, fedIDIRI.String()) 110 | return mustChangeOneRow(r, err, "FedData.Delete") 111 | } 112 | -------------------------------------------------------------------------------- /models/liked.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/apcore/util" 24 | ) 25 | 26 | var _ Model = &Liked{} 27 | 28 | // Liked is a Model that provides additional database methods for Liked. 29 | type Liked struct { 30 | insert *sql.Stmt 31 | containsForActor *sql.Stmt 32 | contains *sql.Stmt 33 | get *sql.Stmt 34 | getLastPage *sql.Stmt 35 | prependItem *sql.Stmt 36 | deleteItem *sql.Stmt 37 | getAllForActor *sql.Stmt 38 | } 39 | 40 | func (i *Liked) Prepare(db *sql.DB, s SqlDialect) error { 41 | return prepareStmtPairs(db, 42 | stmtPairs{ 43 | {&(i.insert), s.InsertLiked()}, 44 | {&(i.containsForActor), s.LikedContainsForActor()}, 45 | {&(i.contains), s.LikedContains()}, 46 | {&(i.get), s.GetLiked()}, 47 | {&(i.getLastPage), s.GetLikedLastPage()}, 48 | {&(i.prependItem), s.PrependLikedItem()}, 49 | {&(i.deleteItem), s.DeleteLikedItem()}, 50 | {&(i.getAllForActor), s.GetAllLikedForActor()}, 51 | }) 52 | } 53 | 54 | func (i *Liked) CreateTable(t *sql.Tx, s SqlDialect) error { 55 | if _, err := t.Exec(s.CreateLikedTable()); err != nil { 56 | return err 57 | } 58 | _, err := t.Exec(s.CreateIndexIDLikedTable()) 59 | return err 60 | } 61 | 62 | func (i *Liked) Close() { 63 | i.insert.Close() 64 | i.containsForActor.Close() 65 | i.contains.Close() 66 | i.get.Close() 67 | i.getLastPage.Close() 68 | i.prependItem.Close() 69 | i.deleteItem.Close() 70 | i.getAllForActor.Close() 71 | } 72 | 73 | // Create a new liked entry for the given actor. 74 | func (i *Liked) Create(c util.Context, tx *sql.Tx, actor *url.URL, liked ActivityStreamsCollection) error { 75 | r, err := tx.Stmt(i.insert).ExecContext(c, 76 | actor.String(), 77 | liked) 78 | return mustChangeOneRow(r, err, "Liked.Create") 79 | } 80 | 81 | // ContainsForActor returns true if the item is in the actor's liked's collection. 82 | func (i *Liked) ContainsForActor(c util.Context, tx *sql.Tx, actor, item *url.URL) (b bool, err error) { 83 | var rows *sql.Rows 84 | rows, err = tx.Stmt(i.containsForActor).QueryContext(c, actor.String(), item.String()) 85 | if err != nil { 86 | return 87 | } 88 | defer rows.Close() 89 | return b, enforceOneRow(rows, "Liked.ContainsForActor", func(r SingleRow) error { 90 | return r.Scan(&b) 91 | }) 92 | } 93 | 94 | // Contains returns true if the item is in the liked's collection. 95 | func (i *Liked) Contains(c util.Context, tx *sql.Tx, liked, item *url.URL) (b bool, err error) { 96 | var rows *sql.Rows 97 | rows, err = tx.Stmt(i.contains).QueryContext(c, liked.String(), item.String()) 98 | if err != nil { 99 | return 100 | } 101 | defer rows.Close() 102 | return b, enforceOneRow(rows, "Liked.Contains", func(r SingleRow) error { 103 | return r.Scan(&b) 104 | }) 105 | } 106 | 107 | // GetPage returns a CollectionPage of the Liked. 108 | // 109 | // The range of elements retrieved are [min, max). 110 | func (i *Liked) GetPage(c util.Context, tx *sql.Tx, liked *url.URL, min, max int) (page ActivityStreamsCollectionPage, isEnd bool, err error) { 111 | var rows *sql.Rows 112 | rows, err = tx.Stmt(i.get).QueryContext(c, liked.String(), min, max-1) 113 | if err != nil { 114 | return 115 | } 116 | defer rows.Close() 117 | return page, isEnd, enforceOneRow(rows, "Liked.GetPage", func(r SingleRow) error { 118 | return r.Scan(&page, &isEnd) 119 | }) 120 | } 121 | 122 | // GetLastPage returns the last CollectionPage of the Liked collection. 123 | func (i *Liked) GetLastPage(c util.Context, tx *sql.Tx, liked *url.URL, n int) (page ActivityStreamsCollectionPage, startIdx int, err error) { 124 | var rows *sql.Rows 125 | rows, err = tx.Stmt(i.getLastPage).QueryContext(c, liked.String(), n) 126 | if err != nil { 127 | return 128 | } 129 | defer rows.Close() 130 | return page, startIdx, enforceOneRow(rows, "Liked.GetLastPage", func(r SingleRow) error { 131 | return r.Scan(&page, &startIdx) 132 | }) 133 | } 134 | 135 | // PrependItem prepends the item to the liked's ordered items list. 136 | func (i *Liked) PrependItem(c util.Context, tx *sql.Tx, liked, item *url.URL) error { 137 | r, err := tx.Stmt(i.prependItem).ExecContext(c, liked.String(), item.String()) 138 | return mustChangeOneRow(r, err, "Liked.PrependItem") 139 | } 140 | 141 | // DeleteItem removes the item from the liked's ordered items list. 142 | func (i *Liked) DeleteItem(c util.Context, tx *sql.Tx, liked, item *url.URL) error { 143 | r, err := tx.Stmt(i.deleteItem).ExecContext(c, liked.String(), item.String()) 144 | return mustChangeOneRow(r, err, "Liked.DeleteItem") 145 | } 146 | 147 | // GetAllForActor returns the entire Liked Collection. 148 | func (i *Liked) GetAllForActor(c util.Context, tx *sql.Tx, liked *url.URL) (col ActivityStreamsCollection, err error) { 149 | var rows *sql.Rows 150 | rows, err = tx.Stmt(i.getAllForActor).QueryContext(c, liked.String()) 151 | if err != nil { 152 | return 153 | } 154 | defer rows.Close() 155 | return col, enforceOneRow(rows, "Liked.GetAllForActor", func(r SingleRow) error { 156 | return r.Scan(&col) 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /models/local_data.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/apcore/util" 24 | ) 25 | 26 | var _ Model = &LocalData{} 27 | 28 | // LocalData is a Model that provides additional database methods for 29 | // ActivityStreams data generated by this instance. 30 | type LocalData struct { 31 | exists *sql.Stmt 32 | get *sql.Stmt 33 | localCreate *sql.Stmt 34 | localUpdate *sql.Stmt 35 | localDelete *sql.Stmt 36 | stats *sql.Stmt 37 | } 38 | 39 | func (f *LocalData) Prepare(db *sql.DB, s SqlDialect) error { 40 | return prepareStmtPairs(db, 41 | stmtPairs{ 42 | {&(f.exists), s.LocalExists()}, 43 | {&(f.get), s.LocalGet()}, 44 | {&(f.localCreate), s.LocalCreate()}, 45 | {&(f.localUpdate), s.LocalUpdate()}, 46 | {&(f.localDelete), s.LocalDelete()}, 47 | {&(f.stats), s.LocalStats()}, 48 | }) 49 | } 50 | 51 | func (f *LocalData) CreateTable(t *sql.Tx, s SqlDialect) error { 52 | if _, err := t.Exec(s.CreateLocalDataTable()); err != nil { 53 | return err 54 | } 55 | _, err := t.Exec(s.CreateIndexIDLocalDataTable()) 56 | return err 57 | } 58 | 59 | func (f *LocalData) Close() { 60 | f.exists.Close() 61 | f.get.Close() 62 | f.localCreate.Close() 63 | f.localUpdate.Close() 64 | f.localDelete.Close() 65 | f.stats.Close() 66 | } 67 | 68 | // Exists determines if the ID is stored in the local table. 69 | func (f *LocalData) Exists(c util.Context, tx *sql.Tx, id *url.URL) (exists bool, err error) { 70 | var rows *sql.Rows 71 | rows, err = tx.Stmt(f.exists).QueryContext(c, id.String()) 72 | if err != nil { 73 | return 74 | } 75 | defer rows.Close() 76 | err = enforceOneRow(rows, "LocalData.Exists", func(r SingleRow) error { 77 | return r.Scan(&exists) 78 | }) 79 | return 80 | } 81 | 82 | // Get retrieves the ID from the local table. 83 | func (f *LocalData) Get(c util.Context, tx *sql.Tx, id *url.URL) (v ActivityStreams, err error) { 84 | var rows *sql.Rows 85 | rows, err = tx.Stmt(f.get).QueryContext(c, id.String()) 86 | if err != nil { 87 | return 88 | } 89 | defer rows.Close() 90 | err = enforceOneRow(rows, "LocalData.Get", func(r SingleRow) error { 91 | return r.Scan(&v) 92 | }) 93 | return 94 | } 95 | 96 | // Create inserts the local data into the table. 97 | func (f *LocalData) Create(c util.Context, tx *sql.Tx, v ActivityStreams) error { 98 | v.SanitizeContentSummaryHTML() 99 | r, err := tx.Stmt(f.localCreate).ExecContext(c, v) 100 | return mustChangeOneRow(r, err, "LocalData.Create") 101 | } 102 | 103 | // Update replaces the local data for the specified IRI. 104 | func (f *LocalData) Update(c util.Context, tx *sql.Tx, localIDIRI *url.URL, v ActivityStreams) error { 105 | v.SanitizeContentSummaryHTML() 106 | r, err := tx.Stmt(f.localUpdate).ExecContext(c, localIDIRI.String(), v) 107 | return mustChangeOneRow(r, err, "LocalData.Update") 108 | } 109 | 110 | // Delete removes the local data with the specified IRI. 111 | func (f *LocalData) Delete(c util.Context, tx *sql.Tx, localIDIRI *url.URL) error { 112 | r, err := tx.Stmt(f.localDelete).ExecContext(c, localIDIRI.String()) 113 | return mustChangeOneRow(r, err, "LocalData.Delete") 114 | } 115 | 116 | type LocalDataActivity struct { 117 | NLocalPosts int 118 | NLocalComments int 119 | } 120 | 121 | func (f *LocalData) Stats(c util.Context, tx *sql.Tx) (la LocalDataActivity, err error) { 122 | var rows *sql.Rows 123 | rows, err = tx.Stmt(f.stats).QueryContext(c) 124 | if err != nil { 125 | return 126 | } 127 | defer rows.Close() 128 | return la, enforceOneRow(rows, "LocalData.Stats", func(r SingleRow) error { 129 | return r.Scan(&(la.NLocalPosts), &(la.NLocalComments)) 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /models/model.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | ) 22 | 23 | // Model handles managing a single database type. 24 | type Model interface { 25 | Prepare(*sql.DB, SqlDialect) error 26 | CreateTable(*sql.Tx, SqlDialect) error 27 | Close() 28 | } 29 | 30 | // stmtPair make a pair of **sql.Stmt and its associated SQL string. 31 | // 32 | // The goal is to populate *stmt based on the associated sqlStr. 33 | type stmtPair struct { 34 | stmt **sql.Stmt 35 | sqlStr string 36 | } 37 | 38 | // prepareStmtPair is a mapper that populates the stmtPair.stmt. 39 | func prepareStmtPair(db *sql.DB, s stmtPair) (err error) { 40 | *s.stmt, err = db.Prepare(s.sqlStr) 41 | return err 42 | } 43 | 44 | // stmtPairs are a list of stmtPair. 45 | type stmtPairs []stmtPair 46 | 47 | // prepareStmtPairs turns stmtPairs into a single error, with a side effect of 48 | // populating all stmt. 49 | func prepareStmtPairs(db *sql.DB, s stmtPairs) (err error) { 50 | doIfNoErr := func(p stmtPair, fn func(*sql.DB, stmtPair) error) error { 51 | if err == nil { 52 | return fn(db, p) 53 | } 54 | return err 55 | } 56 | for _, p := range s { 57 | err = doIfNoErr(p, prepareStmtPair) 58 | } 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /models/private_keys.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | 22 | "github.com/go-fed/apcore/util" 23 | ) 24 | 25 | var _ Model = &PrivateKeys{} 26 | 27 | // PrivateKeys is a Model that provides additional database methods for the 28 | // PrivateKey type. 29 | type PrivateKeys struct { 30 | createPrivateKey *sql.Stmt 31 | getByUserID *sql.Stmt 32 | getInstanceActor *sql.Stmt 33 | } 34 | 35 | func (p *PrivateKeys) Prepare(db *sql.DB, s SqlDialect) error { 36 | return prepareStmtPairs(db, 37 | stmtPairs{ 38 | {&(p.createPrivateKey), s.CreatePrivateKey()}, 39 | {&(p.getByUserID), s.GetPrivateKeyByUserID()}, 40 | {&(p.getInstanceActor), s.GetPrivateKeyForInstanceActor()}, 41 | }) 42 | } 43 | 44 | func (p *PrivateKeys) CreateTable(t *sql.Tx, s SqlDialect) error { 45 | _, err := t.Exec(s.CreatePrivateKeysTable()) 46 | return err 47 | } 48 | 49 | func (p *PrivateKeys) Close() { 50 | p.createPrivateKey.Close() 51 | p.getByUserID.Close() 52 | p.getInstanceActor.Close() 53 | } 54 | 55 | // Create a new private key entry in the database. 56 | func (p *PrivateKeys) Create(c util.Context, tx *sql.Tx, userID, purpose string, privKey []byte) error { 57 | r, err := tx.Stmt(p.createPrivateKey).ExecContext(c, userID, purpose, privKey) 58 | return mustChangeOneRow(r, err, "PrivateKeys.Create") 59 | } 60 | 61 | // GetByUserID fetches a private key by the userID and purpose of the key. 62 | func (p *PrivateKeys) GetByUserID(c util.Context, tx *sql.Tx, userID, purpose string) (b []byte, err error) { 63 | var rows *sql.Rows 64 | rows, err = tx.Stmt(p.getByUserID).QueryContext(c, userID, purpose) 65 | if err != nil { 66 | return 67 | } 68 | defer rows.Close() 69 | return b, enforceOneRow(rows, "PrivateKeys.GetByUserID", func(r SingleRow) error { 70 | return r.Scan(&(b)) 71 | }) 72 | } 73 | 74 | // GetInstanceActor fetches a private key for the single instance actor. 75 | func (p *PrivateKeys) GetInstanceActor(c util.Context, tx *sql.Tx, purpose string) (b []byte, err error) { 76 | var rows *sql.Rows 77 | rows, err = tx.Stmt(p.getInstanceActor).QueryContext(c, purpose) 78 | if err != nil { 79 | return 80 | } 81 | defer rows.Close() 82 | return b, enforceOneRow(rows, "PrivateKeys.GetInstanceActor", func(r SingleRow) error { 83 | return r.Scan(&(b)) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /models/resolutions.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | import ( 20 | "database/sql" 21 | "fmt" 22 | "net/url" 23 | "time" 24 | 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Resolution struct { 29 | Time time.Time `json:"time",omitempty` 30 | 31 | // The following are used by Policies 32 | Matched bool `json:"matched",omitempty` 33 | MatchLog []string `json:"matchLog",omitempty` 34 | } 35 | 36 | func (r *Resolution) Logf(s string, i ...interface{}) { 37 | r.Log(fmt.Sprintf(s, i...)) 38 | } 39 | 40 | func (r *Resolution) Log(s string) { 41 | r.MatchLog = append(r.MatchLog, s) 42 | } 43 | 44 | type CreateResolution struct { 45 | PolicyID string 46 | IRI *url.URL 47 | R Resolution 48 | } 49 | 50 | var _ Model = &Resolutions{} 51 | 52 | // Resolutions is a Model that provides additional database methods for the 53 | // Resolution type. 54 | type Resolutions struct { 55 | create *sql.Stmt 56 | } 57 | 58 | func (r *Resolutions) Prepare(db *sql.DB, s SqlDialect) error { 59 | return prepareStmtPairs(db, 60 | stmtPairs{ 61 | {&(r.create), s.CreateResolution()}, 62 | }) 63 | } 64 | 65 | func (r *Resolutions) CreateTable(t *sql.Tx, s SqlDialect) error { 66 | _, err := t.Exec(s.CreateResolutionsTable()) 67 | return err 68 | } 69 | 70 | func (r *Resolutions) Close() { 71 | r.create.Close() 72 | } 73 | 74 | // Create a new Resolution 75 | func (r *Resolutions) Create(c util.Context, tx *sql.Tx, cr CreateResolution) error { 76 | rows, err := tx.Stmt(r.create).ExecContext(c, 77 | cr.PolicyID, 78 | cr.IRI.String(), 79 | cr.R) 80 | return mustChangeOneRow(rows, err, "Resolutions.Create") 81 | } 82 | -------------------------------------------------------------------------------- /paths/query.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package paths 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "strconv" 23 | ) 24 | 25 | const ( 26 | queryTrue = "true" 27 | queryCollectionPage = "page" 28 | queryCollectionEnd = "end" 29 | queryOffset = "offset" 30 | queryNum = "n" 31 | ) 32 | 33 | // AddPageParams overwrites the query string of a base URL and returns a copy 34 | // with the pagination parameters set. 35 | func AddPageParams(base *url.URL, offset, n int) *url.URL { 36 | c := *base 37 | c.RawQuery = fmt.Sprintf("%s=%s&%s=%d&%s=%d", 38 | queryCollectionPage, 39 | queryTrue, 40 | queryOffset, 41 | offset, 42 | queryNum, 43 | n) 44 | return &c 45 | } 46 | 47 | // IsGetCollectionPage returns true when the IRI requests pagination for an 48 | // OrderedCollection-style of IRI. 49 | func IsGetCollectionPage(u *url.URL) bool { 50 | return u.Query().Get(queryCollectionPage) == queryTrue 51 | } 52 | 53 | // IsGetCollectionEnd returns true when the IRI requests the last page for an 54 | // OrderedCollection-style of IRI. 55 | func IsGetCollectionEnd(u *url.URL) bool { 56 | return u.Query().Get(queryCollectionEnd) == queryTrue 57 | } 58 | 59 | // GetOffsetOrDefault returns the offset requested in the IRI, or default if 60 | // no value or an invalid value is specified. 61 | func GetOffsetOrDefault(u *url.URL, def int) int { 62 | return queryKeyAsIntOrDefault(u, queryOffset, def) 63 | } 64 | 65 | // GetNumOrDefault returns the number requested in the IRI, or default if no 66 | // value or an invalid value is specified. If the requested amount is greater 67 | // than the max, the maximum is returned instead. 68 | func GetNumOrDefault(u *url.URL, def, max int) int { 69 | n := queryKeyAsIntOrDefault(u, queryNum, def) 70 | if n > max { 71 | return max 72 | } 73 | return n 74 | } 75 | 76 | func queryKeyAsIntOrDefault(u *url.URL, key string, def int) int { 77 | v := u.Query().Get(key) 78 | n, err := strconv.Atoi(v) 79 | if err != nil { 80 | return def 81 | } 82 | return n 83 | } 84 | -------------------------------------------------------------------------------- /pkg.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | // Package apcore implements a generic, extensible ActivityPub server using 18 | // the go-fed libraries. 19 | package apcore 20 | 21 | import ( 22 | "github.com/go-fed/apcore/app" 23 | ) 24 | 25 | const ( 26 | apcoreName = "apcore" 27 | apcoreMajorVersion = 0 28 | apcoreMinorVersion = 1 29 | apcorePatchVersion = 0 30 | apcoreRepository = "https://github.com/go-fed/apcore" 31 | ) 32 | 33 | func apCoreSoftware() app.Software { 34 | return app.Software{ 35 | Name: apcoreName, 36 | MajorVersion: apcoreMajorVersion, 37 | MinorVersion: apcoreMinorVersion, 38 | PatchVersion: apcorePatchVersion, 39 | Repository: apcoreRepository, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package apcore 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "io" 23 | "os" 24 | 25 | "github.com/go-fed/apcore/app" 26 | "github.com/go-fed/apcore/util" 27 | ) 28 | 29 | // Run will launch the apcore server. 30 | func Run(a app.Application) { 31 | if !flag.Parsed() { 32 | flag.Parse() 33 | } 34 | if flag.NArg() != 1 { 35 | flag.Usage() 36 | os.Exit(1) 37 | } 38 | 39 | // Check and prepare debug mode 40 | if *devFlag { 41 | util.InfoLogger.Info("Debug mode enabled") 42 | if len(*infoLogFileFlag) > 0 { 43 | util.InfoLogger.Warning("info_log_file flag ignored in debug mode") 44 | } 45 | if len(*errorLogFileFlag) > 0 { 46 | util.InfoLogger.Warning("error_log_file flag ignored in debug mode") 47 | } 48 | } else { 49 | // Prepare production logging 50 | var il, el io.Writer = os.Stdout, os.Stderr 51 | var err error 52 | if len(*infoLogFileFlag) > 0 { 53 | il, err = os.OpenFile( 54 | *infoLogFileFlag, 55 | os.O_CREATE|os.O_WRONLY|os.O_APPEND, 56 | 0660) 57 | if err != nil { 58 | util.ErrorLogger.Errorf("cannot open %s: %s", *infoLogFileFlag, err) 59 | os.Exit(1) 60 | } 61 | } 62 | if len(*errorLogFileFlag) > 0 { 63 | el, err = os.OpenFile( 64 | *errorLogFileFlag, 65 | os.O_CREATE|os.O_WRONLY|os.O_APPEND, 66 | 0660) 67 | if err != nil { 68 | util.ErrorLogger.Errorf("cannot open %s: %s", *infoLogFileFlag, err) 69 | os.Exit(1) 70 | } 71 | } 72 | util.LogInfoTo(*systemLogFlag, il) 73 | defer util.LogInfoToStdout() 74 | util.LogErrorTo(*systemLogFlag, el) 75 | defer util.LogErrorToStderr() 76 | } 77 | 78 | // Conduct the action 79 | var action cmdAction 80 | found := false 81 | for _, v := range allActions { 82 | if v.Name == flag.Arg(0) { 83 | action = v 84 | found = true 85 | break 86 | } 87 | } 88 | if !found { 89 | fmt.Fprintf(os.Stderr, "Unknown action: %s\n", flag.Arg(0)) 90 | fmt.Fprintf(os.Stderr, "Available actions:\n%s", allActionsUsage()) 91 | os.Exit(1) 92 | } else if err := action.Action(a); err != nil { 93 | util.ErrorLogger.Errorf("error running %s: %s", flag.Arg(0), err) 94 | os.Exit(1) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /services/any.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | 23 | "github.com/go-fed/apcore/app" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Any struct { 29 | DB *sql.DB 30 | } 31 | 32 | func (a *Any) Begin() app.TxBuilder { 33 | return &txBuilder{db: a.DB} 34 | } 35 | 36 | type txBuilder struct { 37 | db *sql.DB 38 | ops []*anyOp 39 | } 40 | 41 | func (a *txBuilder) QueryOneRow(sql string, cb func(r app.SingleRow) error, args ...interface{}) { 42 | a.addOp(&anyOp{ 43 | sql: sql, 44 | args: args, 45 | isExec: false, 46 | isOneRow: true, 47 | cb: cb, 48 | }) 49 | } 50 | 51 | func (a *txBuilder) Query(sql string, cb func(r app.SingleRow) error, args ...interface{}) { 52 | a.addOp(&anyOp{ 53 | sql: sql, 54 | args: args, 55 | isExec: false, 56 | isOneRow: false, 57 | cb: cb, 58 | }) 59 | } 60 | 61 | func (a *txBuilder) ExecOneRow(sql string, args ...interface{}) { 62 | a.addOp(&anyOp{ 63 | sql: sql, 64 | args: args, 65 | isExec: true, 66 | isOneRow: true, 67 | }) 68 | } 69 | 70 | func (a *txBuilder) Exec(sql string, args ...interface{}) { 71 | a.addOp(&anyOp{ 72 | sql: sql, 73 | args: args, 74 | isExec: true, 75 | isOneRow: false, 76 | }) 77 | } 78 | 79 | func (a *txBuilder) addOp(op *anyOp) { 80 | a.ops = append(a.ops, op) 81 | } 82 | 83 | func (a *txBuilder) Do(c context.Context) error { 84 | return doInTx(util.Context{c}, a.db, func(tx *sql.Tx) error { 85 | for _, op := range a.ops { 86 | if err := op.Do(c, tx); err != nil { 87 | return err 88 | } 89 | } 90 | return nil 91 | }) 92 | } 93 | 94 | type anyOp struct { 95 | sql string 96 | args []interface{} 97 | isExec bool 98 | isOneRow bool 99 | cb func(r app.SingleRow) error 100 | } 101 | 102 | func (a *anyOp) Do(c context.Context, tx *sql.Tx) (err error) { 103 | if a.isExec { 104 | return a.doExec(c, tx) 105 | } else { 106 | return a.doQuery(c, tx) 107 | } 108 | } 109 | 110 | func (a *anyOp) doQuery(c context.Context, tx *sql.Tx) error { 111 | r, err := tx.QueryContext(c, a.sql, a.args...) 112 | if err != nil { 113 | return err 114 | } 115 | if a.isOneRow { 116 | return models.MustQueryOneRow(r, func(r models.SingleRow) error { 117 | return a.cb(r) 118 | }) 119 | } else { 120 | return models.QueryRows(r, func(r models.SingleRow) error { 121 | return a.cb(r) 122 | }) 123 | } 124 | } 125 | 126 | func (a *anyOp) doExec(c context.Context, tx *sql.Tx) error { 127 | r, err := tx.ExecContext(c, a.sql, a.args...) 128 | if err != nil { 129 | return err 130 | } 131 | if a.isOneRow { 132 | return models.MustChangeOneRow(r) 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /services/crypto.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "crypto/rand" 21 | "database/sql" 22 | "fmt" 23 | 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | "golang.org/x/crypto/bcrypt" 27 | ) 28 | 29 | // Crypto service provides high level service methods relating to crypto 30 | // operations. 31 | type Crypto struct { 32 | DB *sql.DB 33 | Users *models.Users 34 | } 35 | 36 | // Valid determines whether the provided password is valid for the user 37 | // associated with the email address. 38 | func (c *Crypto) Valid(ctx util.Context, email, pass string) (uuid string, valid bool, err error) { 39 | var su *models.SensitiveUser 40 | err = doInTx(ctx, c.DB, func(tx *sql.Tx) error { 41 | su, err = c.Users.SensitiveUserByEmail(ctx, tx, email) 42 | return err 43 | }) 44 | if su == nil && err == nil { 45 | // No email found -- do not return an error. Instead, simply 46 | // ensure we're returning an invalid result. 47 | valid = false 48 | return 49 | } 50 | if err != nil { 51 | return 52 | } 53 | valid = passEquals(pass, su.Salt, su.Hashpass) 54 | uuid = su.ID 55 | return 56 | } 57 | 58 | // HashPasswordParameters contains values used in generating secrets. 59 | type HashPasswordParameters struct { 60 | // Size of the salt in number of bytes. 61 | SaltSize int 62 | // Strength of the bcrypt hashing. 63 | BCryptStrength int 64 | } 65 | 66 | // hashPass hashes a password with a salt using the provided parameters and 67 | // bcrypt. 68 | func hashPass(h HashPasswordParameters, secret string) (salt, hashpass []byte, err error) { 69 | salt, err = newSalt(h.SaltSize) 70 | if err != nil { 71 | return 72 | } 73 | hashpass, err = hashPasswordWithSalt(secret, salt, h.BCryptStrength) 74 | return 75 | } 76 | 77 | // Uses a password and salt to hash with the given strength value. 78 | // 79 | // Strength is dependent on the bcrypt library, which has built-in protections 80 | // against under-strength values. 81 | func hashPasswordWithSalt(pass string, salt []byte, strength int) (b []byte, err error) { 82 | salty := append([]byte(pass), salt...) 83 | b, err = bcrypt.GenerateFromPassword(salty, strength) 84 | return 85 | } 86 | 87 | // Creates a new salt of the given byte size. 88 | // 89 | // The smallest supported salt length is 16 bytes, any shorter request will be 90 | // 16 bytes long. 91 | func newSalt(size int) (b []byte, err error) { 92 | if size < 16 { 93 | size = 16 94 | } 95 | b = make([]byte, size) 96 | var n int 97 | n, err = rand.Read(b) 98 | if err != nil { 99 | return 100 | } else if n != size { 101 | err = fmt.Errorf("salt generation: crypto/rand only read %d of %d bytes", n, size) 102 | return 103 | } 104 | return 105 | } 106 | 107 | // Uses time constant comparison to determine if a password and salt are equal 108 | // to the hash. 109 | func passEquals(pass string, salt, hash []byte) bool { 110 | salty := append([]byte(pass), salt...) 111 | err := bcrypt.CompareHashAndPassword(hash, salty) 112 | return err == nil 113 | } 114 | -------------------------------------------------------------------------------- /services/delivery_attempts.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | "time" 23 | 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/paths" 26 | "github.com/go-fed/apcore/util" 27 | ) 28 | 29 | type DeliveryAttempts struct { 30 | DB *sql.DB 31 | DeliveryAttempts *models.DeliveryAttempts 32 | } 33 | 34 | func (d *DeliveryAttempts) InsertAttempt(c util.Context, from paths.UUID, toActor *url.URL, payload []byte) (id string, err error) { 35 | return id, doInTx(c, d.DB, func(tx *sql.Tx) error { 36 | id, err = d.DeliveryAttempts.Create(c, tx, string(from), toActor, payload) 37 | return err 38 | }) 39 | } 40 | 41 | func (d *DeliveryAttempts) MarkSuccessfulAttempt(c util.Context, id string) (err error) { 42 | return doInTx(c, d.DB, func(tx *sql.Tx) error { 43 | return d.DeliveryAttempts.MarkSuccessful(c, tx, id) 44 | }) 45 | } 46 | 47 | func (d *DeliveryAttempts) MarkRetryFailureAttempt(c util.Context, id string) (err error) { 48 | return doInTx(c, d.DB, func(tx *sql.Tx) error { 49 | return d.DeliveryAttempts.MarkFailed(c, tx, id) 50 | }) 51 | } 52 | 53 | func (d *DeliveryAttempts) MarkAbandonedAttempt(c util.Context, id string) (err error) { 54 | return doInTx(c, d.DB, func(tx *sql.Tx) error { 55 | return d.DeliveryAttempts.MarkAbandoned(c, tx, id) 56 | }) 57 | } 58 | 59 | type RetryableFailure struct { 60 | ID string 61 | UserID string 62 | FetchTime time.Time 63 | DeliverTo *url.URL 64 | Payload []byte 65 | NAttempts int 66 | LastAttempt time.Time 67 | } 68 | 69 | func (d *DeliveryAttempts) FirstPageRetryableFailures(c util.Context, n int) (rf []RetryableFailure, err error) { 70 | now := time.Now() 71 | err = doInTx(c, d.DB, func(tx *sql.Tx) error { 72 | f, err := d.DeliveryAttempts.FirstPageFailures(c, tx, now, n) 73 | if err != nil { 74 | return err 75 | } 76 | for _, a := range f { 77 | r := RetryableFailure{ 78 | ID: a.ID, 79 | UserID: a.UserID, 80 | FetchTime: now, 81 | DeliverTo: a.DeliverTo.URL, 82 | Payload: a.Payload, 83 | NAttempts: a.NAttempts, 84 | LastAttempt: a.LastAttempt, 85 | } 86 | rf = append(rf, r) 87 | } 88 | return nil 89 | }) 90 | return 91 | } 92 | 93 | func (d *DeliveryAttempts) NextPageRetryableFailures(c util.Context, prevID string, fetch time.Time, n int) (rf []RetryableFailure, err error) { 94 | err = doInTx(c, d.DB, func(tx *sql.Tx) error { 95 | f, err := d.DeliveryAttempts.NextPageFailures(c, tx, prevID, fetch, n) 96 | if err != nil { 97 | return err 98 | } 99 | for _, a := range f { 100 | r := RetryableFailure{ 101 | ID: a.ID, 102 | UserID: a.UserID, 103 | FetchTime: fetch, 104 | DeliverTo: a.DeliverTo.URL, 105 | Payload: a.Payload, 106 | NAttempts: a.NAttempts, 107 | LastAttempt: a.LastAttempt, 108 | } 109 | rf = append(rf, r) 110 | } 111 | return nil 112 | }) 113 | return 114 | } 115 | -------------------------------------------------------------------------------- /services/followers.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Followers struct { 29 | DB *sql.DB 30 | Followers *models.Followers 31 | } 32 | 33 | func (f *Followers) ContainsForActor(c util.Context, actor, id *url.URL) (has bool, err error) { 34 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 35 | has, err = f.Followers.ContainsForActor(c, tx, actor, id) 36 | return err 37 | }) 38 | } 39 | 40 | func (f *Followers) Contains(c util.Context, followers, id *url.URL) (has bool, err error) { 41 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 42 | has, err = f.Followers.Contains(c, tx, followers, id) 43 | return err 44 | }) 45 | } 46 | 47 | func (f *Followers) GetPage(c util.Context, followers *url.URL, min, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 48 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 49 | var isEnd bool 50 | var mp models.ActivityStreamsCollectionPage 51 | mp, isEnd, err = f.Followers.GetPage(c, tx, followers, min, min+n) 52 | if err != nil { 53 | return err 54 | } 55 | page = mp.ActivityStreamsCollectionPage 56 | return addNextPrevCol(page, min, n, isEnd) 57 | }) 58 | return 59 | } 60 | 61 | func (f *Followers) GetLastPage(c util.Context, followers *url.URL, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 62 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 63 | var startIdx int 64 | var mp models.ActivityStreamsCollectionPage 65 | mp, startIdx, err = f.Followers.GetLastPage(c, tx, followers, n) 66 | if err != nil { 67 | return err 68 | } 69 | page = mp.ActivityStreamsCollectionPage 70 | return addNextPrevCol(page, startIdx, n, true) 71 | }) 72 | return 73 | } 74 | 75 | func (f *Followers) PrependItem(c util.Context, followers, item *url.URL) error { 76 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 77 | return f.Followers.PrependItem(c, tx, followers, item) 78 | }) 79 | } 80 | 81 | func (f *Followers) DeleteItem(c util.Context, followers, item *url.URL) error { 82 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 83 | return f.Followers.DeleteItem(c, tx, followers, item) 84 | }) 85 | } 86 | 87 | func (f *Followers) GetAllForActor(c util.Context, actor *url.URL) (col vocab.ActivityStreamsCollection, err error) { 88 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 89 | var mc models.ActivityStreamsCollection 90 | mc, err = f.Followers.GetAllForActor(c, tx, actor) 91 | if err != nil { 92 | return err 93 | } 94 | col = mc.ActivityStreamsCollection 95 | return err 96 | }) 97 | return 98 | } 99 | 100 | func (f *Followers) OpenFollowRequests(c util.Context, actorIRI *url.URL) (r []vocab.ActivityStreamsFollow, err error) { 101 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 102 | var fs []models.ActivityStreamsFollow 103 | fs, err = f.Followers.OpenFollowRequests(c, tx, actorIRI) 104 | if err != nil { 105 | return err 106 | } 107 | for _, v := range fs { 108 | r = append(r, v.ActivityStreamsFollow) 109 | } 110 | return err 111 | }) 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /services/following.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Following struct { 29 | DB *sql.DB 30 | Following *models.Following 31 | } 32 | 33 | func (f *Following) ContainsForActor(c util.Context, actor, id *url.URL) (has bool, err error) { 34 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 35 | has, err = f.Following.ContainsForActor(c, tx, actor, id) 36 | return err 37 | }) 38 | } 39 | 40 | func (f *Following) Contains(c util.Context, following, id *url.URL) (has bool, err error) { 41 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 42 | has, err = f.Following.Contains(c, tx, following, id) 43 | return err 44 | }) 45 | } 46 | 47 | func (f *Following) GetPage(c util.Context, following *url.URL, min, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 48 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 49 | var isEnd bool 50 | var mp models.ActivityStreamsCollectionPage 51 | mp, isEnd, err = f.Following.GetPage(c, tx, following, min, min+n) 52 | if err != nil { 53 | return err 54 | } 55 | page = mp.ActivityStreamsCollectionPage 56 | return addNextPrevCol(page, min, n, isEnd) 57 | }) 58 | return 59 | } 60 | 61 | func (f *Following) GetLastPage(c util.Context, following *url.URL, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 62 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 63 | var startIdx int 64 | var mp models.ActivityStreamsCollectionPage 65 | mp, startIdx, err = f.Following.GetLastPage(c, tx, following, n) 66 | if err != nil { 67 | return err 68 | } 69 | page = mp.ActivityStreamsCollectionPage 70 | return addNextPrevCol(page, startIdx, n, true) 71 | }) 72 | return 73 | } 74 | 75 | func (f *Following) PrependItem(c util.Context, following, item *url.URL) error { 76 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 77 | return f.Following.PrependItem(c, tx, following, item) 78 | }) 79 | } 80 | 81 | func (f *Following) DeleteItem(c util.Context, following, item *url.URL) error { 82 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 83 | return f.Following.DeleteItem(c, tx, following, item) 84 | }) 85 | } 86 | 87 | func (f *Following) GetAllForActor(c util.Context, actor *url.URL) (col vocab.ActivityStreamsCollection, err error) { 88 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 89 | var mc models.ActivityStreamsCollection 90 | mc, err = f.Following.GetAllForActor(c, tx, actor) 91 | if err != nil { 92 | return err 93 | } 94 | col = mc.ActivityStreamsCollection 95 | return err 96 | }) 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /services/inboxes.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Inboxes struct { 29 | DB *sql.DB 30 | Inboxes *models.Inboxes 31 | } 32 | 33 | func (i *Inboxes) GetPage(c util.Context, inbox *url.URL, min, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 34 | err = doInTx(c, i.DB, func(tx *sql.Tx) error { 35 | var isEnd bool 36 | var mp models.ActivityStreamsOrderedCollectionPage 37 | mp, isEnd, err = i.Inboxes.GetPage(c, tx, inbox, min, min+n) 38 | if err != nil { 39 | return err 40 | } 41 | page = mp.ActivityStreamsOrderedCollectionPage 42 | return addNextPrev(page, min, n, isEnd) 43 | }) 44 | return 45 | } 46 | 47 | func (i *Inboxes) GetPublicPage(c util.Context, inbox *url.URL, min, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 48 | err = doInTx(c, i.DB, func(tx *sql.Tx) error { 49 | var isEnd bool 50 | var mp models.ActivityStreamsOrderedCollectionPage 51 | mp, isEnd, err = i.Inboxes.GetPublicPage(c, tx, inbox, min, min+n) 52 | if err != nil { 53 | return err 54 | } 55 | page = mp.ActivityStreamsOrderedCollectionPage 56 | return addNextPrev(page, min, n, isEnd) 57 | }) 58 | return 59 | } 60 | 61 | func (i *Inboxes) GetLastPage(c util.Context, inbox *url.URL, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 62 | err = doInTx(c, i.DB, func(tx *sql.Tx) error { 63 | var startIdx int 64 | var mp models.ActivityStreamsOrderedCollectionPage 65 | mp, startIdx, err = i.Inboxes.GetLastPage(c, tx, inbox, n) 66 | if err != nil { 67 | return err 68 | } 69 | page = mp.ActivityStreamsOrderedCollectionPage 70 | return addNextPrev(page, startIdx, n, true) 71 | }) 72 | return 73 | } 74 | 75 | func (i *Inboxes) GetPublicLastPage(c util.Context, inbox *url.URL, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 76 | err = doInTx(c, i.DB, func(tx *sql.Tx) error { 77 | var startIdx int 78 | var mp models.ActivityStreamsOrderedCollectionPage 79 | mp, startIdx, err = i.Inboxes.GetPublicLastPage(c, tx, inbox, n) 80 | if err != nil { 81 | return err 82 | } 83 | page = mp.ActivityStreamsOrderedCollectionPage 84 | return addNextPrev(page, startIdx, n, true) 85 | }) 86 | return 87 | } 88 | 89 | func (i *Inboxes) ContainsForActor(c util.Context, actor, id *url.URL) (has bool, err error) { 90 | return has, doInTx(c, i.DB, func(tx *sql.Tx) error { 91 | has, err = i.Inboxes.ContainsForActor(c, tx, actor, id) 92 | return err 93 | }) 94 | } 95 | 96 | func (i *Inboxes) Contains(c util.Context, inbox, id *url.URL) (has bool, err error) { 97 | return has, doInTx(c, i.DB, func(tx *sql.Tx) error { 98 | has, err = i.Inboxes.Contains(c, tx, inbox, id) 99 | return err 100 | }) 101 | } 102 | 103 | func (i *Inboxes) PrependItem(c util.Context, inbox, item *url.URL) error { 104 | return doInTx(c, i.DB, func(tx *sql.Tx) error { 105 | return i.Inboxes.PrependInboxItem(c, tx, inbox, item) 106 | }) 107 | } 108 | 109 | func (i *Inboxes) DeleteItem(c util.Context, inbox, item *url.URL) error { 110 | return doInTx(c, i.DB, func(tx *sql.Tx) error { 111 | return i.Inboxes.DeleteInboxItem(c, tx, inbox, item) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /services/liked.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Liked struct { 29 | DB *sql.DB 30 | Liked *models.Liked 31 | } 32 | 33 | func (f *Liked) ContainsForActor(c util.Context, actor, id *url.URL) (has bool, err error) { 34 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 35 | has, err = f.Liked.ContainsForActor(c, tx, actor, id) 36 | return err 37 | }) 38 | } 39 | 40 | func (f *Liked) Contains(c util.Context, liked, id *url.URL) (has bool, err error) { 41 | return has, doInTx(c, f.DB, func(tx *sql.Tx) error { 42 | has, err = f.Liked.Contains(c, tx, liked, id) 43 | return err 44 | }) 45 | } 46 | 47 | func (f *Liked) GetPage(c util.Context, liked *url.URL, min, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 48 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 49 | var isEnd bool 50 | var mp models.ActivityStreamsCollectionPage 51 | mp, isEnd, err = f.Liked.GetPage(c, tx, liked, min, min+n) 52 | if err != nil { 53 | return err 54 | } 55 | page = mp.ActivityStreamsCollectionPage 56 | return addNextPrevCol(page, min, n, isEnd) 57 | }) 58 | return 59 | } 60 | 61 | func (f *Liked) GetLastPage(c util.Context, liked *url.URL, n int) (page vocab.ActivityStreamsCollectionPage, err error) { 62 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 63 | var startIdx int 64 | var mp models.ActivityStreamsCollectionPage 65 | mp, startIdx, err = f.Liked.GetLastPage(c, tx, liked, n) 66 | if err != nil { 67 | return err 68 | } 69 | page = mp.ActivityStreamsCollectionPage 70 | return addNextPrevCol(page, startIdx, n, true) 71 | }) 72 | return 73 | } 74 | 75 | func (f *Liked) PrependItem(c util.Context, liked, item *url.URL) error { 76 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 77 | return f.Liked.PrependItem(c, tx, liked, item) 78 | }) 79 | } 80 | 81 | func (f *Liked) DeleteItem(c util.Context, liked, item *url.URL) error { 82 | return doInTx(c, f.DB, func(tx *sql.Tx) error { 83 | return f.Liked.DeleteItem(c, tx, liked, item) 84 | }) 85 | } 86 | 87 | func (f *Liked) GetAllForActor(c util.Context, actor *url.URL) (col vocab.ActivityStreamsCollection, err error) { 88 | err = doInTx(c, f.DB, func(tx *sql.Tx) error { 89 | var mc models.ActivityStreamsCollection 90 | mc, err = f.Liked.GetAllForActor(c, tx, actor) 91 | if err != nil { 92 | return err 93 | } 94 | col = mc.ActivityStreamsCollection 95 | return err 96 | }) 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /services/nodeinfo.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "encoding/json" 22 | "math" 23 | "math/rand" 24 | "sync" 25 | "time" 26 | 27 | "github.com/go-fed/activity/pub" 28 | "github.com/go-fed/apcore/models" 29 | "github.com/go-fed/apcore/util" 30 | ) 31 | 32 | type NodeInfoStats struct { 33 | TotalUsers int 34 | ActiveHalfYear int 35 | ActiveMonth int 36 | ActiveWeek int 37 | NLocalPosts int 38 | NLocalComments int 39 | } 40 | 41 | type ServerPreferences struct { 42 | OnFollow pub.OnFollowBehavior 43 | OpenRegistrations bool 44 | ServerBaseURL string 45 | ServerName string 46 | OrgName string 47 | OrgContact string 48 | OrgAccount string 49 | Payload json.RawMessage 50 | } 51 | 52 | type NodeInfo struct { 53 | DB *sql.DB 54 | Users *models.Users 55 | LocalData *models.LocalData 56 | Rand *rand.Rand 57 | mu sync.RWMutex 58 | CacheInvalidated time.Duration 59 | cache NodeInfoStats 60 | cacheSet bool 61 | cacheWhen time.Time 62 | } 63 | 64 | func (n *NodeInfo) GetAnonymizedStats(c util.Context) (t NodeInfoStats, err error) { 65 | // Cache-hit 66 | n.mu.RLock() 67 | if t, ok := n.getCachedAnonymizedStats(); ok { 68 | n.mu.RUnlock() 69 | return t, nil 70 | } 71 | n.mu.RUnlock() 72 | // Cache-miss... 73 | n.mu.Lock() 74 | defer n.mu.Unlock() 75 | // ... but another goroutine may have refreshed ... 76 | if t, ok := n.getCachedAnonymizedStats(); ok { 77 | return t, nil 78 | } 79 | // ... or we are the one to refresh it. 80 | var uas models.UserActivityStats 81 | var lda models.LocalDataActivity 82 | if err = doInTx(c, n.DB, func(tx *sql.Tx) error { 83 | uas, err = n.Users.ActivityStats(c, tx) 84 | if err != nil { 85 | return err 86 | } 87 | lda, err = n.LocalData.Stats(c, tx) 88 | if err != nil { 89 | return err 90 | } 91 | return nil 92 | }); err != nil { 93 | return 94 | } 95 | now := time.Now() 96 | t = NodeInfoStats{ 97 | TotalUsers: uas.TotalUsers, 98 | ActiveHalfYear: uas.ActiveHalfYear, 99 | ActiveMonth: uas.ActiveMonth, 100 | ActiveWeek: uas.ActiveWeek, 101 | NLocalPosts: lda.NLocalPosts, 102 | NLocalComments: lda.NLocalComments, 103 | } 104 | n.applyNoise(&t) 105 | n.setCachedAnonymizedStats(t, now) 106 | return 107 | } 108 | 109 | // applyNoise ensures that the NodeInfoStats for small instances contains some 110 | // noise around the true value, so that ballpark-correct statistics can be 111 | // obtained from small instances without allowing peers to monitor changes over 112 | // time in number of users or user login activity for the small instances. 113 | // 114 | // The mutex must be locked. 115 | func (n *NodeInfo) applyNoise(t *NodeInfoStats) { 116 | const ( 117 | uSDev = 2.0 118 | vSDev = 1.0 119 | ) 120 | t.TotalUsers = n.maybeGetWithUncertainty(t.TotalUsers, uSDev, vSDev, -1) 121 | t.ActiveHalfYear = n.maybeGetWithUncertainty(t.ActiveHalfYear, uSDev, vSDev, t.TotalUsers) 122 | t.ActiveMonth = n.maybeGetWithUncertainty(t.ActiveMonth, uSDev, vSDev, t.TotalUsers) 123 | t.ActiveWeek = n.maybeGetWithUncertainty(t.ActiveWeek, uSDev, vSDev, t.TotalUsers) 124 | } 125 | 126 | // maybeGetWithUncertainty applies noise to counts that do not meet the 127 | // threshold, to ensure privacy. 128 | // 129 | // The mutex must be locked. 130 | func (n *NodeInfo) maybeGetWithUncertainty(v int, s1, s2 float64, max int) int { 131 | const ( 132 | threshold = 50 133 | ) 134 | if v >= threshold { 135 | return v 136 | } 137 | return n.getWithUncertainty(v, s1, s2, max) 138 | } 139 | 140 | // getWithUncertainty determines a random value using uncertainty in the mean 141 | // and rejection sampling from [0, max]. Max is ignored if <= 0. 142 | // 143 | // The mutex must be locked. 144 | func (n *NodeInfo) getWithUncertainty(v int, s1, s2 float64, max int) int { 145 | i := -1 146 | for i < 0 && (max <= 0 || i < max) { 147 | mu := n.Rand.NormFloat64()*s1 + float64(v) 148 | val := n.Rand.NormFloat64()*s2 + mu 149 | i = int(math.Round(val)) 150 | } 151 | return i 152 | } 153 | 154 | // getCachedAnonymizedStats ensures that any stats computed and anonymized with 155 | // noise is not recomputed frequently. Too frequent samples allows guessing the 156 | // true mean, within a uSDev value. 157 | // 158 | // The mutex must be locked. 159 | func (n *NodeInfo) getCachedAnonymizedStats() (t NodeInfoStats, ok bool) { 160 | now := time.Now() 161 | ok = n.cacheSet && now.Sub(n.cacheWhen) < n.CacheInvalidated 162 | if ok { 163 | t = n.cache 164 | } 165 | return 166 | } 167 | 168 | // setCachedAnonymizedStats saves anonymized statistics. 169 | // 170 | // The mutex must be locked. 171 | func (n *NodeInfo) setCachedAnonymizedStats(t NodeInfoStats, m time.Time) { 172 | n.cache = t 173 | n.cacheSet = true 174 | n.cacheWhen = m 175 | } 176 | -------------------------------------------------------------------------------- /services/oauth2.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | 23 | "github.com/go-fed/apcore/models" 24 | "github.com/go-fed/apcore/util" 25 | "github.com/go-fed/oauth2" 26 | ) 27 | 28 | var _ oauth2.ClientStore = &OAuth2{} 29 | var _ oauth2.TokenStore = &OAuth2{} 30 | 31 | // OAuth2 implements services for the oauth2 server package. 32 | type OAuth2 struct { 33 | DB *sql.DB 34 | Client *models.ClientInfos 35 | Token *models.TokenInfos 36 | Creds *models.Credentials 37 | } 38 | 39 | func (o *OAuth2) GetByID(ctx context.Context, id string) (ci oauth2.ClientInfo, err error) { 40 | c := util.Context{ctx} 41 | return ci, doInTx(c, o.DB, func(tx *sql.Tx) error { 42 | ci, err = o.Client.GetByID(c, tx, id) 43 | return err 44 | }) 45 | } 46 | 47 | func (o *OAuth2) Create(ctx context.Context, info oauth2.TokenInfo) error { 48 | c := util.Context{ctx} 49 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 50 | _, err := o.Token.Create(c, tx, info) 51 | return err 52 | }) 53 | } 54 | 55 | func (o *OAuth2) RemoveByCode(ctx context.Context, code string) error { 56 | c := util.Context{ctx} 57 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 58 | return o.Token.RemoveByCode(c, tx, code) 59 | }) 60 | } 61 | 62 | func (o *OAuth2) RemoveByAccess(ctx context.Context, access string) error { 63 | c := util.Context{ctx} 64 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 65 | return o.Token.RemoveByAccess(c, tx, access) 66 | }) 67 | } 68 | 69 | func (o *OAuth2) RemoveByRefresh(ctx context.Context, refresh string) error { 70 | c := util.Context{ctx} 71 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 72 | return o.Token.RemoveByRefresh(c, tx, refresh) 73 | }) 74 | } 75 | 76 | func (o *OAuth2) GetByCode(ctx context.Context, code string) (ti oauth2.TokenInfo, err error) { 77 | c := util.Context{ctx} 78 | return ti, doInTx(c, o.DB, func(tx *sql.Tx) error { 79 | ti, err = o.Token.GetByCode(c, tx, code) 80 | return err 81 | }) 82 | } 83 | 84 | func (o *OAuth2) GetByAccess(ctx context.Context, access string) (ti oauth2.TokenInfo, err error) { 85 | c := util.Context{ctx} 86 | return ti, doInTx(c, o.DB, func(tx *sql.Tx) error { 87 | ti, err = o.Token.GetByAccess(c, tx, access) 88 | return err 89 | }) 90 | } 91 | 92 | func (o *OAuth2) GetByRefresh(ctx context.Context, refresh string) (ti oauth2.TokenInfo, err error) { 93 | c := util.Context{ctx} 94 | return ti, doInTx(c, o.DB, func(tx *sql.Tx) error { 95 | ti, err = o.Token.GetByRefresh(c, tx, refresh) 96 | return err 97 | }) 98 | } 99 | 100 | func (o *OAuth2) ProxyCreateCredential(ctx context.Context, ti oauth2.TokenInfo) (id string, err error) { 101 | c := util.Context{ctx} 102 | return id, doInTx(c, o.DB, func(tx *sql.Tx) error { 103 | ci := &models.ClientInfo{ 104 | ID: ti.GetClientID(), 105 | Domain: ti.GetRedirectURI(), 106 | UserID: ti.GetUserID(), 107 | } 108 | _, err := o.Client.Create(c, tx, ci) 109 | if err != nil { 110 | return err 111 | } 112 | tID, err := o.Token.Create(c, tx, ti) 113 | if err != nil { 114 | return err 115 | } 116 | id, err = o.Creds.Create(c, tx, ti.GetUserID(), tID, ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn())) 117 | return err 118 | }) 119 | } 120 | 121 | func (o *OAuth2) ProxyUpdateCredential(ctx context.Context, id string, ti oauth2.TokenInfo) error { 122 | c := util.Context{ctx} 123 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 124 | err := o.Creds.Update(c, tx, id, ti) 125 | if err != nil { 126 | return err 127 | } 128 | return o.Creds.UpdateExpires(c, tx, id, ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn())) 129 | }) 130 | } 131 | 132 | func (o *OAuth2) ProxyRemoveCredential(ctx context.Context, id string) error { 133 | c := util.Context{ctx} 134 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 135 | return o.Creds.Delete(c, tx, id) 136 | }) 137 | } 138 | 139 | func (o *OAuth2) ProxyGetCredential(ctx context.Context, id string) (ti oauth2.TokenInfo, err error) { 140 | c := util.Context{ctx} 141 | return ti, doInTx(c, o.DB, func(tx *sql.Tx) error { 142 | ti, err = o.Creds.GetTokenInfo(c, tx, id) 143 | return err 144 | }) 145 | } 146 | 147 | func (o *OAuth2) DeleteExpiredFirstPartyCredentials(ctx context.Context) error { 148 | c := util.Context{ctx} 149 | return doInTx(c, o.DB, func(tx *sql.Tx) error { 150 | return o.Creds.DeleteExpired(c, tx) 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /services/outboxes.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Outboxes struct { 29 | DB *sql.DB 30 | Outboxes *models.Outboxes 31 | } 32 | 33 | func (i *Outboxes) GetPage(c util.Context, outbox *url.URL, min, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 34 | return page, doInTx(c, i.DB, func(tx *sql.Tx) error { 35 | var isEnd bool 36 | var mp models.ActivityStreamsOrderedCollectionPage 37 | mp, isEnd, err = i.Outboxes.GetPage(c, tx, outbox, min, min+n) 38 | if err != nil { 39 | return err 40 | } 41 | page = mp.ActivityStreamsOrderedCollectionPage 42 | return addNextPrev(page, min, n, isEnd) 43 | }) 44 | } 45 | 46 | func (i *Outboxes) GetPublicPage(c util.Context, outbox *url.URL, min, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 47 | return page, doInTx(c, i.DB, func(tx *sql.Tx) error { 48 | var isEnd bool 49 | var mp models.ActivityStreamsOrderedCollectionPage 50 | mp, isEnd, err = i.Outboxes.GetPublicPage(c, tx, outbox, min, min+n) 51 | if err != nil { 52 | return err 53 | } 54 | page = mp.ActivityStreamsOrderedCollectionPage 55 | return addNextPrev(page, min, n, isEnd) 56 | }) 57 | } 58 | 59 | func (i *Outboxes) GetLastPage(c util.Context, outbox *url.URL, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 60 | return page, doInTx(c, i.DB, func(tx *sql.Tx) error { 61 | var startIdx int 62 | var mp models.ActivityStreamsOrderedCollectionPage 63 | mp, startIdx, err = i.Outboxes.GetLastPage(c, tx, outbox, n) 64 | if err != nil { 65 | return err 66 | } 67 | page = mp.ActivityStreamsOrderedCollectionPage 68 | return addNextPrev(page, startIdx, n, true) 69 | }) 70 | } 71 | 72 | func (i *Outboxes) GetPublicLastPage(c util.Context, outbox *url.URL, n int) (page vocab.ActivityStreamsOrderedCollectionPage, err error) { 73 | return page, doInTx(c, i.DB, func(tx *sql.Tx) error { 74 | var startIdx int 75 | var mp models.ActivityStreamsOrderedCollectionPage 76 | mp, startIdx, err = i.Outboxes.GetPublicLastPage(c, tx, outbox, n) 77 | if err != nil { 78 | return err 79 | } 80 | page = mp.ActivityStreamsOrderedCollectionPage 81 | return addNextPrev(page, startIdx, n, true) 82 | }) 83 | } 84 | 85 | func (i *Outboxes) OutboxForInbox(c util.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) { 86 | return outboxIRI, doInTx(c, i.DB, func(tx *sql.Tx) error { 87 | var ob models.URL 88 | ob, err = i.Outboxes.OutboxForInbox(c, tx, inboxIRI) 89 | if err != nil { 90 | return err 91 | } 92 | outboxIRI = ob.URL 93 | return nil 94 | }) 95 | } 96 | 97 | func (i *Outboxes) PrependItem(c util.Context, outbox, item *url.URL) error { 98 | return doInTx(c, i.DB, func(tx *sql.Tx) error { 99 | return i.Outboxes.PrependOutboxItem(c, tx, outbox, item) 100 | }) 101 | } 102 | 103 | func (i *Outboxes) DeleteItem(c util.Context, outbox, item *url.URL) error { 104 | return doInTx(c, i.DB, func(tx *sql.Tx) error { 105 | return i.Outboxes.DeleteOutboxItem(c, tx, outbox, item) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /services/pagination.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "net/url" 21 | 22 | "github.com/go-fed/activity/pub" 23 | "github.com/go-fed/activity/streams/vocab" 24 | "github.com/go-fed/apcore/paths" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | func getOffsetN(iri *url.URL, defaultSize, maxSize int) (offset, n int) { 29 | offset, n = 0, defaultSize 30 | if paths.IsGetCollectionPage(iri) { 31 | offset = paths.GetOffsetOrDefault(iri, 0) 32 | n = paths.GetNumOrDefault(iri, defaultSize, maxSize) 33 | } 34 | return 35 | } 36 | 37 | // AnyOCPageFn fetches any arbitrary OrderedCollectionPage 38 | type AnyOCPageFn func(c util.Context, iri *url.URL, min, n int) (vocab.ActivityStreamsOrderedCollectionPage, error) 39 | 40 | // LastOCPageFn fetches the last page of an OrderedCollection. 41 | type LastOCPageFn func(c util.Context, iri *url.URL, n int) (vocab.ActivityStreamsOrderedCollectionPage, error) 42 | 43 | // DoPagination examines the query parameters of an IRI, and uses it to either 44 | // fetch the bare ordered collection without values, the very last ordered 45 | // collection page, or an arbitrary ordered collection page using the provided 46 | // fetching functions. 47 | func DoOrderedCollectionPagination(c util.Context, iri *url.URL, defaultSize, maxSize int, any AnyOCPageFn, last LastOCPageFn) (p vocab.ActivityStreamsOrderedCollectionPage, err error) { 48 | if paths.IsGetCollectionPage(iri) && paths.IsGetCollectionEnd(iri) { 49 | // The last page was requested 50 | n := paths.GetNumOrDefault(iri, defaultSize, maxSize) 51 | p, err = last(c, paths.Normalize(iri), n) 52 | return 53 | } else { 54 | // The first page, or an arbitrary page, was requested 55 | offset, n := getOffsetN(iri, defaultSize, maxSize) 56 | p, err = any(c, paths.Normalize(iri), offset, n) 57 | return 58 | } 59 | } 60 | 61 | // AnyCPageFn fetches any arbitrary CollectionPage 62 | type AnyCPageFn func(c util.Context, iri *url.URL, min, n int) (vocab.ActivityStreamsCollectionPage, error) 63 | 64 | // LastCPageFn fetches the last page of an Collection. 65 | type LastCPageFn func(c util.Context, iri *url.URL, n int) (vocab.ActivityStreamsCollectionPage, error) 66 | 67 | // DoCollectionPagination examines the query parameters of an IRI, and uses it 68 | // to either fetch the bare ordered collection without values, the very last 69 | // ordered collection page, or an arbitrary ordered collection page using the 70 | // provided fetching functions. 71 | func DoCollectionPagination(c util.Context, iri *url.URL, defaultSize, maxSize int, any AnyCPageFn, last LastCPageFn) (p vocab.ActivityStreamsCollectionPage, err error) { 72 | if paths.IsGetCollectionPage(iri) && paths.IsGetCollectionEnd(iri) { 73 | // The last page was requested 74 | n := paths.GetNumOrDefault(iri, defaultSize, maxSize) 75 | p, err = last(c, paths.Normalize(iri), n) 76 | return 77 | } else { 78 | // The first page, or an arbitrary page, was requested 79 | offset, n := getOffsetN(iri, defaultSize, maxSize) 80 | p, err = any(c, paths.Normalize(iri), offset, n) 81 | return 82 | } 83 | } 84 | 85 | // PrependFn are functions that prepend items to a collection. 86 | type PrependFn func(c util.Context, collectionID, item *url.URL) error 87 | 88 | // UpdateCollectionToPrependCalls takes new beginning elements of a collection 89 | // in order to generate calls to PrependFn in order. 90 | // 91 | // This function only prepends to the very beginning of the collection, and 92 | // expects the page to be the first one, though it is written as if for the 93 | // general case. 94 | // 95 | // TODO: Could generalize this to apply a diff to a portion of the collection 96 | func UpdateCollectionToPrependCalls(c util.Context, updated vocab.ActivityStreamsCollection, defaultSize, maxSize int, firstPageFn AnyCPageFn, prependFn PrependFn) error { 97 | iri, err := pub.GetId(updated) 98 | if err != nil { 99 | return err 100 | } 101 | // Get the updated items -- early out if none. 102 | newItems := updated.GetActivityStreamsItems() 103 | if newItems == nil || newItems.Len() == 0 { 104 | return nil 105 | } 106 | // Obtain the same number as the pre-updated ID 107 | offset, n := getOffsetN(iri, defaultSize, maxSize) 108 | original, err := firstPageFn(c, paths.Normalize(iri), offset, n) 109 | if err != nil { 110 | return err 111 | } 112 | // Call Prepend for items that come before the first element. 113 | var firstIRI *url.URL 114 | if items := original.GetActivityStreamsItems(); items != nil && items.Len() > 0 { 115 | firstIRI, err = pub.ToId(items.At(0)) 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | found := firstIRI == nil // If firstIRI is nil, add everything 121 | for i := newItems.Len() - 1; i >= 0; i-- { 122 | elemID, err := pub.ToId(newItems.At(i)) 123 | if err != nil { 124 | return err 125 | } 126 | if found { 127 | // We already found the matching formerly-first 128 | // element, so prepend the rest. 129 | if err = prependFn(c, iri, elemID); err != nil { 130 | return err 131 | } 132 | } else if elemID.String() == firstIRI.String() { 133 | found = true 134 | } 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /services/policies.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | "net/url" 22 | 23 | "github.com/go-fed/activity/pub" 24 | "github.com/go-fed/apcore/models" 25 | "github.com/go-fed/apcore/util" 26 | ) 27 | 28 | type Policies struct { 29 | Clock pub.Clock 30 | DB *sql.DB 31 | Policies *models.Policies 32 | Resolutions *models.Resolutions 33 | } 34 | 35 | func (p *Policies) IsBlocked(c util.Context, actorID *url.URL, a pub.Activity) (blocked bool, err error) { 36 | var iri *url.URL 37 | iri, err = pub.GetId(a) 38 | if err != nil { 39 | return 40 | } 41 | var jsonb []byte 42 | jsonb, err = models.Marshal(a) 43 | if err != nil { 44 | return 45 | } 46 | err = doInTx(c, p.DB, func(tx *sql.Tx) error { 47 | pd, err := p.Policies.GetForActorAndPurpose(c, tx, actorID, models.FederatedBlockPurpose) 48 | if err != nil { 49 | return err 50 | } 51 | for _, policy := range pd { 52 | var res models.Resolution 53 | res.Time = p.Clock.Now() 54 | err = policy.Policy.Resolve(jsonb, &res) 55 | if err != nil { 56 | return err 57 | } 58 | err = p.Resolutions.Create(c, tx, models.CreateResolution{ 59 | PolicyID: policy.ID, 60 | IRI: iri, 61 | R: res, 62 | }) 63 | if err != nil { 64 | return err 65 | } 66 | blocked = blocked || res.Matched 67 | } 68 | return nil 69 | }) 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /services/private_keys.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2020 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "crypto" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "crypto/x509" 24 | "database/sql" 25 | "encoding/pem" 26 | "errors" 27 | "fmt" 28 | "io/ioutil" 29 | "net/url" 30 | "os" 31 | 32 | "github.com/go-fed/apcore/models" 33 | "github.com/go-fed/apcore/paths" 34 | "github.com/go-fed/apcore/util" 35 | ) 36 | 37 | const ( 38 | minKeySize = 1024 39 | ) 40 | 41 | const ( 42 | pKeyHttpSigPurpose = "http-signature" 43 | ) 44 | 45 | type PrivateKeys struct { 46 | Scheme string 47 | Host string 48 | DB *sql.DB 49 | PrivateKeys *models.PrivateKeys 50 | } 51 | 52 | func (p *PrivateKeys) GetUserHTTPSignatureKey(c util.Context, userID paths.UUID) (k *rsa.PrivateKey, iri *url.URL, err error) { 53 | var kb []byte 54 | err = doInTx(c, p.DB, func(tx *sql.Tx) error { 55 | kb, err = p.PrivateKeys.GetByUserID(c, tx, string(userID), pKeyHttpSigPurpose) 56 | return err 57 | }) 58 | if err != nil { 59 | return 60 | } 61 | var pk crypto.PrivateKey 62 | pk, err = deserializeRSAPrivateKey(kb) 63 | var ok bool 64 | k, ok = pk.(*rsa.PrivateKey) 65 | if !ok { 66 | err = errors.New("private key is not of type *rsa.PrivateKey") 67 | return 68 | } 69 | iri = paths.UUIDIRIFor(p.Scheme, p.Host, paths.HttpSigPubKeyKey, userID) 70 | return 71 | } 72 | 73 | func (p *PrivateKeys) GetUserHTTPSignatureKeyForInstanceActor(c util.Context) (k *rsa.PrivateKey, iri *url.URL, err error) { 74 | var kb []byte 75 | err = doInTx(c, p.DB, func(tx *sql.Tx) error { 76 | kb, err = p.PrivateKeys.GetInstanceActor(c, tx, pKeyHttpSigPurpose) 77 | return err 78 | }) 79 | if err != nil { 80 | return 81 | } 82 | var pk crypto.PrivateKey 83 | pk, err = deserializeRSAPrivateKey(kb) 84 | var ok bool 85 | k, ok = pk.(*rsa.PrivateKey) 86 | if !ok { 87 | err = errors.New("private key is not of type *rsa.PrivateKey") 88 | return 89 | } 90 | iri = paths.ActorIRIFor(p.Scheme, p.Host, paths.HttpSigPubKeyKey, paths.InstanceActor) 91 | return 92 | } 93 | 94 | // CreateKeyFile writes a symmetric key of random bytes to a file. 95 | func CreateKeyFile(file string) (err error) { 96 | c := 32 97 | k := make([]byte, c) 98 | var n int 99 | n, err = rand.Read(k) 100 | if err != nil { 101 | return 102 | } else if n != c { 103 | err = fmt.Errorf("crypto/rand read %d of %d bytes", n, c) 104 | return 105 | } 106 | err = ioutil.WriteFile(file, k, os.FileMode(0660)) 107 | return 108 | } 109 | 110 | // createandSerializeRSAKeys creates a new RSA Private key of a given size 111 | // and returns its PKCS8 encoded form and the public key's PEM form. 112 | func createAndSerializeRSAKeys(n int) (priv []byte, pub string, err error) { 113 | var k *rsa.PrivateKey 114 | k, err = createRSAPrivateKey(n) 115 | if err != nil { 116 | return 117 | } 118 | priv, err = serializeRSAPrivateKey(k) 119 | if err != nil { 120 | return 121 | } 122 | pub, err = marshalPublicKey(&(k.PublicKey)) 123 | return 124 | } 125 | 126 | // createRSAPrivateKey creates a new RSA Private key of a given size. 127 | // 128 | // Returns an error if the size is less than minKeySize. 129 | func createRSAPrivateKey(n int) (k *rsa.PrivateKey, err error) { 130 | if n < minKeySize { 131 | err = fmt.Errorf("Creating a key of size < %d is forbidden: %d", minKeySize, n) 132 | return 133 | } 134 | k, err = rsa.GenerateKey(rand.Reader, n) 135 | return 136 | } 137 | 138 | // marshalPublicKey encodes a public key into PEM format. 139 | func marshalPublicKey(p crypto.PublicKey) (string, error) { 140 | pkix, err := x509.MarshalPKIXPublicKey(p) 141 | if err != nil { 142 | return "", err 143 | } 144 | pb := pem.EncodeToMemory(&pem.Block{ 145 | Type: "PUBLIC KEY", 146 | Bytes: pkix, 147 | }) 148 | return string(pb), nil 149 | } 150 | 151 | // serializeRSAPrivateKey encodes a private key into PKCS8 format. 152 | func serializeRSAPrivateKey(k *rsa.PrivateKey) ([]byte, error) { 153 | return x509.MarshalPKCS8PrivateKey(k) 154 | } 155 | 156 | // deserializeRSAPrivateKey decodes a private key from PKCS8 format. 157 | func deserializeRSAPrivateKey(b []byte) (crypto.PrivateKey, error) { 158 | return x509.ParsePKCS8PrivateKey(b) 159 | } 160 | -------------------------------------------------------------------------------- /services/tx.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package services 18 | 19 | import ( 20 | "database/sql" 21 | 22 | "github.com/go-fed/apcore/util" 23 | ) 24 | 25 | // doInTx wraps the operations in fn with a single database transaction. 26 | func doInTx(c util.Context, db *sql.DB, fn func(*sql.Tx) error) error { 27 | tx, err := db.BeginTx(c, nil) 28 | if err != nil { 29 | return err 30 | } 31 | defer tx.Rollback() 32 | 33 | err = fn(tx) 34 | if err != nil { 35 | return err 36 | } 37 | return tx.Commit() 38 | } 39 | -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package util 18 | 19 | import ( 20 | "io" 21 | "os" 22 | 23 | "github.com/google/logger" 24 | ) 25 | 26 | var ( 27 | // These loggers will only respect the logging flags while the call to 28 | // Run is executing. Otherwise, they log to os.Stdout and os.Stderr. 29 | InfoLogger *logger.Logger = logger.Init("apcore", false, false, os.Stdout) 30 | infoClose bool = false 31 | ErrorLogger *logger.Logger = logger.Init("apcore", false, false, os.Stderr) 32 | errorClose bool = false 33 | ) 34 | 35 | func LogInfoTo(system bool, w io.Writer) { 36 | maybeCloseAndLogTo(&InfoLogger, system, w, &infoClose) 37 | } 38 | 39 | func LogErrorTo(system bool, w io.Writer) { 40 | maybeCloseAndLogTo(&ErrorLogger, system, w, &errorClose) 41 | } 42 | 43 | func LogInfoToStdout() { 44 | maybeCloseAndLogTo(&InfoLogger, false, os.Stdout, &infoClose) 45 | } 46 | 47 | func LogErrorToStderr() { 48 | maybeCloseAndLogTo(&ErrorLogger, false, os.Stderr, &errorClose) 49 | } 50 | 51 | func maybeCloseAndLogTo(l **logger.Logger, system bool, w io.Writer, shouldClose *bool) { 52 | if *shouldClose { 53 | (*l).Close() 54 | } 55 | *l = logger.Init("apcore", false, system, w) 56 | *shouldClose = !(w == os.Stdout || w == os.Stderr) 57 | } 58 | -------------------------------------------------------------------------------- /util/resolvers.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fed/activity/streams" 7 | "github.com/go-fed/activity/streams/vocab" 8 | ) 9 | 10 | func ToActivityStreamsFollow(c Context, t vocab.Type) (f vocab.ActivityStreamsFollow, err error) { 11 | var res *streams.TypeResolver 12 | res, err = streams.NewTypeResolver(func(c context.Context, follow vocab.ActivityStreamsFollow) error { 13 | f = follow 14 | return nil 15 | }) 16 | if err != nil { 17 | return 18 | } 19 | err = res.Resolve(c, t) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /util/safe_start_stop.go: -------------------------------------------------------------------------------- 1 | // apcore is a server framework for implementing an ActivityPub application. 2 | // Copyright (C) 2019 Cory Slep 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package util 18 | 19 | import ( 20 | "context" 21 | "sync" 22 | "time" 23 | ) 24 | 25 | // SafeStartStop guarantees at most one asynchronous function is being 26 | // periodically run, no matter how many asynchronous calls to Start or Stop 27 | // are being invoked. 28 | // 29 | // There is no order to how the 30 | type SafeStartStop struct { 31 | // Immutable 32 | goFunc func(context.Context) 33 | period time.Duration 34 | wg sync.WaitGroup // To coordinate when goFunc is done stopping 35 | mu sync.Mutex // Must be locked to modify any of the below 36 | // Mutable 37 | fnTimer *time.Timer // For periodic invocation of goFunc 38 | fnCtx context.Context // For managing stopping 39 | fnCancel context.CancelFunc // For beginning the stopping process 40 | } 41 | 42 | func NewSafeStartStop(fn func(context.Context), period time.Duration) *SafeStartStop { 43 | return &SafeStartStop{ 44 | goFunc: fn, 45 | period: period, 46 | } 47 | } 48 | 49 | func (s *SafeStartStop) Start() { 50 | s.mu.Lock() 51 | defer s.mu.Unlock() 52 | if s.fnCtx != nil { 53 | return 54 | } 55 | s.fnCtx, s.fnCancel = context.WithCancel(context.Background()) 56 | s.fnTimer = time.NewTimer(s.period) 57 | s.wg.Add(1) 58 | go func() { 59 | defer s.wg.Done() 60 | for { 61 | select { 62 | case <-s.fnTimer.C: 63 | s.goFunc(s.fnCtx) 64 | // Timers are tricky to get correct, especially 65 | // when calling Reset. From the documentation: 66 | // 67 | // Reset should be invoked only on stopped or 68 | // expired timers with drained channels. If a 69 | // program has already received a value from 70 | // t.C, the timer is known to have expired and 71 | // the channel drained, so t.Reset can be used 72 | // directly. 73 | s.fnTimer.Reset(s.period) 74 | case <-s.fnCtx.Done(): 75 | return 76 | } 77 | } 78 | }() 79 | } 80 | 81 | func (s *SafeStartStop) Stop() { 82 | s.mu.Lock() 83 | defer s.mu.Unlock() 84 | if s.fnCancel == nil { 85 | return 86 | } 87 | s.fnCancel() 88 | s.wg.Wait() 89 | if !s.fnTimer.Stop() { 90 | <-s.fnTimer.C 91 | } 92 | s.fnTimer = nil 93 | s.fnCtx = nil 94 | s.fnCancel = nil 95 | } 96 | --------------------------------------------------------------------------------