aboutsummaryrefslogtreecommitdiff
path: root/setup/config/config_appservice.go
diff options
context:
space:
mode:
Diffstat (limited to 'setup/config/config_appservice.go')
-rw-r--r--setup/config/config_appservice.go353
1 files changed, 353 insertions, 0 deletions
diff --git a/setup/config/config_appservice.go b/setup/config/config_appservice.go
new file mode 100644
index 00000000..a042691d
--- /dev/null
+++ b/setup/config/config_appservice.go
@@ -0,0 +1,353 @@
+// Copyright 2017 Andrew Morgan <andrew@amorgan.xyz>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ yaml "gopkg.in/yaml.v2"
+)
+
+type AppServiceAPI struct {
+ Matrix *Global `yaml:"-"`
+ Derived *Derived `yaml:"-"` // TODO: Nuke Derived from orbit
+
+ InternalAPI InternalAPIOptions `yaml:"internal_api"`
+
+ Database DatabaseOptions `yaml:"database"`
+
+ ConfigFiles []string `yaml:"config_files"`
+}
+
+func (c *AppServiceAPI) Defaults() {
+ c.InternalAPI.Listen = "http://localhost:7777"
+ c.InternalAPI.Connect = "http://localhost:7777"
+ c.Database.Defaults()
+ c.Database.ConnectionString = "file:appservice.db"
+}
+
+func (c *AppServiceAPI) Verify(configErrs *ConfigErrors, isMonolith bool) {
+ checkURL(configErrs, "app_service_api.internal_api.listen", string(c.InternalAPI.Listen))
+ checkURL(configErrs, "app_service_api.internal_api.bind", string(c.InternalAPI.Connect))
+ checkNotEmpty(configErrs, "app_service_api.database.connection_string", string(c.Database.ConnectionString))
+}
+
+// ApplicationServiceNamespace is the namespace that a specific application
+// service has management over.
+type ApplicationServiceNamespace struct {
+ // Whether or not the namespace is managed solely by this application service
+ Exclusive bool `yaml:"exclusive"`
+ // A regex pattern that represents the namespace
+ Regex string `yaml:"regex"`
+ // The ID of an existing group that all users of this application service will
+ // be added to. This field is only relevant to the `users` namespace.
+ // Note that users who are joined to this group through an application service
+ // are not to be listed when querying for the group's members, however the
+ // group should be listed when querying an application service user's groups.
+ // This is to prevent making spamming all users of an application service
+ // trivial.
+ GroupID string `yaml:"group_id"`
+ // Regex object representing our pattern. Saves having to recompile every time
+ RegexpObject *regexp.Regexp
+}
+
+// ApplicationService represents a Matrix application service.
+// https://matrix.org/docs/spec/application_service/unstable.html
+type ApplicationService struct {
+ // User-defined, unique, persistent ID of the application service
+ ID string `yaml:"id"`
+ // Base URL of the application service
+ URL string `yaml:"url"`
+ // Application service token provided in requests to a homeserver
+ ASToken string `yaml:"as_token"`
+ // Homeserver token provided in requests to an application service
+ HSToken string `yaml:"hs_token"`
+ // Localpart of application service user
+ SenderLocalpart string `yaml:"sender_localpart"`
+ // Information about an application service's namespaces. Key is either
+ // "users", "aliases" or "rooms"
+ NamespaceMap map[string][]ApplicationServiceNamespace `yaml:"namespaces"`
+ // Whether rate limiting is applied to each application service user
+ RateLimited bool `yaml:"rate_limited"`
+ // Any custom protocols that this application service provides (e.g. IRC)
+ Protocols []string `yaml:"protocols"`
+}
+
+// IsInterestedInRoomID returns a bool on whether an application service's
+// namespace includes the given room ID
+func (a *ApplicationService) IsInterestedInRoomID(
+ roomID string,
+) bool {
+ if namespaceSlice, ok := a.NamespaceMap["rooms"]; ok {
+ for _, namespace := range namespaceSlice {
+ if namespace.RegexpObject.MatchString(roomID) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// IsInterestedInUserID returns a bool on whether an application service's
+// namespace includes the given user ID
+func (a *ApplicationService) IsInterestedInUserID(
+ userID string,
+) bool {
+ if namespaceSlice, ok := a.NamespaceMap["users"]; ok {
+ for _, namespace := range namespaceSlice {
+ if namespace.RegexpObject.MatchString(userID) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// OwnsNamespaceCoveringUserId returns a bool on whether an application service's
+// namespace is exclusive and includes the given user ID
+func (a *ApplicationService) OwnsNamespaceCoveringUserId(
+ userID string,
+) bool {
+ if namespaceSlice, ok := a.NamespaceMap["users"]; ok {
+ for _, namespace := range namespaceSlice {
+ if namespace.Exclusive && namespace.RegexpObject.MatchString(userID) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// IsInterestedInRoomAlias returns a bool on whether an application service's
+// namespace includes the given room alias
+func (a *ApplicationService) IsInterestedInRoomAlias(
+ roomAlias string,
+) bool {
+ if namespaceSlice, ok := a.NamespaceMap["aliases"]; ok {
+ for _, namespace := range namespaceSlice {
+ if namespace.RegexpObject.MatchString(roomAlias) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// loadAppServices iterates through all application service config files
+// and loads their data into the config object for later access.
+func loadAppServices(config *AppServiceAPI, derived *Derived) error {
+ for _, configPath := range config.ConfigFiles {
+ // Create a new application service with default options
+ appservice := ApplicationService{
+ RateLimited: true,
+ }
+
+ // Create an absolute path from a potentially relative path
+ absPath, err := filepath.Abs(configPath)
+ if err != nil {
+ return err
+ }
+
+ // Read the application service's config file
+ configData, err := ioutil.ReadFile(absPath)
+ if err != nil {
+ return err
+ }
+
+ // Load the config data into our struct
+ if err = yaml.UnmarshalStrict(configData, &appservice); err != nil {
+ return err
+ }
+
+ // Append the parsed application service to the global config
+ derived.ApplicationServices = append(
+ derived.ApplicationServices, appservice,
+ )
+ }
+
+ // Check for any errors in the loaded application services
+ return checkErrors(config, derived)
+}
+
+// setupRegexps will create regex objects for exclusive and non-exclusive
+// usernames, aliases and rooms of all application services, so that other
+// methods can quickly check if a particular string matches any of them.
+func setupRegexps(_ *AppServiceAPI, derived *Derived) (err error) {
+ // Combine all exclusive namespaces for later string checking
+ var exclusiveUsernameStrings, exclusiveAliasStrings []string
+
+ // If an application service's regex is marked as exclusive, add
+ // its contents to the overall exlusive regex string. Room regex
+ // not necessary as we aren't denying exclusive room ID creation
+ for _, appservice := range derived.ApplicationServices {
+ for key, namespaceSlice := range appservice.NamespaceMap {
+ switch key {
+ case "users":
+ appendExclusiveNamespaceRegexs(&exclusiveUsernameStrings, namespaceSlice)
+ case "aliases":
+ appendExclusiveNamespaceRegexs(&exclusiveAliasStrings, namespaceSlice)
+ }
+ }
+ }
+
+ // Join the regexes together into one big regex.
+ // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)"
+ // Later we can check if a username or alias matches any exclusive regex and
+ // deny access if it isn't from an application service
+ exclusiveUsernames := strings.Join(exclusiveUsernameStrings, "|")
+ exclusiveAliases := strings.Join(exclusiveAliasStrings, "|")
+
+ // If there are no exclusive regexes, compile string so that it will not match
+ // any valid usernames/aliases/roomIDs
+ if exclusiveUsernames == "" {
+ exclusiveUsernames = "^$"
+ }
+ if exclusiveAliases == "" {
+ exclusiveAliases = "^$"
+ }
+
+ // Store compiled Regex
+ if derived.ExclusiveApplicationServicesUsernameRegexp, err = regexp.Compile(exclusiveUsernames); err != nil {
+ return err
+ }
+ if derived.ExclusiveApplicationServicesAliasRegexp, err = regexp.Compile(exclusiveAliases); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// appendExclusiveNamespaceRegexs takes a slice of strings and a slice of
+// namespaces and will append the regexes of only the exclusive namespaces
+// into the string slice
+func appendExclusiveNamespaceRegexs(
+ exclusiveStrings *[]string, namespaces []ApplicationServiceNamespace,
+) {
+ for index, namespace := range namespaces {
+ if namespace.Exclusive {
+ // We append parenthesis to later separate each regex when we compile
+ // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)"
+ *exclusiveStrings = append(*exclusiveStrings, "("+namespace.Regex+")")
+ }
+
+ // Compile this regex into a Regexp object for later use
+ namespaces[index].RegexpObject, _ = regexp.Compile(namespace.Regex)
+ }
+}
+
+// checkErrors checks for any configuration errors amongst the loaded
+// application services according to the application service spec.
+func checkErrors(config *AppServiceAPI, derived *Derived) (err error) {
+ var idMap = make(map[string]bool)
+ var tokenMap = make(map[string]bool)
+
+ // Compile regexp object for checking groupIDs
+ groupIDRegexp := regexp.MustCompile(`\+.*:.*`)
+
+ // Check each application service for any config errors
+ for _, appservice := range derived.ApplicationServices {
+ // Namespace-related checks
+ for key, namespaceSlice := range appservice.NamespaceMap {
+ for _, namespace := range namespaceSlice {
+ if err := validateNamespace(&appservice, key, &namespace, groupIDRegexp); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Check if the url has trailing /'s. If so, remove them
+ appservice.URL = strings.TrimRight(appservice.URL, "/")
+
+ // Check if we've already seen this ID. No two application services
+ // can have the same ID or token.
+ if idMap[appservice.ID] {
+ return ConfigErrors([]string{fmt.Sprintf(
+ "Application service ID %s must be unique", appservice.ID,
+ )})
+ }
+ // Check if we've already seen this token
+ if tokenMap[appservice.ASToken] {
+ return ConfigErrors([]string{fmt.Sprintf(
+ "Application service Token %s must be unique", appservice.ASToken,
+ )})
+ }
+
+ // Add the id/token to their respective maps if we haven't already
+ // seen them.
+ idMap[appservice.ID] = true
+ tokenMap[appservice.ASToken] = true
+
+ // TODO: Remove once rate_limited is implemented
+ if appservice.RateLimited {
+ log.Warn("WARNING: Application service option rate_limited is currently unimplemented")
+ }
+ // TODO: Remove once protocols is implemented
+ if len(appservice.Protocols) > 0 {
+ log.Warn("WARNING: Application service option protocols is currently unimplemented")
+ }
+ }
+
+ return setupRegexps(config, derived)
+}
+
+// validateNamespace returns nil or an error based on whether a given
+// application service namespace is valid. A namespace is valid if it has the
+// required fields, and its regex is correct.
+func validateNamespace(
+ appservice *ApplicationService,
+ key string,
+ namespace *ApplicationServiceNamespace,
+ groupIDRegexp *regexp.Regexp,
+) error {
+ // Check that namespace(s) are valid regex
+ if !IsValidRegex(namespace.Regex) {
+ return ConfigErrors([]string{fmt.Sprintf(
+ "Invalid regex string for Application Service %s", appservice.ID,
+ )})
+ }
+
+ // Check if GroupID for the users namespace is in the correct format
+ if key == "users" && namespace.GroupID != "" {
+ // TODO: Remove once group_id is implemented
+ log.Warn("WARNING: Application service option group_id is currently unimplemented")
+
+ correctFormat := groupIDRegexp.MatchString(namespace.GroupID)
+ if !correctFormat {
+ return ConfigErrors([]string{fmt.Sprintf(
+ "Invalid user group_id field for application service %s.",
+ appservice.ID,
+ )})
+ }
+ }
+
+ return nil
+}
+
+// IsValidRegex returns true or false based on whether the
+// given string is valid regex or not
+func IsValidRegex(regexString string) bool {
+ _, err := regexp.Compile(regexString)
+
+ return err == nil
+}