diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c79aed7..3fc1eea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,7 @@ jobs: - uses: actions/checkout@master - name: Build Docker Images run: | + git fetch --prune --unshallow docker build -t activity-relay:$(echo ${GITHUB_SHA}|head -c7) . - name: Push Docker Images to DockerHub run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f79c63e..1495ee5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - name: Execute test and upload coverage run: | go version - go test -coverprofile=coverage.txt -covermode=atomic -p 1 . ./worker ./cli ./State + go test -coverprofile=coverage.txt -covermode=atomic -p 1 ./api ./deliver ./control ./models bash <(curl -s https://codecov.io/bash) env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Dockerfile b/Dockerfile index f9420ef..79ff001 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,12 +5,10 @@ COPY . /Activity-Relay RUN mkdir -p /rootfs/usr/bin && \ apk add -U --no-cache git && \ - go build -o /rootfs/usr/bin/server -ldflags "-X main.version=$(git describe --tags HEAD)" . && \ - go build -o /rootfs/usr/bin/worker -ldflags "-X main.version=$(git describe --tags HEAD)" ./worker && \ - go build -o /rootfs/usr/bin/ar-cli -ldflags "-X main.version=$(git describe --tags HEAD)" ./cli + go build -o /rootfs/usr/bin/relay -ldflags "-X main.version=$(git describe --tags HEAD)" . FROM alpine COPY --from=build /rootfs/usr/bin /usr/bin -RUN chmod +x /usr/bin/server /usr/bin/worker /usr/bin/ar-cli && \ +RUN chmod +x /usr/bin/relay /usr/bin/worker /usr/bin/ar-cli && \ apk add -U --no-cache ca-certificates diff --git a/KeyLoader/keyloader.go b/KeyLoader/keyloader.go deleted file mode 100644 index 212b337..0000000 --- a/KeyLoader/keyloader.go +++ /dev/null @@ -1,49 +0,0 @@ -package keyloader - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "io/ioutil" - "os" -) - -func ReadPrivateKeyRSAfromPath(path string) (*rsa.PrivateKey, error) { - file, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - decoded, _ := pem.Decode(file) - priv, err := x509.ParsePKCS1PrivateKey(decoded.Bytes) - if err != nil { - return nil, err - } - return priv, nil -} - -func ReadPublicKeyRSAfromString(pemString string) (*rsa.PublicKey, error) { - pemByte := []byte(pemString) - decoded, _ := pem.Decode(pemByte) - defer func() { - recover() - }() - keyInterface, err := x509.ParsePKIXPublicKey(decoded.Bytes) - if err != nil { - fmt.Fprintln(os.Stderr, err) - return nil, err - } - pub := keyInterface.(*rsa.PublicKey) - return pub, nil -} - -func GeneratePublicKeyPEMString(publicKey *rsa.PublicKey) string { - publicKeyByte := x509.MarshalPKCS1PublicKey(publicKey) - publicKeyPem := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: publicKeyByte, - }, - ) - return string(publicKeyPem) -} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..4de3177 --- /dev/null +++ b/api/api.go @@ -0,0 +1,81 @@ +package api + +import ( + "fmt" + "net/http" + "time" + + "github.com/RichardKnop/machinery/v1" + cache "github.com/patrickmn/go-cache" + "github.com/yukimochi/Activity-Relay/models" +) + +var ( + version string + globalConfig *models.RelayConfig + + // Actor : Relay's Actor + Actor models.Actor + + // WebfingerResource : Relay's Webfinger resource + WebfingerResource models.WebfingerResource + + // Nodeinfo : Relay's Nodeinfo + Nodeinfo models.NodeinfoResources + + relayState models.RelayState + machineryServer *machinery.Server + actorCache *cache.Cache +) + +func Entrypoint(g *models.RelayConfig, v string) error { + var err error + globalConfig = g + version = v + + err = initialize(globalConfig) + if err != nil { + return err + } + + registResourceHandlers() + + fmt.Println("Staring API Server at", globalConfig.ServerBind()) + err = http.ListenAndServe(globalConfig.ServerBind(), nil) + if err != nil { + return err + } + + return nil +} + +func initialize(globalConfig *models.RelayConfig) error { + var err error + + redisClient := globalConfig.RedisClient() + relayState = models.NewState(redisClient, true) + relayState.ListenNotify(nil) + + machineryServer, err = models.NewMachineryServer(globalConfig) + if err != nil { + return err + } + + Actor = models.NewActivityPubActorFromSelfKey(globalConfig) + actorCache = cache.New(5*time.Minute, 10*time.Minute) + + WebfingerResource.GenerateFromActor(globalConfig.ServerHostname(), &Actor) + Nodeinfo.GenerateFromActor(globalConfig.ServerHostname(), &Actor, version) + + return nil +} + +func registResourceHandlers() { + http.HandleFunc("/.well-known/nodeinfo", handleNodeinfoLink) + http.HandleFunc("/.well-known/webfinger", handleWebfinger) + http.HandleFunc("/nodeinfo/2.1", handleNodeinfo) + http.HandleFunc("/actor", handleActor) + http.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) { + handleInbox(w, r, decodeActivity) + }) +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 0000000..b22e574 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,39 @@ +package api + +import ( + "fmt" + "os" + "testing" + + "github.com/spf13/viper" + "github.com/yukimochi/Activity-Relay/models" +) + +func TestMain(m *testing.M) { + var err error + + testConfigPath := "../misc/config.yml" + file, _ := os.Open(testConfigPath) + defer file.Close() + + viper.SetConfigType("yaml") + viper.ReadConfig(file) + viper.Set("ACTOR_PEM", "../misc/testKey.pem") + viper.BindEnv("REDIS_URL") + + globalConfig, err = models.NewRelayConfig() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + err = initialize(globalConfig) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + relayState = models.NewState(relayState.RedisClient, false) + relayState.RedisClient.FlushAll().Result() + code := m.Run() + os.Exit(code) +} diff --git a/decode.go b/api/decode.go similarity index 65% rename from decode.go rename to api/decode.go index f9e3414..99ec81e 100644 --- a/decode.go +++ b/api/decode.go @@ -1,4 +1,4 @@ -package main +package api import ( "crypto/sha256" @@ -9,13 +9,11 @@ import ( "net/http" "strconv" - "github.com/spf13/viper" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - keyloader "github.com/yukimochi/Activity-Relay/KeyLoader" + "github.com/yukimochi/Activity-Relay/models" "github.com/yukimochi/httpsig" ) -func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) { +func decodeActivity(request *http.Request) (*models.Activity, *models.Actor, []byte, error) { request.Header.Set("Host", request.Host) dataLen, _ := strconv.Atoi(request.Header.Get("Content-Length")) body := make([]byte, dataLen) @@ -27,12 +25,12 @@ func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub. return nil, nil, nil, err } KeyID := verifier.KeyId() - keyOwnerActor := new(activitypub.Actor) - err = keyOwnerActor.RetrieveRemoteActor(KeyID, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", viper.GetString("relay_servicename"), version, hostURL.Host), actorCache) + keyOwnerActor := new(models.Actor) + err = keyOwnerActor.RetrieveRemoteActor(KeyID, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", globalConfig.ServerServicename(), version, globalConfig.ServerHostname().Host), actorCache) if err != nil { return nil, nil, nil, err } - PubKey, err := keyloader.ReadPublicKeyRSAfromString(keyOwnerActor.PublicKey.PublicKeyPem) + PubKey, err := models.ReadPublicKeyRSAfromString(keyOwnerActor.PublicKey.PublicKeyPem) if PubKey == nil { return nil, nil, nil, errors.New("Failed parse PublicKey from string") } @@ -56,14 +54,14 @@ func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub. } // Parse Activity - var activity activitypub.Activity + var activity models.Activity err = json.Unmarshal(body, &activity) if err != nil { return nil, nil, nil, err } - var remoteActor activitypub.Actor - err = remoteActor.RetrieveRemoteActor(activity.Actor, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", viper.GetString("relay_servicename"), version, hostURL.Host), actorCache) + var remoteActor models.Actor + err = remoteActor.RetrieveRemoteActor(activity.Actor, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", globalConfig.ServerServicename(), version, globalConfig.ServerHostname().Host), actorCache) if err != nil { return nil, nil, nil, err } diff --git a/decode_test.go b/api/decode_test.go similarity index 91% rename from decode_test.go rename to api/decode_test.go index 7580053..7782164 100644 --- a/decode_test.go +++ b/api/decode_test.go @@ -1,4 +1,4 @@ -package main +package api import ( "bytes" @@ -9,18 +9,18 @@ import ( "strconv" "testing" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) func TestDecodeActivity(t *testing.T) { relayState.RedisClient.FlushAll().Result() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "innocent.yukimochi.io", InboxURL: "https://innocent.yukimochi.io/inbox", }) - file, _ := os.Open("./misc/create.json") + file, _ := os.Open("../misc/create.json") body, _ := ioutil.ReadAll(file) length := strconv.Itoa(len(body)) req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body)) @@ -45,12 +45,12 @@ func TestDecodeActivity(t *testing.T) { func TestDecodeActivityWithNoSignature(t *testing.T) { relayState.RedisClient.FlushAll().Result() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "innocent.yukimochi.io", InboxURL: "https://innocent.yukimochi.io/inbox", }) - file, _ := os.Open("./misc/create.json") + file, _ := os.Open("../misc/create.json") body, _ := ioutil.ReadAll(file) length := strconv.Itoa(len(body)) req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body)) @@ -69,12 +69,12 @@ func TestDecodeActivityWithNoSignature(t *testing.T) { func TestDecodeActivityWithNotFoundKeyId(t *testing.T) { relayState.RedisClient.FlushAll().Result() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "innocent.yukimochi.io", InboxURL: "https://innocent.yukimochi.io/inbox", }) - file, _ := os.Open("./misc/create.json") + file, _ := os.Open("../misc/create.json") body, _ := ioutil.ReadAll(file) length := strconv.Itoa(len(body)) req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body)) @@ -94,12 +94,12 @@ func TestDecodeActivityWithNotFoundKeyId(t *testing.T) { func TestDecodeActivityWithInvalidDigest(t *testing.T) { relayState.RedisClient.FlushAll().Result() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "innocent.yukimochi.io", InboxURL: "https://innocent.yukimochi.io/inbox", }) - file, _ := os.Open("./misc/create.json") + file, _ := os.Open("../misc/create.json") body, _ := ioutil.ReadAll(file) length := strconv.Itoa(len(body)) req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body)) diff --git a/handle.go b/api/handle.go similarity index 89% rename from handle.go rename to api/handle.go index d6da6b8..ea4a562 100644 --- a/handle.go +++ b/api/handle.go @@ -1,4 +1,4 @@ -package main +package api import ( "encoding/json" @@ -9,8 +9,7 @@ import ( "os" "github.com/RichardKnop/machinery/v1/tasks" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) func handleWebfinger(writer http.ResponseWriter, request *http.Request) { @@ -95,7 +94,7 @@ func contains(entries interface{}, finder string) bool { } } return false - case []state.Subscription: + case []models.Subscription: for i := 0; i < len(entry); i++ { if entry[i].Domain == finder { return true @@ -156,7 +155,7 @@ func pushRegistorJob(inboxURL string, body []byte) { } } -func followAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error { +func followAcceptable(activity *models.Activity, actor *models.Actor) error { if contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public") { return nil } else { @@ -164,7 +163,7 @@ func followAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) } } -func unFollowAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error { +func unFollowAcceptable(activity *models.Activity, actor *models.Actor) error { if contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public") { return nil } else { @@ -172,7 +171,7 @@ func unFollowAcceptable(activity *activitypub.Activity, actor *activitypub.Actor } } -func suitableFollow(activity *activitypub.Activity, actor *activitypub.Actor) bool { +func suitableFollow(activity *models.Activity, actor *models.Actor) bool { domain, _ := url.Parse(activity.Actor) if contains(relayState.BlockedDomains, domain.Host) { return false @@ -180,7 +179,7 @@ func suitableFollow(activity *activitypub.Activity, actor *activitypub.Actor) bo return true } -func relayAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error { +func relayAcceptable(activity *models.Activity, actor *models.Actor) error { if !contains(activity.To, "https://www.w3.org/ns/activitystreams#Public") && !contains(activity.Cc, "https://www.w3.org/ns/activitystreams#Public") { return errors.New("Activity should contain https://www.w3.org/ns/activitystreams#Public as receiver") } @@ -191,7 +190,7 @@ func relayAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) e return errors.New("To use the relay service, Subscribe me in advance") } -func suitableRelay(activity *activitypub.Activity, actor *activitypub.Actor) bool { +func suitableRelay(activity *models.Activity, actor *models.Actor) bool { domain, _ := url.Parse(activity.Actor) if contains(relayState.LimitedDomains, domain.Host) { return false @@ -202,7 +201,7 @@ func suitableRelay(activity *activitypub.Activity, actor *activitypub.Actor) boo return true } -func handleInbox(writer http.ResponseWriter, request *http.Request, activityDecoder func(*http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error)) { +func handleInbox(writer http.ResponseWriter, request *http.Request, activityDecoder func(*http.Request) (*models.Activity, *models.Actor, []byte, error)) { switch request.Method { case "POST": activity, actor, body, err := activityDecoder(request) @@ -215,7 +214,7 @@ func handleInbox(writer http.ResponseWriter, request *http.Request, activityDeco case "Follow": err = followAcceptable(activity, actor) if err != nil { - resp := activity.GenerateResponse(hostURL, "Reject") + resp := activity.GenerateResponse(globalConfig.ServerHostname(), "Reject") jsonData, _ := json.Marshal(&resp) go pushRegistorJob(actor.Inbox, jsonData) fmt.Println("Reject Follow Request : ", err.Error(), activity.Actor) @@ -234,10 +233,10 @@ func handleInbox(writer http.ResponseWriter, request *http.Request, activityDeco }) fmt.Println("Pending Follow Request : ", activity.Actor) } else { - resp := activity.GenerateResponse(hostURL, "Accept") + resp := activity.GenerateResponse(globalConfig.ServerHostname(), "Accept") jsonData, _ := json.Marshal(&resp) go pushRegistorJob(actor.Inbox, jsonData) - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: actor.Endpoints.SharedInbox, ActivityID: activity.ID, @@ -246,7 +245,7 @@ func handleInbox(writer http.ResponseWriter, request *http.Request, activityDeco fmt.Println("Accept Follow Request : ", activity.Actor) } } else { - resp := activity.GenerateResponse(hostURL, "Reject") + resp := activity.GenerateResponse(globalConfig.ServerHostname(), "Reject") jsonData, _ := json.Marshal(&resp) go pushRegistorJob(actor.Inbox, jsonData) fmt.Println("Reject Follow Request : ", activity.Actor) @@ -298,7 +297,7 @@ func handleInbox(writer http.ResponseWriter, request *http.Request, activityDeco } switch nestedObject.Type { case "Note": - resp := nestedObject.GenerateAnnounce(hostURL) + resp := nestedObject.GenerateAnnounce(globalConfig.ServerHostname()) jsonData, _ := json.Marshal(&resp) go pushRelayJob(domain.Host, jsonData) fmt.Println("Accept Announce Note : ", activity.Actor) diff --git a/handle_test.go b/api/handle_test.go similarity index 91% rename from handle_test.go rename to api/handle_test.go index 6171309..7880654 100644 --- a/handle_test.go +++ b/api/handle_test.go @@ -1,4 +1,4 @@ -package main +package api import ( "encoding/json" @@ -11,12 +11,11 @@ import ( "strconv" "testing" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) const ( - BlockService state.Config = iota + BlockService models.Config = iota ManuallyAccept CreateAsAnnounce ) @@ -27,7 +26,7 @@ func TestHandleWebfingerGet(t *testing.T) { req, _ := http.NewRequest("GET", s.URL, nil) q := req.URL.Query() - q.Add("resource", "acct:relay@"+hostURL.Host) + q.Add("resource", "acct:relay@"+globalConfig.ServerHostname().Host) req.URL.RawQuery = q.Encode() client := new(http.Client) r, err := client.Do(req) @@ -43,14 +42,14 @@ func TestHandleWebfingerGet(t *testing.T) { defer r.Body.Close() data, _ := ioutil.ReadAll(r.Body) - var wfresource activitypub.WebfingerResource + var wfresource models.WebfingerResource err = json.Unmarshal(data, &wfresource) if err != nil { t.Fatalf("WebfingerResource response is not valid.") } domain, _ := url.Parse(wfresource.Links[0].Href) - if domain.Host != hostURL.Host { + if domain.Host != globalConfig.ServerHostname().Host { t.Fatalf("WebfingerResource's Host not valid.") } } @@ -92,7 +91,7 @@ func TestHandleNodeinfoLinkGet(t *testing.T) { defer r.Body.Close() data, _ := ioutil.ReadAll(r.Body) - var nodeinfoLinks activitypub.NodeinfoLinks + var nodeinfoLinks models.NodeinfoLinks err = json.Unmarshal(data, &nodeinfoLinks) if err != nil { t.Fatalf("NodeinfoLinks response is not valid.") @@ -133,7 +132,7 @@ func TestHandleNodeinfoGet(t *testing.T) { defer r.Body.Close() data, _ := ioutil.ReadAll(r.Body) - var nodeinfo activitypub.Nodeinfo + var nodeinfo models.Nodeinfo err = json.Unmarshal(data, &nodeinfo) if err != nil { t.Fatalf("Nodeinfo response is not valid.") @@ -187,14 +186,14 @@ func TestHandleActorGet(t *testing.T) { defer r.Body.Close() data, _ := ioutil.ReadAll(r.Body) - var actor activitypub.Actor + var actor models.Actor err = json.Unmarshal(data, &actor) if err != nil { t.Fatalf("Actor response is not valid.") } domain, _ := url.Parse(actor.ID) - if domain.Host != hostURL.Host { + if domain.Host != globalConfig.ServerHostname().Host { t.Fatalf("Actor's Host not valid.") } } @@ -241,8 +240,8 @@ func TestContains(t *testing.T) { } } -func mockActivityDecoderProvider(activity *activitypub.Activity, actor *activitypub.Actor) func(r *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) { - return func(r *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) { +func mockActivityDecoderProvider(activity *models.Activity, actor *models.Actor) func(r *http.Request) (*models.Activity, *models.Actor, []byte, error) { + return func(r *http.Request) (*models.Activity, *models.Actor, []byte, error) { body, err := ioutil.ReadAll(r.Body) if err != nil { log.Fatal(err) @@ -252,57 +251,57 @@ func mockActivityDecoderProvider(activity *activitypub.Activity, actor *activity } } -func mockActivity(req string) activitypub.Activity { +func mockActivity(req string) models.Activity { switch req { case "Follow": - file, _ := os.Open("./misc/follow.json") + file, _ := os.Open("../misc/follow.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity case "Invalid-Follow": - file, _ := os.Open("./misc/followAsActor.json") + file, _ := os.Open("../misc/followAsActor.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity case "Unfollow": - file, _ := os.Open("./misc/unfollow.json") + file, _ := os.Open("../misc/unfollow.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity case "Invalid-Unfollow": body := "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Undo\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://hacked.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Follow\",\"actor\":\"https://hacked.test.yukimochi.io/users/yukimochi\",\"object\":\"https://www.w3.org/ns/activitystreams#Public\"}}" - var activity activitypub.Activity + var activity models.Activity json.Unmarshal([]byte(body), &activity) return activity case "UnfollowAsActor": body := "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Undo\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://hacked.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Follow\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":\"https://relay.yukimochi.example.org/actor\"}}" - var activity activitypub.Activity + var activity models.Activity json.Unmarshal([]byte(body), &activity) return activity case "Create": - file, _ := os.Open("./misc/create.json") + file, _ := os.Open("../misc/create.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity case "Create-Article": body := "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857/activity\",\"type\":\"Create\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"published\":\"2018-11-15T11:07:26Z\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://mastodon.test.yukimochi.io/users/yukimochi/followers\"],\"object\":{\"id\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857\",\"type\":\"Article\",\"summary\":null,\"inReplyTo\":null,\"published\":\"2018-11-15T11:07:26Z\",\"url\":\"https://mastodon.test.yukimochi.io/@yukimochi/101075045564444857\",\"attributedTo\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://mastodon.test.yukimochi.io/users/yukimochi/followers\"],\"sensitive\":false,\"atomUri\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857\",\"inReplyToAtomUri\":null,\"conversation\":\"tag:mastodon.test.yukimochi.io,2018-11-15:objectId=68:objectType=Conversation\",\"content\":\"
Actvity-Relay
\",\"contentMap\":{\"en\":\"Actvity-Relay
\"},\"attachment\":[],\"tag\":[]},\"signature\":{\"type\":\"RsaSignature2017\",\"creator\":\"https://mastodon.test.yukimochi.io/users/yukimochi#main-key\",\"created\":\"2018-11-15T11:07:26Z\",\"signatureValue\":\"mMgl2GgVPgb1Kw6a2iDIZc7r0j3ob+Cl9y+QkCxIe6KmnUzb15e60UuhkE5j3rJnoTwRKqOFy1PMkSxlYW6fPG/5DBxW9I4kX+8sw8iH/zpwKKUOnXUJEqfwRrNH2ix33xcs/GkKPdedY6iAPV9vGZ10MSMOdypfYgU9r+UI0sTaaC2iMXH0WPnHQuYAI+Q1JDHIbDX5FH1WlDL6+8fKAicf3spBMxDwPHGPK8W2jmDLWdN2Vz4ffsCtWs5BCuqOKZrtTW0Rdd4HWzo40MnRXvBjv7yNlnnKzokANBqiOLWT7kNfK0+Vtnt6c/bNX64KBro53KR7wL3ZBvPVuv5rdQ==\"}}" - var activity activitypub.Activity + var activity models.Activity json.Unmarshal([]byte(body), &activity) return activity case "Announce": - file, _ := os.Open("./misc/announce.json") + file, _ := os.Open("../misc/announce.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity case "Undo": - file, _ := os.Open("./misc/undo.json") + file, _ := os.Open("../misc/undo.json") body, _ := ioutil.ReadAll(file) - var activity activitypub.Activity + var activity models.Activity json.Unmarshal(body, &activity) return activity default: @@ -310,24 +309,24 @@ func mockActivity(req string) activitypub.Activity { } } -func mockActor(req string) activitypub.Actor { +func mockActor(req string) models.Actor { switch req { case "Person": - file, _ := os.Open("./misc/person.json") + file, _ := os.Open("../misc/person.json") body, _ := ioutil.ReadAll(file) - var actor activitypub.Actor + var actor models.Actor json.Unmarshal(body, &actor) return actor case "Service": - file, _ := os.Open("./misc/service.json") + file, _ := os.Open("../misc/service.json") body, _ := ioutil.ReadAll(file) - var actor activitypub.Actor + var actor models.Actor json.Unmarshal(body, &actor) return actor case "Application": - file, _ := os.Open("./misc/application.json") + file, _ := os.Open("../misc/application.json") body, _ := ioutil.ReadAll(file) - var actor activitypub.Actor + var actor models.Actor json.Unmarshal(body, &actor) return actor default: @@ -529,7 +528,7 @@ func TestHandleInboxValidUnfollow(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) @@ -559,7 +558,7 @@ func TestHandleInboxInvalidUnfollow(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) @@ -589,7 +588,7 @@ func TestHandleInboxUnfollowAsActor(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) @@ -619,11 +618,11 @@ func TestHandleInboxValidCreate(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "example.org", InboxURL: "https://example.org/inbox", }) @@ -652,7 +651,7 @@ func TestHandleInboxlimitedCreate(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) @@ -680,11 +679,11 @@ func TestHandleInboxValidCreateAsAnnounceNote(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "example.org", InboxURL: "https://example.org/inbox", }) @@ -713,11 +712,11 @@ func TestHandleInboxValidCreateAsAnnounceNoNote(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: "example.org", InboxURL: "https://example.org/inbox", }) @@ -765,7 +764,7 @@ func TestHandleInboxUndo(t *testing.T) { })) defer s.Close() - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain.Host, InboxURL: "https://mastodon.test.yukimochi.io/inbox", }) diff --git a/cli/cli.go b/cli/cli.go deleted file mode 100644 index 8aeefaf..0000000 --- a/cli/cli.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "crypto/rsa" - "fmt" - "net/url" - - "github.com/RichardKnop/machinery/v1" - "github.com/RichardKnop/machinery/v1/config" - "github.com/go-redis/redis" - "github.com/spf13/cobra" - "github.com/spf13/viper" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - keyloader "github.com/yukimochi/Activity-Relay/KeyLoader" - state "github.com/yukimochi/Activity-Relay/State" -) - -var ( - version string - - // Actor : Relay's Actor - Actor activitypub.Actor - - hostname *url.URL - hostkey *rsa.PrivateKey - relayState state.RelayState - machineryServer *machinery.Server -) - -func initConfig() { - viper.SetConfigName("config") - viper.AddConfigPath(".") - err := viper.ReadInConfig() - if err != nil { - fmt.Println("Config file is not exists. Use environment variables.") - viper.BindEnv("actor_pem") - viper.BindEnv("redis_url") - viper.BindEnv("relay_bind") - viper.BindEnv("relay_domain") - viper.BindEnv("relay_servicename") - } else { - Actor.Summary = viper.GetString("relay_summary") - Actor.Icon = activitypub.Image{URL: viper.GetString("relay_icon")} - Actor.Image = activitypub.Image{URL: viper.GetString("relay_image")} - } - Actor.Name = viper.GetString("relay_servicename") - - hostname, err = url.Parse("https://" + viper.GetString("relay_domain")) - if err != nil { - panic(err) - } - hostkey, err := keyloader.ReadPrivateKeyRSAfromPath(viper.GetString("actor_pem")) - if err != nil { - panic(err) - } - redisOption, err := redis.ParseURL(viper.GetString("redis_url")) - if err != nil { - panic(err) - } - redisClient := redis.NewClient(redisOption) - relayState = state.NewState(redisClient, true) - var machineryConfig = &config.Config{ - Broker: viper.GetString("redis_url"), - DefaultQueue: "relay", - ResultBackend: viper.GetString("redis_url"), - ResultsExpireIn: 5, - } - machineryServer, err = machinery.NewServer(machineryConfig) - if err != nil { - panic(err) - } - - Actor.GenerateSelfKey(hostname, &hostkey.PublicKey) -} - -func buildNewCmd() *cobra.Command { - var app = &cobra.Command{} - app.AddCommand(domainCmdInit()) - app.AddCommand(followCmdInit()) - app.AddCommand(configCmdInit()) - return app -} - -func main() { - initConfig() - var app = buildNewCmd() - app.Execute() -} diff --git a/cli/cli_test.go b/cli/cli_test.go deleted file mode 100644 index d12f523..0000000 --- a/cli/cli_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "os" - "testing" - - "github.com/spf13/viper" - state "github.com/yukimochi/Activity-Relay/State" -) - -func TestMain(m *testing.M) { - viper.Set("actor_pem", "../misc/testKey.pem") - viper.Set("relay_domain", "relay.yukimochi.example.org") - initConfig() - relayState = state.NewState(relayState.RedisClient, false) - relayState.RedisClient.FlushAll().Result() - - code := m.Run() - os.Exit(code) -} diff --git a/cli/config.go b/control/config.go similarity index 89% rename from cli/config.go rename to control/config.go index 856162b..ca9d404 100644 --- a/cli/config.go +++ b/control/config.go @@ -1,4 +1,4 @@ -package main +package control import ( "encoding/json" @@ -7,11 +7,11 @@ import ( "os" "github.com/spf13/cobra" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) const ( - BlockService state.Config = iota + BlockService models.Config = iota ManuallyAccept CreateAsAnnounce ) @@ -27,7 +27,9 @@ func configCmdInit() *cobra.Command { Use: "list", Short: "List all relay configration", Long: "List all relay configration.", - Run: listConfig, + Run: func(cmd *cobra.Command, args []string) { + initProxy(listConfig, cmd, args) + }, } config.AddCommand(configList) @@ -35,7 +37,9 @@ func configCmdInit() *cobra.Command { Use: "export", Short: "Export all relay information", Long: "Export all relay information by JSON format.", - Run: exportConfig, + Run: func(cmd *cobra.Command, args []string) { + initProxy(exportConfig, cmd, args) + }, } config.AddCommand(configExport) @@ -43,7 +47,9 @@ func configCmdInit() *cobra.Command { Use: "import [flags]", Short: "Import all relay information", Long: "Import all relay information from JSON file.", - Run: importConfig, + Run: func(cmd *cobra.Command, args []string) { + initProxy(importConfig, cmd, args) + }, } configImport.Flags().String("json", "", "JSON file-path") configImport.MarkFlagRequired("json") @@ -60,7 +66,9 @@ func configCmdInit() *cobra.Command { - create-as-announce Enable announce activity instead of relay create activity (not recommend)`, Args: cobra.MinimumNArgs(1), - RunE: configEnable, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(configEnable, cmd, args) + }, } configEnable.Flags().BoolP("disable", "d", false, "Disable configration instead of Enable") config.AddCommand(configEnable) @@ -126,7 +134,7 @@ func importConfig(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stderr, err) return } - var data state.RelayState + var data models.RelayState err = json.Unmarshal(jsonData, &data) if err != nil { fmt.Fprintln(os.Stderr, err) @@ -154,7 +162,7 @@ func importConfig(cmd *cobra.Command, args []string) { cmd.Println("Set [" + BlockedDomain + "] as blocked domain") } for _, Subscription := range data.Subscriptions { - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: Subscription.Domain, InboxURL: Subscription.InboxURL, ActivityID: Subscription.ActivityID, diff --git a/cli/config_test.go b/control/config_test.go similarity index 78% rename from cli/config_test.go rename to control/config_test.go index c18e8e5..c9cb99c 100644 --- a/cli/config_test.go +++ b/control/config_test.go @@ -1,4 +1,4 @@ -package main +package control import ( "bytes" @@ -11,16 +11,16 @@ import ( func TestServiceBlock(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "enable", "service-block"}) + app.SetArgs([]string{"enable", "service-block"}) app.Execute() relayState.Load() if !relayState.RelayConfig.BlockService { t.Fatalf("Not Enabled Blocking feature for service-type actor") } - app.SetArgs([]string{"config", "enable", "-d", "service-block"}) + app.SetArgs([]string{"enable", "-d", "service-block"}) app.Execute() relayState.Load() if relayState.RelayConfig.BlockService { @@ -31,16 +31,16 @@ func TestServiceBlock(t *testing.T) { func TestManuallyAccept(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "enable", "manually-accept"}) + app.SetArgs([]string{"enable", "manually-accept"}) app.Execute() relayState.Load() if !relayState.RelayConfig.ManuallyAccept { t.Fatalf("Not Enabled Manually accept follow-request feature") } - app.SetArgs([]string{"config", "enable", "-d", "manually-accept"}) + app.SetArgs([]string{"enable", "-d", "manually-accept"}) app.Execute() relayState.Load() if relayState.RelayConfig.ManuallyAccept { @@ -51,16 +51,16 @@ func TestManuallyAccept(t *testing.T) { func TestCreateAsAnnounce(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "enable", "create-as-announce"}) + app.SetArgs([]string{"enable", "create-as-announce"}) app.Execute() relayState.Load() if !relayState.RelayConfig.CreateAsAnnounce { t.Fatalf("Enable announce activity instead of relay create activity") } - app.SetArgs([]string{"config", "enable", "-d", "create-as-announce"}) + app.SetArgs([]string{"enable", "-d", "create-as-announce"}) app.Execute() relayState.Load() if relayState.RelayConfig.CreateAsAnnounce { @@ -71,11 +71,11 @@ func TestCreateAsAnnounce(t *testing.T) { func TestInvalidConfig(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"config", "enable", "hoge"}) + app.SetArgs([]string{"enable", "hoge"}) app.Execute() output := buffer.String() @@ -87,11 +87,11 @@ func TestInvalidConfig(t *testing.T) { func TestListConfig(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"config", "list"}) + app.SetArgs([]string{"list"}) app.Execute() output := buffer.String() @@ -116,11 +116,11 @@ func TestListConfig(t *testing.T) { func TestExportConfig(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"config", "export"}) + app.SetArgs([]string{"export"}) app.Execute() file, err := os.Open("../misc/blankConfig.json") @@ -137,16 +137,16 @@ func TestExportConfig(t *testing.T) { func TestImportConfig(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"config", "export"}) + app.SetArgs([]string{"export"}) app.Execute() file, err := os.Open("../misc/exampleConfig.json") diff --git a/control/control.go b/control/control.go new file mode 100644 index 0000000..8f02494 --- /dev/null +++ b/control/control.go @@ -0,0 +1,90 @@ +package control + +import ( + "fmt" + "os" + + "github.com/RichardKnop/machinery/v1" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/yukimochi/Activity-Relay/models" +) + +var ( + globalConfig *models.RelayConfig + + initProxy = initializeProxy + initProxyE = initializeProxyE + + // Actor : Relay's Actor + Actor models.Actor + + relayState models.RelayState + machineryServer *machinery.Server +) + +func BuildCommand(command *cobra.Command) { + command.AddCommand(configCmdInit()) + command.AddCommand(domainCmdInit()) + command.AddCommand(followCmdInit()) +} + +func initializeProxy(function func(cmd *cobra.Command, args []string), cmd *cobra.Command, args []string) { + initConfig(cmd) + function(cmd, args) +} + +func initializeProxyE(function func(cmd *cobra.Command, args []string) error, cmd *cobra.Command, args []string) error { + initConfig(cmd) + return function(cmd, args) +} + +func initConfig(cmd *cobra.Command) error { + var err error + + configPath := cmd.Flag("config").Value.String() + file, err := os.Open(configPath) + defer file.Close() + + if err == nil { + viper.SetConfigType("yaml") + viper.ReadConfig(file) + } else { + fmt.Fprintln(os.Stderr, "Config file not exist. Use environment variables.") + + viper.BindEnv("ACTOR_PEM") + viper.BindEnv("REDIS_URL") + viper.BindEnv("RELAY_BIND") + viper.BindEnv("RELAY_DOMAIN") + viper.BindEnv("RELAY_SERVICENAME") + viper.BindEnv("JOB_CONCURRENCY") + viper.BindEnv("RELAY_SUMMARY") + viper.BindEnv("RELAY_ICON") + viper.BindEnv("RELAY_IMAGE") + } + + globalConfig, err = models.NewRelayConfig() + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + return nil +} + +func initialize(globalconfig *models.RelayConfig) error { + var err error + + redisClient := globalConfig.RedisClient() + relayState = models.NewState(redisClient, true) + relayState.ListenNotify(nil) + + machineryServer, err = models.NewMachineryServer(globalConfig) + if err != nil { + return err + } + + Actor = models.NewActivityPubActorFromSelfKey(globalConfig) + + return nil +} diff --git a/control/control_test.go b/control/control_test.go new file mode 100644 index 0000000..2b8d693 --- /dev/null +++ b/control/control_test.go @@ -0,0 +1,52 @@ +package control + +import ( + "fmt" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/yukimochi/Activity-Relay/models" +) + +func TestMain(m *testing.M) { + var err error + + testConfigPath := "../misc/config.yml" + file, _ := os.Open(testConfigPath) + defer file.Close() + + viper.SetConfigType("yaml") + viper.ReadConfig(file) + viper.Set("ACTOR_PEM", "../misc/testKey.pem") + viper.BindEnv("REDIS_URL") + + globalConfig, err = models.NewRelayConfig() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + err = initialize(globalConfig) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + relayState = models.NewState(globalConfig.RedisClient(), false) + relayState.RedisClient.FlushAll().Result() + + initProxy = emptyProxy + initProxyE = emptyProxyE + + code := m.Run() + os.Exit(code) +} + +func emptyProxy(function func(cmd *cobra.Command, args []string), cmd *cobra.Command, args []string) { + function(cmd, args) +} + +func emptyProxyE(function func(cmd *cobra.Command, args []string) error, cmd *cobra.Command, args []string) error { + return function(cmd, args) +} diff --git a/cli/domain.go b/control/domain.go similarity index 85% rename from cli/domain.go rename to control/domain.go index 6e1d443..f8220e6 100644 --- a/cli/domain.go +++ b/control/domain.go @@ -1,12 +1,11 @@ -package main +package control import ( "encoding/json" "fmt" "github.com/spf13/cobra" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) func domainCmdInit() *cobra.Command { @@ -20,7 +19,9 @@ func domainCmdInit() *cobra.Command { Use: "list [flags]", Short: "List domain", Long: "List domain which filtered given type.", - RunE: listDomains, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(listDomains, cmd, args) + }, } domainList.Flags().StringP("type", "t", "subscriber", "domain type [subscriber,limited,blocked]") domain.AddCommand(domainList) @@ -30,7 +31,9 @@ func domainCmdInit() *cobra.Command { Short: "Set or unset domain as limited or blocked", Long: "Set or unset domain as limited or blocked.", Args: cobra.MinimumNArgs(1), - RunE: setDomainType, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(setDomainType, cmd, args) + }, } domainSet.Flags().StringP("type", "t", "", "Apply domain type [limited,blocked]") domainSet.MarkFlagRequired("type") @@ -41,15 +44,17 @@ func domainCmdInit() *cobra.Command { Use: "unfollow [flags]", Short: "Send Unfollow request for given domains", Long: "Send unfollow request for given domains.", - RunE: unfollowDomains, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(unfollowDomains, cmd, args) + }, } domain.AddCommand(domainUnfollow) return domain } -func createUnfollowRequestResponse(subscription state.Subscription) error { - activity := activitypub.Activity{ +func createUnfollowRequestResponse(subscription models.Subscription) error { + activity := models.Activity{ Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"}, ID: subscription.ActivityID, Actor: subscription.ActorID, @@ -57,7 +62,7 @@ func createUnfollowRequestResponse(subscription state.Subscription) error { Object: "https://www.w3.org/ns/activitystreams#Public", } - resp := activity.GenerateResponse(hostname, "Reject") + resp := activity.GenerateResponse(globalConfig.ServerHostname(), "Reject") jsonData, _ := json.Marshal(&resp) pushRegistorJob(subscription.InboxURL, jsonData) diff --git a/cli/domain_test.go b/control/domain_test.go similarity index 67% rename from cli/domain_test.go rename to control/domain_test.go index 18e7279..0a9e751 100644 --- a/cli/domain_test.go +++ b/control/domain_test.go @@ -1,4 +1,4 @@ -package main +package control import ( "bytes" @@ -9,15 +9,16 @@ import ( func TestListDomainSubscriber(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app := configCmdInit() + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) - app.SetOutput(buffer) - app.SetArgs([]string{"domain", "list"}) + app = domainCmdInit() + app.SetOutput(buffer) + app.SetArgs([]string{"list"}) app.Execute() output := buffer.String() @@ -33,16 +34,17 @@ Total : 1 func TestListDomainLimited(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) - app.SetOutput(buffer) - app.SetArgs([]string{"domain", "list", "-t", "limited"}) + app = domainCmdInit() + app.SetOutput(buffer) + app.SetArgs([]string{"list", "-t", "limited"}) app.Execute() output := buffer.String() @@ -58,16 +60,17 @@ Total : 1 func TestListDomainBlocked(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) - app.SetOutput(buffer) - app.SetArgs([]string{"domain", "list", "-t", "blocked"}) + app = domainCmdInit() + app.SetOutput(buffer) + app.SetArgs([]string{"list", "-t", "blocked"}) app.Execute() output := buffer.String() @@ -83,9 +86,9 @@ Total : 1 func TestSetDomainBlocked(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := domainCmdInit() - app.SetArgs([]string{"domain", "set", "-t", "blocked", "testdomain.example.jp"}) + app.SetArgs([]string{"set", "-t", "blocked", "testdomain.example.jp"}) app.Execute() relayState.Load() @@ -104,9 +107,9 @@ func TestSetDomainBlocked(t *testing.T) { func TestSetDomainLimited(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := domainCmdInit() - app.SetArgs([]string{"domain", "set", "-t", "limited", "testdomain.example.jp"}) + app.SetArgs([]string{"set", "-t", "limited", "testdomain.example.jp"}) app.Execute() relayState.Load() @@ -125,12 +128,13 @@ func TestSetDomainLimited(t *testing.T) { func TestUnsetDomainBlocked(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() - app.SetArgs([]string{"domain", "set", "-t", "blocked", "-u", "blockedDomain.example.jp"}) + app = domainCmdInit() + app.SetArgs([]string{"set", "-t", "blocked", "-u", "blockedDomain.example.jp"}) app.Execute() relayState.Load() @@ -149,12 +153,13 @@ func TestUnsetDomainBlocked(t *testing.T) { func TestUnsetDomainLimited(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() - app.SetArgs([]string{"domain", "set", "-t", "limited", "-u", "limitedDomain.example.jp"}) + app = domainCmdInit() + app.SetArgs([]string{"set", "-t", "limited", "-u", "limitedDomain.example.jp"}) app.Execute() relayState.Load() @@ -173,16 +178,17 @@ func TestUnsetDomainLimited(t *testing.T) { func TestSetDomainInvalid(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) - app.SetOutput(buffer) - app.SetArgs([]string{"domain", "set", "-t", "hoge", "hoge.example.jp"}) + app = domainCmdInit() + app.SetOutput(buffer) + app.SetArgs([]string{"set", "-t", "hoge", "hoge.example.jp"}) app.Execute() output := buffer.String() @@ -194,12 +200,13 @@ func TestSetDomainInvalid(t *testing.T) { func TestUnfollowDomain(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() - app.SetArgs([]string{"domain", "unfollow", "subscription.example.jp"}) + app = domainCmdInit() + app.SetArgs([]string{"unfollow", "subscription.example.jp"}) app.Execute() relayState.Load() @@ -218,16 +225,17 @@ func TestUnfollowDomain(t *testing.T) { func TestInvalidUnfollowDomain(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() relayState.Load() buffer := new(bytes.Buffer) - app.SetOutput(buffer) - app.SetArgs([]string{"domain", "unfollow", "unknown.tld"}) + app = domainCmdInit() + app.SetOutput(buffer) + app.SetArgs([]string{"unfollow", "unknown.tld"}) app.Execute() output := buffer.String() diff --git a/cli/follow.go b/control/follow.go similarity index 83% rename from cli/follow.go rename to control/follow.go index 0d5f08d..46b5eaf 100644 --- a/cli/follow.go +++ b/control/follow.go @@ -1,4 +1,4 @@ -package main +package control import ( "encoding/json" @@ -9,8 +9,7 @@ import ( "github.com/RichardKnop/machinery/v1/tasks" uuid "github.com/satori/go.uuid" "github.com/spf13/cobra" - activitypub "github.com/yukimochi/Activity-Relay/ActivityPub" - state "github.com/yukimochi/Activity-Relay/State" + "github.com/yukimochi/Activity-Relay/models" ) func followCmdInit() *cobra.Command { @@ -24,7 +23,9 @@ func followCmdInit() *cobra.Command { Use: "list", Short: "List follow request", Long: "List follow request.", - RunE: listFollows, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(listFollows, cmd, args) + }, } follow.AddCommand(followList) @@ -33,7 +34,9 @@ func followCmdInit() *cobra.Command { Short: "Accept follow request", Long: "Accept follow request by domain.", Args: cobra.MinimumNArgs(1), - RunE: acceptFollow, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(acceptFollow, cmd, args) + }, } follow.AddCommand(followAccept) @@ -42,7 +45,9 @@ func followCmdInit() *cobra.Command { Short: "Reject follow request", Long: "Reject follow request by domain.", Args: cobra.MinimumNArgs(1), - RunE: rejectFollow, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(rejectFollow, cmd, args) + }, } follow.AddCommand(followReject) @@ -50,7 +55,9 @@ func followCmdInit() *cobra.Command { Use: "update", Short: "Update actor object", Long: "Update actor object for whole subscribers.", - RunE: updateActor, + RunE: func(cmd *cobra.Command, args []string) error { + return initProxyE(updateActor, cmd, args) + }, } follow.AddCommand(updateActor) @@ -85,7 +92,7 @@ func createFollowRequestResponse(domain string, response string) error { if err != nil { return err } - activity := activitypub.Activity{ + activity := models.Activity{ Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"}, ID: data["activity_id"], Actor: data["actor"], @@ -93,7 +100,7 @@ func createFollowRequestResponse(domain string, response string) error { Object: data["object"], } - resp := activity.GenerateResponse(hostname, response) + resp := activity.GenerateResponse(globalConfig.ServerHostname(), response) jsonData, err := json.Marshal(&resp) if err != nil { return err @@ -101,7 +108,7 @@ func createFollowRequestResponse(domain string, response string) error { pushRegistorJob(data["inbox_url"], jsonData) relayState.RedisClient.Del("relay:pending:" + domain) if response == "Accept" { - relayState.AddSubscription(state.Subscription{ + relayState.AddSubscription(models.Subscription{ Domain: domain, InboxURL: data["inbox_url"], ActivityID: data["activity_id"], @@ -112,11 +119,11 @@ func createFollowRequestResponse(domain string, response string) error { return nil } -func createUpdateActorActivity(subscription state.Subscription) error { - activity := activitypub.Activity{ +func createUpdateActorActivity(subscription models.Subscription) error { + activity := models.Activity{ Context: []string{"https://www.w3.org/ns/activitystreams"}, - ID: hostname.String() + "/activities/" + uuid.NewV4().String(), - Actor: hostname.String() + "/actor", + ID: globalConfig.ServerHostname().String() + "/activities/" + uuid.NewV4().String(), + Actor: globalConfig.ServerHostname().String() + "/actor", Type: "Update", To: []string{"https://www.w3.org/ns/activitystreams#Public"}, Object: Actor, diff --git a/cli/follow_test.go b/control/follow_test.go similarity index 77% rename from cli/follow_test.go rename to control/follow_test.go index f61b9db..d9940a1 100644 --- a/cli/follow_test.go +++ b/control/follow_test.go @@ -1,4 +1,4 @@ -package main +package control import ( "bytes" @@ -9,7 +9,7 @@ import ( func TestListFollows(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := followCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) @@ -19,10 +19,10 @@ func TestListFollows(t *testing.T) { "activity_id": "https://example.com/UUID", "type": "Follow", "actor": "https://example.com/user/example", - "object": "https://" + hostname.Host + "/actor", + "object": "https://" + globalConfig.ServerHostname().Host + "/actor", }) - app.SetArgs([]string{"follow", "list"}) + app.SetArgs([]string{"list"}) app.Execute() output := buffer.String() @@ -38,17 +38,17 @@ Total : 1 func TestAcceptFollow(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := followCmdInit() relayState.RedisClient.HMSet("relay:pending:example.com", map[string]interface{}{ "inbox_url": "https://example.com/inbox", "activity_id": "https://example.com/UUID", "type": "Follow", "actor": "https://example.com/user/example", - "object": "https://" + hostname.Host + "/actor", + "object": "https://" + globalConfig.ServerHostname().Host + "/actor", }) - app.SetArgs([]string{"follow", "accept", "example.com"}) + app.SetArgs([]string{"accept", "example.com"}) app.Execute() valid, _ := relayState.RedisClient.Exists("relay:pending:example.com").Result() @@ -65,17 +65,17 @@ func TestAcceptFollow(t *testing.T) { func TestRejectFollow(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := followCmdInit() relayState.RedisClient.HMSet("relay:pending:example.com", map[string]interface{}{ "inbox_url": "https://example.com/inbox", "activity_id": "https://example.com/UUID", "type": "Follow", "actor": "https://example.com/user/example", - "object": "https://" + hostname.Host + "/actor", + "object": "https://" + globalConfig.ServerHostname().Host + "/actor", }) - app.SetArgs([]string{"follow", "reject", "example.com"}) + app.SetArgs([]string{"reject", "example.com"}) app.Execute() valid, _ := relayState.RedisClient.Exists("relay:pending:example.com").Result() @@ -92,12 +92,12 @@ func TestRejectFollow(t *testing.T) { func TestInvalidFollow(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := followCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"follow", "accept", "unknown.tld"}) + app.SetArgs([]string{"accept", "unknown.tld"}) app.Execute() output := buffer.String() @@ -109,12 +109,12 @@ func TestInvalidFollow(t *testing.T) { func TestInvalidRejectFollow(t *testing.T) { relayState.RedisClient.FlushAll().Result() - app := buildNewCmd() + app := followCmdInit() buffer := new(bytes.Buffer) app.SetOutput(buffer) - app.SetArgs([]string{"follow", "reject", "unknown.tld"}) + app.SetArgs([]string{"reject", "unknown.tld"}) app.Execute() output := buffer.String() @@ -124,11 +124,13 @@ func TestInvalidRejectFollow(t *testing.T) { } func TestCreateUpdateActorActivity(t *testing.T) { - app := buildNewCmd() + app := configCmdInit() - app.SetArgs([]string{"config", "import", "--json", "../misc/exampleConfig.json"}) + app.SetArgs([]string{"import", "--json", "../misc/exampleConfig.json"}) app.Execute() - app.SetArgs([]string{"follow", "update"}) + app = followCmdInit() + + app.SetArgs([]string{"update"}) app.Execute() } diff --git a/cli/contain.go b/control/utils.go similarity index 78% rename from cli/contain.go rename to control/utils.go index 7c122e0..fe05e1f 100644 --- a/cli/contain.go +++ b/control/utils.go @@ -1,6 +1,6 @@ -package main +package control -import state "github.com/yukimochi/Activity-Relay/State" +import "github.com/yukimochi/Activity-Relay/models" func contains(entries interface{}, finder string) bool { switch entry := entries.(type) { @@ -12,7 +12,7 @@ func contains(entries interface{}, finder string) bool { return true } } - case []state.Subscription: + case []models.Subscription: for i := 0; i < len(entry); i++ { if entry[i].Domain == finder { return true diff --git a/cli/contain_test.go b/control/utils_test.go similarity index 97% rename from cli/contain_test.go rename to control/utils_test.go index 444f201..6ca9cc9 100644 --- a/cli/contain_test.go +++ b/control/utils_test.go @@ -1,4 +1,4 @@ -package main +package control import "testing" diff --git a/deliver/deriver.go b/deliver/deriver.go new file mode 100644 index 0000000..93766b9 --- /dev/null +++ b/deliver/deriver.go @@ -0,0 +1,93 @@ +package deliver + +import ( + "fmt" + "net/http" + "net/url" + "os" + "time" + + "github.com/RichardKnop/machinery/v1" + "github.com/RichardKnop/machinery/v1/log" + "github.com/go-redis/redis" + uuid "github.com/satori/go.uuid" + "github.com/yukimochi/Activity-Relay/models" +) + +var ( + version string + globalConfig *models.RelayConfig + + // Actor : Relay's Actor + Actor models.Actor + + redisClient *redis.Client + machineryServer *machinery.Server + httpClient *http.Client +) + +func relayActivity(args ...string) error { + inboxURL := args[0] + body := args[1] + err := sendActivity(inboxURL, Actor.ID, []byte(body), globalConfig.ActorKey()) + if err != nil { + domain, _ := url.Parse(inboxURL) + eval_script := "local change = redis.call('HSETNX',KEYS[1], 'last_error', ARGV[1]); if change == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end;" + redisClient.Eval(eval_script, []string{"relay:statistics:" + domain.Host}, err.Error(), 60).Result() + } + return err +} + +func registorActivity(args ...string) error { + inboxURL := args[0] + body := args[1] + err := sendActivity(inboxURL, Actor.ID, []byte(body), globalConfig.ActorKey()) + return err +} + +func Entrypoint(g *models.RelayConfig, v string) error { + var err error + globalConfig = g + version = v + + err = initialize(globalConfig) + if err != nil { + return err + } + + err = machineryServer.RegisterTask("registor", registorActivity) + if err != nil { + return err + } + err = machineryServer.RegisterTask("relay", relayActivity) + if err != nil { + return err + } + + workerID := uuid.NewV4() + worker := machineryServer.NewWorker(workerID.String(), globalConfig.JobConcurrency()) + err = worker.Launch() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + + return nil +} + +func initialize(globalConfig *models.RelayConfig) error { + var err error + + redisClient = globalConfig.RedisClient() + + machineryServer, err = models.NewMachineryServer(globalConfig) + if err != nil { + return err + } + httpClient = &http.Client{Timeout: time.Duration(5) * time.Second} + + Actor = models.NewActivityPubActorFromSelfKey(globalConfig) + newNullLogger := NewNullLogger() + log.DEBUG = newNullLogger + + return nil +} diff --git a/worker/worker_test.go b/deliver/deriver_test.go similarity index 78% rename from worker/worker_test.go rename to deliver/deriver_test.go index dfd9d28..56af7bb 100644 --- a/worker/worker_test.go +++ b/deliver/deriver_test.go @@ -1,6 +1,7 @@ -package main +package deliver import ( + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -9,15 +10,33 @@ import ( "testing" "github.com/spf13/viper" + "github.com/yukimochi/Activity-Relay/models" ) func TestMain(m *testing.M) { - viper.Set("actor_pem", "../misc/testKey.pem") - viper.Set("relay_domain", "relay.yukimochi.example.org") - initConfig() - redisClient.FlushAll().Result() + var err error - // Load Config + testConfigPath := "../misc/config.yml" + file, _ := os.Open(testConfigPath) + defer file.Close() + + viper.SetConfigType("yaml") + viper.ReadConfig(file) + viper.Set("ACTOR_PEM", "../misc/testKey.pem") + viper.BindEnv("REDIS_URL") + + globalConfig, err = models.NewRelayConfig() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + err = initialize(globalConfig) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + redisClient.FlushAll().Result() code := m.Run() os.Exit(code) } @@ -52,7 +71,7 @@ func TestRelayActivityNoHost(t *testing.T) { t.Fatal("Failed - Error not reported.") } domain, _ := url.Parse("http://nohost.example.jp") - data, err := redisClient.HGet("relay:statistics:"+domain.Host, "last_error").Result() + data, _ := redisClient.HGet("relay:statistics:"+domain.Host, "last_error").Result() if data == "" { t.Fatal("Failed - Error not cached.") } @@ -70,7 +89,7 @@ func TestRelayActivityResp500(t *testing.T) { t.Fatal("Failed - Error not reported.") } domain, _ := url.Parse(s.URL) - data, err := redisClient.HGet("relay:statistics:"+domain.Host, "last_error").Result() + data, _ := redisClient.HGet("relay:statistics:"+domain.Host, "last_error").Result() if data == "" { t.Fatal("Failed - Error not cached.") } diff --git a/worker/logger.go b/deliver/logger.go similarity index 97% rename from worker/logger.go rename to deliver/logger.go index 234cd43..16dbfe3 100644 --- a/worker/logger.go +++ b/deliver/logger.go @@ -1,4 +1,4 @@ -package main +package deliver // NullLogger : Null logger for debug output type NullLogger struct { diff --git a/worker/sender.go b/deliver/sender.go similarity index 91% rename from worker/sender.go rename to deliver/sender.go index 8028902..5391bd1 100644 --- a/worker/sender.go +++ b/deliver/sender.go @@ -1,4 +1,4 @@ -package main +package deliver import ( "bytes" @@ -11,7 +11,6 @@ import ( "time" httpdate "github.com/Songmu/go-httpdate" - "github.com/spf13/viper" "github.com/yukimochi/httpsig" ) @@ -36,7 +35,7 @@ func appendSignature(request *http.Request, body *[]byte, KeyID string, publicKe func sendActivity(inboxURL string, KeyID string, body []byte, publicKey *rsa.PrivateKey) error { req, _ := http.NewRequest("POST", inboxURL, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/activity+json") - req.Header.Set("User-Agent", fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", viper.GetString("relay_servicename"), version, hostURL.Host)) + req.Header.Set("User-Agent", fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", globalConfig.ServerServicename(), version, globalConfig.ServerHostname().Host)) req.Header.Set("Date", httpdate.Time2Str(time.Now())) appendSignature(req, &body, KeyID, publicKey) resp, err := httpClient.Do(req) diff --git a/docker-compose.yml b/docker-compose.yml index 8215bf3..0eec641 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: image: yukimochi/activity-relay restart: always init: true - command: worker + command: relay worker environment: - "ACTOR_PEM=/actor.pem" - "RELAY_DOMAIN=relay.toot.yukimochi.jp" @@ -31,7 +31,7 @@ services: image: yukimochi/activity-relay restart: always init: true - command: server + command: relay server environment: - "ACTOR_PEM=/actor.pem" - "RELAY_DOMAIN=relay.toot.yukimochi.jp" diff --git a/main.go b/main.go index 2310c0d..36fc07c 100644 --- a/main.go +++ b/main.go @@ -1,112 +1,145 @@ +/* +Yet another powerful customizable ActivityPub relay server written in Go. + +Run Activity-Relay + +API Server + ./Activity-Relay -c