diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bea509f539e23ae0637275f72feaab6da5b2e90..b13ba0b6794808efa449a22d87db8d23aaca8642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] -*Nothing yet.* +### Added + +* Introduced early plugin support (only for antispam for now). + * Includes a simple OCR plugin to help mitigate text-based image spam. ## [1.2.2] - December 8th, 2020 diff --git a/Dockerfile b/Dockerfile index 6474057eeacf7a86536c04bfa949681175b623cc..e07b8289e367c127c6307e5fcd7782a9a2c5d109 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,8 @@ RUN ./build.sh # Final runtime stage. FROM alpine +RUN mkdir /plugins +COPY --from=builder /opt/bin/plugin_antispam_ocr /plugins/ COPY --from=builder /opt/bin/media_repo /opt/bin/import_synapse /opt/bin/gdpr_export /opt/bin/gdpr_import /usr/local/bin/ RUN apk add --no-cache \ diff --git a/cmd/media_repo/reloads.go b/cmd/media_repo/reloads.go index 6deb426d3927f357bf19b613904c469d5ebaeed3..a51a1c28dd7912016ea8a727673743cb72dec0e8 100644 --- a/cmd/media_repo/reloads.go +++ b/cmd/media_repo/reloads.go @@ -8,6 +8,7 @@ import ( "github.com/turt2live/matrix-media-repo/internal_cache" "github.com/turt2live/matrix-media-repo/ipfs_proxy" "github.com/turt2live/matrix-media-repo/metrics" + "github.com/turt2live/matrix-media-repo/plugins" "github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/tasks" ) @@ -21,6 +22,7 @@ func setupReloads() { reloadIpfsOnChan(globals.IPFSReloadChan) reloadAccessTokensOnChan(globals.AccessTokenReloadChan) reloadCacheOnChan(globals.CacheReplaceChan) + reloadPluginsOnChan(globals.PluginReloadChan) } func stopReloads() { @@ -33,6 +35,7 @@ func stopReloads() { globals.RecurringTasksReloadChan <- false globals.IPFSReloadChan <- false globals.CacheReplaceChan <- false + globals.PluginReloadChan <- false } func reloadWebOnChan(reloadChan chan bool) { @@ -147,3 +150,17 @@ func reloadCacheOnChan(reloadChan chan bool) { } }() } + +func reloadPluginsOnChan(reloadChan chan bool) { + go func() { + defer close(reloadChan) + for { + shouldReload := <-reloadChan + if shouldReload { + plugins.ReloadPlugins() + } else { + plugins.StopPlugins() + } + } + }() +} diff --git a/cmd/plugin_antispam_ocr/main.go b/cmd/plugin_antispam_ocr/main.go new file mode 100644 index 0000000000000000000000000000000000000000..2edbb78c383a79643bde693e454ab6d07b9eab55 --- /dev/null +++ b/cmd/plugin_antispam_ocr/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "image" + "io/ioutil" + "math" + "net/http" + "os" + "regexp" + "strings" + "time" + + "github.com/disintegration/imaging" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/turt2live/matrix-media-repo/plugins/plugin_common" + "github.com/turt2live/matrix-media-repo/plugins/plugin_interfaces" + "github.com/turt2live/matrix-media-repo/util" +) + +type AntispamOCR struct { + logger hclog.Logger + config map[string]interface{} + + userIdRegex *regexp.Regexp + contentTypes []string + minSize int + maxSize int + keywordGroups [][]string + ocrServer string + topPercentage float64 +} + +func (a *AntispamOCR) HandleConfig(config map[string]interface{}) error { + a.config = config + + a.ocrServer = a.config["ocrServer"].(string) + a.minSize = int(a.config["minSizeBytes"].(float64)) + a.maxSize = int(a.config["maxSizeBytes"].(float64)) + a.topPercentage = a.config["percentageOfHeight"].(float64) + + ctypes := make([]string, 0) + for _, t := range a.config["types"].([]interface{}) { + ctypes = append(ctypes, fmt.Sprintf("%v", t)) + } + a.contentTypes = ctypes + + kwg := make([][]string, 0) + for _, c := range a.config["keywordGroups"].([]interface{}) { + kwg2 := make([]string, 0) + for _, kw := range c.([]interface{}) { + kwg2 = append(kwg2, fmt.Sprintf("%v", kw)) + } + kwg = append(kwg, kwg2) + } + a.keywordGroups = kwg + + r, err := regexp.Compile(a.config["userIds"].(string)) + if err != nil { + return err + } + a.userIdRegex = r + + return nil +} + +func (a *AntispamOCR) CheckForSpam(b64 string, filename string, contentType string, userId string, origin string, mediaId string) (bool, error) { + b, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return false, err + } + + if len(b) < a.minSize || len(b) > a.maxSize { + return false, nil + } + if !util.ArrayContains(a.contentTypes, contentType) { + return false, nil + } + if !a.userIdRegex.MatchString(userId) { + return false, nil + } + + img, _, err := image.Decode(bytes.NewBuffer(b)) + if err != nil { + return false, err + } + + // For certain kinds of spam we don't really need to consider the whole image but just the upper third. + if a.topPercentage < 1.0 && a.topPercentage > 0 { + newHeight := int(math.Round(float64(img.Bounds().Max.Y) * a.topPercentage)) + img = imaging.Fill(img, img.Bounds().Max.X, newHeight, imaging.Top, imaging.Linear) + } + + // Steps: + // 1. Crush the image to reasonable dimensions (helps with later transforms). Use Lanczos to soften lines on letters. + // 2. Double the image size, using Lanczos again to do a second round of softening. + // 3. Try to remove any background noise (usually introduced during upload and by resizing). + // 4. Adjust contrast to make text more obvious on the background. + // 5. Convert to grayscale, thus avoiding any colour issues with the OCR. + img = imaging.Fit(img, 512, 512, imaging.Lanczos) + img = imaging.Fill(img, img.Bounds().Max.X * 2, img.Bounds().Max.Y * 2, imaging.Top, imaging.Lanczos) + img = imaging.Sharpen(img, 50) + img = imaging.AdjustContrast(img, 2) + img = imaging.Grayscale(img) + + imgData := &bytes.Buffer{} + err = imaging.Encode(imgData, img, imaging.PNG) + if err != nil { + return false, err + } + b64 = base64.StdEncoding.EncodeToString(imgData.Bytes()) + + bodyBytes, err := json.Marshal(map[string]interface{}{ + "base64": b64, + "trim": "\n", + }) + if err != nil { + return false, err + } + + ocrUrl := util.MakeUrl(a.ocrServer, "/base64") + req, err := http.NewRequest("POST", ocrUrl, bytes.NewBuffer(bodyBytes)) + if err != nil { + return false, err + } + req.Header.Set("User-Agent", "matrix-media-repo") + client := &http.Client{ + Timeout: 20 * time.Second, + } + res, err := client.Do(req) + if err != nil { + a.logger.Error("non-fatal error checking spam: ", err) + return false, nil + } + contents, err := ioutil.ReadAll(res.Body) + if err != nil { + return false, err + } + var resp map[string]interface{} + err = json.Unmarshal(contents, &resp) + if err != nil { + return false, err + } + if res.StatusCode != http.StatusOK { + return false, errors.New(fmt.Sprintf("unexpected status code: %d", res.StatusCode)) + } + ocr := strings.ToLower(resp["result"].(string)) + + for _, kwg := range a.keywordGroups { + hasKeyword := false + for _, kw := range kwg { + if strings.Contains(ocr, kw) { + hasKeyword = true + break + } + } + if !hasKeyword { + return false, nil + } + } + + a.logger.Warn("spam detected") + return true, nil +} + +func main() { + logger := hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: os.Stderr, + JSONFormat: true, + }) + + antispam := &AntispamOCR{logger: logger} + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugin_common.HandshakeConfig, + Plugins: map[string]plugin.Plugin{ + "antispam": &plugin_interfaces.AntispamPlugin{Impl: antispam}, + }, + }) +} diff --git a/common/config/conf_main.go b/common/config/conf_main.go index e87aa50620ca3fc4947f6e81f25813d2a4c027d1..559ef2c2698a7e0bb1e7266c3c39c9c49f13d793 100644 --- a/common/config/conf_main.go +++ b/common/config/conf_main.go @@ -13,6 +13,7 @@ type MainRepoConfig struct { Metrics MetricsConfig `yaml:"metrics"` SharedSecret SharedSecretConfig `yaml:"sharedSecretAuth"` Federation FederationConfig `yaml:"federation"` + Plugins []PluginConfig `yaml:"plugins,flow"` } func NewDefaultMainConfig() MainRepoConfig { @@ -124,5 +125,6 @@ func NewDefaultMainConfig() MainRepoConfig { Federation: FederationConfig{ BackoffAt: 20, }, + Plugins: []PluginConfig{}, } } diff --git a/common/config/models_main.go b/common/config/models_main.go index 86649ffea04639992b7a064a2769dda2c6e16dc4..9a93db0b00c0413e6e682af930097c9d419bd12b 100644 --- a/common/config/models_main.go +++ b/common/config/models_main.go @@ -74,3 +74,8 @@ type SharedSecretConfig struct { type FederationConfig struct { BackoffAt int `yaml:"backoffAt"` } + +type PluginConfig struct { + Executable string `yaml:"exec"` + Config map[string]interface{} `yaml:"config"` +} diff --git a/common/config/watch.go b/common/config/watch.go index 5dede9f0eb3610500248e7ca17dadbc75be5a236..3b224e7fc3477e8fda282c5d10e4804786cd5c3e 100644 --- a/common/config/watch.go +++ b/common/config/watch.go @@ -117,6 +117,9 @@ func onFileChanged() { logrus.Warn("Updating datastores to ensure accuracy") globals.DatastoresReloadChan <- true + logrus.Info("Reloading all plugins") + globals.PluginReloadChan <- true + logrus.Info("Restarting recurring tasks") globals.RecurringTasksReloadChan <- true } diff --git a/common/globals/reload.go b/common/globals/reload.go index f1aa971fff146621cdaaf631aa2fedd8a0d10c34..4bd8978e91c6e87177b7e3e5ebca14fd91422cab 100644 --- a/common/globals/reload.go +++ b/common/globals/reload.go @@ -7,4 +7,5 @@ var DatastoresReloadChan = make(chan bool) var RecurringTasksReloadChan = make(chan bool) var IPFSReloadChan = make(chan bool) var AccessTokenReloadChan = make(chan bool) -var CacheReplaceChan = make(chan bool) \ No newline at end of file +var CacheReplaceChan = make(chan bool) +var PluginReloadChan = make(chan bool) diff --git a/common/runtime/init.go b/common/runtime/init.go index fdb6bcbd2975d17433b1c2b68ce3996fd0f152a4..a573dbfc8b167c5ac407a1969e20bfa5ca2f8443 100644 --- a/common/runtime/init.go +++ b/common/runtime/init.go @@ -8,6 +8,7 @@ import ( "github.com/turt2live/matrix-media-repo/common/rcontext" "github.com/turt2live/matrix-media-repo/common/version" "github.com/turt2live/matrix-media-repo/ipfs_proxy" + "github.com/turt2live/matrix-media-repo/plugins" "github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/storage/datastore" "github.com/turt2live/matrix-media-repo/storage/datastore/ds_s3" @@ -19,6 +20,7 @@ func RunStartupSequence() { config.CheckDeprecations() LoadDatabase() LoadDatastores() + plugins.ReloadPlugins() logrus.Info("Starting IPFS (if enabled)...") ipfs_proxy.Reload() diff --git a/config.sample.yaml b/config.sample.yaml index f237af47a06fc2f79342127d65c1122feb38c948..9839689cb477ec516ca54768e0234201c7be96db 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -484,6 +484,40 @@ metrics: # The port to listen on. Cannot be the same as the general web server port. port: 9000 +# Plugins are optional pieces of the media repo used to extend the functionality offered. +# Currently there are only antispam plugins, but in future there should be more options. +# Plugins are not supported on per-domain paths and are instead repo-wide. For more +# information on writing plugins, please visit #matrix-media-repo:t2bot.io on Matrix. +plugins: + - exec: /path/to/plugin/executable + # Note: the exact config varies by plugin. + config: + hello: world + + # An example OCR plugin to block images with certain text. Note that the Docker image + # for the media repo automatically ships this at /plugins/plugin_antispam_ocr +# - exec: /plugins/plugin_antispam_ocr +# config: +# # The URL to your OCR server (https://github.com/otiai10/ocrserver) +# ocrServer: "http://localhost:8080" +# # The keywords to scan for. The image must contain at least one of the keywords +# # from each list to qualify for spam. +# keywordGroups: +# - - elon +# - musk +# - elonmusk +# - - bitcoin +# # The minimum (and maximum) sizes of images to process. +# minSizeBytes: 20000 +# maxSizeBytes: 200000 +# # The types of files to process +# types: ["image/png", "image/jpeg", "image/jpg"] +# # The user ID regex to check against +# userIds: "@telegram_.*" +# # How much of the image's height, starting from the top, to consider before +# # discarding the rest. Set to 1.0 to consider the whole image. +# percentageOfHeight: 0.35 + # Options for controlling various MSCs/unstable features of the media repo # Sections of this config might disappear or be added over time. By default all # features are disabled in here and must be explicitly enabled to be used. diff --git a/controllers/upload_controller/upload_controller.go b/controllers/upload_controller/upload_controller.go index 1cf789d109487deb4184c704dede76987ccb6b77..8dda069944f4212525733cce0be0cae139263aae 100644 --- a/controllers/upload_controller/upload_controller.go +++ b/controllers/upload_controller/upload_controller.go @@ -13,6 +13,7 @@ import ( "github.com/turt2live/matrix-media-repo/common" "github.com/turt2live/matrix-media-repo/common/rcontext" "github.com/turt2live/matrix-media-repo/internal_cache" + "github.com/turt2live/matrix-media-repo/plugins" "github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/storage/datastore" "github.com/turt2live/matrix-media-repo/types" @@ -175,9 +176,23 @@ func trackUploadAsLastAccess(ctx rcontext.RequestContext, media *types.Media) { } } +func checkSpam(contents []byte, filename string, contentType string, userId string, origin string, mediaId string) error { + spam, err := plugins.CheckForSpam(contents, filename, contentType, userId, origin, mediaId) + if err != nil { + logrus.Warn("Error checking spam - assuming not spam: " + err.Error()) + return nil + } + if spam { + return common.ErrMediaQuarantined + } + return nil +} + func StoreDirect(f *AlreadyUploadedFile, contents io.ReadCloser, expectedSize int64, contentType string, filename string, userId string, origin string, mediaId string, kind string, ctx rcontext.RequestContext, filterUserDuplicates bool) (*types.Media, error) { + var err error var ds *datastore.DatastoreRef var info *types.ObjectInfo + var contentBytes []byte if f == nil { dsPicked, err := datastore.PickDatastore(kind, ctx) if err != nil { @@ -185,7 +200,12 @@ func StoreDirect(f *AlreadyUploadedFile, contents io.ReadCloser, expectedSize in } ds = dsPicked - fInfo, err := ds.UploadFile(contents, expectedSize, ctx) + contentBytes, err = ioutil.ReadAll(contents) + if err != nil { + return nil, err + } + + fInfo, err := ds.UploadFile(util.BytesToStream(contentBytes), expectedSize, ctx) if err != nil { return nil, err } @@ -193,6 +213,16 @@ func StoreDirect(f *AlreadyUploadedFile, contents io.ReadCloser, expectedSize in } else { ds = f.DS info = f.ObjectInfo + + // download the contents for antispam + contents, err = ds.DownloadFile(info.Location) + if err != nil { + return nil, err + } + contentBytes, err = ioutil.ReadAll(contents) + if err != nil { + return nil, err + } } db := storage.GetDatabase().GetMediaStore(ctx) @@ -223,9 +253,16 @@ func StoreDirect(f *AlreadyUploadedFile, contents io.ReadCloser, expectedSize in } } + err = checkSpam(contentBytes, filename, contentType, userId, origin, mediaId) + if err != nil { + ds.DeleteObject(info.Location) // delete temp object + return nil, err + } + // We'll use the location from the first record record := records[0] if record.Quarantined { + ds.DeleteObject(info.Location) // delete temp object ctx.Log.Warn("User attempted to upload quarantined content - rejecting") return nil, common.ErrMediaQuarantined } @@ -276,6 +313,12 @@ func StoreDirect(f *AlreadyUploadedFile, contents io.ReadCloser, expectedSize in return nil, errors.New("file has no contents") } + err = checkSpam(contentBytes, filename, contentType, userId, origin, mediaId) + if err != nil { + ds.DeleteObject(info.Location) // delete temp object + return nil, err + } + ctx.Log.Info("Persisting new media record") media := &types.Media{ diff --git a/go.mod b/go.mod index eb34d640589d0b970cb17f5e1ad7f40b8d198b5f..97d8a8f28e9cfd90bfbe03eaab90d8e195d6d5c2 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,8 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/snappy v0.0.1 // indirect github.com/gorilla/mux v1.7.4 + github.com/hashicorp/go-hclog v0.14.1 + github.com/hashicorp/go-plugin v1.4.0 github.com/ipfs/go-cid v0.0.4 github.com/ipfs/go-ipfs v0.4.22-0.20191119151441-b8ec598d5801 github.com/ipfs/go-ipfs-config v0.0.11 diff --git a/go.sum b/go.sum index 0b9099b8d84a12be3638ddb8ee21f1d06edf9aff..63cc8fdb98d3c4665084b4e6bbba9e7b89125cf7 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,7 @@ github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8 h1:6muCmMJat6 github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4= github.com/elgris/jsondiff v0.0.0-20160530203242-765b5c24c302/go.mod h1:qBlWZqWeVx9BjvqBsnC/8RUlAYpIFmPvgROcw0n1scE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A= @@ -129,6 +130,7 @@ github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ= github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fd/go-nat v1.0.0/go.mod h1:BTBu/CKvMmOMUPkKVef1pngt2WFH/lg7E6yQnulfp6E= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= @@ -172,6 +174,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -217,13 +220,19 @@ github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQ github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.4.0 h1:b0O7rs5uiJ99Iu9HugEzsM67afboErkHUWddUSpUO3A= +github.com/hashicorp/go-plugin v1.4.0/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= 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.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v0.0.0-20180415215157-1395d1447324/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag= @@ -400,6 +409,8 @@ github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uY github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U= github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= @@ -662,10 +673,14 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -689,6 +704,8 @@ github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKU github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -737,6 +754,8 @@ github.com/multiformats/go-multistream v0.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wS github.com/multiformats/go-varint v0.0.1 h1:TR/0rdQtnNxuN2IhiB639xC3tWM4IUi7DkTBVTdGW/M= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U= github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -941,6 +960,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -994,6 +1014,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190926180325-855e68c8590b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1021,6 +1042,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1028,10 +1050,12 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/matrix/admin.go b/matrix/admin.go index ad6622238336cb8847753202381f42a17e028470..be399ab13264789deecf5125151687dc83a1fba0 100644 --- a/matrix/admin.go +++ b/matrix/admin.go @@ -4,6 +4,7 @@ import ( "time" "github.com/turt2live/matrix-media-repo/common/rcontext" + "github.com/turt2live/matrix-media-repo/util" ) func IsUserAdmin(ctx rcontext.RequestContext, serverName string, accessToken string, ipAddr string) (bool, error) { @@ -19,7 +20,7 @@ func IsUserAdmin(ctx rcontext.RequestContext, serverName string, accessToken str if hs.AdminApiKind == "synapse" { path = "/_synapse/admin/v1/whois/" } - url := makeUrl(hs.ClientServerApi, path, fakeUser) + url := util.MakeUrl(hs.ClientServerApi, path, fakeUser) err := doRequest(ctx, "GET", url, nil, response, accessToken, ipAddr) if err != nil { err, replyError = filterError(err) @@ -43,7 +44,7 @@ func ListMedia(ctx rcontext.RequestContext, serverName string, accessToken strin if hs.AdminApiKind == "synapse" { path = "/_synapse/admin/v1/room/" } - url := makeUrl(hs.ClientServerApi, path, roomId, "/media") + url := util.MakeUrl(hs.ClientServerApi, path, roomId, "/media") err := doRequest(ctx, "GET", url, nil, response, accessToken, ipAddr) if err != nil { err, replyError = filterError(err) diff --git a/matrix/auth.go b/matrix/auth.go index 2845c8933536e9a8b9d5b960b6a2232209bcc0a5..96f0704a53f88e2b3d878445f159441707eaa58d 100644 --- a/matrix/auth.go +++ b/matrix/auth.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/turt2live/matrix-media-repo/common/rcontext" + "github.com/turt2live/matrix-media-repo/util" ) var ErrInvalidToken = errors.New("Missing or invalid access token") @@ -25,7 +26,7 @@ func doBreakerRequest(ctx rcontext.RequestContext, serverName string, accessToke query["user_id"] = appserviceUserId } - target, _ := url.Parse(makeUrl(hs.ClientServerApi, path)) + target, _ := url.Parse(util.MakeUrl(hs.ClientServerApi, path)) q := target.Query() for k, v := range query { q.Set(k, v) diff --git a/matrix/client_server.go b/matrix/client_server.go index a9ef7e66716febbaae91b87f1b13ac9fd5d31534..7e20e62ab0dd977a5e16d35846fbf17be2f72975 100644 --- a/matrix/client_server.go +++ b/matrix/client_server.go @@ -72,17 +72,3 @@ func doRequest(ctx rcontext.RequestContext, method string, urlStr string, body i return nil } - -func makeUrl(parts ...string) string { - res := "" - for i, p := range parts { - if p[len(p)-1:] == "/" { - res += p[:len(p)-1] - } else if p[0] != '/' && i > 0 { - res += "/" + p - } else { - res += p - } - } - return res -} diff --git a/plugins/manager.go b/plugins/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..5b451c6d0dad984f8c4a4c349e3d5f6262a51c9e --- /dev/null +++ b/plugins/manager.go @@ -0,0 +1,64 @@ +package plugins + +import ( + "encoding/base64" + + "github.com/hashicorp/go-plugin" + "github.com/sirupsen/logrus" + "github.com/turt2live/matrix-media-repo/common/config" + "github.com/turt2live/matrix-media-repo/plugins/plugin_interfaces" +) + +var pluginTypes = map[string]plugin.Plugin{ + "antispam": &plugin_interfaces.AntispamPlugin{}, +} + + +var existingPlugins = make([]*mmrPlugin, 0) + +func ReloadPlugins() { + logrus.Info("Reloading plugins...") + + for _, pl := range config.Get().Plugins { + logrus.Info("Loading plugin: ", pl.Executable) + mmr, err := newPlugin(pl.Executable, pl.Config) + if err != nil { + logrus.Errorf("failed to load plugin %s: %s", pl.Executable, err.Error()) + continue + } + + existingPlugins = append(existingPlugins, mmr) + } +} + +func StopPlugins() { + if len(existingPlugins) == 0 { + return + } + + logrus.Info("Stopping plugin instances...") + for _, pl := range existingPlugins { + pl.Stop() + } + existingPlugins = make([]*mmrPlugin, 0) +} + +func CheckForSpam(contents []byte, filename string, contentType string, userId string, origin string, mediaId string) (bool, error) { + for _, pl := range existingPlugins { + as, err := pl.Antispam() + if err != nil { + logrus.Warnf("error loading antispam plugin: %s", err.Error()) + continue + } + + b64 := base64.StdEncoding.EncodeToString(contents) + spam, err := as.CheckForSpam(b64, filename, contentType, userId, origin, mediaId) + if err != nil { + return false, err + } + if spam { + return true, err + } + } + return false, nil +} diff --git a/plugins/mmr_plugin.go b/plugins/mmr_plugin.go new file mode 100644 index 0000000000000000000000000000000000000000..24bc040ea880133d884f11f68b84478049962041 --- /dev/null +++ b/plugins/mmr_plugin.go @@ -0,0 +1,63 @@ +package plugins + +import ( + "os/exec" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/sirupsen/logrus" + "github.com/turt2live/matrix-media-repo/plugins/plugin_common" + "github.com/turt2live/matrix-media-repo/plugins/plugin_interfaces" +) + +type mmrPlugin struct { + hcClient *plugin.Client + rpcClient plugin.ClientProtocol + config map[string]interface{} + + antispamPlugin plugin_interfaces.Antispam +} + +func newPlugin(path string, config map[string]interface{}) (*mmrPlugin, error) { + logger := hclog.New(&hclog.LoggerOptions{ + Name: "plugin", + Output: logrus.WithField("plugin", path).Writer(), + Level: hclog.Debug, + }) + client := plugin.NewClient(&plugin.ClientConfig{ + Cmd: exec.Command(path), + Logger: logger, + HandshakeConfig: plugin_common.HandshakeConfig, + Plugins: pluginTypes, + }) + rpcClient, err := client.Client() + if err != nil { + client.Kill() + return nil, err + } + return &mmrPlugin{ + hcClient: client, + rpcClient: rpcClient, + config: config, + }, nil +} + +func (p *mmrPlugin) Antispam() (plugin_interfaces.Antispam, error) { + if p.antispamPlugin != nil { + return p.antispamPlugin, nil + } + + raw, err := p.rpcClient.Dispense("antispam") + if err != nil { + return nil, err + } + + p.antispamPlugin = raw.(plugin_interfaces.Antispam) + p.antispamPlugin.HandleConfig(p.config) + return p.antispamPlugin, nil +} + +func (p *mmrPlugin) Stop() { + p.antispamPlugin = nil + p.hcClient.Kill() +} diff --git a/plugins/plugin_common/handshake.go b/plugins/plugin_common/handshake.go new file mode 100644 index 0000000000000000000000000000000000000000..c0278fd7cddb40d3a50e4a858fdd20da4e3bf6b1 --- /dev/null +++ b/plugins/plugin_common/handshake.go @@ -0,0 +1,12 @@ +package plugin_common + +import ( + "github.com/hashicorp/go-plugin" +) + +// UX, not security +var HandshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "MEDIA_REPO_PLUGIN", + MagicCookieValue: "hello world - I am a media repo", +} \ No newline at end of file diff --git a/plugins/plugin_interfaces/antispam.go b/plugins/plugin_interfaces/antispam.go new file mode 100644 index 0000000000000000000000000000000000000000..4c203ec9f7b29e34b43df0c17fd8860905575976 --- /dev/null +++ b/plugins/plugin_interfaces/antispam.go @@ -0,0 +1,78 @@ +package plugin_interfaces + +import ( + "encoding/json" + "net/rpc" + + "github.com/hashicorp/go-plugin" +) + +type Antispam interface { + HandleConfig(config map[string]interface{}) error + CheckForSpam(b64 string, filename string, contentType string, userId string, origin string, mediaId string) (bool, error) +} + +type AntispamRPC struct { + client *rpc.Client +} + +func (g *AntispamRPC) HandleConfig(config map[string]interface{}) error { + var i string + b, err := json.Marshal(config) + if err != nil { + return err + } + return g.client.Call("Plugin.HandleConfig", map[string]interface{}{"c": string(b)}, &i) +} + +func (g *AntispamRPC) CheckForSpam(b64 string, filename string, contentType string, userId string, origin string, mediaId string) (bool, error) { + var resp bool + err := g.client.Call("Plugin.CheckForSpam", map[string]interface{}{ + "b64": b64, + "filename": filename, + "contentType": contentType, + "userId": userId, + "origin": origin, + "mediaId": mediaId, + }, &resp) + return resp, err +} + +type AntispamRPCServer struct { + Impl Antispam +} + +func (s *AntispamRPCServer) HandleConfig(args map[string]interface{}, resp *string) error { + *resp = "not_used" + var conf map[string]interface{} + err := json.Unmarshal(([]byte)(args["c"].(string)), &conf) + if err != nil { + return err + } + return s.Impl.HandleConfig(conf) +} + +func (s *AntispamRPCServer) CheckForSpam(args map[string]interface{}, resp *bool) error { + var err error + *resp, err = s.Impl.CheckForSpam( + args["b64"].(string), + args["filename"].(string), + args["contentType"].(string), + args["userId"].(string), + args["origin"].(string), + args["mediaId"].(string), + ) + return err +} + +type AntispamPlugin struct { + Impl Antispam +} + +func (p *AntispamPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) { + return &AntispamRPCServer{Impl: p.Impl}, nil +} + +func (p *AntispamPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &AntispamRPC{client: c}, nil +} diff --git a/util/streams.go b/util/streams.go index 6c6c513a8a0e026de57e9d04c7ef69ac99ee6fd0..9cafc8ba5d38db9f14893367246149513db74347 100644 --- a/util/streams.go +++ b/util/streams.go @@ -16,6 +16,10 @@ func BufferToStream(buf *bytes.Buffer) io.ReadCloser { return ioutil.NopCloser(newBuf) } +func BytesToStream(b []byte) io.ReadCloser { + return ioutil.NopCloser(bytes.NewBuffer(b)) +} + func CloneReader(input io.ReadCloser, numReaders int) []io.ReadCloser { readers := make([]io.ReadCloser, 0) writers := make([]io.WriteCloser, 0) diff --git a/util/urls.go b/util/urls.go new file mode 100644 index 0000000000000000000000000000000000000000..7b02ede39041d4ecb7e30f20501de08f65b4de57 --- /dev/null +++ b/util/urls.go @@ -0,0 +1,15 @@ +package util + +func MakeUrl(parts ...string) string { + res := "" + for i, p := range parts { + if p[len(p)-1:] == "/" { + res += p[:len(p)-1] + } else if p[0] != '/' && i > 0 { + res += "/" + p + } else { + res += p + } + } + return res +} \ No newline at end of file