Mono-binarify for api server.

This commit is contained in:
Naoki Kosaka
2021-06-18 09:25:55 +09:00
parent 7894e78c3f
commit 73a1f2231c
30 changed files with 786 additions and 334 deletions

121
models/config.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
})
}