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