mirror of
https://git.ptzo.gdn/feditools/relay.git
synced 2024-09-21 11:27:11 +00:00
Inbox (#25)
This commit is contained in:
parent
0bd3cb0ea1
commit
809924d33d
@ -21,11 +21,7 @@ builds:
|
||||
- arm64
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
universal_binaries:
|
||||
- replace: true
|
||||
|
18
Makefile
18
Makefile
@ -18,6 +18,14 @@ clean:
|
||||
@rm -Rvf coverage.txt dist relay
|
||||
@find . -name ".DS_Store" -exec rm -v {} \;
|
||||
|
||||
docker-restart: docker-stop docker-start
|
||||
|
||||
docker-start:
|
||||
docker-compose --project-name ${PROJECT_NAME} -f deployments/docker-compose-test.yaml up -d
|
||||
|
||||
docker-stop:
|
||||
docker-compose --project-name ${PROJECT_NAME} -f deployments/docker-compose-test.yaml down
|
||||
|
||||
fmt:
|
||||
@echo formatting
|
||||
@go fmt $(shell go list ./... | grep -v /vendor/)
|
||||
@ -35,14 +43,6 @@ lint:
|
||||
@echo linting
|
||||
@golint $(shell go list ./... | grep -v /vendor/)
|
||||
|
||||
test-docker-restart: test-docker-stop test-docker-start
|
||||
|
||||
test-docker-start:
|
||||
docker-compose --project-name ${PROJECT_NAME} -f deployments/docker-compose-test.yaml up -d
|
||||
|
||||
test-docker-stop:
|
||||
docker-compose --project-name ${PROJECT_NAME} -f deployments/docker-compose-test.yaml down
|
||||
|
||||
test: tidy fmt lint #gosec
|
||||
go test -cover ./...
|
||||
|
||||
@ -58,4 +58,4 @@ tidy:
|
||||
vendor: tidy
|
||||
go mod vendor
|
||||
|
||||
.PHONY: build-snapshot bun-new-migration clean fmt lint stage-static npm-scss npm-upgrade test-docker-restart test-docker-start test-docker-stop test test-ext test-race test-race-ext test-verbose tidy vendor
|
||||
.PHONY: build-snapshot bun-new-migration clean fmt lint stage-static npm-scss npm-upgrade docker-restart docker-start docker-stop test test-ext test-race test-race-ext test-verbose tidy vendor
|
||||
|
@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/feditools/relay/cmd/relay/action"
|
||||
"github.com/feditools/relay/internal/activitypub"
|
||||
"github.com/feditools/relay/internal/clock"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/feditools/relay/internal/db/bun"
|
||||
"github.com/feditools/relay/internal/http"
|
||||
"github.com/feditools/relay/internal/logic"
|
||||
"github.com/feditools/relay/internal/metrics/statsd"
|
||||
"github.com/feditools/relay/internal/runner/faktory"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tyrm/go-util"
|
||||
"os"
|
||||
@ -18,15 +20,18 @@ import (
|
||||
)
|
||||
|
||||
// Start starts the server
|
||||
var Start action.Action = func(ctx context.Context) error {
|
||||
var Start action.Action = func(topCtx context.Context) error {
|
||||
l := logger.WithField("func", "Start")
|
||||
|
||||
l.Info("starting")
|
||||
|
||||
ctx, cancel := context.WithCancel(topCtx)
|
||||
|
||||
// create metrics collector
|
||||
metricsCollector, err := statsd.New()
|
||||
if err != nil {
|
||||
l.Errorf("metrics: %s", err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
@ -41,6 +46,8 @@ var Start action.Action = func(ctx context.Context) error {
|
||||
dbClient, err := bun.New(ctx, metricsCollector)
|
||||
if err != nil {
|
||||
l.Errorf("db: %s", err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
@ -50,19 +57,38 @@ var Start action.Action = func(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// create clock module
|
||||
l.Debug("creating clock")
|
||||
clockMod := clock.NewClock()
|
||||
|
||||
// create logic module
|
||||
l.Debug("creating database client")
|
||||
logicMod, err := logic.New(dbClient)
|
||||
l.Debug("creating logic module")
|
||||
logicMod, err := logic.New(ctx, clockMod, dbClient)
|
||||
if err != nil {
|
||||
l.Errorf("db: %s", err.Error())
|
||||
l.Errorf("logic: %s", err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// create runner
|
||||
runnerMod, err := faktory.New(logicMod)
|
||||
if err != nil {
|
||||
l.Errorf("runner: %s", err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
logicMod.SetRunner(runnerMod)
|
||||
runnerMod.Start(ctx)
|
||||
|
||||
// create http server
|
||||
l.Debug("creating http server")
|
||||
server, err := http.NewServer(ctx, metricsCollector)
|
||||
if err != nil {
|
||||
l.Errorf("http server: %s", err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -70,9 +96,11 @@ var Start action.Action = func(ctx context.Context) error {
|
||||
var webModules []http.Module
|
||||
if util.ContainsString(viper.GetStringSlice(config.Keys.ServerRoles), config.ServerRoleActivityPub) {
|
||||
l.Infof("adding %s module", config.ServerRoleActivityPub)
|
||||
apMod, err := activitypub.New(ctx, dbClient, logicMod)
|
||||
apMod, err := activitypub.New(ctx, logicMod, runnerMod)
|
||||
if err != nil {
|
||||
l.Errorf("%s: %s", config.ServerRoleActivityPub, err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
webModules = append(webModules, apMod)
|
||||
@ -83,6 +111,8 @@ var Start action.Action = func(ctx context.Context) error {
|
||||
err := mod.Route(server)
|
||||
if err != nil {
|
||||
l.Errorf("loading %s module: %s", mod.Name(), err.Error())
|
||||
cancel()
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -107,10 +137,13 @@ var Start action.Action = func(ctx context.Context) error {
|
||||
select {
|
||||
case sig := <-stopSigChan:
|
||||
l.Infof("got sig: %s", sig)
|
||||
cancel()
|
||||
case err := <-errChan:
|
||||
l.Fatal(err.Error())
|
||||
cancel()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
l.Infof("done")
|
||||
return nil
|
||||
}
|
||||
|
@ -11,7 +11,11 @@ func Global(cmd *cobra.Command, values config.Values) {
|
||||
cmd.PersistentFlags().String(config.Keys.LogLevel, values.LogLevel, usage.LogLevel)
|
||||
|
||||
// application
|
||||
cmd.PersistentFlags().Int(config.Keys.ActorKeySize, values.ActorKeySize, usage.ActorKeySize)
|
||||
cmd.PersistentFlags().String(config.Keys.ApplicationName, values.ApplicationName, usage.ApplicationName)
|
||||
cmd.PersistentFlags().Int(config.Keys.CachedActivityLimit, values.CachedActivityLimit, usage.CachedActivityLimit)
|
||||
cmd.PersistentFlags().Int(config.Keys.CachedActorLimit, values.CachedActorLimit, usage.CachedActorLimit)
|
||||
cmd.PersistentFlags().Int(config.Keys.CachedDigestLimit, values.CachedDigestLimit, usage.CachedDigestLimit)
|
||||
|
||||
// database
|
||||
cmd.PersistentFlags().String(config.Keys.DbType, values.DbType, usage.DbType)
|
||||
|
@ -12,4 +12,7 @@ func Server(cmd *cobra.Command, values config.Values) {
|
||||
cmd.PersistentFlags().String(config.Keys.ServerHTTPBind, values.ServerHTTPBind, usage.ServerHTTPBind)
|
||||
cmd.PersistentFlags().Bool(config.Keys.ServerMinifyHTML, values.ServerMinifyHTML, usage.ServerMinifyHTML)
|
||||
cmd.PersistentFlags().StringArray(config.Keys.ServerRoles, values.ServerRoles, usage.ServerRoles)
|
||||
|
||||
// runner
|
||||
cmd.PersistentFlags().Int(config.Keys.RunnerConcurrency, values.RunnerConcurrency, usage.RunnerConcurrency)
|
||||
}
|
||||
|
@ -10,6 +10,15 @@ services:
|
||||
- POSTGRES_USER=test
|
||||
- POSTGRES_DB=test
|
||||
restart: always
|
||||
faktory:
|
||||
image: contribsys/faktory:1.6.1
|
||||
command: /faktory -b :7419 -w :7420 -e production
|
||||
ports:
|
||||
- 127.0.0.1:7419:7419/tcp
|
||||
- 127.0.0.1:7420:7420/tcp
|
||||
environment:
|
||||
- FAKTORY_PASSWORD=test
|
||||
restart: always
|
||||
|
||||
influxdb:
|
||||
image: influxdb:2.1
|
||||
|
9
go.mod
9
go.mod
@ -4,22 +4,27 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/cactus/go-statsd-client/v5 v5.0.0
|
||||
github.com/contribsys/faktory v1.6.0
|
||||
github.com/contribsys/faktory_worker_go v1.6.0
|
||||
github.com/go-fed/activity v1.0.0
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/jackc/pgconn v1.12.1
|
||||
github.com/jackc/pgx/v4 v4.16.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
|
||||
github.com/tyrm/go-util v0.4.2
|
||||
github.com/uptrace/bun v1.1.6
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.6
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.1.6
|
||||
github.com/uptrace/bun/extra/bundebug v1.1.6
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||
modernc.org/sqlite v1.17.0
|
||||
)
|
||||
|
||||
@ -32,7 +37,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
@ -58,6 +62,7 @@ require (
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -52,11 +52,16 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/contribsys/faktory v1.6.0 h1:tgJji4TDX8I8/BL1OZNf0Ik748lh0cARgUyhKMxWi64=
|
||||
github.com/contribsys/faktory v1.6.0/go.mod h1:i0je6v+dcCoAVdPZZaW8nQIHTj3az4ZOwBXegJK39aA=
|
||||
github.com/contribsys/faktory_worker_go v1.6.0 h1:ov69BLHL62i/wRLJwvuj5UphwgjMOINRCGW3KzrKOjk=
|
||||
github.com/contribsys/faktory_worker_go v1.6.0/go.mod h1:XMNGn3sBJdqFGfTH4SkmYkMovhdkq5cDJj36wowfbNY=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -79,6 +84,9 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/go-fed/activity v1.0.0 h1:j7w3auHZnVCjUcgA1mE+UqSOjFBhvW2Z2res3vNol+o=
|
||||
github.com/go-fed/activity v1.0.0/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@ -98,6 +106,8 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@ -110,6 +120,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@ -169,6 +180,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -373,6 +386,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -474,7 +488,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -1,136 +1,54 @@
|
||||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"github.com/feditools/relay/internal/logic"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/feditools/relay/internal/runner"
|
||||
"github.com/spf13/viper"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Module is an http module that handles activity pub activity
|
||||
type Module struct {
|
||||
db db.DB
|
||||
logic *logic.Logic
|
||||
logic *logic.Logic
|
||||
runner runner.Runner
|
||||
|
||||
appName string
|
||||
appVersion string
|
||||
domain string
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
publicKeyPem string
|
||||
}
|
||||
|
||||
// New creates a new activity pub module
|
||||
func New(ctx context.Context, d db.DB, l *logic.Logic) (*Module, error) {
|
||||
func New(ctx context.Context, l *logic.Logic, r runner.Runner) (*Module, error) {
|
||||
log := logger.WithField("func", "New")
|
||||
|
||||
module := &Module{
|
||||
db: d,
|
||||
logic: l,
|
||||
logic: l,
|
||||
runner: r,
|
||||
|
||||
appName: viper.GetString(config.Keys.ApplicationName),
|
||||
appVersion: viper.GetString(config.Keys.SoftwareVersion),
|
||||
domain: viper.GetString(config.Keys.ServerExternalHostname),
|
||||
}
|
||||
|
||||
var instanceSelf *models.Instance
|
||||
var err error
|
||||
instanceSelf, err = d.ReadInstanceByDomain(ctx, module.domain)
|
||||
// get self
|
||||
instanceSelf, err := module.logic.GetInstanceSelf(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
log.Errorf("getting self: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if instanceSelf == nil {
|
||||
instanceSelf, err = module.createInstanceSelf(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("create self: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// get public key pem
|
||||
publicKeyPem, err := instanceSelf.PublicKeyPEM()
|
||||
if err != nil {
|
||||
log.Errorf("getting instance pem: %s", err.Error())
|
||||
|
||||
privateKey, err := instanceSelf.GetPrivateKey()
|
||||
if err != nil {
|
||||
log.Errorf("decrypting private key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add keys
|
||||
module.privateKey = privateKey
|
||||
module.publicKey = instanceSelf.PublicKey
|
||||
|
||||
// make public key pem
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(instanceSelf.PublicKey)
|
||||
if err != nil {
|
||||
log.Errorf("marshaling public key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
publicKeyBlock := &pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}
|
||||
publicPem := new(bytes.Buffer)
|
||||
err = pem.Encode(publicPem, publicKeyBlock)
|
||||
if err != nil {
|
||||
log.Errorf("encoding pem: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
publicPemBytes, err := io.ReadAll(publicPem)
|
||||
if err != nil {
|
||||
log.Errorf("reading pem: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
module.publicKeyPem = string(publicPemBytes)
|
||||
module.publicKeyPem = publicKeyPem
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func (m *Module) createInstanceSelf(ctx context.Context) (*models.Instance, error) {
|
||||
l := logger.WithField("func", "createInstanceSelf")
|
||||
|
||||
// generate key
|
||||
privatekey, err := rsa.GenerateKey(rand.Reader, ActorKeySize)
|
||||
if err != nil {
|
||||
l.Errorf("genrating private key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publickey := &privatekey.PublicKey
|
||||
|
||||
// create new instance
|
||||
newInstance := &models.Instance{
|
||||
Domain: m.domain,
|
||||
InboxIRI: path.GenInbox(m.domain),
|
||||
|
||||
PublicKey: publickey,
|
||||
}
|
||||
|
||||
// set private key
|
||||
err = newInstance.SetPrivateKey(privatekey)
|
||||
if err != nil {
|
||||
l.Errorf("setting private key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add to database
|
||||
err = m.db.CreateInstance(ctx, newInstance)
|
||||
if err != nil {
|
||||
l.Errorf("db create: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newInstance, nil
|
||||
}
|
||||
|
||||
// Name return the module name
|
||||
func (m *Module) Name() string {
|
||||
return config.ServerRoleActivityPub
|
||||
|
@ -2,7 +2,7 @@ package activitypub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
apmodels "github.com/feditools/relay/internal/activitypub/models"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/http"
|
||||
@ -20,24 +20,25 @@ func (m *Module) actorGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) genRelayActor() *apmodels.Actor {
|
||||
return &apmodels.Actor{
|
||||
func (m *Module) genRelayActor() *models.Actor {
|
||||
return &models.Actor{
|
||||
Context: ContextActivityStreams,
|
||||
Endpoints: apmodels.Endpoints{
|
||||
SharedInbox: path.GenInbox(m.domain),
|
||||
Endpoints: models.Endpoints{
|
||||
SharedInbox: path.GenInbox(m.logic.Domain()),
|
||||
},
|
||||
Followers: path.GenFollowers(m.domain),
|
||||
Following: path.GenFollowing(m.domain),
|
||||
Followers: path.GenFollowers(m.logic.Domain()),
|
||||
Following: path.GenFollowing(m.logic.Domain()),
|
||||
Inbox: path.GenInbox(m.logic.Domain()),
|
||||
Name: m.appName,
|
||||
Type: "Application",
|
||||
ID: path.GenActor(m.domain),
|
||||
PublicKey: apmodels.PublicKey{
|
||||
ID: path.GenPublicKey(m.domain),
|
||||
Owner: path.GenActor(m.domain),
|
||||
ID: path.GenActor(m.logic.Domain()),
|
||||
PublicKey: models.PublicKey{
|
||||
ID: path.GenPublicKey(m.logic.Domain()),
|
||||
Owner: path.GenActor(m.logic.Domain()),
|
||||
PublicKeyPEM: m.publicKeyPem,
|
||||
},
|
||||
Summary: ActorSummary,
|
||||
PreferredUsername: "relay",
|
||||
URL: path.GenActor(m.domain),
|
||||
URL: path.GenActor(m.logic.Domain()),
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,22 @@
|
||||
package activitypub
|
||||
|
||||
import "github.com/go-fed/httpsig"
|
||||
|
||||
const (
|
||||
|
||||
// ActorKeySize is the key size used to generate a new key
|
||||
ActorKeySize = 4096
|
||||
ActorKeySize = 2048
|
||||
// ActorSummary is the description of the actor for the relay
|
||||
ActorSummary = "Feditools ActivityPub Relay - https://github.com/feditools/relay"
|
||||
|
||||
// ContextActivityStreams contains the context document for activity streams
|
||||
ContextActivityStreams = "https://www.w3.org/ns/activitystreams"
|
||||
)
|
||||
|
||||
var (
|
||||
validAlgs = []httpsig.Algorithm{
|
||||
httpsig.RSA_SHA512,
|
||||
httpsig.RSA_SHA256,
|
||||
httpsig.ED25519,
|
||||
}
|
||||
)
|
||||
|
129
internal/activitypub/inbox.go
Normal file
129
internal/activitypub/inbox.go
Normal file
@ -0,0 +1,129 @@
|
||||
package activitypub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
nethttp "net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (m *Module) inboxPostHandler(w nethttp.ResponseWriter, r *nethttp.Request) {
|
||||
l := logger.WithField("func", "inboxPostHandler")
|
||||
|
||||
// parse activity
|
||||
var activity models.Activity
|
||||
err := json.NewDecoder(r.Body).Decode(&activity)
|
||||
if err != nil {
|
||||
l.Errorf("decoding activity: %+v", err)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
l.Tracef("got activity: %#v", activity)
|
||||
|
||||
// parse actor uri
|
||||
actorI, ok := activity["actor"]
|
||||
if !ok {
|
||||
l.Debugf("activity missing actor: %+v", activity)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
actorStr, ok := actorI.(string)
|
||||
if !ok {
|
||||
l.Debugf("activity actor isn't string: %+v", activity)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
actorIRI, err := url.Parse(actorStr)
|
||||
if err != nil {
|
||||
l.Errorf("can't parts actor uri from %s: %s", actorStr, err.Error())
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// check request validation
|
||||
l.Tracef("validating actor: %s", actorStr)
|
||||
validated, actor := m.logic.ValidateRequest(r, actorIRI)
|
||||
if !validated {
|
||||
l.Debugf("validation failed for actor: %s", actor)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusUnauthorized), nethttp.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var instance *models.Instance
|
||||
switch actor.Type {
|
||||
case models.TypeApplication:
|
||||
instance, err = m.logic.GetInstanceForActor(r.Context(), actorIRI)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusUnauthorized), nethttp.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
l.Errorf("can't get instance %s: %s", actorStr, err.Error())
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusInternalServerError), nethttp.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
case models.TypePerson:
|
||||
instance, err = m.logic.GetInstance(r.Context(), actorIRI.Host)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusUnauthorized), nethttp.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
l.Errorf("can't get instance %s: %s", actorStr, err.Error())
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusInternalServerError), nethttp.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
l.Errorf("unknown actor type: %s", actor.Type)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// get activity type
|
||||
activityTypeI, ok := activity["type"]
|
||||
if !ok {
|
||||
l.Debugf("activity missing type: %+v", activity)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
activityType, ok := activityTypeI.(string)
|
||||
if !ok {
|
||||
l.Debugf("activity type isn't string: %+v", activity)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusBadRequest), nethttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// drop non-Follow activities from instances that don't follow our relay
|
||||
if activityType != models.TypeFollow && !instance.Followed {
|
||||
l.Debugf("got non follow from an unfollowed instance: %s", activityType)
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusUnauthorized), nethttp.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugf("headers: %+v", r.Header)
|
||||
l.Debugf("body: %s", activity)
|
||||
|
||||
// enqueue activity
|
||||
err = m.runner.EnqueueInboxActivity(r.Context(), instance.ID, actorStr, activity)
|
||||
if err != nil {
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusInternalServerError), nethttp.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(nethttp.StatusAccepted)
|
||||
}
|
@ -2,18 +2,23 @@ package activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
rhttp "github.com/feditools/relay/internal/http"
|
||||
"github.com/feditools/relay/internal/http"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/http"
|
||||
nethttp "net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (m *Module) middlewareCheckHTTPSig(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
type CTXVerifier struct {
|
||||
Verifier httpsig.Verifier
|
||||
}
|
||||
|
||||
func (m *Module) middlewareCheckHTTPSig(next nethttp.Handler) nethttp.Handler {
|
||||
return nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) {
|
||||
l := logger.WithField("func", "middlewareCheckHTTPSig")
|
||||
|
||||
l.Debugf("running middleware")
|
||||
|
||||
// create verifier
|
||||
verifier, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
@ -33,29 +38,28 @@ func (m *Module) middlewareCheckHTTPSig(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), rhttp.ContextKeyKeyVerifier, verifier)
|
||||
|
||||
ctx := context.WithValue(r.Context(), http.ContextKeyKeyVerifier, verifier)
|
||||
|
||||
// check for domain block
|
||||
isBlocked, err := m.logic.IsDomainBlocked(ctx, KeyIDURI.Host)
|
||||
if err != nil {
|
||||
l.Errorf("is domain blocked: %s", err.Error())
|
||||
w.Header().Set("Content-Type", mimetype.TextPlain)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))))
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusInternalServerError), nethttp.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if isBlocked {
|
||||
l.Debugf("domain %s is blocked", KeyIDURI.Host)
|
||||
w.Header().Set("Content-Type", mimetype.TextPlain)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(fmt.Sprintf("%d %s", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))))
|
||||
nethttp.Error(w, nethttp.StatusText(nethttp.StatusUnauthorized), nethttp.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// get signature
|
||||
signature := r.Header.Get("signature")
|
||||
if signature != "" {
|
||||
ctx = context.WithValue(ctx, rhttp.ContextKeyHTTPSignature, signature)
|
||||
ctx = context.WithValue(ctx, http.ContextKeyHTTPSignature, signature)
|
||||
}
|
||||
|
||||
// do request with verifier
|
||||
|
@ -1,29 +0,0 @@
|
||||
package models
|
||||
|
||||
// Actor represents an activity pub actor
|
||||
type Actor struct {
|
||||
Context string `json:"@context"`
|
||||
Endpoints Endpoints `json:"endpoints"`
|
||||
Followers string `json:"followers"`
|
||||
Following string `json:"following"`
|
||||
Inbox string `json:"inbox"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
PublicKey PublicKey `json:"publicKey"`
|
||||
Summary string `json:"summary"`
|
||||
PreferredUsername string `json:"preferredUsername"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Endpoints represents known activity pub endpoints
|
||||
type Endpoints struct {
|
||||
SharedInbox string `json:"sharedInbox"`
|
||||
}
|
||||
|
||||
// PublicKey represents an actor's public key
|
||||
type PublicKey struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
PublicKeyPEM string `json:"publicKeyPem"`
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package models
|
||||
|
||||
// WebFinger represents a web finger response
|
||||
type WebFinger struct {
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
}
|
@ -2,8 +2,7 @@ package activitypub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
rmodels "github.com/feditools/relay/internal/activitypub/models"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/http"
|
||||
@ -16,28 +15,27 @@ func (m *Module) nodeinfo20GetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
l.Errorf("get peers: %s", err.Error())
|
||||
w.Header().Set("Content-Type", mimetype.TextPlain)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
nodeinfo := rmodels.NodeInfo{
|
||||
nodeinfo := models.NodeInfo{
|
||||
Metadata: map[string]interface{}{
|
||||
"peers": peers,
|
||||
},
|
||||
OpenRegistrations: true,
|
||||
Protocols: []string{"activitypub"},
|
||||
Services: rmodels.Services{
|
||||
Services: models.Services{
|
||||
Inbound: []string{},
|
||||
Outbound: []string{},
|
||||
},
|
||||
Software: rmodels.Software{
|
||||
Software: models.Software{
|
||||
Name: m.appName,
|
||||
Version: m.appVersion,
|
||||
},
|
||||
Usage: rmodels.Usage{
|
||||
Usage: models.Usage{
|
||||
LocalPosts: 0,
|
||||
Users: rmodels.UsageUsers{
|
||||
Users: models.UsageUsers{
|
||||
Total: 1,
|
||||
},
|
||||
},
|
||||
@ -54,11 +52,11 @@ func (m *Module) nodeinfo20GetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) wellknownNodeInfoGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.WithField("func", "wellknownNodeInfoGetHandler")
|
||||
|
||||
wellknown := rmodels.NodeInfoWellKnown{
|
||||
Links: []rmodels.Link{
|
||||
wellknown := models.NodeInfoWellKnown{
|
||||
Links: []models.Link{
|
||||
{
|
||||
Rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
Href: path.GenNodeinfo20(m.domain),
|
||||
Href: path.GenNodeinfo20(m.logic.Domain()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -7,9 +7,12 @@ import (
|
||||
|
||||
// Route attaches routes to the web server
|
||||
func (m *Module) Route(s *http.Server) error {
|
||||
s.HandleFunc(path.APActor, m.actorGetHandler).Methods("GET")
|
||||
s.HandleFunc(path.APNodeInfo20, m.nodeinfo20GetHandler).Methods("GET")
|
||||
s.HandleFunc(path.APWellKnownNodeInfo, m.wellknownNodeInfoGetHandler).Methods("GET")
|
||||
s.HandleFunc(path.APWellKnownWebFinger, m.wellknownWebFingerGetHandler).Methods("GET")
|
||||
ap := s.PathPrefix("/").Subrouter()
|
||||
ap.Use(m.middlewareCheckHTTPSig)
|
||||
ap.HandleFunc(path.APActor, m.actorGetHandler).Methods("GET")
|
||||
ap.HandleFunc(path.APInbox, m.inboxPostHandler).Methods("POST")
|
||||
ap.HandleFunc(path.APNodeInfo20, m.nodeinfo20GetHandler).Methods("GET")
|
||||
ap.HandleFunc(path.APWellKnownNodeInfo, m.wellknownNodeInfoGetHandler).Methods("GET")
|
||||
ap.HandleFunc(path.APWellKnownWebFinger, m.wellknownWebFingerGetHandler).Methods("GET")
|
||||
return nil
|
||||
}
|
||||
|
9
internal/activitypub/validator.go
Normal file
9
internal/activitypub/validator.go
Normal file
@ -0,0 +1,9 @@
|
||||
package activitypub
|
||||
|
||||
import "github.com/go-playground/validator/v10"
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
func init() {
|
||||
validate = validator.New()
|
||||
}
|
@ -3,7 +3,7 @@ package activitypub
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
rmodels "github.com/feditools/relay/internal/activitypub/models"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/http"
|
||||
@ -13,23 +13,22 @@ func (m *Module) wellknownWebFingerGetHandler(w http.ResponseWriter, r *http.Req
|
||||
l := logger.WithField("func", "wellknownWebFingerGetHandler")
|
||||
|
||||
subject := r.URL.Query().Get("resource")
|
||||
if subject != fmt.Sprintf("acct:relay@%s", m.domain) {
|
||||
if subject != fmt.Sprintf("acct:relay@%s", m.logic.Domain()) {
|
||||
w.Header().Set("Content-Type", mimetype.TextPlain)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte(fmt.Sprintf("%d %s", http.StatusNotFound, http.StatusText(http.StatusNotFound))))
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
webfinger := rmodels.WebFinger{
|
||||
Aliases: []string{path.GenActor(m.domain)},
|
||||
Links: []rmodels.Link{
|
||||
webfinger := models.WebFinger{
|
||||
Aliases: []string{path.GenActor(m.logic.Domain())},
|
||||
Links: []models.Link{
|
||||
{
|
||||
Href: path.GenActor(m.domain),
|
||||
Href: path.GenActor(m.logic.Domain()),
|
||||
Rel: "self",
|
||||
Type: mimetype.ApplicationActivityJSON,
|
||||
},
|
||||
{
|
||||
Href: path.GenActor(m.domain),
|
||||
Href: path.GenActor(m.logic.Domain()),
|
||||
Rel: "self",
|
||||
Type: mimetype.ApplicationLDJSONActivityStreams,
|
||||
},
|
||||
|
19
internal/clock/clock.go
Normal file
19
internal/clock/clock.go
Normal file
@ -0,0 +1,19 @@
|
||||
package clock
|
||||
|
||||
import (
|
||||
"github.com/go-fed/activity/pub"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock implements the Clock interface of go-fed
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the current time
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// NewClock returns a simple pub.Clock for use in federation interfaces.
|
||||
func NewClock() pub.Clock {
|
||||
return &Clock{}
|
||||
}
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
// Init starts config collection
|
||||
func Init(flags *pflag.FlagSet) error {
|
||||
viper.SetEnvPrefix("mb")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
|
@ -6,9 +6,12 @@ type KeyNames struct {
|
||||
ConfigPath string
|
||||
|
||||
// application
|
||||
ApplicationName string
|
||||
SoftwareVersion string
|
||||
TokenSalt string
|
||||
ActorKeySize string
|
||||
ApplicationName string
|
||||
CachedActivityLimit string
|
||||
CachedActorLimit string
|
||||
CachedDigestLimit string
|
||||
SoftwareVersion string
|
||||
|
||||
// database
|
||||
DbType string
|
||||
@ -22,6 +25,9 @@ type KeyNames struct {
|
||||
DbLoadTestData string
|
||||
DbEncryptionKey string
|
||||
|
||||
// running
|
||||
RunnerConcurrency string
|
||||
|
||||
// server
|
||||
ServerExternalHostname string
|
||||
ServerHTTPBind string
|
||||
@ -39,9 +45,12 @@ var Keys = KeyNames{
|
||||
LogLevel: "log-level",
|
||||
|
||||
// application
|
||||
ApplicationName: "application-name",
|
||||
SoftwareVersion: "software-version", // Set at build
|
||||
TokenSalt: "token-salt",
|
||||
ActorKeySize: "actor-key-size",
|
||||
ApplicationName: "application-name",
|
||||
CachedActivityLimit: "cached-activity-limit",
|
||||
CachedActorLimit: "cached-actor-limit",
|
||||
CachedDigestLimit: "cached-digest-limit",
|
||||
SoftwareVersion: "software-version", // Set at build
|
||||
|
||||
// database
|
||||
DbType: "db-type",
|
||||
@ -55,6 +64,9 @@ var Keys = KeyNames{
|
||||
DbLoadTestData: "test-data", // CLI only
|
||||
DbEncryptionKey: "db-crypto-key",
|
||||
|
||||
// runner
|
||||
RunnerConcurrency: "runner-concurrency",
|
||||
|
||||
// server
|
||||
ServerExternalHostname: "external-hostname",
|
||||
ServerHTTPBind: "http-bind",
|
||||
|
@ -6,8 +6,12 @@ type Values struct {
|
||||
LogLevel string
|
||||
|
||||
// application
|
||||
ApplicationName string
|
||||
SoftwareVersion string
|
||||
ActorKeySize int
|
||||
ApplicationName string
|
||||
CachedActivityLimit int
|
||||
CachedActorLimit int
|
||||
CachedDigestLimit int
|
||||
SoftwareVersion string
|
||||
|
||||
// database
|
||||
DbType string
|
||||
@ -21,6 +25,9 @@ type Values struct {
|
||||
DbLoadTestData bool
|
||||
DbEncryptionKey string
|
||||
|
||||
// running
|
||||
RunnerConcurrency int
|
||||
|
||||
// server
|
||||
ServerExternalHostname string
|
||||
ServerHTTPBind string
|
||||
@ -38,7 +45,11 @@ var Defaults = Values{
|
||||
LogLevel: "info",
|
||||
|
||||
// application
|
||||
ApplicationName: "feditools-relay",
|
||||
ActorKeySize: 2048,
|
||||
ApplicationName: "feditools-relay",
|
||||
CachedActivityLimit: 1024,
|
||||
CachedActorLimit: 1024,
|
||||
CachedDigestLimit: 1024,
|
||||
|
||||
// database
|
||||
DbType: "postgres",
|
||||
@ -51,6 +62,9 @@ var Defaults = Values{
|
||||
DbTLSCACert: "",
|
||||
DbLoadTestData: false,
|
||||
|
||||
// runner
|
||||
RunnerConcurrency: 4,
|
||||
|
||||
// server
|
||||
ServerExternalHostname: "localhost",
|
||||
ServerHTTPBind: ":5000",
|
||||
|
@ -35,7 +35,7 @@ func (c *Client) ReadBlockByID(ctx context.Context, id int64) (*models.Block, db
|
||||
if err == sql.ErrNoRows {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadBlockByID", false)
|
||||
return nil, nil
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
if err != nil {
|
||||
ended := time.Since(start)
|
||||
@ -58,7 +58,7 @@ func (c *Client) ReadBlockByDomain(ctx context.Context, domain string) (*models.
|
||||
if err == sql.ErrNoRows {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadBlockByDomain", false)
|
||||
return nil, nil
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
if err != nil {
|
||||
ended := time.Since(start)
|
||||
|
@ -48,8 +48,10 @@ type Client struct {
|
||||
metrics metrics.Collector
|
||||
}
|
||||
|
||||
var _ db.DB = (*Client)(nil)
|
||||
|
||||
// New creates a new bun database client
|
||||
func New(ctx context.Context, m metrics.Collector) (db.DB, error) {
|
||||
func New(ctx context.Context, m metrics.Collector) (*Client, error) {
|
||||
var newBun *Bun
|
||||
var err error
|
||||
dbType := strings.ToLower(viper.GetString(config.Keys.DbType))
|
||||
|
@ -35,7 +35,7 @@ func (c *Client) ReadInstanceByID(ctx context.Context, id int64) (*models.Instan
|
||||
if err == sql.ErrNoRows {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadInstanceByID", false)
|
||||
return nil, nil
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
if err != nil {
|
||||
ended := time.Since(start)
|
||||
@ -48,6 +48,29 @@ func (c *Client) ReadInstanceByID(ctx context.Context, id int64) (*models.Instan
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// ReadInstanceByActorIRI returns one federated social instance
|
||||
func (c *Client) ReadInstanceByActorIRI(ctx context.Context, actorIRI string) (*models.Instance, db.Error) {
|
||||
start := time.Now()
|
||||
|
||||
instance := new(models.Instance)
|
||||
|
||||
err := c.newInstanceQ(instance).Where("actor_iri = ?", actorIRI).Scan(ctx)
|
||||
if err == sql.ErrNoRows {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadInstanceByActorIRI", false)
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
if err != nil {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadInstanceByActorIRI", true)
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadInstanceByActorIRI", false)
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// ReadInstanceByDomain returns one federated social instance
|
||||
func (c *Client) ReadInstanceByDomain(ctx context.Context, domain string) (*models.Instance, db.Error) {
|
||||
start := time.Now()
|
||||
@ -58,7 +81,7 @@ func (c *Client) ReadInstanceByDomain(ctx context.Context, domain string) (*mode
|
||||
if err == sql.ErrNoRows {
|
||||
ended := time.Since(start)
|
||||
go c.metrics.DBQuery(ended, "ReadInstanceByDomain", false)
|
||||
return nil, nil
|
||||
return nil, c.bun.ProcessError(err)
|
||||
}
|
||||
if err != nil {
|
||||
ended := time.Since(start)
|
||||
|
@ -7,14 +7,16 @@ import (
|
||||
|
||||
// Instance represents a federated social instance
|
||||
type Instance struct {
|
||||
ID int64 `validate:"-" bun:",pk,autoincrement,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
Domain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
PublicKey *rsa.PublicKey `validate:"-"`
|
||||
PrivateKey []byte `validate:"-"`
|
||||
InboxIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
Followed bool `validate:"-" bun:",notnull,default:false"`
|
||||
BlockID int64 `validate:"-" bun:",nullzero"`
|
||||
Block *Block `validate:"-" bun:"rel:belongs-to"`
|
||||
ID int64 `validate:"-" bun:",pk,autoincrement,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
AccountDomain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
Domain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
PublicKey *rsa.PublicKey `validate:"-"`
|
||||
PrivateKey *rsa.PrivateKey `validate:"-"`
|
||||
ActorIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
InboxIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
Followed bool `validate:"-" bun:",notnull,default:false"`
|
||||
BlockID int64 `validate:"-" bun:",nullzero"`
|
||||
Block *Block `validate:"-" bun:"rel:belongs-to"`
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ type DB interface {
|
||||
CreateInstance(ctx context.Context, instance *models.Instance) (err Error)
|
||||
// ReadInstanceByID returns one federated social instance
|
||||
ReadInstanceByID(ctx context.Context, id int64) (instance *models.Instance, err Error)
|
||||
// ReadInstanceByActorIRI returns one federated social instance
|
||||
ReadInstanceByActorIRI(ctx context.Context, actorIRI string) (instance *models.Instance, err Error)
|
||||
// ReadInstanceByDomain returns one federated social instance
|
||||
ReadInstanceByDomain(ctx context.Context, domain string) (instance *models.Instance, err Error)
|
||||
// ReadInstancesWhereFollowing returns all federated social instances which are following this relay
|
||||
|
13
internal/http/transport.go
Normal file
13
internal/http/transport.go
Normal file
@ -0,0 +1,13 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Transport adds the expected http User-Agent to any request
|
||||
type Transport struct {
|
||||
}
|
||||
|
||||
// RoundTrip executes the default http.Transport with expected http User-Agent
|
||||
func (*Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", GetUserAgent())
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
36
internal/http/user_agent.go
Normal file
36
internal/http/user_agent.go
Normal file
@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
initOnce sync.Once
|
||||
userAgentLock sync.RWMutex
|
||||
|
||||
userAgent string
|
||||
)
|
||||
|
||||
// doInit sets the User-Agent for all subsequent requests
|
||||
func doInit() {
|
||||
userAgentLock.Lock()
|
||||
userAgent = fmt.Sprintf("Go-http-client/2.0 (%s/%s; +https://%s/)",
|
||||
viper.GetString(config.Keys.ApplicationName),
|
||||
viper.GetString(config.Keys.SoftwareVersion),
|
||||
viper.GetString(config.Keys.ServerExternalHostname),
|
||||
)
|
||||
userAgentLock.Unlock()
|
||||
}
|
||||
|
||||
// GetUserAgent returns the generated http User-Agent
|
||||
func GetUserAgent() string {
|
||||
initOnce.Do(doInit)
|
||||
|
||||
userAgentLock.RLock()
|
||||
ua := userAgent
|
||||
userAgentLock.RUnlock()
|
||||
return ua
|
||||
}
|
@ -23,9 +23,9 @@ func Init() error {
|
||||
}
|
||||
logrus.SetLevel(level)
|
||||
|
||||
if level == logrus.TraceLevel {
|
||||
/*if level == logrus.TraceLevel {
|
||||
logrus.SetReportCaller(true)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
return nil
|
||||
|
372
internal/logic/activity.go
Normal file
372
internal/logic/activity.go
Normal file
@ -0,0 +1,372 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (l *Logic) DeliverActivity(ctx context.Context, instanceID int64, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "DeliverActivity",
|
||||
})
|
||||
|
||||
// get instance
|
||||
instance, err := l.db.ReadInstanceByID(ctx, instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
// send activity
|
||||
body, err := json.Marshal(activity)
|
||||
if err != nil {
|
||||
log.Errorf("can't marshal response: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't marshal response: %s", err.Error())
|
||||
}
|
||||
|
||||
inboxIRI, err := url.Parse(instance.InboxIRI)
|
||||
if err != nil {
|
||||
log.Errorf("can't parse actor iri: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't parse actor iri: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Debugf("sending activity: %s to %s", string(body), inboxIRI.String())
|
||||
resp, err := l.transport.InstancePost(ctx, inboxIRI, body, mimetype.ApplicationActivityJSON, mimetype.ApplicationActivityJSON)
|
||||
if err != nil {
|
||||
log.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
|
||||
return fmt.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logic) ProcessActivity(ctx context.Context, instanceID int64, actorIRI *url.URL, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "ProcessActivity",
|
||||
})
|
||||
|
||||
actType, ok := activity["type"]
|
||||
if !ok {
|
||||
log.Debugf("activity missing type")
|
||||
|
||||
return errors.New("activity missing type")
|
||||
}
|
||||
|
||||
log.Tracef("new %s activity from %s ", actType, actorIRI.String())
|
||||
|
||||
switch actType {
|
||||
case models.TypeAnnounce, models.TypeCreate:
|
||||
return l.doRelay(ctx, instanceID, activity)
|
||||
case models.TypeDelete, models.TypeUpdate:
|
||||
return l.doForward(ctx, instanceID, activity)
|
||||
case models.TypeFollow:
|
||||
return l.doFollow(ctx, instanceID, activity)
|
||||
case models.TypeUndo:
|
||||
return l.doUndo(ctx, instanceID, activity)
|
||||
default:
|
||||
log.Debugf("unhandled activity type: %s", actType)
|
||||
|
||||
return fmt.Errorf("unhandled activity type: %s", actType)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logic) doFollow(ctx context.Context, instanceID int64, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "doFollow",
|
||||
})
|
||||
log.Trace("doFollow called")
|
||||
|
||||
// get id
|
||||
idi, ok := activity["id"]
|
||||
if !ok {
|
||||
log.Debugf("activity missing id")
|
||||
|
||||
return errors.New("activity missing id")
|
||||
}
|
||||
id, ok := idi.(string)
|
||||
if !ok {
|
||||
log.Debugf("activity id is not string")
|
||||
|
||||
return errors.New("activity id is not string")
|
||||
}
|
||||
|
||||
// set followed
|
||||
instance, err := l.db.ReadInstanceByID(ctx, instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
instance.Followed = true
|
||||
err = l.db.UpdateInstance(ctx, instance)
|
||||
if err != nil {
|
||||
log.Errorf("db update: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db update: %s", err.Error())
|
||||
}
|
||||
|
||||
// send accept
|
||||
outgoingActivity := genActivityAccept(l.domain, instance.ActorIRI, id)
|
||||
body, err := json.Marshal(outgoingActivity)
|
||||
if err != nil {
|
||||
log.Errorf("can't marshal response: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't marshal response: %s", err.Error())
|
||||
}
|
||||
|
||||
inboxIRI, err := url.Parse(instance.InboxIRI)
|
||||
if err != nil {
|
||||
log.Errorf("can't parse actor iri: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't parse actor iri: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Debugf("sending activity: %s", string(body))
|
||||
resp, err := l.transport.InstancePost(ctx, inboxIRI, body, mimetype.ApplicationActivityJSON, mimetype.ApplicationActivityJSON)
|
||||
if err != nil {
|
||||
log.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
|
||||
return fmt.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logic) doForward(ctx context.Context, instanceID int64, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "doForward",
|
||||
})
|
||||
log.Trace("doForward called")
|
||||
|
||||
// check if we've already forwarded
|
||||
activityID, err := activity.ID()
|
||||
if err != nil {
|
||||
log.Warnf("missing activity id: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("object id: %s", err.Error())
|
||||
}
|
||||
_, ok := l.cacheActivity.Get(activityID)
|
||||
if ok {
|
||||
log.Infof("already forwarded message: %v", activityID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get instance
|
||||
signingInstance, err := l.db.ReadInstanceByID(ctx, instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
// forward activity
|
||||
log.Debugf("forwarding messagge from %s", signingInstance.ActorIRI)
|
||||
log.Tracef("forwarding activity: %#v", activity)
|
||||
|
||||
// read from db
|
||||
followedInstances, err := l.GetInstancesForForwarding(ctx, signingInstance.ActorIRI, activityID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
log.Debugf("got %d followed instances", len(followedInstances))
|
||||
|
||||
// enqueue deliveries
|
||||
for _, instance := range followedInstances {
|
||||
err = l.runner.EnqueueDeliverActivity(ctx, instance.ID, activity)
|
||||
if err != nil {
|
||||
log.Errorf("enqueueing delivery: %s", err.Error())
|
||||
}
|
||||
}
|
||||
_ = l.cacheActivity.Add(activityID, activityID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logic) doRelay(ctx context.Context, instanceID int64, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "doRelay",
|
||||
})
|
||||
log.Trace("doRelay called")
|
||||
|
||||
// check if we've already forwarded
|
||||
objectID, err := activity.ObjectID()
|
||||
if err != nil {
|
||||
log.Warnf("object id: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("object id: %s", err.Error())
|
||||
}
|
||||
_, ok := l.cacheActivity.Get(objectID)
|
||||
if ok {
|
||||
log.Infof("already forwarded message: %v", objectID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get instance
|
||||
signingInstance, err := l.db.ReadInstanceByID(ctx, instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
// forward activity
|
||||
log.Debugf("relaying post from %s", signingInstance.ActorIRI)
|
||||
log.Tracef("relaying activity: %#v", activity)
|
||||
|
||||
// send announce
|
||||
outgoingActivity := genActivityAnnounce(l.domain, objectID)
|
||||
outgoingActivityID, err := outgoingActivity.ID()
|
||||
if err != nil {
|
||||
log.Errorf("can't get new activity id: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't get new activity id: %s", err.Error())
|
||||
}
|
||||
|
||||
// read from db
|
||||
followedInstances, err := l.GetInstancesForForwarding(ctx, signingInstance.ActorIRI, objectID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
log.Debugf("got %d followed instances", len(followedInstances))
|
||||
|
||||
// enqueue deliveries
|
||||
for _, instance := range followedInstances {
|
||||
err = l.runner.EnqueueDeliverActivity(ctx, instance.ID, outgoingActivity)
|
||||
if err != nil {
|
||||
log.Errorf("enqueueing delivery: %s", err.Error())
|
||||
}
|
||||
}
|
||||
_ = l.cacheActivity.Add(objectID, outgoingActivityID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logic) doUndo(ctx context.Context, instanceID int64, activity models.Activity) error {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "doUndo",
|
||||
})
|
||||
log.Trace("doUndo called")
|
||||
|
||||
aType, err := activity.ObjectType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch aType {
|
||||
case models.TypeAnnounce:
|
||||
return l.doForward(ctx, instanceID, activity)
|
||||
case models.TypeFollow:
|
||||
// get instance
|
||||
instance, err := l.db.ReadInstanceByID(ctx, instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
// unset followed
|
||||
instance.Followed = false
|
||||
err = l.db.UpdateInstance(ctx, instance)
|
||||
if err != nil {
|
||||
log.Errorf("db update: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("db update: %s", err.Error())
|
||||
}
|
||||
|
||||
// send accept
|
||||
outgoingActivity := genActivityUndo(l.domain, instance.ActorIRI)
|
||||
body, err := json.Marshal(outgoingActivity)
|
||||
if err != nil {
|
||||
log.Errorf("can't marshal response: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't marshal response: %s", err.Error())
|
||||
}
|
||||
|
||||
inboxIRI, err := url.Parse(instance.InboxIRI)
|
||||
if err != nil {
|
||||
log.Errorf("can't parse actor iri: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("can't parse actor iri: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Debugf("sending activity: %s", string(body))
|
||||
resp, err := l.transport.InstancePost(ctx, inboxIRI, body, mimetype.ApplicationActivityJSON, mimetype.ApplicationActivityJSON)
|
||||
if err != nil {
|
||||
log.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
|
||||
return fmt.Errorf("can't post to instance: %s\n%s", err.Error(), resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
log.Debugf("dropping activity object type: %s", aType)
|
||||
|
||||
// drop activity
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func genActivityID(domain string) string {
|
||||
return fmt.Sprintf("https://%s/activities/%s", domain, uuid.New().String())
|
||||
}
|
||||
|
||||
func genActivityAnnounce(domain string, objectID string) models.Activity {
|
||||
return models.Activity{
|
||||
"@context": models.ContextActivityStreams,
|
||||
"type": models.TypeAnnounce,
|
||||
"to": []string{path.GenFollowers(domain)},
|
||||
"actor": path.GenActor(domain),
|
||||
"object": objectID,
|
||||
"id": genActivityID(domain),
|
||||
}
|
||||
}
|
||||
|
||||
func genActivityAccept(domain, to, activityID string) models.Activity {
|
||||
return models.Activity{
|
||||
"@context": models.ContextActivityStreams,
|
||||
"type": models.TypeAccept,
|
||||
"to": []string{to},
|
||||
"actor": genActorSelf(domain),
|
||||
"object": map[string]interface{}{
|
||||
"type": models.TypeFollow,
|
||||
"id": activityID,
|
||||
"object": genActorSelf(domain),
|
||||
"actor": to,
|
||||
},
|
||||
"id": genActivityID(domain),
|
||||
}
|
||||
}
|
||||
|
||||
func genActivityUndo(domain, to string) models.Activity {
|
||||
return models.Activity{
|
||||
"@context": models.ContextActivityStreams,
|
||||
"type": models.TypeUndo,
|
||||
"to": []string{to},
|
||||
"actor": genActorSelf(domain),
|
||||
"object": map[string]interface{}{
|
||||
"type": models.TypeFollow,
|
||||
"id": genActivityID(domain),
|
||||
"object": genActorSelf(domain),
|
||||
"actor": genActorSelf(domain),
|
||||
},
|
||||
"id": genActivityID(domain),
|
||||
}
|
||||
}
|
93
internal/logic/actor.go
Normal file
93
internal/logic/actor.go
Normal file
@ -0,0 +1,93 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/tyrm/go-util/mimetype"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (l *Logic) fetchActor(ctx context.Context, actorIRI *url.URL) (*models.Actor, error) {
|
||||
log := logger.WithField("func", "fetchActor")
|
||||
|
||||
// do request
|
||||
v, err, shared := l.outgoingRequestGroup.Do(fmt.Sprintf("fetchactor-%s", actorIRI.String()), func() (interface{}, error) {
|
||||
// check cache
|
||||
cachedActor, ok := l.cacheActor.Get(actorIRI.String())
|
||||
if ok {
|
||||
return cachedActor, nil
|
||||
}
|
||||
|
||||
// get actor data
|
||||
body, err := l.transport.InstanceGet(ctx, actorIRI, mimetype.ApplicationActivityJSON)
|
||||
if err != nil {
|
||||
log.Errorf("instance get %s: %s", actorIRI.String(), err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// unmarshal json to object
|
||||
var newActor models.Actor
|
||||
err = json.Unmarshal(body, &newActor)
|
||||
if err != nil {
|
||||
log.Errorf("unmarshal json %s: %s", actorIRI.String(), err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update cache
|
||||
_ = l.cacheActor.Add(actorIRI.String(), newActor)
|
||||
|
||||
return newActor, err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("singleflight (shared: %v): %s", shared, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actor := v.(models.Actor)
|
||||
return &actor, nil
|
||||
}
|
||||
|
||||
func (l *Logic) getActorFromDomain(ctx context.Context, domain string) (*models.Actor, error) {
|
||||
log := logger.WithField("func", "getActorFromDomain")
|
||||
|
||||
// pull host meta
|
||||
hostMeta, err := l.getHostMeta(ctx, domain)
|
||||
if err != nil {
|
||||
log.Debugf("can't retrieve host meta: %s", err.Error())
|
||||
|
||||
return nil, NewErrorf("host meta: %s", err.Error())
|
||||
}
|
||||
|
||||
// perform web finger
|
||||
webfingerURI := hostMeta.WebfingerURI()
|
||||
if webfingerURI == "" {
|
||||
log.Debug("host meta missing webfinger URI")
|
||||
|
||||
return nil, NewError("host meta missing webfinger URI")
|
||||
}
|
||||
webFinger, err := l.fetchWebFinger(ctx, webfingerURI, domain, domain)
|
||||
if err != nil {
|
||||
log.Debugf("can't retrieve webfinger: %s", err.Error())
|
||||
|
||||
return nil, NewErrorf("host meta: %s", err.Error())
|
||||
}
|
||||
|
||||
// fetch actor
|
||||
actorIRI, err := webFinger.ActorURI()
|
||||
if err != nil {
|
||||
log.Debugf("can't get actor uri: %s", err.Error())
|
||||
|
||||
return nil, NewErrorf("host meta: %s", err.Error())
|
||||
}
|
||||
actor, err := l.fetchActor(ctx, actorIRI)
|
||||
if err != nil {
|
||||
log.Debugf("can't fetch actor: %s", err.Error())
|
||||
|
||||
return nil, NewErrorf("host meta: %s", err.Error())
|
||||
}
|
||||
|
||||
return actor, nil
|
||||
}
|
@ -2,6 +2,8 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -10,25 +12,27 @@ func (l *Logic) IsDomainBlocked(ctx context.Context, d string) (bool, error) {
|
||||
log := logger.WithField("func", "IsDomainBlocked")
|
||||
|
||||
// check domain for block
|
||||
block, err := l.db.ReadBlockByDomain(ctx, d)
|
||||
if err != nil {
|
||||
_, err := l.db.ReadBlockByDomain(ctx, d)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read %s: %s", d, err.Error())
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// this domain is blocked
|
||||
if block != nil {
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// check top domains
|
||||
for _, domain := range topDomains(d) {
|
||||
block, err = l.db.ReadBlockByDomain(ctx, domain)
|
||||
if err != nil {
|
||||
block, err := l.db.ReadBlockByDomain(ctx, domain)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read %s: %s", domain, err.Error())
|
||||
|
||||
return false, err
|
||||
}
|
||||
if block != nil {
|
||||
if err == nil {
|
||||
if block.BlockSubdomains {
|
||||
return true, nil
|
||||
}
|
||||
@ -46,5 +50,6 @@ func topDomains(d string) []string {
|
||||
for i := 0; i < len(tds); i++ {
|
||||
tds[i] = strings.Join(parts[i+1:end], ".")
|
||||
}
|
||||
|
||||
return tds
|
||||
}
|
||||
|
5
internal/logic/domain.go
Normal file
5
internal/logic/domain.go
Normal file
@ -0,0 +1,5 @@
|
||||
package logic
|
||||
|
||||
func (l *Logic) Domain() string {
|
||||
return l.domain
|
||||
}
|
27
internal/logic/error.go
Normal file
27
internal/logic/error.go
Normal file
@ -0,0 +1,27 @@
|
||||
package logic
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error represents a logic specific error.
|
||||
type Error struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// Error returns the error message as a string.
|
||||
func (e *Error) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// NewError wraps a message in an Error object.
|
||||
func NewError(m string) *Error {
|
||||
return &Error{
|
||||
message: m,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorf wraps a message in an Error object.
|
||||
func NewErrorf(m string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
message: fmt.Sprintf(m, args...),
|
||||
}
|
||||
}
|
51
internal/logic/host_meta.go
Normal file
51
internal/logic/host_meta.go
Normal file
@ -0,0 +1,51 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (l *Logic) getHostMeta(ctx context.Context, domain string) (*models.HostMeta, error) {
|
||||
log := logger.WithField("func", "getHostMeta")
|
||||
hostMetaURI := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: domain,
|
||||
Path: "/.well-known/host-meta",
|
||||
}
|
||||
|
||||
v, err, _ := l.outgoingRequestGroup.Do(fmt.Sprintf("hostmeta-%s", hostMetaURI.String()), func() (interface{}, error) {
|
||||
// do request
|
||||
resp, err := l.transport.InstanceGet(ctx, hostMetaURI)
|
||||
if err != nil {
|
||||
log.Errorf("http get: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostMeta := new(models.HostMeta)
|
||||
err = xml.Unmarshal(resp, hostMeta)
|
||||
if err != nil {
|
||||
log.Errorf("decode xml: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostMeta, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("singleflight: %s", err.Error())
|
||||
|
||||
return nil, NewErrorf("singleflight: %s", err.Error())
|
||||
}
|
||||
|
||||
hostMeta, ok := v.(*models.HostMeta)
|
||||
if !ok {
|
||||
return nil, NewError("invalid response type from single flight")
|
||||
}
|
||||
|
||||
return hostMeta, nil
|
||||
}
|
@ -2,10 +2,15 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetPeers returns true if a domain matches a block in the database
|
||||
// GetPeers returns a list of peers
|
||||
func (l *Logic) GetPeers(ctx context.Context) (*[]string, error) {
|
||||
log := logger.WithField("func", "GetPeers")
|
||||
|
||||
@ -35,6 +40,92 @@ func (l *Logic) GetPeers(ctx context.Context) (*[]string, error) {
|
||||
return &newPeers, nil
|
||||
}
|
||||
|
||||
func (l *Logic) GetInstance(ctx context.Context, domain string) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "GetInstance")
|
||||
|
||||
// try to get instance from db
|
||||
instance := new(models.Instance)
|
||||
var err error
|
||||
instance, err = l.db.ReadInstanceByDomain(ctx, domain)
|
||||
if err != nil {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (l *Logic) GetInstanceForActor(ctx context.Context, actorID *url.URL) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "GetInstanceForActor")
|
||||
|
||||
// try to get instance from db
|
||||
instance := new(models.Instance)
|
||||
var err error
|
||||
instance, err = l.db.ReadInstanceByActorIRI(ctx, actorID.String())
|
||||
if err == nil {
|
||||
return instance, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// not in db, fetch actor
|
||||
actor, err := l.fetchActor(ctx, actorID)
|
||||
if err != nil {
|
||||
log.Errorf("fetching actor: %s", err.Error())
|
||||
|
||||
return nil, fmt.Errorf("fetching actor: %s", err.Error())
|
||||
}
|
||||
newInstance, err := actor.Instance()
|
||||
err = l.db.CreateInstance(ctx, newInstance)
|
||||
if err != nil {
|
||||
log.Errorf("db create: %s", err.Error())
|
||||
|
||||
return nil, fmt.Errorf("db create: %s", err.Error())
|
||||
}
|
||||
|
||||
return newInstance, nil
|
||||
}
|
||||
|
||||
func (l *Logic) GetInstancesForForwarding(ctx context.Context, actorIRI, objectID string) ([]*models.Instance, error) {
|
||||
log := logger.WithField("func", "GetInstance")
|
||||
|
||||
objectIDURI, err := url.Parse(objectID)
|
||||
if err != nil {
|
||||
log.Errorf("parsing object id uri: %s", err.Error())
|
||||
|
||||
return nil, fmt.Errorf("parsing object id uri: %s", err.Error())
|
||||
}
|
||||
|
||||
instances, err := l.db.ReadInstancesWhereFollowing(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("got %d instances", len(instances))
|
||||
|
||||
// remove sender and origin instances
|
||||
selectedInstances := make([]*models.Instance, 0)
|
||||
for _, instance := range instances {
|
||||
aIRI, err := url.Parse(instance.ActorIRI)
|
||||
if err != nil {
|
||||
log.Errorf("parsing object id uri: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if instance.ActorIRI != actorIRI && aIRI.Host != objectIDURI.Host {
|
||||
selectedInstances = append(selectedInstances, instance)
|
||||
}
|
||||
}
|
||||
|
||||
return selectedInstances, nil
|
||||
}
|
||||
|
||||
// peer list "cache" functions
|
||||
func (l *Logic) getCachedPeerList() (*[]string, bool) {
|
||||
l.cPeerListLock.RLock()
|
||||
@ -53,3 +144,76 @@ func (l *Logic) setCachedPeerList(peers *[]string) {
|
||||
l.cPeerListExpires = time.Now().Add(l.cPeerListValidity)
|
||||
l.cPeerListLock.Unlock()
|
||||
}
|
||||
|
||||
/*func (l *Logic) getInstanceWithPublicKey(ctx context.Context, actorURI *url.URL) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "getInstanceWithPublicKey")
|
||||
|
||||
instance, err := l.db.ReadInstanceByDomain(ctx, actorURI.Host)
|
||||
if err != nil {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if instance == nil {
|
||||
log.Debugf("creating instance %s from actor", actorURI.Host)
|
||||
instance, err = l.makeInstanceFromActor(ctx, actorURI)
|
||||
if err != nil {
|
||||
log.Errorf("make actor: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
if instance.PublicKey == nil {
|
||||
// fetch remote actorURI
|
||||
actor, err := l.fetchActor(ctx, actorURI)
|
||||
if err != nil {
|
||||
log.Errorf("fetch actor: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make public key
|
||||
pubKey, err := actor.RSAPublicKey()
|
||||
if err != nil {
|
||||
log.Errorf("extracting public key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instance.PublicKey = pubKey
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}*/
|
||||
|
||||
/*func (l *Logic) makeInstanceFromActor(ctx context.Context, actorURI *url.URL) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "makeInstanceFromActor")
|
||||
|
||||
// fetch remote actor
|
||||
actor, err := l.fetchActor(ctx, actorURI)
|
||||
if err != nil {
|
||||
log.Errorf("fetch actor: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make public key
|
||||
pubKey, err := actor.RSAPublicKey()
|
||||
if err != nil {
|
||||
log.Errorf("extracting public key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create new instance
|
||||
newInstance := &models.Instance{
|
||||
Domain: actorURI.Host,
|
||||
ActorIRI: actorURI.String(),
|
||||
InboxIRI: actor.Endpoints.SharedInbox,
|
||||
PublicKey: pubKey,
|
||||
}
|
||||
err = l.db.CreateInstance(ctx, newInstance)
|
||||
if err != nil {
|
||||
log.Errorf("db create: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newInstance, nil
|
||||
}*/
|
||||
|
84
internal/logic/instance_self.go
Normal file
84
internal/logic/instance_self.go
Normal file
@ -0,0 +1,84 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (l *Logic) createInstanceSelf(ctx context.Context) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "createInstanceSelf")
|
||||
|
||||
// generate key
|
||||
privatekey, err := rsa.GenerateKey(rand.Reader, viper.GetInt(config.Keys.ActorKeySize))
|
||||
if err != nil {
|
||||
log.Errorf("genrating private key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create new instance
|
||||
newInstance := &models.Instance{
|
||||
AccountDomain: l.domain,
|
||||
Domain: l.domain,
|
||||
ActorIRI: genActorSelf(l.domain),
|
||||
InboxIRI: path.GenInbox(l.domain),
|
||||
|
||||
PublicKey: &privatekey.PublicKey,
|
||||
PrivateKey: privatekey,
|
||||
}
|
||||
|
||||
// add to database
|
||||
err = l.db.CreateInstance(ctx, newInstance)
|
||||
if err != nil {
|
||||
log.Errorf("db create: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newInstance, nil
|
||||
}
|
||||
|
||||
func (l *Logic) GetInstanceSelf(ctx context.Context) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "GetInstanceSelf")
|
||||
|
||||
instance := new(models.Instance)
|
||||
var err error
|
||||
instance, err = l.db.ReadInstanceByDomain(ctx, l.domain)
|
||||
if err != nil {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (l *Logic) getOrCreateSelfInstance(ctx context.Context) (*models.Instance, error) {
|
||||
log := logger.WithField("func", "getOrCreateSelfInstance")
|
||||
|
||||
instance, err := l.GetInstanceSelf(ctx)
|
||||
if err == nil {
|
||||
return instance, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf("db read: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newInstance, err := l.createInstanceSelf(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("create self: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newInstance, nil
|
||||
}
|
@ -1,28 +1,105 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/feditools/relay/internal/db"
|
||||
"github.com/feditools/relay/internal/path"
|
||||
"github.com/feditools/relay/internal/runner"
|
||||
"github.com/feditools/relay/internal/transport"
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/httpsig"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logic contains shared logic for the application
|
||||
type Logic struct {
|
||||
db db.DB
|
||||
db db.DB
|
||||
runner runner.Runner
|
||||
transport *transport.Transport
|
||||
|
||||
domain string
|
||||
validAlgs []httpsig.Algorithm
|
||||
|
||||
outgoingRequestGroup singleflight.Group
|
||||
|
||||
// peer list
|
||||
cPeerList *[]string
|
||||
cPeerListExpires time.Time
|
||||
cPeerListValidity time.Duration
|
||||
cPeerListLock sync.RWMutex
|
||||
|
||||
// caches
|
||||
cacheActivity *lru.Cache
|
||||
cacheActor *lru.Cache
|
||||
cacheDigest *lru.Cache
|
||||
}
|
||||
|
||||
// New created a new logic module
|
||||
func New(d db.DB) (*Logic, error) {
|
||||
return &Logic{
|
||||
func New(ctx context.Context, c pub.Clock, d db.DB) (*Logic, error) {
|
||||
log := logger.WithFields(logrus.Fields{
|
||||
"func": "New",
|
||||
})
|
||||
|
||||
// create module
|
||||
l := Logic{
|
||||
db: d,
|
||||
|
||||
domain: viper.GetString(config.Keys.ServerExternalHostname),
|
||||
validAlgs: []httpsig.Algorithm{
|
||||
httpsig.RSA_SHA512,
|
||||
httpsig.RSA_SHA256,
|
||||
httpsig.ED25519,
|
||||
},
|
||||
|
||||
cPeerList: &[]string{},
|
||||
cPeerListValidity: time.Second * 15,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// get self
|
||||
instanceSelf, err := l.getOrCreateSelfInstance(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("unable to get self: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate transport
|
||||
l.transport, err = transport.New(c, path.GenPublicKey(l.domain), instanceSelf.PrivateKey)
|
||||
if err != nil {
|
||||
log.Errorf("creating transport: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make caches
|
||||
l.cacheActivity, err = lru.New(viper.GetInt(config.Keys.CachedActivityLimit))
|
||||
if err != nil {
|
||||
log.Errorf("creating activity cache: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
l.cacheActor, err = lru.New(viper.GetInt(config.Keys.CachedActorLimit))
|
||||
if err != nil {
|
||||
log.Errorf("creating actor cache: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
l.cacheDigest, err = lru.New(viper.GetInt(config.Keys.CachedDigestLimit))
|
||||
if err != nil {
|
||||
log.Errorf("creating digest cache: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
func (l *Logic) SetRunner(r runner.Runner) {
|
||||
l.runner = r
|
||||
}
|
||||
|
67
internal/logic/request.go
Normal file
67
internal/logic/request.go
Normal file
@ -0,0 +1,67 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"github.com/feditools/relay/internal/http"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/go-fed/httpsig"
|
||||
nethttp "net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (l *Logic) ValidateRequest(r *nethttp.Request, actorURI *url.URL) (bool, *models.Actor) {
|
||||
log := logger.WithField("func", "ValidateRequest")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// get verifier from context
|
||||
cVerifier := ctx.Value(http.ContextKeyKeyVerifier)
|
||||
if cVerifier == nil {
|
||||
log.Debug("verifier missing in context")
|
||||
return false, nil
|
||||
}
|
||||
verifier, ok := cVerifier.(httpsig.Verifier)
|
||||
if !ok {
|
||||
log.Warnf("can't cast verifier")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// parse key uri
|
||||
publicKeyID, err := url.Parse(verifier.KeyId())
|
||||
if err != nil {
|
||||
log.Debug("can't parse public key URI")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// relay should never talk to itself
|
||||
if strings.EqualFold(publicKeyID.Host, l.domain) {
|
||||
log.Warnf("received request from self")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO: check blocks
|
||||
|
||||
// fetch actor
|
||||
actor, err := l.fetchActor(ctx, actorURI)
|
||||
if err != nil {
|
||||
log.Errorf("fetch actor: %s", err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pk, err := actor.RSAPublicKey()
|
||||
if err != nil {
|
||||
log.Errorf("getting actor rsa public key: %s", err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// try to verify known algos
|
||||
for _, algo := range l.validAlgs {
|
||||
err := verifier.Verify(pk, algo)
|
||||
if err == nil {
|
||||
log.Tracef("request passed %s algo", algo)
|
||||
return true, actor
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
7
internal/logic/util.go
Normal file
7
internal/logic/util.go
Normal file
@ -0,0 +1,7 @@
|
||||
package logic
|
||||
|
||||
import "fmt"
|
||||
|
||||
func genActorSelf(domain string) string {
|
||||
return fmt.Sprintf("https://%s/actor", domain)
|
||||
}
|
56
internal/logic/webfinger.go
Normal file
56
internal/logic/webfinger.go
Normal file
@ -0,0 +1,56 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// webFinger retrieves web finger resource from a federated instance.
|
||||
func (l *Logic) fetchWebFinger(ctx context.Context, wfURI models.WebfingerURI, username, domain string) (*models.WebFinger, error) {
|
||||
log := logger.WithField("func", "webFinger")
|
||||
webFingerString := fmt.Sprintf(wfURI.FTemplate(), username, domain)
|
||||
webFingerURI, err := url.Parse(webFingerString)
|
||||
if err != nil {
|
||||
log.Errorf("parsing url '%s': %s", webFingerString, err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err, _ := l.outgoingRequestGroup.Do(fmt.Sprintf("webfinger-%s", webFingerURI.String()), func() (interface{}, error) {
|
||||
log.Tracef("webfingering %s", webFingerURI.String())
|
||||
|
||||
// do request
|
||||
body, err := l.transport.InstanceGet(ctx, webFingerURI)
|
||||
if err != nil {
|
||||
log.Errorf("http get: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var webFinger models.WebFinger
|
||||
err = json.Unmarshal(body, &webFinger)
|
||||
if err != nil {
|
||||
log.Errorf("decode json: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return webFinger, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("singleflight: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webFinger, ok := v.(models.WebFinger)
|
||||
if !ok {
|
||||
return nil, NewError("invalid response type from single flight")
|
||||
}
|
||||
|
||||
return &webFinger, nil
|
||||
}
|
75
internal/models/activity.go
Normal file
75
internal/models/activity.go
Normal file
@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Activity map[string]interface{}
|
||||
|
||||
func (a Activity) ID() (string, error) {
|
||||
idi, ok := a["id"]
|
||||
if !ok {
|
||||
return "", errors.New("activity is missing id")
|
||||
}
|
||||
id, ok := idi.(string)
|
||||
if !ok {
|
||||
return "", errors.New("activity id is wrong type")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a Activity) ObjectID() (string, error) {
|
||||
l := logger.WithFields(logrus.Fields{
|
||||
"func": "ObjectID",
|
||||
"model": "Activity",
|
||||
})
|
||||
|
||||
object, ok := a["object"]
|
||||
if !ok {
|
||||
return "", errors.New("activity is missing object")
|
||||
}
|
||||
|
||||
switch o := object.(type) {
|
||||
case map[string]interface{}:
|
||||
idi, ok := o["id"]
|
||||
if !ok {
|
||||
return "", errors.New("object is missing id")
|
||||
}
|
||||
id, ok := idi.(string)
|
||||
if !ok {
|
||||
return "", errors.New("object id is not string")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
case string:
|
||||
return o, nil
|
||||
default:
|
||||
l.Warn("unknown object type")
|
||||
|
||||
return "", errors.New("unknown object type")
|
||||
}
|
||||
}
|
||||
|
||||
func (a Activity) ObjectType() (string, error) {
|
||||
objecti, ok := a["object"]
|
||||
if !ok {
|
||||
return "", errors.New("activity is missing object")
|
||||
}
|
||||
object, ok := objecti.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("activity object is wrong type")
|
||||
}
|
||||
|
||||
aTypei, ok := object["type"]
|
||||
if !ok {
|
||||
return "", errors.New("activity object is missing type")
|
||||
}
|
||||
aType, ok := aTypei.(string)
|
||||
if !ok {
|
||||
return "", errors.New("activity object is wrong type")
|
||||
}
|
||||
|
||||
return aType, nil
|
||||
}
|
93
internal/models/actor.go
Normal file
93
internal/models/actor.go
Normal file
@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Actor represents an activity pub actor
|
||||
type Actor struct {
|
||||
Context interface{} `json:"@context"`
|
||||
Endpoints Endpoints `json:"endpoints"`
|
||||
Followers string `json:"followers"`
|
||||
Following string `json:"following"`
|
||||
Inbox string `json:"inbox"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
PublicKey PublicKey `json:"publicKey"`
|
||||
Summary string `json:"summary"`
|
||||
PreferredUsername string `json:"preferredUsername"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Endpoints represents known activity pub endpoints
|
||||
type Endpoints struct {
|
||||
SharedInbox string `json:"sharedInbox"`
|
||||
}
|
||||
|
||||
// PublicKey represents an actor's public key
|
||||
type PublicKey struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
PublicKeyPEM string `json:"publicKeyPem"`
|
||||
}
|
||||
|
||||
func (a *Actor) Instance() (*Instance, error) {
|
||||
if a.Type != TypeApplication {
|
||||
return nil, NewErrorf("actor is not type %s", TypeApplication)
|
||||
}
|
||||
|
||||
actorID, err := url.Parse(a.ID)
|
||||
if a.Type != TypeApplication {
|
||||
return nil, NewErrorf("can't parse id: %s", err.Error())
|
||||
}
|
||||
|
||||
return &Instance{
|
||||
AccountDomain: a.PreferredUsername,
|
||||
Domain: actorID.Host,
|
||||
ActorIRI: a.ID,
|
||||
InboxIRI: a.Endpoints.SharedInbox,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Actor) RSAPublicKey() (*rsa.PublicKey, error) {
|
||||
l := logger.WithField("func", "RSAPublicKey")
|
||||
|
||||
actorPem, _ := pem.Decode([]byte(a.PublicKey.PublicKeyPEM))
|
||||
if actorPem == nil {
|
||||
msg := fmt.Sprintf("actor %s has invalid public key", a.URL)
|
||||
l.Debugf(msg)
|
||||
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
l.Debugf("public key type: %s", actorPem.Type)
|
||||
if actorPem.Type != "PUBLIC KEY" {
|
||||
msg := fmt.Sprintf("actor %s has wrong key type", a.URL)
|
||||
l.Debugf(msg)
|
||||
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
parsedKey, err := x509.ParsePKIXPublicKey(actorPem.Bytes)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("can't parse public key for %s", a.URL)
|
||||
l.Debugf(msg)
|
||||
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
pubKey, ok := parsedKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("can't cast public key for %s", a.URL)
|
||||
l.Debugf(msg)
|
||||
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
return pubKey, nil
|
||||
}
|
@ -3,7 +3,7 @@ package models
|
||||
// Link represents a link
|
||||
type Link struct {
|
||||
Href string `json:"href,omitempty"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Rel string `json:"rel,omitempty" xml:"rel,attr"`
|
||||
Template string `json:"template,omitempty" xml:"template,attr"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
35
internal/models/const.go
Normal file
35
internal/models/const.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
// ContextActivityStreams contains the context document for activity streams
|
||||
ContextActivityStreams = "https://www.w3.org/ns/activitystreams"
|
||||
|
||||
HostMetaWebFingerTemplateRel = "lrdd"
|
||||
|
||||
// MimeAppActivityJSON represents a JSON activity pub action type.
|
||||
MimeAppActivityJSON = `application/activity+json`
|
||||
|
||||
// Activity Types
|
||||
|
||||
// TypeAccept is the Accept activity Type
|
||||
TypeAccept = "Accept"
|
||||
// TypeAnnounce is the Announce activity Type
|
||||
TypeAnnounce = "Announce"
|
||||
// TypeCreate is the Create activity Type
|
||||
TypeCreate = "Create"
|
||||
// TypeDelete is the Delete activity Type
|
||||
TypeDelete = "Delete"
|
||||
// TypeFollow is the Follow activity Type
|
||||
TypeFollow = "Follow"
|
||||
// TypeUndo is the Undo activity Type
|
||||
TypeUndo = "Undo"
|
||||
// TypeUpdate is the Update activity Type
|
||||
TypeUpdate = "Update"
|
||||
|
||||
// Actor Types
|
||||
|
||||
// TypeApplication is the Application actor type
|
||||
TypeApplication = "Application"
|
||||
// TypePerson is the Person actor type
|
||||
TypePerson = "Person"
|
||||
)
|
@ -1,73 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
gocipher "crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func decrypt(b []byte) ([]byte, error) {
|
||||
l := logger.WithField("func", "decrypt")
|
||||
|
||||
gcm, err := getCrypto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(b) < nonceSize {
|
||||
msg := "data too small"
|
||||
l.Error(msg)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
nonce, ciphertext := b[:nonceSize], b[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
l.Errorf("decrypting: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func encrypt(b []byte) ([]byte, error) {
|
||||
l := logger.WithField("func", "encrypt")
|
||||
|
||||
gcm, err := getCrypto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
l.Errorf("reading nonce: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, b, nil), nil
|
||||
}
|
||||
|
||||
func getCrypto() (gocipher.AEAD, error) {
|
||||
l := logger.WithField("func", "getCrypto").WithField("type", "EncryptedString")
|
||||
|
||||
key := []byte(strings.ToLower(viper.GetString(config.Keys.DbEncryptionKey)))
|
||||
cipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
l.Errorf("new cipher: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := gocipher.NewGCM(cipher)
|
||||
if err != nil {
|
||||
l.Errorf("new gcm: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm, nil
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tmthrgd/go-hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecrypt(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
viper.Set(config.Keys.DbEncryptionKey, "0123456789012345")
|
||||
|
||||
byts := hex.MustDecodeString("43dc49ab017fbde685011bc75e7aeecf46e2e6ca2d960681ebca6b7d9b5a74ad0348cfcadbdb71bebb")
|
||||
|
||||
val, err := decrypt(byts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting scanning, got: '%s', want: 'nil", err)
|
||||
return
|
||||
}
|
||||
if string(val) != "test string 1" {
|
||||
t.Errorf("unexpected value, got: '%s', want: '%s'", val, "test string 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt_NoData(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
viper.Set(config.Keys.DbEncryptionKey, "0123456789012345")
|
||||
|
||||
var byts []byte
|
||||
|
||||
_, err := decrypt(byts)
|
||||
errMsg := "data too small"
|
||||
if err == nil {
|
||||
t.Errorf("expected error getting scanning, got: 'nil', want: '%s", errMsg)
|
||||
return
|
||||
}
|
||||
if err.Error() != errMsg {
|
||||
t.Errorf("unexpected error getting scanning, got: '%s', want: '%s", err.Error(), errMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
viper.Set(config.Keys.DbEncryptionKey, "0123456789012345")
|
||||
|
||||
tables := []struct {
|
||||
n string
|
||||
}{
|
||||
{"test string 1"},
|
||||
}
|
||||
|
||||
for i, table := range tables {
|
||||
i := i
|
||||
table := table
|
||||
|
||||
name := fmt.Sprintf("[%d] Getting id", i)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := encrypt([]byte(table.n))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting value: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
gcm, err := getCrypto()
|
||||
if err != nil {
|
||||
t.Errorf("getting crypto: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
t.Errorf("value too small")
|
||||
return
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
t.Errorf("decrypting: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if string(plaintext) != table.n {
|
||||
t.Errorf("unexpected value, got: '%s', want: '%s'", plaintext, table.n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecrypt(b *testing.B) {
|
||||
viper.Reset()
|
||||
|
||||
viper.Set(config.Keys.DbEncryptionKey, "0123456789012345")
|
||||
|
||||
byts := hex.MustDecodeString("43dc49ab017fbde685011bc75e7aeecf46e2e6ca2d960681ebca6b7d9b5a74ad0348cfcadbdb71bebb")
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, _ = decrypt(byts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkEncrypt(b *testing.B) {
|
||||
viper.Reset()
|
||||
|
||||
viper.Set(config.Keys.DbEncryptionKey, "0123456789012345")
|
||||
|
||||
byts := []byte("test string")
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, _ = encrypt(byts)
|
||||
}
|
||||
})
|
||||
}
|
27
internal/models/error.go
Normal file
27
internal/models/error.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error represents a logic specific error.
|
||||
type Error struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// Error returns the error message as a string.
|
||||
func (e *Error) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// NewError wraps a message in an Error object.
|
||||
func NewError(m string) *Error {
|
||||
return &Error{
|
||||
message: m,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorf wraps a message in an Error object.
|
||||
func NewErrorf(m string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
message: fmt.Sprintf(m, args...),
|
||||
}
|
||||
}
|
15
internal/models/host_meta.go
Normal file
15
internal/models/host_meta.go
Normal file
@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
type HostMeta struct {
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Links []Link `xml:"Link"`
|
||||
}
|
||||
|
||||
func (h *HostMeta) WebfingerURI() WebfingerURI {
|
||||
for _, link := range h.Links {
|
||||
if link.Rel == HostMetaWebFingerTemplateRel {
|
||||
return WebfingerURI(link.Template)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,25 +1,31 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/uptrace/bun"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Instance represents a federated social instance
|
||||
type Instance struct {
|
||||
ID int64 `validate:"-" bun:",pk,autoincrement,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
Domain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
PublicKey *rsa.PublicKey `validate:"-"`
|
||||
PrivateKey []byte `validate:"-"`
|
||||
InboxIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
Followed bool `validate:"-" bun:",notnull,default:false"`
|
||||
BlockID int64 `validate:"-" bun:",nullzero"`
|
||||
Block *Block `validate:"-" bun:"rel:belongs-to"`
|
||||
ID int64 `validate:"-" bun:",pk,autoincrement,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"`
|
||||
AccountDomain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
Domain string `validate:"required,fqdn" bun:",nullzero,notnull,unique"`
|
||||
PublicKey *rsa.PublicKey `validate:"-"`
|
||||
PrivateKey *rsa.PrivateKey `validate:"-"`
|
||||
ActorIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
InboxIRI string `validate:"required,url" bun:",nullzero,notnull,unique"`
|
||||
Followed bool `validate:"-" bun:",notnull"`
|
||||
BlockID int64 `validate:"-" bun:",nullzero"`
|
||||
Block *Block `validate:"-" bun:"rel:belongs-to"`
|
||||
}
|
||||
|
||||
var _ bun.BeforeAppendModelHook = (*Instance)(nil)
|
||||
@ -47,33 +53,35 @@ func (i *Instance) BeforeAppendModel(_ context.Context, query bun.Query) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPrivateKey returns unencrypted private key
|
||||
func (i *Instance) GetPrivateKey() (*rsa.PrivateKey, error) {
|
||||
// decrypt bytes
|
||||
b, err := decrypt(i.PrivateKey)
|
||||
func (i *Instance) PublicKeyPEM() (string, error) {
|
||||
l := logger.WithFields(logrus.Fields{
|
||||
"func": "PublicKeyPEM",
|
||||
"model": "Instance",
|
||||
})
|
||||
|
||||
// convert to private key
|
||||
pk, err := x509.ParsePKCS1PrivateKey(b)
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(i.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
l.Errorf("marshaling public key: %s", err.Error())
|
||||
|
||||
return "", err
|
||||
}
|
||||
publicKeyBlock := &pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}
|
||||
publicPem := new(bytes.Buffer)
|
||||
err = pem.Encode(publicPem, publicKeyBlock)
|
||||
if err != nil {
|
||||
l.Errorf("encoding pem: %s", err.Error())
|
||||
|
||||
return "", err
|
||||
}
|
||||
publicPemBytes, err := io.ReadAll(publicPem)
|
||||
if err != nil {
|
||||
l.Errorf("reading pem: %s", err.Error())
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
// return private key
|
||||
return pk, err
|
||||
}
|
||||
|
||||
// SetPrivateKey sets encrypted private key
|
||||
func (i *Instance) SetPrivateKey(pk *rsa.PrivateKey) error {
|
||||
// convert to bytes
|
||||
b := x509.MarshalPKCS1PrivateKey(pk)
|
||||
|
||||
// encrypt bytes
|
||||
data, err := encrypt(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store bytes
|
||||
i.PrivateKey = data
|
||||
return nil
|
||||
return string(publicPemBytes), nil
|
||||
}
|
||||
|
34
internal/models/webfinger.go
Normal file
34
internal/models/webfinger.go
Normal file
@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// WebFinger represents a web finger response
|
||||
type WebFinger struct {
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
}
|
||||
|
||||
// ActorURI an actor uri.
|
||||
func (w *WebFinger) ActorURI() (*url.URL, error) {
|
||||
var actorURIstr string
|
||||
for _, link := range w.Links {
|
||||
if link.Rel == "self" || link.Type == MimeAppActivityJSON {
|
||||
actorURIstr = link.Href
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if actorURIstr == "" {
|
||||
return nil, NewError("missing actor uri")
|
||||
}
|
||||
|
||||
actorURI, err := url.Parse(actorURIstr)
|
||||
if err != nil {
|
||||
return nil, NewErrorf("invalid actor uri: %s", err.Error())
|
||||
}
|
||||
|
||||
return actorURI, err
|
||||
}
|
13
internal/models/webfinger_uri.go
Normal file
13
internal/models/webfinger_uri.go
Normal file
@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import "strings"
|
||||
|
||||
type WebfingerURI string
|
||||
|
||||
func (w WebfingerURI) FTemplate() string {
|
||||
return strings.ReplaceAll(string(w), "{uri}", "acct:%s@%s")
|
||||
}
|
||||
|
||||
func (w WebfingerURI) String() string {
|
||||
return string(w)
|
||||
}
|
@ -24,6 +24,8 @@ const (
|
||||
|
||||
// APActor is the path to the relay actor
|
||||
APActor = "/" + PartActor
|
||||
// APInbox is the path to the relay inbox
|
||||
APInbox = "/" + PartInbox
|
||||
// APNodeInfo20 is the path to the node info 2.0 schema
|
||||
APNodeInfo20 = "/" + PartNodeinfo + "/2.0"
|
||||
// APWellKnownNodeInfo is the path to the well known node info endpoint
|
||||
|
@ -1,6 +1,9 @@
|
||||
package path
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// GenActor returns a url for an actor
|
||||
func GenActor(d string) string {
|
||||
@ -31,3 +34,12 @@ func GenNodeinfo20(d string) string {
|
||||
func GenPublicKey(d string) string {
|
||||
return fmt.Sprintf("%s#%s", GenActor(d), PartPublicKey)
|
||||
}
|
||||
|
||||
// GenWellKnownNodeInfoURL returns a url for well known node info url
|
||||
func GenWellKnownNodeInfoURL(d string) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: d,
|
||||
Path: APWellKnownNodeInfo,
|
||||
}
|
||||
}
|
||||
|
120
internal/runner/faktory/activity.go
Normal file
120
internal/runner/faktory/activity.go
Normal file
@ -0,0 +1,120 @@
|
||||
package faktory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
worker "github.com/contribsys/faktory_worker_go"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (r *Runner) EnqueueDeliverActivity(_ context.Context, instanceID int64, activity models.Activity) error {
|
||||
job := faktory.NewJob(JobDeliverActivity, strconv.FormatInt(instanceID, 10), activity)
|
||||
job.Queue = QueueDelivery
|
||||
|
||||
client, err := r.manager.Pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Push(job)
|
||||
}
|
||||
|
||||
func (r *Runner) deliverActivity(ctx context.Context, args ...interface{}) error {
|
||||
help := worker.HelperFor(ctx)
|
||||
|
||||
l := logger.WithFields(logrus.Fields{
|
||||
"func": "deliverActivity",
|
||||
"jid": help.Jid(),
|
||||
})
|
||||
|
||||
if len(args) != 2 {
|
||||
l.Errorf("wrong number of arguments, got: %d, want: %d", len(args), 2)
|
||||
}
|
||||
|
||||
// cast arguments
|
||||
instanceIDStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
l.Errorf("argument 0 is not an string")
|
||||
|
||||
return fmt.Errorf("argument 0 is not an int")
|
||||
}
|
||||
instanceID, err := strconv.ParseInt(instanceIDStr, 10, 64)
|
||||
if err != nil {
|
||||
l.Errorf("cant parse int from argument 0: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("cant parse int from argument 0: %s", err.Error())
|
||||
|
||||
}
|
||||
activity, ok := args[1].(map[string]interface{})
|
||||
if !ok {
|
||||
l.Errorf("argument 1 is not an activity")
|
||||
|
||||
return fmt.Errorf("argument 1 is not an activity")
|
||||
}
|
||||
|
||||
return r.logic.DeliverActivity(ctx, instanceID, activity)
|
||||
}
|
||||
|
||||
func (r *Runner) EnqueueInboxActivity(_ context.Context, instanceID int64, actorIRI string, activity models.Activity) error {
|
||||
job := faktory.NewJob(JobInboxActivity, strconv.FormatInt(instanceID, 10), actorIRI, activity)
|
||||
job.Queue = QueueDefault
|
||||
|
||||
client, err := r.manager.Pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Push(job)
|
||||
}
|
||||
|
||||
func (r *Runner) inboxActivity(ctx context.Context, args ...interface{}) error {
|
||||
help := worker.HelperFor(ctx)
|
||||
|
||||
l := logger.WithFields(logrus.Fields{
|
||||
"func": "inboxActivity",
|
||||
"jid": help.Jid(),
|
||||
})
|
||||
|
||||
if len(args) != 3 {
|
||||
l.Errorf("wrong number of arguments, got: %d, want: %d", len(args), 2)
|
||||
}
|
||||
|
||||
// cast arguments
|
||||
instanceIDStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
l.Errorf("argument 0 is not an string, got %T", args[0])
|
||||
|
||||
return fmt.Errorf("argument 0 is not an int")
|
||||
}
|
||||
instanceID, err := strconv.ParseInt(instanceIDStr, 10, 64)
|
||||
if err != nil {
|
||||
l.Errorf("cant parse int from argument 0: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("cant parse int from argument 0: %s", err.Error())
|
||||
}
|
||||
|
||||
actorID, ok := args[1].(string)
|
||||
if !ok {
|
||||
l.Errorf("argument 1 is not an string, got %T", args[1])
|
||||
|
||||
return fmt.Errorf("argument 1 is not an actor")
|
||||
}
|
||||
actorIRI, err := url.Parse(actorID)
|
||||
if err != nil {
|
||||
l.Errorf("cant parse url from argument 1: %s", err.Error())
|
||||
|
||||
return fmt.Errorf("cant parse url from argument 1: %s", err.Error())
|
||||
}
|
||||
|
||||
activity, ok := args[2].(map[string]interface{})
|
||||
if !ok {
|
||||
l.Errorf("argument 2 is not an activity, got %T", args[2])
|
||||
|
||||
return fmt.Errorf("argument 2 is not an activity")
|
||||
}
|
||||
|
||||
// process activity
|
||||
return r.logic.ProcessActivity(ctx, instanceID, actorIRI, activity)
|
||||
}
|
9
internal/runner/faktory/const.go
Normal file
9
internal/runner/faktory/const.go
Normal file
@ -0,0 +1,9 @@
|
||||
package faktory
|
||||
|
||||
const (
|
||||
JobDeliverActivity = "DeliverActivity"
|
||||
JobInboxActivity = "InboxActivity"
|
||||
|
||||
QueueDefault = "default"
|
||||
QueueDelivery = "delivery"
|
||||
)
|
9
internal/runner/faktory/logger.go
Normal file
9
internal/runner/faktory/logger.go
Normal file
@ -0,0 +1,9 @@
|
||||
package faktory
|
||||
|
||||
import (
|
||||
"github.com/feditools/relay/internal/log"
|
||||
)
|
||||
|
||||
type empty struct{}
|
||||
|
||||
var logger = log.WithPackageField(empty{})
|
43
internal/runner/faktory/runner.go
Normal file
43
internal/runner/faktory/runner.go
Normal file
@ -0,0 +1,43 @@
|
||||
package faktory
|
||||
|
||||
import (
|
||||
"context"
|
||||
worker "github.com/contribsys/faktory_worker_go"
|
||||
"github.com/feditools/relay/internal/config"
|
||||
"github.com/feditools/relay/internal/logic"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
logic *logic.Logic
|
||||
manager *worker.Manager
|
||||
}
|
||||
|
||||
// New created a new logic module
|
||||
func New(l *logic.Logic) (*Runner, error) {
|
||||
newRunner := &Runner{
|
||||
logic: l,
|
||||
}
|
||||
|
||||
mgr := worker.NewManager()
|
||||
mgr.Concurrency = viper.GetInt(config.Keys.RunnerConcurrency)
|
||||
mgr.ProcessWeightedPriorityQueues(map[string]int{QueueDefault: 2, QueueDelivery: 1})
|
||||
|
||||
mgr.Register(JobDeliverActivity, newRunner.deliverActivity)
|
||||
mgr.Register(JobInboxActivity, newRunner.inboxActivity)
|
||||
|
||||
newRunner.manager = mgr
|
||||
|
||||
return newRunner, nil
|
||||
}
|
||||
|
||||
func (r *Runner) Start(ctx context.Context) {
|
||||
l := logger.WithField("func", "Start")
|
||||
|
||||
go func() {
|
||||
err := r.manager.RunWithContext(ctx)
|
||||
if err != nil {
|
||||
l.Errorf("run error: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
11
internal/runner/runner.go
Normal file
11
internal/runner/runner.go
Normal file
@ -0,0 +1,11 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/feditools/relay/internal/models"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
EnqueueInboxActivity(ctx context.Context, instanceID int64, actorIRI string, activity models.Activity) (err error)
|
||||
EnqueueDeliverActivity(ctx context.Context, instanceID int64, activity models.Activity) (err error)
|
||||
}
|
15
internal/transport/const.go
Normal file
15
internal/transport/const.go
Normal file
@ -0,0 +1,15 @@
|
||||
package transport
|
||||
|
||||
import "github.com/go-fed/httpsig"
|
||||
|
||||
const (
|
||||
rfc1123WithoutZone = "Mon, 02 Jan 2006 15:04:05"
|
||||
)
|
||||
|
||||
var (
|
||||
digestAlgo = httpsig.DigestSha256
|
||||
algoPrefs = []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
|
||||
getHeaders = []string{httpsig.RequestTarget, "host", "date"}
|
||||
postHeaders = []string{httpsig.RequestTarget, "host", "date", "digest", "content-type"}
|
||||
)
|
90
internal/transport/instance.go
Normal file
90
internal/transport/instance.go
Normal file
@ -0,0 +1,90 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (t *Transport) InstanceGet(ctx context.Context, uri *url.URL, accepts ...string) ([]byte, error) {
|
||||
l := logger.WithField("func", "InstanceGet")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", uri.String(), nil)
|
||||
if err != nil {
|
||||
l.Errorf("creating http request: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, accept := range accepts {
|
||||
req.Header.Add("Accept", accept)
|
||||
}
|
||||
req.Header.Add("Date", t.clock.Now().UTC().Format(rfc1123WithoutZone)+" GMT")
|
||||
req.Header.Set("Host", uri.Host)
|
||||
|
||||
t.doSign(func() {
|
||||
err = t.getSigner.SignRequest(t.privKey, t.keyID, req, nil)
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("can't lock signer: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
l.Errorf("http client do: %s", err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http get %s: %d-%s", uri.String(), resp.StatusCode, resp.Status)
|
||||
}
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (t *Transport) InstancePost(ctx context.Context, uri *url.URL, body []byte, contentType string, accepts ...string) ([]byte, error) {
|
||||
l := logger.WithField("func", "InstancePost")
|
||||
|
||||
bodyReader := bytes.NewReader(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), bodyReader)
|
||||
if err != nil {
|
||||
l.Errorf("creating http request: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, accept := range accepts {
|
||||
req.Header.Add("Accept", accept)
|
||||
}
|
||||
req.Header.Add("Date", t.clock.Now().UTC().Format(rfc1123WithoutZone)+" GMT")
|
||||
req.Header.Set("Host", uri.Host)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
|
||||
t.doSign(func() {
|
||||
err = t.postSigner.SignRequest(t.privKey, t.keyID, req, body)
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("can't lock signer: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
l.Errorf("http client do: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
l.Warnf("can't read body: %s", err.Error())
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 && resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("http post %s: %s-%s", uri.String(), resp.Status, string(respBody))
|
||||
}
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
9
internal/transport/logger.go
Normal file
9
internal/transport/logger.go
Normal file
@ -0,0 +1,9 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"github.com/feditools/relay/internal/log"
|
||||
)
|
||||
|
||||
type empty struct{}
|
||||
|
||||
var logger = log.WithPackageField(empty{})
|
70
internal/transport/transport.go
Normal file
70
internal/transport/transport.go
Normal file
@ -0,0 +1,70 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"github.com/feditools/relay/internal/http"
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/httpsig"
|
||||
nethttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transport handled signing outgoing requests to federated instances
|
||||
type Transport struct {
|
||||
client pub.HttpClient
|
||||
clock pub.Clock
|
||||
|
||||
keyID string
|
||||
privKey crypto.PrivateKey
|
||||
|
||||
signerExp time.Time
|
||||
getSigner httpsig.Signer
|
||||
postSigner httpsig.Signer
|
||||
signerMu sync.Mutex
|
||||
}
|
||||
|
||||
// New creates a new Transport module
|
||||
func New(clock pub.Clock, pubKeyID string, privkey crypto.PrivateKey) (*Transport, error) {
|
||||
//l := logger.WithField("func", "New")
|
||||
|
||||
httpClient := &nethttp.Client{
|
||||
Transport: &http.Transport{},
|
||||
}
|
||||
|
||||
return &Transport{
|
||||
client: httpClient,
|
||||
clock: clock,
|
||||
|
||||
keyID: pubKeyID,
|
||||
privKey: privkey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) doSign(do func()) {
|
||||
// Perform within mu safety
|
||||
t.signerMu.Lock()
|
||||
defer t.signerMu.Unlock()
|
||||
|
||||
if now := time.Now(); now.After(t.signerExp) {
|
||||
const expiry = 120
|
||||
|
||||
// Signers have expired and require renewal
|
||||
t.getSigner, _ = genGetSigner(expiry)
|
||||
t.postSigner, _ = genPostSigner(expiry)
|
||||
t.signerExp = now.Add(time.Second * expiry)
|
||||
}
|
||||
|
||||
// Perform signing
|
||||
do()
|
||||
}
|
||||
|
||||
func genGetSigner(expiresIn int64) (httpsig.Signer, error) {
|
||||
sig, _, err := httpsig.NewSigner(algoPrefs, digestAlgo, getHeaders, httpsig.Signature, expiresIn)
|
||||
return sig, err
|
||||
}
|
||||
|
||||
func genPostSigner(expiresIn int64) (httpsig.Signer, error) {
|
||||
sig, _, err := httpsig.NewSigner(algoPrefs, digestAlgo, postHeaders, httpsig.Signature, expiresIn)
|
||||
return sig, err
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
# String. Log level to use throughout the application.
|
||||
# Options: ["trace","debug","info","warn","error","fatal"]
|
||||
# Default: "info"
|
||||
log-level: "debug"
|
||||
log-level: "trace"
|
||||
|
||||
##############
|
||||
## DATABASE ##
|
||||
|
674
vendor/github.com/contribsys/faktory/LICENSE
generated
vendored
Normal file
674
vendor/github.com/contribsys/faktory/LICENSE
generated
vendored
Normal file
@ -0,0 +1,674 @@
|
||||
Faktory is AGPL licensed. What does this mean? In short:
|
||||
|
||||
If you fork Faktory, make changes and allow others to access your modified Faktory,
|
||||
the source code for those changes must be made available to those users.
|
||||
|
||||
Note the Faktory client and worker libraries are NOT covered by this license, only the server is AGPL licensed.
|
||||
You do NOT need to open source any business logic or code for your background jobs.
|
||||
Please email mike@contribsys.com if you have further licensing questions.
|
||||
|
||||
|
||||
---------
|
||||
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
373
vendor/github.com/contribsys/faktory/client/LICENSE
generated
vendored
Normal file
373
vendor/github.com/contribsys/faktory/client/LICENSE
generated
vendored
Normal file
@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
190
vendor/github.com/contribsys/faktory/client/batch.go
generated
vendored
Normal file
190
vendor/github.com/contribsys/faktory/client/batch.go
generated
vendored
Normal file
@ -0,0 +1,190 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type BatchStatus struct {
|
||||
Bid string `json:"bid"`
|
||||
ParentBid string `json:"parent_bid,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Total int64 `json:"total"`
|
||||
Pending int64 `json:"pending"`
|
||||
Failed int64 `json:"failed"`
|
||||
|
||||
// "" if pending,
|
||||
// "1" if callback enqueued,
|
||||
// "2" if callback finished successfully
|
||||
CompleteState string `json:"complete_st"`
|
||||
SuccessState string `json:"success_st"`
|
||||
}
|
||||
|
||||
type Batch struct {
|
||||
// Unique identifier for each batch.
|
||||
// NB: the caller should not set this, it is generated
|
||||
// by Faktory when the batch is persisted to Redis.
|
||||
Bid string `json:"bid"`
|
||||
|
||||
ParentBid string `json:"parent_bid,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Success *Job `json:"success,omitempty"`
|
||||
Complete *Job `json:"complete,omitempty"`
|
||||
|
||||
faktory *Client
|
||||
committed bool
|
||||
new bool
|
||||
}
|
||||
|
||||
//
|
||||
// Allocate a new Batch.
|
||||
// Caller must set one or more callbacks and
|
||||
// push one or more jobs in the batch.
|
||||
//
|
||||
// b := faktory.NewBatch(cl)
|
||||
// b.Success = faktory.NewJob("MySuccessCallback", 12345)
|
||||
// b.Jobs(func() error {
|
||||
// b.Push(...)
|
||||
// })
|
||||
// b.Commit()
|
||||
//
|
||||
func NewBatch(cl *Client) *Batch {
|
||||
return &Batch{
|
||||
committed: false,
|
||||
new: true,
|
||||
faktory: cl,
|
||||
}
|
||||
}
|
||||
|
||||
// Push one or more jobs within this function.
|
||||
// Job processing will start **immediately**
|
||||
// but callbacks will not fire until Commit()
|
||||
// is called, allowing you to push jobs in slowly
|
||||
// and avoid the obvious race condition.
|
||||
func (b *Batch) Jobs(fn func() error) error {
|
||||
if b.new {
|
||||
if _, err := b.faktory.BatchNew(b); err != nil {
|
||||
return fmt.Errorf("cannot create new batch: %w", err)
|
||||
}
|
||||
}
|
||||
if b.faktory == nil || b.committed {
|
||||
return BatchNotOpen
|
||||
}
|
||||
|
||||
if err := fn(); err != nil {
|
||||
return fmt.Errorf("cannot push jobs in the %q batch: %w", b.Bid, err)
|
||||
}
|
||||
|
||||
return b.Commit()
|
||||
}
|
||||
|
||||
func (b *Batch) Push(job *Job) error {
|
||||
if b.new {
|
||||
return BatchNotOpen
|
||||
}
|
||||
if b.faktory == nil || b.committed {
|
||||
return BatchAlreadyCommitted
|
||||
}
|
||||
job.SetCustom("bid", b.Bid)
|
||||
return b.faktory.Push(job)
|
||||
}
|
||||
|
||||
// Commit any pushed jobs in the batch to Redis so they can fire callbacks.
|
||||
// A Batch object can only be committed once.
|
||||
// You must use client.BatchOpen to get a new copy if you want to commit more jobs.
|
||||
func (b *Batch) Commit() error {
|
||||
if b.new {
|
||||
return BatchNotOpen
|
||||
}
|
||||
if b.faktory == nil || b.committed {
|
||||
return BatchAlreadyCommitted
|
||||
}
|
||||
if err := b.faktory.BatchCommit(b.Bid); err != nil {
|
||||
return fmt.Errorf("cannot commit %q batch: %w", b.Bid, err)
|
||||
}
|
||||
|
||||
b.faktory = nil
|
||||
b.committed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
BatchAlreadyCommitted = fmt.Errorf("Batch has already been committed, must reopen")
|
||||
BatchNotOpen = fmt.Errorf("Batch must be opened before it can be used")
|
||||
)
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// Low-level command API
|
||||
|
||||
func (c *Client) BatchCommit(bid string) error {
|
||||
err := c.writeLine(c.wtr, "BATCH COMMIT", []byte(bid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
func (c *Client) BatchNew(def *Batch) (*Batch, error) {
|
||||
if def.Bid != "" {
|
||||
return nil, fmt.Errorf("BID must be blank when creating a new Batch, cannot specify it")
|
||||
}
|
||||
bbytes, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.writeLine(c.wtr, "BATCH NEW", bbytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bid, err := c.readString(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
def.Bid = bid
|
||||
def.new = false
|
||||
def.faktory = c
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func (c *Client) BatchStatus(bid string) (*BatchStatus, error) {
|
||||
err := c.writeLine(c.wtr, "BATCH STATUS", []byte(bid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := c.readResponse(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stat BatchStatus
|
||||
err = json.Unmarshal(data, &stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stat, nil
|
||||
}
|
||||
|
||||
func (c *Client) BatchOpen(bid string) (*Batch, error) {
|
||||
err := c.writeLine(c.wtr, "BATCH OPEN", []byte(bid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bbid, err := c.readString(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Batch{
|
||||
Bid: bbid,
|
||||
new: false,
|
||||
faktory: c,
|
||||
}
|
||||
return b, nil
|
||||
}
|
678
vendor/github.com/contribsys/faktory/client/client.go
generated
vendored
Normal file
678
vendor/github.com/contribsys/faktory/client/client.go
generated
vendored
Normal file
@ -0,0 +1,678 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/contribsys/faktory/internal/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
// This is the protocol version supported by this client.
|
||||
// The server might be running an older or newer version.
|
||||
ExpectedProtocolVersion = 2
|
||||
)
|
||||
|
||||
var (
|
||||
// Set this to a non-empty value in a consumer process
|
||||
// e.g. see how faktory_worker_go sets this.
|
||||
RandomProcessWid = ""
|
||||
Labels = []string{"golang"}
|
||||
)
|
||||
|
||||
// Dialer is the interface for creating a specialized net.Conn.
|
||||
type Dialer interface {
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
}
|
||||
|
||||
// The Client structure represents a thread-unsafe connection
|
||||
// to a Faktory server. It is recommended to use a connection pool
|
||||
// of Clients in a multi-threaded process. See faktory_worker_go's
|
||||
// internal connection pool for example.
|
||||
type Client struct {
|
||||
Location string
|
||||
Options *ClientData
|
||||
rdr *bufio.Reader
|
||||
wtr *bufio.Writer
|
||||
conn net.Conn
|
||||
poolConn *pool.PoolConn
|
||||
}
|
||||
|
||||
// ClientData is serialized to JSON and sent
|
||||
// with the HELLO command. PasswordHash is required
|
||||
// if the server is not listening on localhost.
|
||||
// The WID (worker id) must be random and unique
|
||||
// for each worker process. It can be a UUID, etc.
|
||||
// Non-worker processes should leave WID empty.
|
||||
//
|
||||
// The other elements can be useful for debugging
|
||||
// and are displayed on the Busy tab.
|
||||
type ClientData struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Wid string `json:"wid"`
|
||||
Pid int `json:"pid"`
|
||||
Labels []string `json:"labels"`
|
||||
|
||||
// this can be used by proxies to route the connection.
|
||||
// it is ignored by Faktory.
|
||||
Username string `json:"username"`
|
||||
|
||||
// Hash is hex(sha256(password + nonce))
|
||||
PasswordHash string `json:"pwdhash"`
|
||||
|
||||
// The protocol version used by this client.
|
||||
// The server can reject this connection if the version will not work
|
||||
// The server advertises its protocol version in the HI.
|
||||
Version int `json:"v"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Network string
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
TLS *tls.Config
|
||||
}
|
||||
|
||||
// OpenWithDialer creates a *Client with the dialer.
|
||||
func (s *Server) OpenWithDialer(dialer Dialer) (*Client, error) {
|
||||
return DialWithDialer(s, s.Password, dialer)
|
||||
}
|
||||
|
||||
func (s *Server) Open() (*Client, error) {
|
||||
return Dial(s, s.Password)
|
||||
}
|
||||
|
||||
func (s *Server) ReadFromEnv() error {
|
||||
val, ok := os.LookupEnv("FAKTORY_PROVIDER")
|
||||
if ok {
|
||||
if strings.Contains(val, ":") {
|
||||
return fmt.Errorf(`Error: FAKTORY_PROVIDER is not a URL. It is the name of the ENV var that contains the URL:
|
||||
|
||||
FAKTORY_PROVIDER=FOO_URL
|
||||
FOO_URL=tcp://:mypassword@faktory.example.com:7419`)
|
||||
}
|
||||
|
||||
uval, ok := os.LookupEnv(val)
|
||||
if ok {
|
||||
uri, err := url.Parse(uval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Network = uri.Scheme
|
||||
s.Address = fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port())
|
||||
if uri.User != nil {
|
||||
s.Username = uri.User.Username()
|
||||
s.Password, _ = uri.User.Password()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("FAKTORY_PROVIDER set to invalid value: %s", val)
|
||||
}
|
||||
|
||||
uval, ok := os.LookupEnv("FAKTORY_URL")
|
||||
if ok {
|
||||
uri, err := url.Parse(uval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse value of FAKTORY_URL environment variable: %w", err)
|
||||
}
|
||||
|
||||
s.Network = uri.Scheme
|
||||
s.Address = fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port())
|
||||
if uri.User != nil {
|
||||
s.Username = uri.User.Username()
|
||||
s.Password, _ = uri.User.Password()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultServer() *Server {
|
||||
return &Server{"tcp", "localhost:7419", "", "", 1 * time.Second, &tls.Config{MinVersion: tls.VersionTLS12}}
|
||||
}
|
||||
|
||||
// Open connects to a Faktory server based on
|
||||
// environment variable conventions:
|
||||
//
|
||||
// • Use FAKTORY_PROVIDER to point to a custom URL variable.
|
||||
// • Use FAKTORY_URL as a catch-all default.
|
||||
//
|
||||
// Use the URL to configure any necessary password:
|
||||
//
|
||||
// tcp://:mypassword@localhost:7419
|
||||
//
|
||||
// By default Open assumes localhost with no password
|
||||
// which is appropriate for local development.
|
||||
func Open() (*Client, error) {
|
||||
srv := DefaultServer()
|
||||
if err := srv.ReadFromEnv(); err != nil {
|
||||
return nil, fmt.Errorf("cannot read configuration from env: %w", err)
|
||||
}
|
||||
// Connect to default localhost
|
||||
return srv.Open()
|
||||
}
|
||||
|
||||
// OpenWithDialer connects to a Faktory server
|
||||
// following the same conventions as Open but
|
||||
// instead uses dialer as the transport.
|
||||
func OpenWithDialer(dialer Dialer) (*Client, error) {
|
||||
srv := DefaultServer()
|
||||
if err := srv.ReadFromEnv(); err != nil {
|
||||
return nil, fmt.Errorf("cannot read configuration from env: %w", err)
|
||||
}
|
||||
// Connect to default localhost
|
||||
return srv.OpenWithDialer(dialer)
|
||||
}
|
||||
|
||||
// Dial connects to the remote faktory server with
|
||||
// a Dialer reflecting the value of srv.Network; i.e.,
|
||||
// a *tls.Dialer if "tcp+tls" and a *net.Dialer if
|
||||
// not.
|
||||
//
|
||||
// client.Dial(client.Localhost, "topsecret")
|
||||
//
|
||||
func Dial(srv *Server, password string) (*Client, error) {
|
||||
d := &net.Dialer{Timeout: srv.Timeout}
|
||||
dialer := Dialer(d)
|
||||
if srv.Network == "tcp+tls" {
|
||||
dialer = &tls.Dialer{NetDialer: d, Config: srv.TLS}
|
||||
}
|
||||
return dial(srv, password, dialer)
|
||||
}
|
||||
|
||||
// DialWithDialer connects to the faktory server
|
||||
func DialWithDialer(srv *Server, password string, dialer Dialer) (*Client, error) {
|
||||
return dial(srv, password, dialer)
|
||||
}
|
||||
|
||||
// dial connects to the remote faktory server.
|
||||
func dial(srv *Server, password string, dialer Dialer) (*Client, error) {
|
||||
client := emptyClientData()
|
||||
client.Username = srv.Username
|
||||
|
||||
var err error
|
||||
var conn net.Conn
|
||||
|
||||
conn, err = dialer.Dial("tcp", srv.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if x, ok := conn.(*net.TCPConn); ok {
|
||||
_ = x.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
w := bufio.NewWriter(conn)
|
||||
|
||||
line, err := readString(r)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "HI ") {
|
||||
str := strings.TrimSpace(line)[3:]
|
||||
|
||||
var hi map[string]interface{}
|
||||
err = json.Unmarshal([]byte(str), &hi)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
v, ok := hi["v"].(float64)
|
||||
if ok {
|
||||
if ExpectedProtocolVersion != int(v) {
|
||||
fmt.Println("Warning: server and client protocol versions out of sync:", v, ExpectedProtocolVersion)
|
||||
}
|
||||
}
|
||||
|
||||
salt, ok := hi["s"].(string)
|
||||
if ok {
|
||||
iter := 1
|
||||
iterVal, ok := hi["i"]
|
||||
if ok {
|
||||
iter = int(iterVal.(float64))
|
||||
}
|
||||
|
||||
client.PasswordHash = hash(password, salt, iter)
|
||||
}
|
||||
} else {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("Expecting HI but got: %s", line)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot JSON marshal: %w", err)
|
||||
}
|
||||
|
||||
if err := writeLine(w, "HELLO", data); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ok(r)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{Options: client, Location: srv.Address, conn: conn, rdr: r, wtr: w}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
_ = writeLine(c.wtr, "END", nil)
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Ack(jid string) error {
|
||||
err := c.writeLine(c.wtr, "ACK", []byte(fmt.Sprintf(`{"jid":"%s"}`, jid)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
// Result is map[JID]ErrorMessage
|
||||
func (c *Client) PushBulk(jobs []*Job) (map[string]string, error) {
|
||||
jobBytes, err := json.Marshal(jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.writeLine(c.wtr, "PUSHB", jobBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := c.readResponse(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := map[string]string{}
|
||||
err = json.Unmarshal(data, &results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (c *Client) Push(job *Job) error {
|
||||
jobBytes, err := json.Marshal(job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.writeLine(c.wtr, "PUSH", jobBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
func (c *Client) Fetch(q ...string) (*Job, error) {
|
||||
if len(q) == 0 {
|
||||
return nil, fmt.Errorf("Fetch must be called with one or more queue names")
|
||||
}
|
||||
|
||||
err := c.writeLine(c.wtr, "FETCH", []byte(strings.Join(q, " ")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := c.readResponse(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var job Job
|
||||
err = json.Unmarshal(data, &job)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
/*
|
||||
buff := make([]byte, 4096)
|
||||
count := runtime.Stack(buff, false)
|
||||
str := string(buff[0:count])
|
||||
bt := strings.Split(str, "\n")
|
||||
*/
|
||||
|
||||
// Fail notifies Faktory that a job failed with the given error.
|
||||
// If backtrace is non-nil, it is assumed to be the output from
|
||||
// runtime/debug.Stack().
|
||||
func (c *Client) Fail(jid string, err error, backtrace []byte) error {
|
||||
failure := map[string]interface{}{
|
||||
"message": err.Error(),
|
||||
"errtype": "unknown",
|
||||
"jid": jid,
|
||||
}
|
||||
|
||||
if backtrace != nil {
|
||||
str := string(backtrace)
|
||||
bt := strings.Split(str, "\n")
|
||||
failure["backtrace"] = bt[3:]
|
||||
}
|
||||
failbytes, err := json.Marshal(failure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.writeLine(c.wtr, "FAIL", failbytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
func (c *Client) Flush() error {
|
||||
err := c.writeLine(c.wtr, "FLUSH", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
// List queues explicitly or use "*" to pause all known queues
|
||||
func (c *Client) PauseQueues(names ...string) error {
|
||||
err := c.writeLine(c.wtr, "QUEUE PAUSE", []byte(strings.Join(names, " ")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
// List queues explicitly or use "*" to resume all known queues
|
||||
func (c *Client) ResumeQueues(names ...string) error {
|
||||
err := c.writeLine(c.wtr, "QUEUE RESUME", []byte(strings.Join(names, " ")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
||||
|
||||
func (c *Client) Info() (map[string]interface{}, error) {
|
||||
err := c.writeLine(c.wtr, "INFO", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := c.readResponse(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var hash map[string]interface{}
|
||||
err = json.Unmarshal(data, &hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueueSizes() (map[string]uint64, error) {
|
||||
hash, err := c.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
faktory, ok := hash["faktory"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid info hash: %s", hash)
|
||||
}
|
||||
|
||||
queues, ok := faktory["queues"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid info hash: %s", hash)
|
||||
}
|
||||
|
||||
sizes := make(map[string]uint64)
|
||||
for name, size := range queues {
|
||||
size, ok := size.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid queue size: %v", size)
|
||||
}
|
||||
|
||||
sizes[name] = uint64(size)
|
||||
}
|
||||
|
||||
return sizes, nil
|
||||
}
|
||||
|
||||
func (c *Client) Generic(cmdline string) (string, error) {
|
||||
err := c.writeLine(c.wtr, cmdline, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return c.readString(c.rdr)
|
||||
}
|
||||
|
||||
/*
|
||||
* The first arg to Beat allows a worker process to report its current lifecycle state
|
||||
* to Faktory. All worker processes must follow the same basic lifecycle:
|
||||
*
|
||||
* (startup) -> "" -> "quiet" -> "terminate"
|
||||
*
|
||||
* Quiet allows the process to finish its current work without fetching any new work.
|
||||
* Terminate means the process should exit within X seconds, usually ~30 seconds.
|
||||
*/
|
||||
func (c *Client) Beat(args ...string) (string, error) {
|
||||
state := ""
|
||||
if len(args) > 0 {
|
||||
state = args[0]
|
||||
}
|
||||
hash := map[string]interface{}{}
|
||||
hash["wid"] = RandomProcessWid
|
||||
hash["rss_kb"] = RssKb()
|
||||
|
||||
if state != "" {
|
||||
hash["current_state"] = state
|
||||
}
|
||||
data, err := json.Marshal(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := fmt.Sprintf("BEAT %s", data)
|
||||
val, err := c.Generic(cmd)
|
||||
if val == "OK" {
|
||||
return "", nil
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) writeLine(wtr *bufio.Writer, op string, payload []byte) error {
|
||||
err := writeLine(wtr, op, payload)
|
||||
if err != nil {
|
||||
c.markUnusable()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) readResponse(rdr *bufio.Reader) ([]byte, error) {
|
||||
data, err := readResponse(rdr)
|
||||
if err != nil {
|
||||
if _, ok := err.(*ProtocolError); !ok {
|
||||
c.markUnusable()
|
||||
}
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (c *Client) ok(rdr *bufio.Reader) error {
|
||||
err := ok(rdr)
|
||||
if err != nil {
|
||||
if _, ok := err.(*ProtocolError); !ok {
|
||||
c.markUnusable()
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) readString(rdr *bufio.Reader) (string, error) {
|
||||
s, err := readString(rdr)
|
||||
if err != nil {
|
||||
if _, ok := err.(*ProtocolError); !ok {
|
||||
c.markUnusable()
|
||||
}
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (c *Client) markUnusable() {
|
||||
if c.poolConn == nil {
|
||||
// if this client was not created as part of a pool,
|
||||
// this call becomes a no-op
|
||||
return
|
||||
}
|
||||
c.poolConn.MarkUnusable()
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
func emptyClientData() *ClientData {
|
||||
client := &ClientData{}
|
||||
hs, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
client.Hostname = hs
|
||||
client.Pid = os.Getpid()
|
||||
client.Wid = RandomProcessWid
|
||||
client.Labels = Labels
|
||||
client.Version = ExpectedProtocolVersion
|
||||
return client
|
||||
}
|
||||
|
||||
func writeLine(wtr *bufio.Writer, op string, payload []byte) error {
|
||||
//util.Debugf("> %s %s", op, string(payload))
|
||||
|
||||
_, err := wtr.Write([]byte(op))
|
||||
if payload != nil {
|
||||
if err == nil {
|
||||
_, err = wtr.Write([]byte(" "))
|
||||
}
|
||||
if err == nil {
|
||||
_, err = wtr.Write(payload)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
_, err = wtr.Write([]byte("\r\n"))
|
||||
}
|
||||
if err == nil {
|
||||
err = wtr.Flush()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ok(rdr *bufio.Reader) error {
|
||||
val, err := readResponse(rdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(val) == "OK" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Invalid response: %s", string(val))
|
||||
}
|
||||
|
||||
func readString(rdr *bufio.Reader) (string, error) {
|
||||
val, err := readResponse(rdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if val == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return string(val), nil
|
||||
}
|
||||
|
||||
type ProtocolError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (pe *ProtocolError) Error() string {
|
||||
return pe.msg
|
||||
}
|
||||
|
||||
func readResponse(rdr *bufio.Reader) ([]byte, error) {
|
||||
chr, err := rdr.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
line, err := rdr.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line = line[:len(line)-2]
|
||||
|
||||
switch chr {
|
||||
case '$':
|
||||
// read length $10\r\n
|
||||
count, err := strconv.Atoi(string(line))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == -1 {
|
||||
return nil, nil
|
||||
}
|
||||
var buff []byte
|
||||
if count > 0 {
|
||||
buff = make([]byte, count)
|
||||
_, err = io.ReadFull(rdr, buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err = rdr.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//util.Debugf("< %s%s", string(chr), string(line))
|
||||
//util.Debugf("< %s", string(buff))
|
||||
return buff, nil
|
||||
case '-':
|
||||
return nil, &ProtocolError{msg: string(line)}
|
||||
default:
|
||||
//util.Debugf("< %s%s", string(chr), string(line))
|
||||
return line, nil
|
||||
}
|
||||
}
|
||||
|
||||
func hash(pwd, salt string, iterations int) string {
|
||||
data := []byte(pwd + salt)
|
||||
hash := sha256.Sum256(data)
|
||||
if iterations > 1 {
|
||||
for i := 1; i < iterations; i++ {
|
||||
hash = sha256.Sum256(hash[:])
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
8
vendor/github.com/contribsys/faktory/client/client_bsd.go
generated
vendored
Normal file
8
vendor/github.com/contribsys/faktory/client/client_bsd.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// +build darwin freebsd netbsd openbsd
|
||||
|
||||
package client
|
||||
|
||||
func RssKb() int64 {
|
||||
// TODO Submit a PR?
|
||||
return 0
|
||||
}
|
37
vendor/github.com/contribsys/faktory/client/client_linux.go
generated
vendored
Normal file
37
vendor/github.com/contribsys/faktory/client/client_linux.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RssKb() int64 {
|
||||
path := "/proc/self/status"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
lines := bytes.Split(content, []byte("\n"))
|
||||
for idx := range lines {
|
||||
if lines[idx][0] == 'V' {
|
||||
ls := string(lines[idx])
|
||||
if strings.Contains(ls, "VmRSS") {
|
||||
str := strings.Split(ls, ":")[1]
|
||||
intt, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(intt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
6
vendor/github.com/contribsys/faktory/client/client_windows.go
generated
vendored
Normal file
6
vendor/github.com/contribsys/faktory/client/client_windows.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
package client
|
||||
|
||||
func RssKb() int64 {
|
||||
// TODO Submit a PR?
|
||||
return 0
|
||||
}
|
6
vendor/github.com/contribsys/faktory/client/faktory.go
generated
vendored
Normal file
6
vendor/github.com/contribsys/faktory/client/faktory.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
package client
|
||||
|
||||
var (
|
||||
Name = "Faktory"
|
||||
Version = "1.6.0"
|
||||
)
|
127
vendor/github.com/contribsys/faktory/client/job.go
generated
vendored
Normal file
127
vendor/github.com/contribsys/faktory/client/job.go
generated
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/base64"
|
||||
mathrand "math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UniqueUntil string
|
||||
|
||||
var (
|
||||
RetryPolicyDefault = 25
|
||||
RetryPolicyEmphemeral = 0
|
||||
RetryPolicyDirectToMorgue = -1
|
||||
)
|
||||
|
||||
const (
|
||||
UntilSuccess UniqueUntil = "success" // default
|
||||
UntilStart UniqueUntil = "start"
|
||||
)
|
||||
|
||||
type Failure struct {
|
||||
RetryCount int `json:"retry_count"`
|
||||
RetryRemaining int `json:"remaining"`
|
||||
FailedAt string `json:"failed_at"`
|
||||
NextAt string `json:"next_at,omitempty"`
|
||||
ErrorMessage string `json:"message,omitempty"`
|
||||
ErrorType string `json:"errtype,omitempty"`
|
||||
Backtrace []string `json:"backtrace,omitempty"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
// required
|
||||
Jid string `json:"jid"`
|
||||
Queue string `json:"queue"`
|
||||
Type string `json:"jobtype"`
|
||||
Args []interface{} `json:"args"`
|
||||
|
||||
// optional
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
EnqueuedAt string `json:"enqueued_at,omitempty"`
|
||||
At string `json:"at,omitempty"`
|
||||
ReserveFor int `json:"reserve_for,omitempty"`
|
||||
Retry *int `json:"retry"`
|
||||
Backtrace int `json:"backtrace,omitempty"`
|
||||
Failure *Failure `json:"failure,omitempty"`
|
||||
Custom map[string]interface{} `json:"custom,omitempty"`
|
||||
}
|
||||
|
||||
// Clients should use this constructor to build a Job, not allocate
|
||||
// a bare struct directly.
|
||||
func NewJob(jobtype string, args ...interface{}) *Job {
|
||||
return &Job{
|
||||
Type: jobtype,
|
||||
Queue: "default",
|
||||
Args: args,
|
||||
Jid: RandomJid(),
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
Retry: &RetryPolicyDefault,
|
||||
}
|
||||
}
|
||||
|
||||
func RandomJid() string {
|
||||
bytes := make([]byte, 12)
|
||||
_, err := cryptorand.Read(bytes)
|
||||
if err != nil {
|
||||
//nolint:gosec
|
||||
mathrand.Read(bytes)
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func (j *Job) GetCustom(name string) (interface{}, bool) {
|
||||
if j.Custom == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
val, ok := j.Custom[name]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Set custom metadata for this job. Faktory reserves all
|
||||
// element names starting with "_" for internal use, e.g.
|
||||
// SetCustom("_txid", "12345")
|
||||
func (j *Job) SetCustom(name string, value interface{}) *Job {
|
||||
if j.Custom == nil {
|
||||
j.Custom = map[string]interface{}{}
|
||||
}
|
||||
|
||||
j.Custom[name] = value
|
||||
return j
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
// Faktory Enterprise helpers
|
||||
//
|
||||
// These helpers allow you to configure several Faktory Enterprise features.
|
||||
// They will have no effect unless you are running Faktory Enterprise.
|
||||
|
||||
// Configure this job to be unique for +secs+ seconds or until the job
|
||||
// has been successfully processed.
|
||||
func (j *Job) SetUniqueFor(secs uint) *Job {
|
||||
return j.SetCustom("unique_for", secs)
|
||||
}
|
||||
|
||||
// Configure the uniqueness deadline for this job, legal values
|
||||
// are:
|
||||
//
|
||||
// - "success" - the job will be considered unique until it has successfully processed
|
||||
// or the +unique_for+ TTL has passed, this is the default value.
|
||||
// - "start" - the job will be considered unique until it starts processing. Retries
|
||||
// may lead to multiple copies of the job running.
|
||||
func (j *Job) SetUniqueness(until UniqueUntil) *Job {
|
||||
return j.SetCustom("unique_until", until)
|
||||
}
|
||||
|
||||
// Configure the TTL for this job. After this point in time, the job will be
|
||||
// discarded rather than executed.
|
||||
func (j *Job) SetExpiresAt(expiresAt time.Time) *Job {
|
||||
return j.SetCustom("expires_at", expiresAt.Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
func (j *Job) SetExpiresIn(expiresIn time.Duration) *Job {
|
||||
return j.SetCustom("expires_at", time.Now().Add(expiresIn).Format(time.RFC3339Nano))
|
||||
}
|
153
vendor/github.com/contribsys/faktory/client/mutate.go
generated
vendored
Normal file
153
vendor/github.com/contribsys/faktory/client/mutate.go
generated
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
//
|
||||
// Faktory's Mutate API allows clients to directly mutate the various
|
||||
// persistent sets within Faktory. These use cases are typically for
|
||||
// repair or data migration purposes.
|
||||
//
|
||||
// THESE APIs SHOULD NEVER BE USED WITHIN APP LOGIC.
|
||||
// Many Mutate API use cases will have poor performance:
|
||||
// O(N), O(N log N), or even O(M*N).
|
||||
|
||||
type Structure string
|
||||
|
||||
type JobFilter struct {
|
||||
Jids []string `json:"jids,omitempty"`
|
||||
Regexp string `json:"regexp,omitempty"`
|
||||
Jobtype string `json:"jobtype,omitempty"`
|
||||
}
|
||||
|
||||
func (jf JobFilter) WithJids(jids ...string) JobFilter {
|
||||
jf.Jids = jids
|
||||
return jf
|
||||
}
|
||||
|
||||
func (jf JobFilter) Matching(pattern string) JobFilter {
|
||||
jf.Regexp = pattern
|
||||
return jf
|
||||
}
|
||||
|
||||
func (jf JobFilter) OfType(jobtype string) JobFilter {
|
||||
jf.Jobtype = jobtype
|
||||
return jf
|
||||
}
|
||||
|
||||
const (
|
||||
Scheduled Structure = "scheduled"
|
||||
Retries Structure = "retries"
|
||||
Dead Structure = "dead"
|
||||
)
|
||||
|
||||
var (
|
||||
Everything = JobFilter{
|
||||
Regexp: "*",
|
||||
}
|
||||
)
|
||||
|
||||
// Match jobs with the given JIDs. Warning: O(m*n), very slow
|
||||
// because it has to pull every job into Faktory and check the JID
|
||||
// against the list.
|
||||
//
|
||||
// If you pass in a single JID, it will devolve to matching within Redis
|
||||
// and perform much faster. For that reason, it might be better to
|
||||
// handle one JID at a time.
|
||||
func WithJids(jids ...string) JobFilter {
|
||||
return JobFilter{
|
||||
Jids: jids,
|
||||
}
|
||||
}
|
||||
|
||||
// This is a generic pattern match across the entire job JSON payload.
|
||||
// Be very careful that you don't accidentally match some unintended part
|
||||
// of the payload.
|
||||
//
|
||||
// NB: your pattern should have * on each side. The pattern is passed
|
||||
// directly to Redis.
|
||||
//
|
||||
// Example: discard any job retries whose payload contains the special word "uid:12345":
|
||||
//
|
||||
// client.Discard(faktory.Retries, faktory.Matching("*uid:12345*"))
|
||||
//
|
||||
// See the Redis SCAN documentation for pattern matching examples.
|
||||
// https://redis.io/commands/scan
|
||||
func Matching(pattern string) JobFilter {
|
||||
return JobFilter{
|
||||
Regexp: pattern,
|
||||
}
|
||||
}
|
||||
|
||||
// Matches jobs based on the exact Jobtype. This is pretty fast because
|
||||
// it devolves to Matching(`"jobtype":"$ARG"`) and matches within Redis.
|
||||
func OfType(jobtype string) JobFilter {
|
||||
return JobFilter{
|
||||
Jobtype: jobtype,
|
||||
}
|
||||
}
|
||||
|
||||
type Operation struct {
|
||||
Cmd string `json:"cmd"`
|
||||
Target Structure `json:"target"`
|
||||
Filter *JobFilter `json:"filter,omitempty"`
|
||||
}
|
||||
|
||||
// Commands which allow you to perform admin tasks on various Faktory structures.
|
||||
// These are NOT designed to be used in business logic but rather for maintenance,
|
||||
// data repair, migration, etc. They can have poor scalability or performance edge
|
||||
// cases.
|
||||
//
|
||||
// Generally these operations are O(n) or worse. They will get slower as your
|
||||
// data gets bigger.
|
||||
type MutateClient interface {
|
||||
|
||||
// Move the given jobs from structure to the Dead set.
|
||||
// Faktory will not touch them anymore but you can still see them in the Web UI.
|
||||
//
|
||||
// Kill(Retries, OfType("DataSyncJob").WithJids("abc", "123"))
|
||||
Kill(name Structure, filter JobFilter) error
|
||||
|
||||
// Move the given jobs to their associated queue so they can be immediately
|
||||
// picked up and processed.
|
||||
Requeue(name Structure, filter JobFilter) error
|
||||
|
||||
// Throw away the given jobs, e.g. if you want to delete all jobs named "QuickbooksSyncJob"
|
||||
//
|
||||
// Discard(Dead, OfType("QuickbooksSyncJob"))
|
||||
Discard(name Structure, filter JobFilter) error
|
||||
|
||||
// Empty the entire given structure, e.g. if you want to clear all retries.
|
||||
// This is very fast as it is special cased by Faktory.
|
||||
Clear(name Structure) error
|
||||
}
|
||||
|
||||
func (c *Client) Kill(name Structure, filter JobFilter) error {
|
||||
return c.mutate(Operation{Cmd: "kill", Target: name, Filter: &filter})
|
||||
}
|
||||
|
||||
func (c *Client) Requeue(name Structure, filter JobFilter) error {
|
||||
return c.mutate(Operation{Cmd: "requeue", Target: name, Filter: &filter})
|
||||
}
|
||||
|
||||
func (c *Client) Discard(name Structure, filter JobFilter) error {
|
||||
return c.mutate(Operation{Cmd: "discard", Target: name, Filter: &filter})
|
||||
}
|
||||
|
||||
func (c *Client) Clear(name Structure) error {
|
||||
return c.mutate(Operation{Cmd: "clear", Target: name, Filter: nil})
|
||||
}
|
||||
|
||||
func (c *Client) mutate(op Operation) error {
|
||||
j, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeLine(c.wtr, "MUTATE", j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ok(c.rdr)
|
||||
}
|
4
vendor/github.com/contribsys/faktory/client/package.go
generated
vendored
Normal file
4
vendor/github.com/contribsys/faktory/client/package.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// Code within this package is licensed according to the MPL found
|
||||
// in the LICENSE file.
|
||||
|
||||
package client
|
69
vendor/github.com/contribsys/faktory/client/pool.go
generated
vendored
Normal file
69
vendor/github.com/contribsys/faktory/client/pool.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/contribsys/faktory/internal/pool"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
pool.Pool
|
||||
}
|
||||
|
||||
// NewPool creates a new Pool object through which multiple clients will be managed on your behalf.
|
||||
//
|
||||
// Call Get() to retrieve a client instance and Put() to return it to the pool. If you do not call
|
||||
// Put(), the connection will be leaked, and the pool will stop working once it hits capacity.
|
||||
//
|
||||
// Do NOT call Close() on the client, as the lifecycle is managed internally.
|
||||
//
|
||||
// The dialer clients in this pool use is determined by the URI scheme in FAKTORY_PROVIDER.
|
||||
func NewPool(capacity int) (*Pool, error) {
|
||||
return newPool(capacity, func() (pool.Closeable, error) { return Open() })
|
||||
}
|
||||
|
||||
// NewPoolWithDialer creates a new Pool object similar to NewPool but clients will use the
|
||||
// provided dialer instead of default ones.
|
||||
func NewPoolWithDialer(capacity int, dialer Dialer) (*Pool, error) {
|
||||
fn := func() (pool.Closeable, error) { return OpenWithDialer(dialer) }
|
||||
return newPool(capacity, fn)
|
||||
}
|
||||
|
||||
// newPool creates a *Pool channel with the provided capacity and opener.
|
||||
func newPool(capacity int, opener pool.Factory) (*Pool, error) {
|
||||
var p Pool
|
||||
var err error
|
||||
p.Pool, err = pool.NewChannelPool(0, capacity, opener)
|
||||
return &p, err
|
||||
}
|
||||
|
||||
// Get retrieves a Client from the pool. This Client is created, internally, by calling
|
||||
// the Open() function, and has all the same behaviors.
|
||||
func (p *Pool) Get() (*Client, error) {
|
||||
conn, err := p.Pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc := conn.(*pool.PoolConn)
|
||||
client, ok := pc.Closeable.(*Client)
|
||||
if !ok {
|
||||
// Because we control the entire lifecycle of the pool, internally, this should never happen.
|
||||
panic(fmt.Sprintf("Connection is not a Faktory client instance: %+v", conn))
|
||||
}
|
||||
client.poolConn = pc
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Put returns a client to the pool.
|
||||
func (p *Pool) Put(client *Client) {
|
||||
client.poolConn.Close()
|
||||
}
|
||||
|
||||
func (p *Pool) With(fn func(conn *Client) error) error {
|
||||
conn, err := p.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.Put(conn)
|
||||
return fn(conn)
|
||||
}
|
74
vendor/github.com/contribsys/faktory/client/tracking.go
generated
vendored
Normal file
74
vendor/github.com/contribsys/faktory/client/tracking.go
generated
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/contribsys/faktory/util"
|
||||
)
|
||||
|
||||
type JobTrack struct {
|
||||
Jid string `json:"jid"`
|
||||
Percent int `json:"percent,omitempty"`
|
||||
Description string `json:"desc,omitempty"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (c *Client) TrackGet(jid string) (*JobTrack, error) {
|
||||
err := c.writeLine(c.wtr, "TRACK GET", []byte(jid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := c.readResponse(c.rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var trck JobTrack
|
||||
err = json.Unmarshal(data, &trck)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &trck, nil
|
||||
}
|
||||
|
||||
type setJobTrack struct {
|
||||
Jid string `json:"jid"`
|
||||
Percent int `json:"percent,omitempty"`
|
||||
Description string `json:"desc,omitempty"`
|
||||
ReserveUntil string `json:"reserve_until,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) TrackSet(jid string, percent int, desc string, reserveUntil *time.Time) error {
|
||||
if jid == "" {
|
||||
return fmt.Errorf("Job Track missing JID")
|
||||
}
|
||||
|
||||
tset := setJobTrack{
|
||||
Jid: jid,
|
||||
Description: desc,
|
||||
Percent: percent,
|
||||
}
|
||||
if reserveUntil != nil && time.Now().Before(*reserveUntil) {
|
||||
tset.ReserveUntil = util.Thens(*reserveUntil)
|
||||
}
|
||||
return c.trackSet(&tset)
|
||||
}
|
||||
|
||||
func (c *Client) trackSet(tset *setJobTrack) error {
|
||||
data, err := json.Marshal(tset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.writeLine(c.wtr, "TRACK SET", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ok(c.rdr)
|
||||
}
|
219
vendor/github.com/contribsys/faktory/internal/pool/pool.go
generated
vendored
Normal file
219
vendor/github.com/contribsys/faktory/internal/pool/pool.go
generated
vendored
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Fatih Arslan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package pool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrClosed is the error resulting if the pool is closed via pool.Close().
|
||||
ErrClosed = errors.New("pool is closed")
|
||||
)
|
||||
|
||||
// Closeable interface describes a closable implementation. The underlying procedure of the Close() function is determined by its implementation
|
||||
type Closeable interface {
|
||||
// Close closes the object
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Pool interface describes a pool implementation. A pool should have maximum
|
||||
// capacity. An ideal pool is threadsafe and easy to use.
|
||||
type Pool interface {
|
||||
// Get returns a new connection from the pool. Closing the connections puts
|
||||
// it back to the Pool. Closing it when the pool is destroyed or full will
|
||||
// be counted as an error.
|
||||
Get() (Closeable, error)
|
||||
|
||||
// Close closes the pool and all its connections. After Close() the pool is
|
||||
// no longer usable.
|
||||
Close()
|
||||
|
||||
// Len returns the current number of connections of the pool.
|
||||
Len() int
|
||||
}
|
||||
|
||||
// PoolConn is a wrapper around net.Conn to modify the the behavior of
|
||||
// net.Conn's Close() method.
|
||||
type PoolConn struct {
|
||||
Closeable
|
||||
mu sync.RWMutex
|
||||
c *channelPool
|
||||
unusable bool
|
||||
}
|
||||
|
||||
// Close puts the given connects back to the pool instead of closing it.
|
||||
func (p *PoolConn) Close() error {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if p.unusable {
|
||||
if p.Closeable != nil {
|
||||
return p.Closeable.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return p.c.put(p.Closeable)
|
||||
}
|
||||
|
||||
// MarkUnusable marks the connection not usable any more, to let the pool close it instead of returning it to pool.
|
||||
func (p *PoolConn) MarkUnusable() {
|
||||
p.mu.Lock()
|
||||
p.unusable = true
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// wrapConn wraps a standard net.Conn to a poolConn net.Conn.
|
||||
func (c *channelPool) wrapConn(conn Closeable) Closeable {
|
||||
p := &PoolConn{c: c}
|
||||
p.Closeable = conn
|
||||
return p
|
||||
}
|
||||
|
||||
// channelPool implements the Pool interface based on buffered channels.
|
||||
type channelPool struct {
|
||||
// storage for our net.Conn connections
|
||||
mu sync.Mutex
|
||||
conns chan Closeable
|
||||
|
||||
// net.Conn generator
|
||||
factory Factory
|
||||
}
|
||||
|
||||
// Factory is a function to create new connections.
|
||||
type Factory func() (Closeable, error)
|
||||
|
||||
// NewChannelPool returns a new pool based on buffered channels with an initial
|
||||
// capacity and maximum capacity. Factory is used when initial capacity is
|
||||
// greater than zero to fill the pool. A zero initialCap doesn't fill the Pool
|
||||
// until a new Get() is called. During a Get(), If there is no new connection
|
||||
// available in the pool, a new connection will be created via the Factory()
|
||||
// method.
|
||||
func NewChannelPool(initialCap, maxCap int, factory Factory) (Pool, error) {
|
||||
if initialCap < 0 || maxCap <= 0 || initialCap > maxCap {
|
||||
return nil, errors.New("invalid capacity settings")
|
||||
}
|
||||
|
||||
c := &channelPool{
|
||||
conns: make(chan Closeable, maxCap),
|
||||
factory: factory,
|
||||
}
|
||||
|
||||
// create initial connections, if something goes wrong,
|
||||
// just close the pool error out.
|
||||
for i := 0; i < initialCap; i++ {
|
||||
conn, err := factory()
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("factory is not able to fill the pool: %w", err)
|
||||
}
|
||||
c.conns <- conn
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *channelPool) getConns() chan Closeable {
|
||||
c.mu.Lock()
|
||||
conns := c.conns
|
||||
c.mu.Unlock()
|
||||
return conns
|
||||
}
|
||||
|
||||
// Get implements the Pool interfaces Get() method. If there is no new
|
||||
// connection available in the pool, a new connection will be created via the
|
||||
// Factory() method.
|
||||
func (c *channelPool) Get() (Closeable, error) {
|
||||
conns := c.getConns()
|
||||
if conns == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
// wrap our connections with out custom net.Conn implementation (wrapConn
|
||||
// method) that puts the connection back to the pool if it's closed.
|
||||
select {
|
||||
case conn := <-conns:
|
||||
if conn == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
return c.wrapConn(conn), nil
|
||||
default:
|
||||
conn, err := c.factory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.wrapConn(conn), nil
|
||||
}
|
||||
}
|
||||
|
||||
// put puts the connection back to the pool. If the pool is full or closed,
|
||||
// conn is simply closed. A nil conn will be rejected.
|
||||
func (c *channelPool) put(conn Closeable) error {
|
||||
if conn == nil {
|
||||
return errors.New("connection is nil. rejecting")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conns == nil {
|
||||
// pool is closed, close passed connection
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
// put the resource back into the pool. If the pool is full, this will
|
||||
// block and the default case will be executed.
|
||||
select {
|
||||
case c.conns <- conn:
|
||||
return nil
|
||||
default:
|
||||
// pool is full, close passed connection
|
||||
return conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down all of the Closeable objects in the Pool
|
||||
func (c *channelPool) Close() {
|
||||
c.mu.Lock()
|
||||
conns := c.conns
|
||||
c.conns = nil
|
||||
c.factory = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if conns == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(conns)
|
||||
for conn := range conns {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of Closeable objects in the Pool
|
||||
func (c *channelPool) Len() int { return len(c.getConns()) }
|
123
vendor/github.com/contribsys/faktory/util/logger.go
generated
vendored
Normal file
123
vendor/github.com/contribsys/faktory/util/logger.go
generated
vendored
Normal file
@ -0,0 +1,123 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// colors.
|
||||
const (
|
||||
red = 31
|
||||
green = 32
|
||||
yellow = 33
|
||||
blue = 34
|
||||
)
|
||||
|
||||
type Level int
|
||||
|
||||
const (
|
||||
InvalidLevel Level = iota - 1
|
||||
DebugLevel
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
var colors = [...]int{
|
||||
DebugLevel: green,
|
||||
InfoLevel: blue,
|
||||
WarnLevel: yellow,
|
||||
ErrorLevel: red,
|
||||
FatalLevel: red,
|
||||
}
|
||||
|
||||
var lvlPrefix = [...]string{
|
||||
DebugLevel: "D",
|
||||
InfoLevel: "I",
|
||||
WarnLevel: "W",
|
||||
ErrorLevel: "E",
|
||||
FatalLevel: "F",
|
||||
}
|
||||
|
||||
var (
|
||||
LogInfo = false
|
||||
LogDebug = false
|
||||
logg = os.Stdout
|
||||
colorize = isTTY(logg.Fd())
|
||||
)
|
||||
|
||||
const (
|
||||
TimeFormat = "2006-01-02T15:04:05.000Z"
|
||||
)
|
||||
|
||||
func llog(lvl Level, msg string) {
|
||||
prefix := lvlPrefix[lvl]
|
||||
ts := time.Now().UTC().Format(TimeFormat)
|
||||
|
||||
if colorize {
|
||||
color := colors[lvl]
|
||||
fmt.Fprintf(logg, "\033[%dm%s\033[0m %s %s\n", color, prefix, ts, msg)
|
||||
} else {
|
||||
fmt.Fprintf(logg, "%s %s %s\n", prefix, ts, msg)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Logging functions
|
||||
//
|
||||
|
||||
func InitLogger(level string) {
|
||||
if level == "info" {
|
||||
LogInfo = true
|
||||
}
|
||||
|
||||
if level == "debug" {
|
||||
LogInfo = true
|
||||
LogDebug = true
|
||||
}
|
||||
}
|
||||
|
||||
func Error(msg string, err error) {
|
||||
llog(ErrorLevel, fmt.Sprintf("%s: %v", msg, err))
|
||||
}
|
||||
|
||||
// Uh oh, not good but not worthy of process death
|
||||
func Warn(arg string) {
|
||||
llog(WarnLevel, arg)
|
||||
}
|
||||
|
||||
func Warnf(msg string, args ...interface{}) {
|
||||
llog(WarnLevel, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
// Typical logging output, the default level
|
||||
func Info(arg string) {
|
||||
if LogInfo {
|
||||
llog(InfoLevel, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// Typical logging output, the default level
|
||||
func Infof(msg string, args ...interface{}) {
|
||||
if LogInfo {
|
||||
llog(InfoLevel, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// Verbosity level helps track down production issues:
|
||||
// -l debug
|
||||
func Debug(arg string) {
|
||||
if LogDebug {
|
||||
llog(DebugLevel, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// Verbosity level helps track down production issues:
|
||||
// -l debug
|
||||
func Debugf(msg string, args ...interface{}) {
|
||||
if LogDebug {
|
||||
llog(DebugLevel, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
131
vendor/github.com/contribsys/faktory/util/util.go
generated
vendored
Normal file
131
vendor/github.com/contribsys/faktory/util/util.go
generated
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SIGHUP = 0x1
|
||||
SIGINT = 0x2
|
||||
SIGQUIT = 0x3
|
||||
SIGTERM = 0xF
|
||||
|
||||
maxInt63 = int64(^uint64(0) >> 1)
|
||||
)
|
||||
|
||||
func Retryable(name string, count int, fn func() error) error {
|
||||
var err error
|
||||
for i := 0; i < count; i++ {
|
||||
err = fn()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
Debugf("Retrying %s due to %v", name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Darwin() bool {
|
||||
b, _ := FileExists("/Applications")
|
||||
return b
|
||||
}
|
||||
|
||||
// FileExists checks if given file exists
|
||||
func FileExists(path string) (bool, error) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func RandomJid() string {
|
||||
bytes := make([]byte, 12)
|
||||
_, err := cryptorand.Read(bytes)
|
||||
if err != nil {
|
||||
//nolint:gosec
|
||||
mathrand.Read(bytes)
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func RandomInt63() (int64, error) {
|
||||
r, err := cryptorand.Int(cryptorand.Reader, big.NewInt(maxInt63))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.Int64(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// This is the canonical timestamp format used by Faktory.
|
||||
// Always UTC, lexigraphically sortable. This is the best
|
||||
// timestamp format, accept no others.
|
||||
TimestampFormat = time.RFC3339Nano
|
||||
)
|
||||
|
||||
func Thens(tim time.Time) string {
|
||||
return tim.UTC().Format(TimestampFormat)
|
||||
}
|
||||
|
||||
func Nows() string {
|
||||
return time.Now().UTC().Format(TimestampFormat)
|
||||
}
|
||||
|
||||
func ParseTime(str string) (time.Time, error) {
|
||||
return time.Parse(TimestampFormat, str)
|
||||
}
|
||||
|
||||
func MemoryUsageMB() uint64 {
|
||||
m := runtime.MemStats{}
|
||||
runtime.ReadMemStats(&m)
|
||||
mb := m.Sys / 1024 / 1024
|
||||
return mb
|
||||
}
|
||||
|
||||
// Backtrace gathers a backtrace for the caller.
|
||||
// Return a slice of up to N stack frames.
|
||||
func Backtrace(size int) []string {
|
||||
pc := make([]uintptr, size)
|
||||
n := runtime.Callers(2, pc)
|
||||
if n == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
|
||||
frames := runtime.CallersFrames(pc)
|
||||
|
||||
str := make([]string, size)
|
||||
count := 0
|
||||
|
||||
// Loop to get frames.
|
||||
// A fixed number of pcs can expand to an indefinite number of Frames.
|
||||
for i := 0; i < size; i++ {
|
||||
frame, more := frames.Next()
|
||||
str[i] = fmt.Sprintf("in %s:%d %s", frame.File, frame.Line, frame.Function)
|
||||
count++
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return str[0:count]
|
||||
}
|
||||
|
||||
func DumpProcessTrace() {
|
||||
buf := make([]byte, 64*1024)
|
||||
_ = runtime.Stack(buf, true)
|
||||
Info("FULL PROCESS THREAD DUMP:")
|
||||
Info(string(buf))
|
||||
}
|
20
vendor/github.com/contribsys/faktory/util/util_bsd.go
generated
vendored
Normal file
20
vendor/github.com/contribsys/faktory/util/util_bsd.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
// +build darwin freebsd netbsd openbsd
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func isTTY(fd uintptr) bool {
|
||||
var termios syscall.Termios
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, syscall.TIOCGETA, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||
return err == 0
|
||||
}
|
||||
|
||||
func EnsureChildShutdown(cmd *exec.Cmd, sig int) {
|
||||
// This ensures that, on Linux, if Faktory panics, our Redis child process will immediately
|
||||
// get a SIGTERM signal to shutdown. No such feature on Darwin/BSD, Redis will orphan.
|
||||
}
|
25
vendor/github.com/contribsys/faktory/util/util_linux.go
generated
vendored
Normal file
25
vendor/github.com/contribsys/faktory/util/util_linux.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
// +build linux
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func isTTY(fd uintptr) bool {
|
||||
var termios syscall.Termios
|
||||
// syscall.TCGETS
|
||||
// https://groups.google.com/forum/#!topic/golang-checkins/kieURujjDEk
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, 0x5401, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||
return err == 0
|
||||
}
|
||||
|
||||
func EnsureChildShutdown(cmd *exec.Cmd, sig int) {
|
||||
// This ensures that, on Linux, if Faktory panics, our child process will immediately
|
||||
// get a SIGTERM signal to shutdown. No such feature on Darwin/BSD, child will orphan.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Pdeathsig: syscall.Signal(sig),
|
||||
}
|
||||
}
|
16
vendor/github.com/contribsys/faktory/util/util_windows.go
generated
vendored
Normal file
16
vendor/github.com/contribsys/faktory/util/util_windows.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func isTTY(fd uintptr) bool {
|
||||
// this function controls if we output ANSI coloring to the terminal.
|
||||
// dunno how to do this on Windows so just play safe and assume it is not a TTY
|
||||
return false
|
||||
}
|
||||
|
||||
func EnsureChildShutdown(cmd *exec.Cmd, sig int) {
|
||||
// This ensures that, on Linux, if Faktory panics, the child process will immediately
|
||||
// get a signal. Dunno if this is possible on Windows or how it will behave.
|
||||
}
|
17
vendor/github.com/contribsys/faktory_worker_go/.gitignore
generated
vendored
Normal file
17
vendor/github.com/contribsys/faktory_worker_go/.gitignore
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Folder by any jetbrains IDE
|
||||
/.idea
|
25
vendor/github.com/contribsys/faktory_worker_go/.golangci.yml
generated
vendored
Normal file
25
vendor/github.com/contribsys/faktory_worker_go/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
linters-settings:
|
||||
gocritic:
|
||||
# Which checks should be enabled; can't be combined with 'disabled-checks';
|
||||
# See https://go-critic.github.io/overview#checks-overview
|
||||
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
|
||||
# By default list of stable checks is used.
|
||||
disabled-checks:
|
||||
- commentedOutCode
|
||||
- commentFormatting
|
||||
|
||||
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
|
||||
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
|
||||
enabled-tags:
|
||||
- performance
|
||||
- diagnostic
|
||||
- opinionated
|
||||
|
||||
settings: # settings passed to gocritic
|
||||
rangeExprCopy:
|
||||
sizeThreshold: 16
|
||||
rangeValCopy:
|
||||
sizeThreshold: 16
|
||||
linters:
|
||||
enable:
|
||||
- gocritic
|
68
vendor/github.com/contribsys/faktory_worker_go/Changes.md
generated
vendored
Normal file
68
vendor/github.com/contribsys/faktory_worker_go/Changes.md
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
# faktory\_worker\_go
|
||||
|
||||
## 1.6.0
|
||||
|
||||
- Upgrade to Go 1.17 and Faktory 1.6.0.
|
||||
- Add `Manager.RunWithContext(ctx context.Context) error` [#58]
|
||||
Allows the caller to directly control when FWG stops. See README for usage.
|
||||
|
||||
## 1.5.0
|
||||
|
||||
- Auto-shutdown worker process if heartbeat expires due to network issues [#57]
|
||||
- Send process RSS to Faktory (only available on Linux)
|
||||
- Fix connection issue which causes worker processes to appear and disappear on
|
||||
Busy tab. [#47]
|
||||
|
||||
## 1.4.0
|
||||
|
||||
- **Breaking API changes due to misunderstanding the `context` package.**
|
||||
I've had to make significant changes to FWG's public APIs to allow
|
||||
for mutable contexts. This unfortunately requires breaking changes:
|
||||
```
|
||||
Job Handler
|
||||
Before: func(ctx worker.Context, args ...interface{}) error
|
||||
After: func(ctx context.Context, args ...interface{}) error
|
||||
|
||||
Middleware Handler
|
||||
Before: func(ctx worker.Context, job *faktory.Job) error
|
||||
After: func(ctx context.Context, job *faktory.Job, next func(context.Context) error) error
|
||||
```
|
||||
Middleware funcs now need to call `next` to continue job dispatch.
|
||||
Use `help := worker.HelperFor(ctx)` to get the old APIs provided by `worker.Context`
|
||||
within your handlers.
|
||||
- Fix issue reporting job errors back to Faktory
|
||||
- Add helpers for testing `Perform` funcs
|
||||
```go
|
||||
myFunc := func(ctx context.Context, args ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
pool, err := faktory.NewPool(5)
|
||||
perf := worker.NewTestExecutor(pool)
|
||||
|
||||
somejob := faktory.NewJob("sometype", 12, "foobar")
|
||||
err = perf.Execute(somejob, myFunc)
|
||||
```
|
||||
|
||||
## 1.3.0
|
||||
|
||||
- Add new job context APIs for Faktory Enterprise features
|
||||
- Misc API refactoring to improve test coverage
|
||||
- Remove `faktory_worker_go` Pool interface in favor of the Pool now in `faktory/client`
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- FWG will now dump all thread backtraces upon `kill -TTIN <pid>`,
|
||||
useful for debugging stuck job processing.
|
||||
- Send current state back to Faktory so process state changes are visible on Busy page.
|
||||
- Tweak log output formatting
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Allow process labels (visible in Web UI) to be set [#32]
|
||||
- Add APIs to manage [Batches](https://github.com/contribsys/faktory/wiki/Ent-Batches).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Implement weighted queue fetch [#20, nickpoorman]
|
||||
- Initial version.
|
373
vendor/github.com/contribsys/faktory_worker_go/LICENSE
generated
vendored
Normal file
373
vendor/github.com/contribsys/faktory_worker_go/LICENSE
generated
vendored
Normal file
@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
15
vendor/github.com/contribsys/faktory_worker_go/Makefile
generated
vendored
Normal file
15
vendor/github.com/contribsys/faktory_worker_go/Makefile
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
work:
|
||||
go run test/main.go
|
||||
|
||||
cover:
|
||||
go test -cover -coverprofile .cover.out .
|
||||
go tool cover -html=.cover.out -o coverage.html
|
||||
open coverage.html
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: work test cover
|
190
vendor/github.com/contribsys/faktory_worker_go/README.md
generated
vendored
Normal file
190
vendor/github.com/contribsys/faktory_worker_go/README.md
generated
vendored
Normal file
@ -0,0 +1,190 @@
|
||||
# faktory_worker_go
|
||||
|
||||
![travis](https://travis-ci.org/contribsys/faktory_worker_go.svg?branch=master)
|
||||
|
||||
This repository provides a Faktory worker process for Go apps. This
|
||||
worker process fetches background jobs from the Faktory server and processes them.
|
||||
|
||||
How is this different from all the other Go background worker libraries?
|
||||
They all use Redis or another "dumb" datastore. This library is far
|
||||
simpler because the Faktory server implements most of the data storage, retry logic,
|
||||
Web UI, etc.
|
||||
|
||||
# Installation
|
||||
|
||||
You must install [Faktory](https://github.com/contribsys/faktory) first.
|
||||
Then:
|
||||
|
||||
```
|
||||
go get -u github.com/contribsys/faktory_worker_go
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
To process background jobs, follow these steps:
|
||||
|
||||
1. Register your job types and their associated funcs
|
||||
2. Set a few optional parameters
|
||||
3. Start the processing
|
||||
|
||||
There are a couple ways to stop the process.
|
||||
In this example, send the TERM or INT signal.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
worker "github.com/contribsys/faktory_worker_go"
|
||||
)
|
||||
|
||||
func someFunc(ctx context.Context, args ...interface{}) error {
|
||||
help := worker.HelperFor(ctx)
|
||||
log.Printf("Working on job %s\n", help.Jid())
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
mgr := worker.NewManager()
|
||||
|
||||
// register job types and the function to execute them
|
||||
mgr.Register("SomeJob", someFunc)
|
||||
//mgr.Register("AnotherJob", anotherFunc)
|
||||
|
||||
// use up to N goroutines to execute jobs
|
||||
mgr.Concurrency = 20
|
||||
|
||||
// pull jobs from these queues, in this order of precedence
|
||||
mgr.ProcessStrictPriorityQueues("critical", "default", "bulk")
|
||||
|
||||
// alternatively you can use weights to avoid starvation
|
||||
//mgr.ProcessWeightedPriorityQueues(map[string]int{"critical":3, "default":2, "bulk":1})
|
||||
|
||||
// Start processing jobs, this method does not return.
|
||||
mgr.Run()
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively you can control the stopping of the Manager using
|
||||
`RunWithContext`. **You must process any signals yourself.**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
worker "github.com/contribsys/faktory_worker_go"
|
||||
)
|
||||
|
||||
func someFunc(ctx context.Context, args ...interface{}) error {
|
||||
help := worker.HelperFor(ctx)
|
||||
log.Printf("Working on job %s\n", help.Jid())
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
mgr := worker.NewManager()
|
||||
|
||||
// register job types and the function to execute them
|
||||
mgr.Register("SomeJob", someFunc)
|
||||
//mgr.Register("AnotherJob", anotherFunc)
|
||||
|
||||
// use up to N goroutines to execute jobs
|
||||
mgr.Concurrency = 20
|
||||
|
||||
// pull jobs from these queues, in this order of precedence
|
||||
mgr.ProcessStrictPriorityQueues("critical", "default", "bulk")
|
||||
|
||||
// alternatively you can use weights to avoid starvation
|
||||
//mgr.ProcessWeightedPriorityQueues(map[string]int{"critical":3, "default":2, "bulk":1})
|
||||
|
||||
go func(){
|
||||
// Start processing jobs in background routine, this method does not return
|
||||
// unless an error is returned or cancel() is called
|
||||
mgr.RunWithContext(ctx)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stopSignals := []os.Signal{
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGINT,
|
||||
}
|
||||
stop := make(chan os.Signal, len(stopSignals))
|
||||
for _, s := range stopSignals {
|
||||
signal.Notify(stop, s)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-stop:
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
```
|
||||
|
||||
See `test/main.go` for a working example.
|
||||
|
||||
# FAQ
|
||||
|
||||
* How do I specify the Faktory server location?
|
||||
|
||||
By default, it will use localhost:7419 which is sufficient for local development.
|
||||
Use FAKTORY\_URL to specify the URL, e.g. `tcp://faktory.example.com:12345` or
|
||||
use FAKTORY\_PROVIDER to specify the environment variable which does
|
||||
contain the URL: FAKTORY\_PROVIDER=FAKTORYTOGO\_URL. This level of
|
||||
indirection is useful for SaaSes, Heroku Addons, etc.
|
||||
|
||||
* How do I push new jobs to Faktory?
|
||||
|
||||
1. Inside a job, you can check out a connection from the Pool of Faktory
|
||||
connections using the job helper's `With` method:
|
||||
```go
|
||||
func someFunc(ctx context.Context, args ...interface{}) error {
|
||||
help := worker.HelperFor(ctx)
|
||||
return help.With(func(cl *faktory.Client) error {
|
||||
job := faktory.NewJob("SomeJob", 1, 2, 3)
|
||||
return cl.Push(job)
|
||||
})
|
||||
}
|
||||
```
|
||||
2. You can always open a client connection to Faktory directly but this
|
||||
won't perform as well:
|
||||
```go
|
||||
import (
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
client, err := faktory.Open()
|
||||
job := faktory.NewJob("SomeJob", 1, 2, 3)
|
||||
err = client.Push(job)
|
||||
```
|
||||
|
||||
**NB:** Client instances are **not safe to share**, you can use a Pool of Clients
|
||||
which is thread-safe.
|
||||
|
||||
See the Faktory Client API for
|
||||
[Go](https://github.com/contribsys/faktory/blob/master/client) or
|
||||
[Ruby](https://github.com/contribsys/faktory-ruby/blob/master/lib/faktory/client.rb).
|
||||
You can implement a Faktory client in any programming langauge.
|
||||
See [the wiki](https://github.com/contribsys/faktory/wiki) for details.
|
||||
|
||||
# Author
|
||||
|
||||
Mike Perham, @getajobmike, @contribsys
|
||||
|
||||
# License
|
||||
|
||||
This codebase is licensed via the Mozilla Public License, v2.0. https://choosealicense.com/licenses/mpl-2.0/
|
148
vendor/github.com/contribsys/faktory_worker_go/context.go
generated
vendored
Normal file
148
vendor/github.com/contribsys/faktory_worker_go/context.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
// internal keys for context value storage
|
||||
type valueKey int
|
||||
|
||||
const (
|
||||
poolKey valueKey = 2
|
||||
jobKey valueKey = 3
|
||||
)
|
||||
|
||||
var (
|
||||
NoAssociatedBatchError = fmt.Errorf("No batch associated with this job")
|
||||
)
|
||||
|
||||
// The Helper provides access to valuable data and APIs
|
||||
// within an executing job.
|
||||
//
|
||||
// We're pretty strict about what's exposed in the Helper
|
||||
// because execution should be orthogonal to
|
||||
// most of the Job payload contents.
|
||||
//
|
||||
// func myJob(ctx context.Context, args ...interface{}) error {
|
||||
// helper := worker.HelperFor(ctx)
|
||||
// jid := helper.Jid()
|
||||
//
|
||||
// helper.With(func(cl *faktory.Client) error {
|
||||
// cl.Push("anotherJob", 4, "arg")
|
||||
// })
|
||||
//
|
||||
type Helper interface {
|
||||
Jid() string
|
||||
JobType() string
|
||||
|
||||
// Faktory Enterprise:
|
||||
// the BID of the Batch associated with this job
|
||||
Bid() string
|
||||
|
||||
// Faktory Enterprise:
|
||||
// open the batch associated with this job so we can add more jobs to it.
|
||||
//
|
||||
// func myJob(ctx context.Context, args ...interface{}) error {
|
||||
// helper := worker.HelperFor(ctx)
|
||||
// helper.Batch(func(b *faktory.Batch) error {
|
||||
// return b.Push(faktory.NewJob("sometype", 1, 2, 3))
|
||||
// })
|
||||
Batch(func(*faktory.Batch) error) error
|
||||
|
||||
// allows direct access to the Faktory server from the job
|
||||
With(func(*faktory.Client) error) error
|
||||
|
||||
// Faktory Enterprise:
|
||||
// this method integrates with Faktory Enterprise's Job Tracking feature.
|
||||
// `reserveUntil` is optional, only needed for long jobs which have more dynamic
|
||||
// lifetimes.
|
||||
//
|
||||
// helper.TrackProgress(10, "Updating code...", nil)
|
||||
// helper.TrackProgress(20, "Cleaning caches...", &time.Now().Add(1 * time.Hour)))
|
||||
//
|
||||
TrackProgress(percent int, desc string, reserveUntil *time.Time) error
|
||||
}
|
||||
|
||||
type jobHelper struct {
|
||||
job *faktory.Job
|
||||
pool *faktory.Pool
|
||||
}
|
||||
|
||||
func (h *jobHelper) Jid() string {
|
||||
return h.job.Jid
|
||||
}
|
||||
func (h *jobHelper) Bid() string {
|
||||
if b, ok := h.job.GetCustom("bid"); ok {
|
||||
return b.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (h *jobHelper) JobType() string {
|
||||
return h.job.Type
|
||||
}
|
||||
|
||||
// Caution: this method must only be called within the
|
||||
// context of an executing job. It will panic if it cannot
|
||||
// create a Helper due to missing context values.
|
||||
func HelperFor(ctx context.Context) Helper {
|
||||
if j := ctx.Value(jobKey); j != nil {
|
||||
job := j.(*faktory.Job)
|
||||
if p := ctx.Value(poolKey); p != nil {
|
||||
pool := p.(*faktory.Pool)
|
||||
return &jobHelper{
|
||||
job: job,
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Panic("Invalid job context, cannot create faktory_worker_go job helper")
|
||||
return nil
|
||||
}
|
||||
|
||||
func jobContext(pool *faktory.Pool, job *faktory.Job) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, poolKey, pool)
|
||||
ctx = context.WithValue(ctx, jobKey, job)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// requires Faktory Enterprise
|
||||
func (h *jobHelper) TrackProgress(percent int, desc string, reserveUntil *time.Time) error {
|
||||
return h.With(func(cl *faktory.Client) error {
|
||||
return cl.TrackSet(h.Jid(), percent, desc, reserveUntil)
|
||||
})
|
||||
}
|
||||
|
||||
// requires Faktory Enterprise
|
||||
// Open the current batch so we can add more jobs to it.
|
||||
func (h *jobHelper) Batch(fn func(*faktory.Batch) error) error {
|
||||
bid := h.Bid()
|
||||
if bid == "" {
|
||||
return NoAssociatedBatchError
|
||||
}
|
||||
|
||||
var b *faktory.Batch
|
||||
var err error
|
||||
|
||||
err = h.pool.With(func(cl *faktory.Client) error {
|
||||
b, err = cl.BatchOpen(bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(b)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *jobHelper) With(fn func(*faktory.Client) error) error {
|
||||
return h.pool.With(fn)
|
||||
}
|
60
vendor/github.com/contribsys/faktory_worker_go/log.go
generated
vendored
Normal file
60
vendor/github.com/contribsys/faktory_worker_go/log.go
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debug(v ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(v ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Warn(v ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(v ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type StdLogger struct {
|
||||
*log.Logger
|
||||
}
|
||||
|
||||
func NewStdLogger() Logger {
|
||||
flags := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC
|
||||
return &StdLogger{log.New(os.Stdout, "", flags)}
|
||||
}
|
||||
|
||||
func (l *StdLogger) Debug(v ...interface{}) {
|
||||
l.Println(v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Debugf(format string, v ...interface{}) {
|
||||
l.Printf(format+"\n", v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Error(v ...interface{}) {
|
||||
l.Println(v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Errorf(format string, v ...interface{}) {
|
||||
l.Printf(format+"\n", v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Info(v ...interface{}) {
|
||||
l.Println(v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Infof(format string, v ...interface{}) {
|
||||
l.Printf(format+"\n", v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Warn(v ...interface{}) {
|
||||
l.Println(v...)
|
||||
}
|
||||
|
||||
func (l *StdLogger) Warnf(format string, v ...interface{}) {
|
||||
l.Printf(format+"\n", v...)
|
||||
}
|
253
vendor/github.com/contribsys/faktory_worker_go/manager.go
generated
vendored
Normal file
253
vendor/github.com/contribsys/faktory_worker_go/manager.go
generated
vendored
Normal file
@ -0,0 +1,253 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
// Manager coordinates the processes for the worker. It is responsible for
|
||||
// starting and stopping goroutines to perform work at the desired concurrency level
|
||||
type Manager struct {
|
||||
mut sync.Mutex
|
||||
|
||||
Concurrency int
|
||||
Logger Logger
|
||||
ProcessWID string
|
||||
Labels []string
|
||||
Pool *faktory.Pool
|
||||
|
||||
queues []string
|
||||
middleware []MiddlewareFunc
|
||||
state string // "", "quiet" or "terminate"
|
||||
// The done channel will always block unless
|
||||
// the system is shutting down.
|
||||
done chan interface{}
|
||||
shutdownWaiter *sync.WaitGroup
|
||||
jobHandlers map[string]Handler
|
||||
eventHandlers map[lifecycleEventType][]LifecycleEventHandler
|
||||
|
||||
// This only needs to be computed once. Store it here to keep things fast.
|
||||
weightedPriorityQueuesEnabled bool
|
||||
weightedQueues []string
|
||||
}
|
||||
|
||||
// Register a handler for the given jobtype. It is expected that all jobtypes
|
||||
// are registered upon process startup.
|
||||
//
|
||||
// mgr.Register("ImportantJob", ImportantFunc)
|
||||
func (mgr *Manager) Register(name string, fn Perform) {
|
||||
mgr.jobHandlers[name] = func(ctx context.Context, job *faktory.Job) error {
|
||||
return fn(ctx, job.Args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Register a callback to be fired when a process lifecycle event occurs.
|
||||
// These are useful for hooking into process startup or shutdown.
|
||||
func (mgr *Manager) On(event lifecycleEventType, fn LifecycleEventHandler) {
|
||||
mgr.eventHandlers[event] = append(mgr.eventHandlers[event], fn)
|
||||
}
|
||||
|
||||
// After calling Quiet(), no more jobs will be pulled
|
||||
// from Faktory by this process.
|
||||
func (mgr *Manager) Quiet() {
|
||||
mgr.mut.Lock()
|
||||
defer mgr.mut.Unlock()
|
||||
|
||||
if mgr.state == "quiet" {
|
||||
return
|
||||
}
|
||||
|
||||
mgr.Logger.Info("Quieting...")
|
||||
mgr.state = "quiet"
|
||||
mgr.fireEvent(Quiet)
|
||||
}
|
||||
|
||||
// Terminate signals that the various components should shutdown.
|
||||
// Blocks on the shutdownWaiter until all components have finished.
|
||||
func (mgr *Manager) Terminate(reallydie bool) {
|
||||
mgr.mut.Lock()
|
||||
defer mgr.mut.Unlock()
|
||||
|
||||
if mgr.state == "terminate" {
|
||||
return
|
||||
}
|
||||
|
||||
mgr.Logger.Info("Shutting down...")
|
||||
mgr.state = "terminate"
|
||||
close(mgr.done)
|
||||
mgr.fireEvent(Shutdown)
|
||||
mgr.shutdownWaiter.Wait()
|
||||
mgr.Pool.Close()
|
||||
mgr.Logger.Info("Goodbye")
|
||||
if reallydie {
|
||||
os.Exit(0) // nolint:gocritic
|
||||
}
|
||||
}
|
||||
|
||||
// NewManager returns a new manager with default values.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
Concurrency: 20,
|
||||
Logger: NewStdLogger(),
|
||||
Labels: []string{"golang-" + Version},
|
||||
Pool: nil,
|
||||
|
||||
state: "",
|
||||
queues: []string{"default"},
|
||||
done: make(chan interface{}),
|
||||
shutdownWaiter: &sync.WaitGroup{},
|
||||
jobHandlers: map[string]Handler{},
|
||||
eventHandlers: map[lifecycleEventType][]LifecycleEventHandler{
|
||||
Startup: {},
|
||||
Quiet: {},
|
||||
Shutdown: {},
|
||||
},
|
||||
weightedPriorityQueuesEnabled: false,
|
||||
weightedQueues: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (mgr *Manager) setUpWorkerProcess() error {
|
||||
mgr.mut.Lock()
|
||||
defer mgr.mut.Unlock()
|
||||
|
||||
if mgr.state != "" {
|
||||
return fmt.Errorf("cannot start worker process for the mananger in %v state", mgr.state)
|
||||
}
|
||||
|
||||
// This will signal to Faktory that all connections from this process
|
||||
// are worker connections.
|
||||
if len(mgr.ProcessWID) == 0 {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
faktory.RandomProcessWid = strconv.FormatInt(rand.Int63(), 32)
|
||||
} else {
|
||||
faktory.RandomProcessWid = mgr.ProcessWID
|
||||
}
|
||||
// Set labels to be displayed in the UI
|
||||
faktory.Labels = mgr.Labels
|
||||
|
||||
if mgr.Pool == nil {
|
||||
pool, err := faktory.NewPool(mgr.Concurrency + 2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create Faktory connection pool: %w", err)
|
||||
}
|
||||
mgr.Pool = pool
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunWithContext starts processing jobs. The method will return if an error is encountered while starting.
|
||||
// If the context is present then os signals will be ignored, the context must be canceled for the method to return
|
||||
// after running.
|
||||
func (mgr *Manager) RunWithContext(ctx context.Context) error {
|
||||
err := mgr.boot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-ctx.Done()
|
||||
mgr.Terminate(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mgr *Manager) boot() error {
|
||||
err := mgr.setUpWorkerProcess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.fireEvent(Startup)
|
||||
go heartbeat(mgr)
|
||||
|
||||
mgr.Logger.Infof("faktory_worker_go %s PID %d now ready to process jobs", Version, os.Getpid())
|
||||
mgr.Logger.Debugf("Using Faktory Client API %s", faktory.Version)
|
||||
for i := 0; i < mgr.Concurrency; i++ {
|
||||
go process(mgr, i)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts processing jobs.
|
||||
// This method does not return unless an error is encountered while starting.
|
||||
func (mgr *Manager) Run() error {
|
||||
err := mgr.boot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
sig := <-hookSignals()
|
||||
mgr.handleEvent(signalMap[sig])
|
||||
}
|
||||
}
|
||||
|
||||
// One of the Process*Queues methods should be called once before Run()
|
||||
func (mgr *Manager) ProcessStrictPriorityQueues(queues ...string) {
|
||||
mgr.queues = queues
|
||||
mgr.weightedPriorityQueuesEnabled = false
|
||||
}
|
||||
|
||||
func (mgr *Manager) ProcessWeightedPriorityQueues(queues map[string]int) {
|
||||
uniqueQueues := queueKeys(queues)
|
||||
weightedQueues := expandWeightedQueues(queues)
|
||||
|
||||
mgr.queues = uniqueQueues
|
||||
mgr.weightedQueues = weightedQueues
|
||||
mgr.weightedPriorityQueuesEnabled = true
|
||||
}
|
||||
|
||||
func (mgr *Manager) queueList() []string {
|
||||
if mgr.weightedPriorityQueuesEnabled {
|
||||
sq := shuffleQueues(mgr.weightedQueues)
|
||||
return uniqQueues(len(mgr.queues), sq)
|
||||
}
|
||||
return mgr.queues
|
||||
}
|
||||
|
||||
func (mgr *Manager) fireEvent(event lifecycleEventType) {
|
||||
for _, fn := range mgr.eventHandlers[event] {
|
||||
err := fn(mgr)
|
||||
if err != nil {
|
||||
mgr.Logger.Errorf("Error running lifecycle event handler: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mgr *Manager) with(fn func(cl *faktory.Client) error) error {
|
||||
if mgr.Pool == nil {
|
||||
panic("No Pool set on Manager, have you called manager.Run() yet?")
|
||||
}
|
||||
return mgr.Pool.With(fn)
|
||||
}
|
||||
|
||||
func (mgr *Manager) handleEvent(sig string) string {
|
||||
if sig == mgr.state {
|
||||
return mgr.state
|
||||
}
|
||||
if sig == "quiet" && mgr.state == "terminate" {
|
||||
// this is a no-op, a terminating process is quiet already
|
||||
return mgr.state
|
||||
}
|
||||
|
||||
switch sig {
|
||||
case "terminate":
|
||||
go func() {
|
||||
mgr.Terminate(true)
|
||||
}()
|
||||
case "quiet":
|
||||
go func() {
|
||||
mgr.Quiet()
|
||||
}()
|
||||
case "dump":
|
||||
dumpThreads(mgr.Logger)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
27
vendor/github.com/contribsys/faktory_worker_go/middleware.go
generated
vendored
Normal file
27
vendor/github.com/contribsys/faktory_worker_go/middleware.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
type Handler func(ctx context.Context, job *faktory.Job) error
|
||||
type MiddlewareFunc func(ctx context.Context, job *faktory.Job, next func(ctx context.Context) error) error
|
||||
|
||||
// Use(...) adds middleware to the chain.
|
||||
func (mgr *Manager) Use(middleware ...MiddlewareFunc) {
|
||||
mgr.middleware = append(mgr.middleware, middleware...)
|
||||
}
|
||||
|
||||
func dispatch(chain []MiddlewareFunc, ctx context.Context, job *faktory.Job, perform Handler) error {
|
||||
if len(chain) == 0 {
|
||||
return perform(ctx, job)
|
||||
}
|
||||
|
||||
link := chain[0]
|
||||
rest := chain[1:]
|
||||
return link(ctx, job, func(ctx context.Context) error {
|
||||
return dispatch(rest, ctx, job, perform)
|
||||
})
|
||||
}
|
243
vendor/github.com/contribsys/faktory_worker_go/runner.go
generated
vendored
Normal file
243
vendor/github.com/contribsys/faktory_worker_go/runner.go
generated
vendored
Normal file
@ -0,0 +1,243 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
type lifecycleEventType int
|
||||
|
||||
const (
|
||||
Startup lifecycleEventType = 1
|
||||
Quiet lifecycleEventType = 2
|
||||
Shutdown lifecycleEventType = 3
|
||||
)
|
||||
|
||||
type NoHandlerError struct {
|
||||
JobType string
|
||||
}
|
||||
|
||||
func (s *NoHandlerError) Error() string {
|
||||
return fmt.Sprintf("No handler registered for job type %s", s.JobType)
|
||||
}
|
||||
|
||||
func heartbeat(mgr *Manager) {
|
||||
mgr.shutdownWaiter.Add(1)
|
||||
defer mgr.shutdownWaiter.Done()
|
||||
|
||||
timer := time.NewTicker(15 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
// we don't care about errors, assume any network
|
||||
// errors will heal eventually
|
||||
err := mgr.with(func(c *faktory.Client) error {
|
||||
data, err := c.Beat(mgr.state)
|
||||
if err != nil && strings.Contains(err.Error(), "Unknown worker") {
|
||||
// If our heartbeat expires, we must restart and re-authenticate.
|
||||
// Use a signal so we can unwind and shutdown cleanly.
|
||||
mgr.Logger.Warn("Faktory heartbeat has expired, shutting down...")
|
||||
_ = syscall.Kill(os.Getpid(), syscall.SIGTERM)
|
||||
}
|
||||
if err != nil || data == "" {
|
||||
return err
|
||||
}
|
||||
var hash map[string]string
|
||||
err = json.Unmarshal([]byte(data), &hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if state, ok := hash["state"]; ok && state != "" {
|
||||
mgr.handleEvent(state)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
mgr.Logger.Error(fmt.Sprintf("heartbeat error: %v", err))
|
||||
}
|
||||
case <-mgr.done:
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process(mgr *Manager, idx int) {
|
||||
mgr.shutdownWaiter.Add(1)
|
||||
defer mgr.shutdownWaiter.Done()
|
||||
|
||||
// delay initial fetch randomly to prevent thundering herd.
|
||||
// this will pause between 0 and 2B nanoseconds, i.e. 0-2 seconds
|
||||
time.Sleep(time.Duration(rand.Int31()))
|
||||
|
||||
for {
|
||||
if mgr.state != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// check for shutdown
|
||||
select {
|
||||
case <-mgr.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
err := processOne(mgr)
|
||||
if err != nil {
|
||||
mgr.Logger.Debug(err)
|
||||
if _, ok := err.(*NoHandlerError); !ok {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processOne(mgr *Manager) error {
|
||||
var job *faktory.Job
|
||||
|
||||
// explicit scopes to limit variable visibility
|
||||
{
|
||||
var e error
|
||||
err := mgr.with(func(c *faktory.Client) error {
|
||||
job, e = c.Fetch(mgr.queueList()...)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
perform := mgr.jobHandlers[job.Type]
|
||||
|
||||
if perform == nil {
|
||||
je := &NoHandlerError{JobType: job.Type}
|
||||
err := mgr.with(func(c *faktory.Client) error {
|
||||
return c.Fail(job.Jid, je, nil)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return je
|
||||
}
|
||||
|
||||
joberr := dispatch(mgr.middleware, jobContext(mgr.Pool, job), job, perform)
|
||||
if joberr != nil {
|
||||
// job errors are normal and expected, we don't return early from them
|
||||
mgr.Logger.Errorf("Error running %s job %s: %v", job.Type, job.Jid, joberr)
|
||||
}
|
||||
|
||||
for {
|
||||
// we want to report the result back to Faktory.
|
||||
// we stay in this loop until we successfully report.
|
||||
err := mgr.with(func(c *faktory.Client) error {
|
||||
if joberr != nil {
|
||||
return c.Fail(job.Jid, joberr, nil)
|
||||
} else {
|
||||
return c.Ack(job.Jid)
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-mgr.done:
|
||||
mgr.Logger.Error(fmt.Errorf("Unable to report JID %v result to Faktory: %w", job.Jid, err))
|
||||
return nil
|
||||
default:
|
||||
mgr.Logger.Debug(err)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expandWeightedQueues builds a slice of queues represented the number of times equal to their weights.
|
||||
func expandWeightedQueues(queueWeights map[string]int) []string {
|
||||
weightsTotal := 0
|
||||
for _, queueWeight := range queueWeights {
|
||||
weightsTotal += queueWeight
|
||||
}
|
||||
|
||||
weightedQueues := make([]string, weightsTotal)
|
||||
fillIndex := 0
|
||||
|
||||
for queue, nTimes := range queueWeights {
|
||||
// Fill weightedQueues with queue n times
|
||||
for idx := 0; idx < nTimes; idx++ {
|
||||
weightedQueues[fillIndex] = queue
|
||||
fillIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// weightedQueues has to be stable so we can write tests
|
||||
sort.Strings(weightedQueues)
|
||||
return weightedQueues
|
||||
}
|
||||
|
||||
func queueKeys(queues map[string]int) []string {
|
||||
keys := make([]string, len(queues))
|
||||
i := 0
|
||||
for k := range queues {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
// queues has to be stable so we can write tests
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// shuffleQueues returns a copy of the slice with the elements shuffled.
|
||||
func shuffleQueues(queues []string) []string {
|
||||
wq := make([]string, len(queues))
|
||||
copy(wq, queues)
|
||||
|
||||
rand.Shuffle(len(wq), func(i, j int) {
|
||||
wq[i], wq[j] = wq[j], wq[i]
|
||||
})
|
||||
|
||||
return wq
|
||||
}
|
||||
|
||||
// uniqQueues returns a slice of length len, of the unique elements while maintaining order.
|
||||
// The underlying array is modified to avoid allocating another one.
|
||||
func uniqQueues(length int, queues []string) []string {
|
||||
// Record the unique values and position.
|
||||
pos := 0
|
||||
uniqMap := make(map[string]int)
|
||||
for idx := range queues {
|
||||
if _, ok := uniqMap[queues[idx]]; !ok {
|
||||
uniqMap[queues[idx]] = pos
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the copied array, by updating the values.
|
||||
for queue, position := range uniqMap {
|
||||
queues[position] = queue
|
||||
}
|
||||
|
||||
// Slice only what we need.
|
||||
return queues[:length]
|
||||
}
|
||||
|
||||
func dumpThreads(logg Logger) {
|
||||
buf := make([]byte, 64*1024)
|
||||
_ = runtime.Stack(buf, true)
|
||||
logg.Info("FULL PROCESS THREAD DUMP:")
|
||||
logg.Info(string(buf))
|
||||
}
|
32
vendor/github.com/contribsys/faktory_worker_go/runner_unix.go
generated
vendored
Normal file
32
vendor/github.com/contribsys/faktory_worker_go/runner_unix.go
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
// +build linux freebsd netbsd openbsd dragonfly solaris illumos aix darwin
|
||||
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
SIGTERM os.Signal = syscall.SIGTERM
|
||||
SIGTSTP os.Signal = syscall.SIGTSTP
|
||||
SIGTTIN os.Signal = syscall.SIGTTIN
|
||||
SIGINT os.Signal = os.Interrupt
|
||||
|
||||
signalMap = map[os.Signal]string{
|
||||
SIGTERM: "terminate",
|
||||
SIGINT: "terminate",
|
||||
SIGTSTP: "quiet",
|
||||
SIGTTIN: "dump",
|
||||
}
|
||||
)
|
||||
|
||||
func hookSignals() chan os.Signal {
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, SIGINT)
|
||||
signal.Notify(sigchan, SIGTERM)
|
||||
signal.Notify(sigchan, SIGTSTP)
|
||||
signal.Notify(sigchan, SIGTTIN)
|
||||
return sigchan
|
||||
}
|
28
vendor/github.com/contribsys/faktory_worker_go/runner_windows.go
generated
vendored
Normal file
28
vendor/github.com/contribsys/faktory_worker_go/runner_windows.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
// +build windows
|
||||
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
// SIGTERM is an alias for syscall.SIGTERM
|
||||
SIGTERM os.Signal = syscall.SIGTERM
|
||||
// SIGINT is and alias for syscall.SIGINT
|
||||
SIGINT os.Signal = os.Interrupt
|
||||
|
||||
signalMap = map[os.Signal]string{
|
||||
SIGTERM: "terminate",
|
||||
SIGINT: "terminate",
|
||||
}
|
||||
)
|
||||
|
||||
func hookSignals() chan os.Signal {
|
||||
sigchan := make(chan os.Signal)
|
||||
signal.Notify(sigchan, SIGINT)
|
||||
signal.Notify(sigchan, SIGTERM)
|
||||
return sigchan
|
||||
}
|
36
vendor/github.com/contribsys/faktory_worker_go/testing.go
generated
vendored
Normal file
36
vendor/github.com/contribsys/faktory_worker_go/testing.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
package faktory_worker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
faktory "github.com/contribsys/faktory/client"
|
||||
)
|
||||
|
||||
type PerformExecutor interface {
|
||||
Execute(*faktory.Job, Perform) error
|
||||
}
|
||||
|
||||
type testExecutor struct {
|
||||
*faktory.Pool
|
||||
}
|
||||
|
||||
func NewTestExecutor(p *faktory.Pool) PerformExecutor {
|
||||
return &testExecutor{Pool: p}
|
||||
}
|
||||
|
||||
func (tp *testExecutor) Execute(specjob *faktory.Job, p Perform) error {
|
||||
// perform a JSON round trip to ensure Perform gets the arguments
|
||||
// exactly how a round trip to Faktory would look.
|
||||
data, err := json.Marshal(specjob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var job faktory.Job
|
||||
err = json.Unmarshal(data, &job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := jobContext(tp.Pool, &job)
|
||||
return p(c, job.Args...)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user