1// Copyright 2017 Andrew Morgan <andrew@amorgan.xyz> 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package config 16 17import ( 18 "fmt" 19 "io/ioutil" 20 "path/filepath" 21 "regexp" 22 "strings" 23 24 log "github.com/sirupsen/logrus" 25 yaml "gopkg.in/yaml.v2" 26) 27 28type AppServiceAPI struct { 29 Matrix *Global `yaml:"-"` 30 Derived *Derived `yaml:"-"` // TODO: Nuke Derived from orbit 31 32 InternalAPI InternalAPIOptions `yaml:"internal_api"` 33 34 Database DatabaseOptions `yaml:"database"` 35 36 // DisableTLSValidation disables the validation of X.509 TLS certs 37 // on appservice endpoints. This is not recommended in production! 38 DisableTLSValidation bool `yaml:"disable_tls_validation"` 39 40 ConfigFiles []string `yaml:"config_files"` 41} 42 43func (c *AppServiceAPI) Defaults() { 44 c.InternalAPI.Listen = "http://localhost:7777" 45 c.InternalAPI.Connect = "http://localhost:7777" 46 c.Database.Defaults(5) 47 c.Database.ConnectionString = "file:appservice.db" 48} 49 50func (c *AppServiceAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { 51 checkURL(configErrs, "app_service_api.internal_api.listen", string(c.InternalAPI.Listen)) 52 checkURL(configErrs, "app_service_api.internal_api.bind", string(c.InternalAPI.Connect)) 53 checkNotEmpty(configErrs, "app_service_api.database.connection_string", string(c.Database.ConnectionString)) 54} 55 56// ApplicationServiceNamespace is the namespace that a specific application 57// service has management over. 58type ApplicationServiceNamespace struct { 59 // Whether or not the namespace is managed solely by this application service 60 Exclusive bool `yaml:"exclusive"` 61 // A regex pattern that represents the namespace 62 Regex string `yaml:"regex"` 63 // The ID of an existing group that all users of this application service will 64 // be added to. This field is only relevant to the `users` namespace. 65 // Note that users who are joined to this group through an application service 66 // are not to be listed when querying for the group's members, however the 67 // group should be listed when querying an application service user's groups. 68 // This is to prevent making spamming all users of an application service 69 // trivial. 70 GroupID string `yaml:"group_id"` 71 // Regex object representing our pattern. Saves having to recompile every time 72 RegexpObject *regexp.Regexp 73} 74 75// ApplicationService represents a Matrix application service. 76// https://matrix.org/docs/spec/application_service/unstable.html 77type ApplicationService struct { 78 // User-defined, unique, persistent ID of the application service 79 ID string `yaml:"id"` 80 // Base URL of the application service 81 URL string `yaml:"url"` 82 // Application service token provided in requests to a homeserver 83 ASToken string `yaml:"as_token"` 84 // Homeserver token provided in requests to an application service 85 HSToken string `yaml:"hs_token"` 86 // Localpart of application service user 87 SenderLocalpart string `yaml:"sender_localpart"` 88 // Information about an application service's namespaces. Key is either 89 // "users", "aliases" or "rooms" 90 NamespaceMap map[string][]ApplicationServiceNamespace `yaml:"namespaces"` 91 // Whether rate limiting is applied to each application service user 92 RateLimited bool `yaml:"rate_limited"` 93 // Any custom protocols that this application service provides (e.g. IRC) 94 Protocols []string `yaml:"protocols"` 95} 96 97// IsInterestedInRoomID returns a bool on whether an application service's 98// namespace includes the given room ID 99func (a *ApplicationService) IsInterestedInRoomID( 100 roomID string, 101) bool { 102 if namespaceSlice, ok := a.NamespaceMap["rooms"]; ok { 103 for _, namespace := range namespaceSlice { 104 if namespace.RegexpObject != nil && namespace.RegexpObject.MatchString(roomID) { 105 return true 106 } 107 } 108 } 109 110 return false 111} 112 113// IsInterestedInUserID returns a bool on whether an application service's 114// namespace includes the given user ID 115func (a *ApplicationService) IsInterestedInUserID( 116 userID string, 117) bool { 118 if namespaceSlice, ok := a.NamespaceMap["users"]; ok { 119 for _, namespace := range namespaceSlice { 120 if namespace.RegexpObject.MatchString(userID) { 121 return true 122 } 123 } 124 } 125 126 return false 127} 128 129// OwnsNamespaceCoveringUserId returns a bool on whether an application service's 130// namespace is exclusive and includes the given user ID 131func (a *ApplicationService) OwnsNamespaceCoveringUserId( 132 userID string, 133) bool { 134 if namespaceSlice, ok := a.NamespaceMap["users"]; ok { 135 for _, namespace := range namespaceSlice { 136 if namespace.Exclusive && namespace.RegexpObject.MatchString(userID) { 137 return true 138 } 139 } 140 } 141 142 return false 143} 144 145// IsInterestedInRoomAlias returns a bool on whether an application service's 146// namespace includes the given room alias 147func (a *ApplicationService) IsInterestedInRoomAlias( 148 roomAlias string, 149) bool { 150 if namespaceSlice, ok := a.NamespaceMap["aliases"]; ok { 151 for _, namespace := range namespaceSlice { 152 if namespace.RegexpObject.MatchString(roomAlias) { 153 return true 154 } 155 } 156 } 157 158 return false 159} 160 161// loadAppServices iterates through all application service config files 162// and loads their data into the config object for later access. 163func loadAppServices(config *AppServiceAPI, derived *Derived) error { 164 for _, configPath := range config.ConfigFiles { 165 // Create a new application service with default options 166 appservice := ApplicationService{ 167 RateLimited: true, 168 } 169 170 // Create an absolute path from a potentially relative path 171 absPath, err := filepath.Abs(configPath) 172 if err != nil { 173 return err 174 } 175 176 // Read the application service's config file 177 configData, err := ioutil.ReadFile(absPath) 178 if err != nil { 179 return err 180 } 181 182 // Load the config data into our struct 183 if err = yaml.UnmarshalStrict(configData, &appservice); err != nil { 184 return err 185 } 186 187 // Append the parsed application service to the global config 188 derived.ApplicationServices = append( 189 derived.ApplicationServices, appservice, 190 ) 191 } 192 193 // Check for any errors in the loaded application services 194 return checkErrors(config, derived) 195} 196 197// setupRegexps will create regex objects for exclusive and non-exclusive 198// usernames, aliases and rooms of all application services, so that other 199// methods can quickly check if a particular string matches any of them. 200func setupRegexps(asAPI *AppServiceAPI, derived *Derived) (err error) { 201 // Combine all exclusive namespaces for later string checking 202 var exclusiveUsernameStrings, exclusiveAliasStrings []string 203 204 // If an application service's regex is marked as exclusive, add 205 // its contents to the overall exlusive regex string. Room regex 206 // not necessary as we aren't denying exclusive room ID creation 207 for _, appservice := range derived.ApplicationServices { 208 // The sender_localpart can be considered an exclusive regex for a single user, so let's do that 209 // to simplify the code 210 var senderUserIDSlice = []string{fmt.Sprintf("@%s:%s", appservice.SenderLocalpart, asAPI.Matrix.ServerName)} 211 usersSlice, found := appservice.NamespaceMap["users"] 212 if !found { 213 usersSlice = []ApplicationServiceNamespace{} 214 appservice.NamespaceMap["users"] = usersSlice 215 } 216 appendExclusiveNamespaceRegexs(&senderUserIDSlice, usersSlice) 217 218 for key, namespaceSlice := range appservice.NamespaceMap { 219 switch key { 220 case "users": 221 appendExclusiveNamespaceRegexs(&exclusiveUsernameStrings, namespaceSlice) 222 case "aliases": 223 appendExclusiveNamespaceRegexs(&exclusiveAliasStrings, namespaceSlice) 224 } 225 226 if err = compileNamespaceRegexes(namespaceSlice); err != nil { 227 return fmt.Errorf("invalid regex in appservice %q, namespace %q: %w", appservice.ID, key, err) 228 } 229 } 230 } 231 232 // Join the regexes together into one big regex. 233 // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)" 234 // Later we can check if a username or alias matches any exclusive regex and 235 // deny access if it isn't from an application service 236 exclusiveUsernames := strings.Join(exclusiveUsernameStrings, "|") 237 exclusiveAliases := strings.Join(exclusiveAliasStrings, "|") 238 239 // If there are no exclusive regexes, compile string so that it will not match 240 // any valid usernames/aliases/roomIDs 241 if exclusiveUsernames == "" { 242 exclusiveUsernames = "^$" 243 } 244 if exclusiveAliases == "" { 245 exclusiveAliases = "^$" 246 } 247 248 // Store compiled Regex 249 if derived.ExclusiveApplicationServicesUsernameRegexp, err = regexp.Compile(exclusiveUsernames); err != nil { 250 return err 251 } 252 if derived.ExclusiveApplicationServicesAliasRegexp, err = regexp.Compile(exclusiveAliases); err != nil { 253 return err 254 } 255 256 return nil 257} 258 259// appendExclusiveNamespaceRegexs takes a slice of strings and a slice of 260// namespaces and will append the regexes of only the exclusive namespaces 261// into the string slice 262func appendExclusiveNamespaceRegexs( 263 exclusiveStrings *[]string, namespaces []ApplicationServiceNamespace, 264) { 265 for _, namespace := range namespaces { 266 if namespace.Exclusive { 267 // We append parenthesis to later separate each regex when we compile 268 // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)" 269 *exclusiveStrings = append(*exclusiveStrings, "("+namespace.Regex+")") 270 } 271 } 272} 273 274// compileNamespaceRegexes turns strings into regex objects and complains 275// if some of there are bad 276func compileNamespaceRegexes(namespaces []ApplicationServiceNamespace) (err error) { 277 for index, namespace := range namespaces { 278 // Compile this regex into a Regexp object for later use 279 r, err := regexp.Compile(namespace.Regex) 280 if err != nil { 281 return fmt.Errorf("regex at namespace %d: %w", index, err) 282 } 283 284 namespaces[index].RegexpObject = r 285 } 286 287 return nil 288} 289 290// checkErrors checks for any configuration errors amongst the loaded 291// application services according to the application service spec. 292func checkErrors(config *AppServiceAPI, derived *Derived) (err error) { 293 var idMap = make(map[string]bool) 294 var tokenMap = make(map[string]bool) 295 296 // Compile regexp object for checking groupIDs 297 groupIDRegexp := regexp.MustCompile(`\+.*:.*`) 298 299 // Check each application service for any config errors 300 for _, appservice := range derived.ApplicationServices { 301 // Namespace-related checks 302 for key, namespaceSlice := range appservice.NamespaceMap { 303 for _, namespace := range namespaceSlice { 304 if err := validateNamespace(&appservice, key, &namespace, groupIDRegexp); err != nil { 305 return err 306 } 307 } 308 } 309 310 // Check if the url has trailing /'s. If so, remove them 311 appservice.URL = strings.TrimRight(appservice.URL, "/") 312 313 // Check if we've already seen this ID. No two application services 314 // can have the same ID or token. 315 if idMap[appservice.ID] { 316 return ConfigErrors([]string{fmt.Sprintf( 317 "Application service ID %s must be unique", appservice.ID, 318 )}) 319 } 320 // Check if we've already seen this token 321 if tokenMap[appservice.ASToken] { 322 return ConfigErrors([]string{fmt.Sprintf( 323 "Application service Token %s must be unique", appservice.ASToken, 324 )}) 325 } 326 327 // Add the id/token to their respective maps if we haven't already 328 // seen them. 329 idMap[appservice.ID] = true 330 tokenMap[appservice.ASToken] = true 331 332 // TODO: Remove once rate_limited is implemented 333 if appservice.RateLimited { 334 log.Warn("WARNING: Application service option rate_limited is currently unimplemented") 335 } 336 // TODO: Remove once protocols is implemented 337 if len(appservice.Protocols) > 0 { 338 log.Warn("WARNING: Application service option protocols is currently unimplemented") 339 } 340 } 341 342 return setupRegexps(config, derived) 343} 344 345// validateNamespace returns nil or an error based on whether a given 346// application service namespace is valid. A namespace is valid if it has the 347// required fields, and its regex is correct. 348func validateNamespace( 349 appservice *ApplicationService, 350 key string, 351 namespace *ApplicationServiceNamespace, 352 groupIDRegexp *regexp.Regexp, 353) error { 354 // Check that namespace(s) are valid regex 355 if !IsValidRegex(namespace.Regex) { 356 return ConfigErrors([]string{fmt.Sprintf( 357 "Invalid regex string for Application Service %s", appservice.ID, 358 )}) 359 } 360 361 // Check if GroupID for the users namespace is in the correct format 362 if key == "users" && namespace.GroupID != "" { 363 // TODO: Remove once group_id is implemented 364 log.Warn("WARNING: Application service option group_id is currently unimplemented") 365 366 correctFormat := groupIDRegexp.MatchString(namespace.GroupID) 367 if !correctFormat { 368 return ConfigErrors([]string{fmt.Sprintf( 369 "Invalid user group_id field for application service %s.", 370 appservice.ID, 371 )}) 372 } 373 } 374 375 return nil 376} 377 378// IsValidRegex returns true or false based on whether the 379// given string is valid regex or not 380func IsValidRegex(regexString string) bool { 381 _, err := regexp.Compile(regexString) 382 383 return err == nil 384} 385