Mono-binarify for api server.
This commit is contained in:
121
models/config.go
Normal file
121
models/config.go
Normal file
@ -0,0 +1,121 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/RichardKnop/machinery/v1"
|
||||
"github.com/RichardKnop/machinery/v1/config"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// RelayConfig contains valid configuration.
|
||||
type RelayConfig struct {
|
||||
actorKey *rsa.PrivateKey
|
||||
domain *url.URL
|
||||
redisClient *redis.Client
|
||||
redisURL string
|
||||
serverBind string
|
||||
serviceName string
|
||||
serviceSummary string
|
||||
serviceIconURL *url.URL
|
||||
serviceImageURL *url.URL
|
||||
}
|
||||
|
||||
// NewRelayConfig create valid RelayConfig from viper configuration. If invalid configuration detected, return error.
|
||||
func NewRelayConfig() (*RelayConfig, error) {
|
||||
domain, err := url.ParseRequestURI("https://" + viper.GetString("RELAY_DOMAIN"))
|
||||
if err != nil {
|
||||
return nil, errors.New("RELAY_DOMAIN: " + err.Error())
|
||||
}
|
||||
|
||||
iconURL, err := url.ParseRequestURI(viper.GetString("RELAY_ICON"))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "RELAY_ICON IS INVALID OR EMPTY. THIS COLUMN IS DISABLED.")
|
||||
iconURL = nil
|
||||
}
|
||||
|
||||
imageURL, err := url.ParseRequestURI(viper.GetString("RELAY_IMAGE"))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "RELAY_IMAGE IS INVALID OR EMPTY. THIS COLUMN IS DISABLED.")
|
||||
imageURL = nil
|
||||
}
|
||||
|
||||
privateKey, err := readPrivateKeyRSA(viper.GetString("ACTOR_PEM"))
|
||||
if err != nil {
|
||||
return nil, errors.New("ACTOR_PEM: " + err.Error())
|
||||
}
|
||||
|
||||
redisURL := viper.GetString("REDIS_URL")
|
||||
redisOption, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
return nil, errors.New("REDIS_URL: " + err.Error())
|
||||
}
|
||||
redisClient := redis.NewClient(redisOption)
|
||||
err = redisClient.Ping().Err()
|
||||
if err != nil {
|
||||
return nil, errors.New("Redis Connection Test: " + err.Error())
|
||||
}
|
||||
|
||||
serverBind := viper.GetString("RELAY_BIND")
|
||||
|
||||
return &RelayConfig{
|
||||
actorKey: privateKey,
|
||||
domain: domain,
|
||||
redisClient: redisClient,
|
||||
redisURL: redisURL,
|
||||
serverBind: serverBind,
|
||||
serviceName: viper.GetString("RELAY_SERVICENAME"),
|
||||
serviceSummary: viper.GetString("RELAY_SUMMARY"),
|
||||
serviceIconURL: iconURL,
|
||||
serviceImageURL: imageURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ServerBind is API Server's bind interface definition.
|
||||
func (relayConfig *RelayConfig) ServerBind() string {
|
||||
return relayConfig.serverBind
|
||||
}
|
||||
|
||||
// ServerHostname is API Server's hostname definition.
|
||||
func (relayConfig *RelayConfig) ServerHostname() *url.URL {
|
||||
return relayConfig.domain
|
||||
}
|
||||
|
||||
// ServerHostname is API Server's hostname definition.
|
||||
func (relayConfig *RelayConfig) ServerServicename() string {
|
||||
return relayConfig.serviceName
|
||||
}
|
||||
|
||||
// CreateRedisClient is create new redis client from RelayConfig.
|
||||
func (relayConfig *RelayConfig) RedisClient() *redis.Client {
|
||||
return relayConfig.redisClient
|
||||
}
|
||||
|
||||
// DumpWelcomeMessage provide build and config information string.
|
||||
func (relayConfig *RelayConfig) DumpWelcomeMessage(moduleName string, version string) string {
|
||||
return fmt.Sprintf(`Welcome to YUKIMOCHI Activity-Relay %s - %s
|
||||
- Configuration
|
||||
RELAY NAME : %s
|
||||
RELAY DOMAIN : %s
|
||||
REDIS URL : %s
|
||||
BIND ADDRESS : %s
|
||||
`, version, moduleName, relayConfig.serviceName, relayConfig.domain.Host, relayConfig.redisURL, relayConfig.serverBind)
|
||||
}
|
||||
|
||||
// NewMachineryServer create Redis backed Machinery Server from RelayConfig.
|
||||
func NewMachineryServer(globalConfig *RelayConfig) (*machinery.Server, error) {
|
||||
cnf := &config.Config{
|
||||
Broker: globalConfig.redisURL,
|
||||
DefaultQueue: "relay",
|
||||
ResultBackend: globalConfig.redisURL,
|
||||
ResultsExpireIn: 1,
|
||||
}
|
||||
newServer, err := machinery.NewServer(cnf)
|
||||
|
||||
return newServer, err
|
||||
}
|
109
models/config_test.go
Normal file
109
models/config_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestNewRelayConfig(t *testing.T) {
|
||||
t.Run("success valid configuration", func(t *testing.T) {
|
||||
relayConfig, err := NewRelayConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if relayConfig.serverBind != "0.0.0.0:8080" {
|
||||
t.Error("Failed parse: RelayConfig.serverBind")
|
||||
}
|
||||
if relayConfig.domain.Host != "relay.toot.yukimochi.jp" {
|
||||
t.Error("Failed parse: RelayConfig.domain")
|
||||
}
|
||||
if relayConfig.serviceName != "YUKIMOCHI Toot Relay Service" {
|
||||
t.Error("Failed parse: RelayConfig.serviceName")
|
||||
}
|
||||
if relayConfig.serviceSummary != "YUKIMOCHI Toot Relay Service is Running by Activity-Relay" {
|
||||
t.Error("Failed parse: RelayConfig.serviceSummary")
|
||||
}
|
||||
if relayConfig.serviceIconURL.String() != "https://example.com/example_icon.png" {
|
||||
t.Error("Failed parse: RelayConfig.serviceIconURL")
|
||||
}
|
||||
if relayConfig.serviceImageURL.String() != "https://example.com/example_image.png" {
|
||||
t.Error("Failed parse: RelayConfig.serviceImageURL")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail invalid configuration", func(t *testing.T) {
|
||||
invalidConfig := map[string]string{
|
||||
"ACTOR_PEM@notFound": "../misc/test/notfound.pem",
|
||||
"ACTOR_PEM@invalidKey": "../misc/test/actor.dh.pem",
|
||||
"REDIS_URL@invalidURL": "",
|
||||
"REDIS_URL@unreachableHost": "redis://localhost:6380",
|
||||
}
|
||||
|
||||
for key, value := range invalidConfig {
|
||||
viperKey := strings.Split(key, "@")[0]
|
||||
valid := viper.GetString(viperKey)
|
||||
|
||||
viper.Set(viperKey, value)
|
||||
_, err := NewRelayConfig()
|
||||
if err == nil {
|
||||
t.Error("Failed catch error: " + key)
|
||||
}
|
||||
|
||||
viper.Set(viperKey, valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createRelayConfig(t *testing.T) *RelayConfig {
|
||||
relayConfig, err := NewRelayConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return relayConfig
|
||||
}
|
||||
|
||||
func TestRelayConfig_ServerBind(t *testing.T) {
|
||||
relayConfig := createRelayConfig(t)
|
||||
if relayConfig.ServerBind() != relayConfig.serverBind {
|
||||
t.Error("Failed accessor: ServerBind()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelayConfig_ServerHostname(t *testing.T) {
|
||||
relayConfig := createRelayConfig(t)
|
||||
if relayConfig.ServerHostname() != relayConfig.domain {
|
||||
t.Error("Failed accessor: ServerHostname()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelayConfig_DumpWelcomeMessage(t *testing.T) {
|
||||
relayconfig := createRelayConfig(t)
|
||||
w := relayconfig.DumpWelcomeMessage("Testing", "")
|
||||
|
||||
informations := map[string]string{
|
||||
"module NAME": "Testing",
|
||||
"RELAY NANE": relayconfig.serviceName,
|
||||
"RELAY DOMAIN": relayconfig.domain.Host,
|
||||
"REDIS URL": relayconfig.redisURL,
|
||||
"BIND ADDRESS": relayconfig.serverBind,
|
||||
}
|
||||
|
||||
for key, information := range informations {
|
||||
if !strings.Contains(w, information) {
|
||||
t.Error("Missed welcome message information: ", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMachineryServer(t *testing.T) {
|
||||
relayConfig := createRelayConfig(t)
|
||||
|
||||
_, err := NewMachineryServer(relayConfig)
|
||||
if err != nil {
|
||||
t.Error("Failed create machinery server: ", err)
|
||||
}
|
||||
}
|
320
models/models.go
Normal file
320
models/models.go
Normal file
@ -0,0 +1,320 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// PublicKey : Activity Certificate.
|
||||
type PublicKey struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
PublicKeyPem string `json:"publicKeyPem,omitempty"`
|
||||
}
|
||||
|
||||
//Endpoints : Contains SharedInbox address.
|
||||
type Endpoints struct {
|
||||
SharedInbox string `json:"sharedInbox,omitempty"`
|
||||
}
|
||||
|
||||
// Image : Image Object.
|
||||
type Image struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Actor : ActivityPub Actor.
|
||||
type Actor struct {
|
||||
Context interface{} `json:"@context,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
PreferredUsername string `json:"preferredUsername,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Inbox string `json:"inbox,omitempty"`
|
||||
Endpoints *Endpoints `json:"endpoints,omitempty"`
|
||||
PublicKey PublicKey `json:"publicKey,omitempty"`
|
||||
Icon *Image `json:"icon,omitempty"`
|
||||
Image *Image `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateSelfKey : Generate relay Actor from Publickey.
|
||||
func (actor *Actor) GenerateSelfKey(hostname *url.URL, publickey *rsa.PublicKey) {
|
||||
actor.Context = []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"}
|
||||
actor.ID = hostname.String() + "/actor"
|
||||
actor.Type = "Service"
|
||||
actor.PreferredUsername = "relay"
|
||||
actor.Inbox = hostname.String() + "/inbox"
|
||||
actor.PublicKey = PublicKey{
|
||||
hostname.String() + "/actor#main-key",
|
||||
hostname.String() + "/actor",
|
||||
generatePublicKeyPEMString(publickey),
|
||||
}
|
||||
}
|
||||
|
||||
func NewActivityPubActorFromSelfKey(globalConfig *RelayConfig) Actor {
|
||||
hostname := globalConfig.domain.String()
|
||||
publicKey := &globalConfig.actorKey.PublicKey
|
||||
publicKeyPemString := generatePublicKeyPEMString(publicKey)
|
||||
|
||||
newActor := Actor{
|
||||
Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"},
|
||||
ID: hostname + "/actor",
|
||||
Type: "Service",
|
||||
Name: globalConfig.serviceName,
|
||||
PreferredUsername: "relay",
|
||||
Summary: globalConfig.serviceSummary,
|
||||
Inbox: hostname + "/inbox",
|
||||
PublicKey: struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
PublicKeyPem string `json:"publicKeyPem,omitempty"`
|
||||
}{
|
||||
ID: hostname + "/actor#main-key",
|
||||
Owner: hostname + "/actor",
|
||||
PublicKeyPem: publicKeyPemString,
|
||||
},
|
||||
}
|
||||
|
||||
if globalConfig.serviceIconURL != nil {
|
||||
newActor.Icon = &Image{
|
||||
URL: globalConfig.serviceIconURL.String(),
|
||||
}
|
||||
}
|
||||
if globalConfig.serviceImageURL != nil {
|
||||
newActor.Image = &Image{
|
||||
URL: globalConfig.serviceImageURL.String(),
|
||||
}
|
||||
}
|
||||
|
||||
return newActor
|
||||
}
|
||||
|
||||
// RetrieveRemoteActor : Retrieve Actor from remote instance.
|
||||
func (actor *Actor) RetrieveRemoteActor(url string, uaString string, cache *cache.Cache) error {
|
||||
var err error
|
||||
cacheData, found := cache.Get(url)
|
||||
if found {
|
||||
err = json.Unmarshal(cacheData.([]byte), &actor)
|
||||
if err != nil {
|
||||
cache.Delete(url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Accept", "application/activity+json")
|
||||
req.Header.Set("User-Agent", uaString)
|
||||
client := new(http.Client)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New(resp.Status)
|
||||
}
|
||||
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(data, &actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache.Set(url, data, 5*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activity : ActivityPub Activity.
|
||||
type Activity struct {
|
||||
Context interface{} `json:"@context,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Actor string `json:"actor,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Object interface{} `json:"object,omitempty"`
|
||||
To []string `json:"to,omitempty"`
|
||||
Cc []string `json:"cc,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateResponse : Generate activity response.
|
||||
func (activity *Activity) GenerateResponse(host *url.URL, responseType string) Activity {
|
||||
return Activity{
|
||||
[]string{"https://www.w3.org/ns/activitystreams"},
|
||||
host.String() + "/activities/" + uuid.NewV4().String(),
|
||||
host.String() + "/actor",
|
||||
responseType,
|
||||
&activity,
|
||||
nil,
|
||||
nil,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAnnounce : Generate Announce of activity.
|
||||
func (activity *Activity) GenerateAnnounce(host *url.URL) Activity {
|
||||
return Activity{
|
||||
[]string{"https://www.w3.org/ns/activitystreams"},
|
||||
host.String() + "/activities/" + uuid.NewV4().String(),
|
||||
host.String() + "/actor",
|
||||
"Announce",
|
||||
activity.ID,
|
||||
[]string{host.String() + "/actor/followers"},
|
||||
nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NestedActivity : Unwrap nested activity.
|
||||
func (activity *Activity) NestedActivity() (*Activity, error) {
|
||||
mappedObject := activity.Object.(map[string]interface{})
|
||||
if id, ok := mappedObject["id"].(string); ok {
|
||||
if nestedType, ok := mappedObject["type"].(string); ok {
|
||||
actor, ok := mappedObject["actor"].(string)
|
||||
if !ok {
|
||||
actor = ""
|
||||
}
|
||||
switch object := mappedObject["object"].(type) {
|
||||
case string:
|
||||
return &Activity{
|
||||
ID: id,
|
||||
Type: nestedType,
|
||||
Actor: actor,
|
||||
Object: object,
|
||||
}, nil
|
||||
default:
|
||||
return &Activity{
|
||||
ID: id,
|
||||
Type: nestedType,
|
||||
Actor: actor,
|
||||
Object: mappedObject["object"],
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Can't assart type")
|
||||
}
|
||||
return nil, errors.New("Can't assart id")
|
||||
}
|
||||
|
||||
// ActivityObject : ActivityPub Activity.
|
||||
type ActivityObject struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
To []string `json:"to,omitempty"`
|
||||
Cc []string `json:"cc,omitempty"`
|
||||
}
|
||||
|
||||
// Signature : ActivityPub Header Signature.
|
||||
type Signature struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Creator string `json:"creator,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
SignatureValue string `json:"signatureValue,omitempty"`
|
||||
}
|
||||
|
||||
// WebfingerLink : Webfinger Link Resource.
|
||||
type WebfingerLink struct {
|
||||
Rel string `json:"rel,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// WebfingerResource : Webfinger Resource.
|
||||
type WebfingerResource struct {
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Links []WebfingerLink `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateFromActor : Generate Webfinger resource from Actor.
|
||||
func (resource *WebfingerResource) GenerateFromActor(hostname *url.URL, actor *Actor) {
|
||||
resource.Subject = "acct:" + actor.PreferredUsername + "@" + hostname.Host
|
||||
resource.Links = []WebfingerLink{
|
||||
WebfingerLink{
|
||||
"self",
|
||||
"application/activity+json",
|
||||
actor.ID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NodeinfoResources : Nodeinfo Resources.
|
||||
type NodeinfoResources struct {
|
||||
NodeinfoLinks NodeinfoLinks
|
||||
Nodeinfo Nodeinfo
|
||||
}
|
||||
|
||||
// NodeinfoLinks : Nodeinfo Link Resource.
|
||||
type NodeinfoLinks struct {
|
||||
Links []NodeinfoLink `json:"links"`
|
||||
}
|
||||
|
||||
// NodeinfoLink : Nodeinfo Link Resource.
|
||||
type NodeinfoLink struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
// Nodeinfo : Nodeinfo Resource.
|
||||
type Nodeinfo struct {
|
||||
Version string `json:"version"`
|
||||
Software NodeinfoSoftware `json:"software"`
|
||||
Protocols []string `json:"protocols"`
|
||||
Services NodeinfoServices `json:"services"`
|
||||
OpenRegistrations bool `json:"openRegistrations"`
|
||||
Usage NodeinfoUsage `json:"usage"`
|
||||
Metadata NodeinfoMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// NodeinfoSoftware : NodeinfoSoftware Resource.
|
||||
type NodeinfoSoftware struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
// NodeinfoServices : NodeinfoSoftware Resource.
|
||||
type NodeinfoServices struct {
|
||||
Inbound []string `json:"inbound"`
|
||||
Outbound []string `json:"outbound"`
|
||||
}
|
||||
|
||||
// NodeinfoUsage : NodeinfoUsage Resource.
|
||||
type NodeinfoUsage struct {
|
||||
Users NodeinfoUsageUsers `json:"users"`
|
||||
}
|
||||
|
||||
// NodeinfoUsageUsers : NodeinfoUsageUsers Resource.
|
||||
type NodeinfoUsageUsers struct {
|
||||
Total int `json:"total"`
|
||||
ActiveMonth int `json:"activeMonth"`
|
||||
ActiveHalfyear int `json:"activeHalfyear"`
|
||||
}
|
||||
|
||||
// NodeinfoMetadata : NodeinfoMetadata Resource.
|
||||
type NodeinfoMetadata struct {
|
||||
}
|
||||
|
||||
// GenerateFromActor : Generate Webfinger resource from Actor.
|
||||
func (resource *NodeinfoResources) GenerateFromActor(hostname *url.URL, actor *Actor, serverVersion string) {
|
||||
resource.NodeinfoLinks.Links = []NodeinfoLink{
|
||||
NodeinfoLink{
|
||||
"http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
"https://" + hostname.Host + "/nodeinfo/2.1",
|
||||
},
|
||||
}
|
||||
resource.Nodeinfo = Nodeinfo{
|
||||
"2.1",
|
||||
NodeinfoSoftware{"activity-relay", serverVersion, "https://github.com/yukimochi/Activity-Relay"},
|
||||
[]string{"activitypub"},
|
||||
NodeinfoServices{[]string{}, []string{}},
|
||||
true,
|
||||
NodeinfoUsage{NodeinfoUsageUsers{0, 0, 0}},
|
||||
NodeinfoMetadata{},
|
||||
}
|
||||
}
|
38
models/models_test.go
Normal file
38
models/models_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var globalConfig *RelayConfig
|
||||
var relayState RelayState
|
||||
var ch chan bool
|
||||
|
||||
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")
|
||||
|
||||
globalConfig, err = NewRelayConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
relayState = NewState(globalConfig.RedisClient(), true)
|
||||
ch = make(chan bool)
|
||||
relayState.ListenNotify(ch)
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
202
models/state.go
Normal file
202
models/state.go
Normal file
@ -0,0 +1,202 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
// Config : Enum for RelayConfig
|
||||
type Config int
|
||||
|
||||
const (
|
||||
// BlockService : Blocking for service-type actor
|
||||
BlockService Config = iota
|
||||
// ManuallyAccept : Manually accept follow-request
|
||||
ManuallyAccept
|
||||
// CreateAsAnnounce : Announce activity instead of relay create activity
|
||||
CreateAsAnnounce
|
||||
)
|
||||
|
||||
// RelayState : Store subscriptions and relay configurations
|
||||
type RelayState struct {
|
||||
RedisClient *redis.Client
|
||||
notifiable bool
|
||||
|
||||
RelayConfig relayConfig `json:"relayConfig,omitempty"`
|
||||
LimitedDomains []string `json:"limitedDomains,omitempty"`
|
||||
BlockedDomains []string `json:"blockedDomains,omitempty"`
|
||||
Subscriptions []Subscription `json:"subscriptions,omitempty"`
|
||||
}
|
||||
|
||||
// NewState : Create new RelayState instance with redis client
|
||||
func NewState(redisClient *redis.Client, notifiable bool) RelayState {
|
||||
var config RelayState
|
||||
config.RedisClient = redisClient
|
||||
config.notifiable = notifiable
|
||||
|
||||
config.Load()
|
||||
return config
|
||||
}
|
||||
|
||||
func (config *RelayState) ListenNotify(c chan<- bool) {
|
||||
_, err := config.RedisClient.Subscribe("relay_refresh").Receive()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ch := config.RedisClient.Subscribe("relay_refresh").Channel()
|
||||
|
||||
cNotify := c != nil
|
||||
go func() {
|
||||
for range ch {
|
||||
fmt.Println("Config refreshed from state changed notify.")
|
||||
config.Load()
|
||||
if cNotify {
|
||||
c <- true
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Load : Refrash content from redis
|
||||
func (config *RelayState) Load() {
|
||||
config.RelayConfig.load(config.RedisClient)
|
||||
var limitedDomains []string
|
||||
var blockedDomains []string
|
||||
var subscriptions []Subscription
|
||||
domains, _ := config.RedisClient.HKeys("relay:config:limitedDomain").Result()
|
||||
for _, domain := range domains {
|
||||
limitedDomains = append(limitedDomains, domain)
|
||||
}
|
||||
domains, _ = config.RedisClient.HKeys("relay:config:blockedDomain").Result()
|
||||
for _, domain := range domains {
|
||||
blockedDomains = append(blockedDomains, domain)
|
||||
}
|
||||
domains, _ = config.RedisClient.Keys("relay:subscription:*").Result()
|
||||
for _, domain := range domains {
|
||||
domainName := strings.Replace(domain, "relay:subscription:", "", 1)
|
||||
inboxURL, _ := config.RedisClient.HGet(domain, "inbox_url").Result()
|
||||
activityID, err := config.RedisClient.HGet(domain, "activity_id").Result()
|
||||
if err != nil {
|
||||
activityID = ""
|
||||
}
|
||||
actorID, err := config.RedisClient.HGet(domain, "actor_id").Result()
|
||||
if err != nil {
|
||||
actorID = ""
|
||||
}
|
||||
subscriptions = append(subscriptions, Subscription{domainName, inboxURL, activityID, actorID})
|
||||
}
|
||||
config.LimitedDomains = limitedDomains
|
||||
config.BlockedDomains = blockedDomains
|
||||
config.Subscriptions = subscriptions
|
||||
}
|
||||
|
||||
// SetConfig : Set relay configration
|
||||
func (config *RelayState) SetConfig(key Config, value bool) {
|
||||
strValue := 0
|
||||
if value {
|
||||
strValue = 1
|
||||
}
|
||||
switch key {
|
||||
case BlockService:
|
||||
config.RedisClient.HSet("relay:config", "block_service", strValue).Result()
|
||||
case ManuallyAccept:
|
||||
config.RedisClient.HSet("relay:config", "manually_accept", strValue).Result()
|
||||
case CreateAsAnnounce:
|
||||
config.RedisClient.HSet("relay:config", "create_as_announce", strValue).Result()
|
||||
}
|
||||
|
||||
config.refresh()
|
||||
}
|
||||
|
||||
// AddSubscription : Add new instance for subscription list
|
||||
func (config *RelayState) AddSubscription(domain Subscription) {
|
||||
config.RedisClient.HMSet("relay:subscription:"+domain.Domain, map[string]interface{}{
|
||||
"inbox_url": domain.InboxURL,
|
||||
"activity_id": domain.ActivityID,
|
||||
"actor_id": domain.ActorID,
|
||||
})
|
||||
|
||||
config.refresh()
|
||||
}
|
||||
|
||||
// DelSubscription : Delete instance from subscription list
|
||||
func (config *RelayState) DelSubscription(domain string) {
|
||||
config.RedisClient.Del("relay:subscription:" + domain).Result()
|
||||
config.RedisClient.Del("relay:pending:" + domain).Result()
|
||||
|
||||
config.refresh()
|
||||
}
|
||||
|
||||
// SelectSubscription : Select instance from string
|
||||
func (config *RelayState) SelectSubscription(domain string) *Subscription {
|
||||
for _, subscription := range config.Subscriptions {
|
||||
if domain == subscription.Domain {
|
||||
return &subscription
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetBlockedDomain : Set/Unset instance for blocked domain
|
||||
func (config *RelayState) SetBlockedDomain(domain string, value bool) {
|
||||
if value {
|
||||
config.RedisClient.HSet("relay:config:blockedDomain", domain, "1").Result()
|
||||
} else {
|
||||
config.RedisClient.HDel("relay:config:blockedDomain", domain).Result()
|
||||
}
|
||||
|
||||
config.refresh()
|
||||
}
|
||||
|
||||
// SetLimitedDomain : Set/Unset instance for limited domain
|
||||
func (config *RelayState) SetLimitedDomain(domain string, value bool) {
|
||||
if value {
|
||||
config.RedisClient.HSet("relay:config:limitedDomain", domain, "1").Result()
|
||||
} else {
|
||||
config.RedisClient.HDel("relay:config:limitedDomain", domain).Result()
|
||||
}
|
||||
|
||||
config.refresh()
|
||||
}
|
||||
|
||||
func (config *RelayState) refresh() {
|
||||
if config.notifiable {
|
||||
config.RedisClient.Publish("relay_refresh", "Config refreshing request.")
|
||||
} else {
|
||||
config.Load()
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription : Instance subscription information
|
||||
type Subscription struct {
|
||||
Domain string `json:"domain,omitempty"`
|
||||
InboxURL string `json:"inbox_url,omitempty"`
|
||||
ActivityID string `json:"activity_id,omitempty"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
}
|
||||
|
||||
type relayConfig struct {
|
||||
BlockService bool `json:"blockService,omitempty"`
|
||||
ManuallyAccept bool `json:"manuallyAccept,omitempty"`
|
||||
CreateAsAnnounce bool `json:"createAsAnnounce,omitempty"`
|
||||
}
|
||||
|
||||
func (config *relayConfig) load(redisClient *redis.Client) {
|
||||
blockService, err := redisClient.HGet("relay:config", "block_service").Result()
|
||||
if err != nil {
|
||||
blockService = "0"
|
||||
}
|
||||
manuallyAccept, err := redisClient.HGet("relay:config", "manually_accept").Result()
|
||||
if err != nil {
|
||||
manuallyAccept = "0"
|
||||
}
|
||||
createAsAnnounce, err := redisClient.HGet("relay:config", "create_as_announce").Result()
|
||||
if err != nil {
|
||||
createAsAnnounce = "0"
|
||||
}
|
||||
config.BlockService = blockService == "1"
|
||||
config.ManuallyAccept = manuallyAccept == "1"
|
||||
config.CreateAsAnnounce = createAsAnnounce == "1"
|
||||
}
|
190
models/state_test.go
Normal file
190
models/state_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadEmpty(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
relayState.Load()
|
||||
|
||||
if relayState.RelayConfig.BlockService != false {
|
||||
t.Fatalf("Failed read config.")
|
||||
}
|
||||
if relayState.RelayConfig.CreateAsAnnounce != false {
|
||||
t.Fatalf("Failed read config.")
|
||||
}
|
||||
if relayState.RelayConfig.ManuallyAccept != false {
|
||||
t.Fatalf("Failed read config.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfig(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
relayState.SetConfig(BlockService, true)
|
||||
<-ch
|
||||
if relayState.RelayConfig.BlockService != true {
|
||||
t.Fatalf("Failed enable config.")
|
||||
}
|
||||
relayState.SetConfig(CreateAsAnnounce, true)
|
||||
<-ch
|
||||
if relayState.RelayConfig.CreateAsAnnounce != true {
|
||||
t.Fatalf("Failed enable config.")
|
||||
}
|
||||
relayState.SetConfig(ManuallyAccept, true)
|
||||
<-ch
|
||||
if relayState.RelayConfig.ManuallyAccept != true {
|
||||
t.Fatalf("Failed enable config.")
|
||||
}
|
||||
|
||||
relayState.SetConfig(BlockService, false)
|
||||
<-ch
|
||||
if relayState.RelayConfig.BlockService != false {
|
||||
t.Fatalf("Failed disable config.")
|
||||
}
|
||||
relayState.SetConfig(CreateAsAnnounce, false)
|
||||
<-ch
|
||||
if relayState.RelayConfig.CreateAsAnnounce != false {
|
||||
t.Fatalf("Failed disable config.")
|
||||
}
|
||||
relayState.SetConfig(ManuallyAccept, false)
|
||||
<-ch
|
||||
if relayState.RelayConfig.ManuallyAccept != false {
|
||||
t.Fatalf("Failed disable config.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreatSubscriptionNotify(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
relayState.AddSubscription(Subscription{
|
||||
Domain: "example.com",
|
||||
InboxURL: "https://example.com/inbox",
|
||||
})
|
||||
<-ch
|
||||
|
||||
valid := false
|
||||
for _, domain := range relayState.Subscriptions {
|
||||
if domain.Domain == "example.com" && domain.InboxURL == "https://example.com/inbox" {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
|
||||
relayState.DelSubscription("example.com")
|
||||
<-ch
|
||||
|
||||
for _, domain := range relayState.Subscriptions {
|
||||
if domain.Domain == "example.com" {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectDomain(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
exampleSubscription := Subscription{
|
||||
Domain: "example.com",
|
||||
InboxURL: "https://example.com/inbox",
|
||||
}
|
||||
|
||||
relayState.AddSubscription(exampleSubscription)
|
||||
<-ch
|
||||
|
||||
subscription := relayState.SelectSubscription("example.com")
|
||||
if *subscription != exampleSubscription {
|
||||
t.Fatalf("Failed select domain.")
|
||||
}
|
||||
|
||||
subscription = relayState.SelectSubscription("example.org")
|
||||
if subscription != nil {
|
||||
t.Fatalf("Failed select domain.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockedDomain(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
relayState.SetBlockedDomain("example.com", true)
|
||||
<-ch
|
||||
|
||||
valid := false
|
||||
for _, domain := range relayState.BlockedDomains {
|
||||
if domain == "example.com" {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
|
||||
relayState.SetBlockedDomain("example.com", false)
|
||||
<-ch
|
||||
|
||||
for _, domain := range relayState.BlockedDomains {
|
||||
if domain == "example.com" {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitedDomain(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
relayState.SetLimitedDomain("example.com", true)
|
||||
<-ch
|
||||
|
||||
valid := false
|
||||
for _, domain := range relayState.LimitedDomains {
|
||||
if domain == "example.com" {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
|
||||
relayState.SetLimitedDomain("example.com", false)
|
||||
<-ch
|
||||
|
||||
for _, domain := range relayState.LimitedDomains {
|
||||
if domain == "example.com" {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed write config.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCompatiSubscription(t *testing.T) {
|
||||
relayState.RedisClient.FlushAll().Result()
|
||||
|
||||
relayState.AddSubscription(Subscription{
|
||||
Domain: "example.com",
|
||||
InboxURL: "https://example.com/inbox",
|
||||
})
|
||||
|
||||
relayState.RedisClient.HDel("relay:subscription:example.com", "activity_id", "actor_id")
|
||||
relayState.Load()
|
||||
|
||||
valid := false
|
||||
for _, domain := range relayState.Subscriptions {
|
||||
if domain.Domain == "example.com" && domain.InboxURL == "https://example.com/inbox" {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
t.Fatalf("Failed load compati config.")
|
||||
}
|
||||
}
|
71
models/utils.go
Normal file
71
models/utils.go
Normal file
@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
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 redisHGetOrCreateWithDefault(redisClient *redis.Client, key string, field string, defaultValue string) (string, error) {
|
||||
keyExist, err := redisClient.HExists(key, field).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if keyExist {
|
||||
value, err := redisClient.HGet(key, field).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
} else {
|
||||
_, err := redisClient.HSet(key, field, defaultValue).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readPrivateKeyRSA(keyPath string) (*rsa.PrivateKey, error) {
|
||||
file, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, _ := pem.Decode(file)
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decoded.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privateKey, 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)
|
||||
}
|
51
models/utils_test.go
Normal file
51
models/utils_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedisHGetOrCreateWithDefault(t *testing.T) {
|
||||
relayConfig := createRelayConfig(t)
|
||||
|
||||
t.Run("Execute HGet when value exist", func(t *testing.T) {
|
||||
_, err := relayConfig.redisClient.HSet("gotest:redis:hget:or:create:with:default", "exist", "1").Result()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
value, err := redisHGetOrCreateWithDefault(relayConfig.redisClient, "gotest:redis:hget:or:create:with:default", "exist", "2")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if value != "1" {
|
||||
t.Error(errors.New("value is override by redisHGetOrCreateWithDefault"))
|
||||
}
|
||||
|
||||
_, err = relayConfig.redisClient.HDel("gotest:redis:hget:or:create:with:default", "exist").Result()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Execute HGet when value not exist", func(t *testing.T) {
|
||||
_, err := redisHGetOrCreateWithDefault(relayConfig.redisClient, "gotest:redis:hget:or:create:with:default", "not_exist", "2")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
value, err := relayConfig.redisClient.HGet("gotest:redis:hget:or:create:with:default", "not_exist").Result()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if value != "2" {
|
||||
t.Error(errors.New("redisHGetOrCreateWithDefault is not write default value successfully"))
|
||||
}
|
||||
|
||||
_, err = relayConfig.redisClient.HDel("gotest:redis:hget:or:create:with:default", "not_exist").Result()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user