config.go 17.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Copyright 2017 Vector Creations Ltd
//
// 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 (
	"bytes"
	"crypto/sha256"
	"encoding/pem"
	"fmt"
22
	"io"
23 24 25 26
	"io/ioutil"
	"path/filepath"
	"strings"
	"time"
27

28
	"github.com/sirupsen/logrus"
29 30 31
	"github.com/matrix-org/gomatrixserverlib"
	"golang.org/x/crypto/ed25519"
	"gopkg.in/yaml.v2"
32 33 34

	jaegerconfig "github.com/uber/jaeger-client-go/config"
	jaegermetrics "github.com/uber/jaeger-lib/metrics"
35 36 37 38
)

// Version is the current version of the config format.
// This will change whenever we make breaking changes to the config format.
39
const Version = 0
40 41 42 43 44 45 46 47 48 49

// Dendrite contains all the config used by a dendrite process.
// Relative paths are resolved relative to the current working directory
type Dendrite struct {
	// The version of the configuration file.
	// If the version in a file doesn't match the current dendrite config
	// version then we can give a clear error message telling the user
	// to update their config file to the current version.
	// The version of the file should only be different if there has
	// been a breaking change to the config file format.
50
	Version int `yaml:"version"`
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

	// The configuration required for a matrix server.
	Matrix struct {
		// The name of the server. This is usually the domain name, e.g 'matrix.org', 'localhost'.
		ServerName gomatrixserverlib.ServerName `yaml:"server_name"`
		// Path to the private key which will be used to sign requests and events.
		PrivateKeyPath Path `yaml:"private_key"`
		// The private key which will be used to sign requests and events.
		PrivateKey ed25519.PrivateKey `yaml:"-"`
		// An arbitrary string used to uniquely identify the PrivateKey. Must start with the
		// prefix "ed25519:".
		KeyID gomatrixserverlib.KeyID `yaml:"-"`
		// List of paths to X509 certificates used by the external federation listeners.
		// These are used to calculate the TLS fingerprints to publish for this server.
		// Other matrix servers talking to this server will expect the x509 certificate
		// to match one of these certificates.
		// The certificates should be in PEM format.
		FederationCertificatePaths []Path `yaml:"federation_certificates"`
		// A list of SHA256 TLS fingerprints for the X509 certificates used by the
		// federation listener for this server.
		TLSFingerPrints []gomatrixserverlib.TLSFingerprint `yaml:"-"`
		// How long a remote server can cache our server key for before requesting it again.
		// Increasing this number will reduce the number of requests made by remote servers
		// for our key, but increases the period a compromised key will be considered valid
		// by remote servers.
		// Defaults to 24 hours.
		KeyValidityPeriod time.Duration `yaml:"key_validity_period"`
78 79 80 81
		// List of domains that the server will trust as identity servers to
		// verify third-party identifiers.
		// Defaults to an empty array.
		TrustedIDServers []string `yaml:"trusted_third_party_id_servers"`
82 83 84
		// If set, allows registration by anyone who also has the shared
		// secret, even if registration is otherwise disabled.
		RegistrationSharedSecret string `yaml:"registration_shared_secret"`
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
	} `yaml:"matrix"`

	// The configuration specific to the media repostitory.
	Media struct {
		// The base path to where the media files will be stored. May be relative or absolute.
		BasePath Path `yaml:"base_path"`
		// The absolute base path to where media files will be stored.
		AbsBasePath Path `yaml:"-"`
		// The maximum file size in bytes that is allowed to be stored on this server.
		// Note: if max_file_size_bytes is set to 0, the size is unlimited.
		// Note: if max_file_size_bytes is not set, it will default to 10485760 (10MB)
		MaxFileSizeBytes *FileSizeBytes `yaml:"max_file_size_bytes,omitempty"`
		// Whether to dynamically generate thumbnails on-the-fly if the requested resolution is not already generated
		DynamicThumbnails bool `yaml:"dynamic_thumbnails"`
		// The maximum number of simultaneous thumbnail generators. default: 10
		MaxThumbnailGenerators int `yaml:"max_thumbnail_generators"`
		// A list of thumbnail sizes to be pre-generated for downloaded remote / uploaded content
		ThumbnailSizes []ThumbnailSize `yaml:"thumbnail_sizes"`
	} `yaml:"media"`

	// The configuration for talking to kafka.
	Kafka struct {
		// A list of kafka addresses to connect to.
		Addresses []string `yaml:"addresses"`
109 110 111 112 113
		// Whether to use naffka instead of kafka.
		// Naffka can only be used when running dendrite as a single monolithic server.
		// Kafka can be used both with a monolithic server and when running the
		// components as separate servers.
		UseNaffka bool `yaml:"use_naffka,omitempty"`
114 115 116 117
		// The names of the topics to use when reading and writing from kafka.
		Topics struct {
			// Topic for roomserver/api.OutputRoomEvent events.
			OutputRoomEvent Topic `yaml:"output_room_event"`
118 119
			// Topic for sending account data from client API to sync API
			OutputClientData Topic `yaml:"output_client_data"`
B
Brendan Abolivier 已提交
120 121
			// Topic for user updates (profile, presence)
			UserUpdates Topic `yaml:"user_updates"`
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
		}
	} `yaml:"kafka"`

	// Postgres Config
	Database struct {
		// The Account database stores the login details and account information
		// for local users. It is accessed by the ClientAPI.
		Account DataSource `yaml:"account"`
		// The Device database stores session information for the devices of logged
		// in local users. It is accessed by the ClientAPI, the MediaAPI and the SyncAPI.
		Device DataSource `yaml:"device"`
		// The MediaAPI database stores information about files uploaded and downloaded
		// by local users. It is only accessed by the MediaAPI.
		MediaAPI DataSource `yaml:"media_api"`
		// The ServerKey database caches the public keys of remote servers.
		// It may be accessed by the FederationAPI, the ClientAPI, and the MediaAPI.
		ServerKey DataSource `yaml:"server_key"`
		// The SyncAPI stores information used by the SyncAPI server.
		// It is only accessed by the SyncAPI server.
		SyncAPI DataSource `yaml:"sync_api"`
		// The RoomServer database stores information about matrix rooms.
		// It is only accessed by the RoomServer.
		RoomServer DataSource `yaml:"room_server"`
145 146 147
		// The FederationSender database stores information used by the FederationSender
		// It is only accessed by the FederationSender.
		FederationSender DataSource `yaml:"federation_sender"`
148 149 150
		// The PublicRoomsAPI database stores information used to compute the public
		// room directory. It is only accessed by the PublicRoomsAPI server.
		PublicRoomsAPI DataSource `yaml:"public_rooms_api"`
151 152
	} `yaml:"database"`

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
	// TURN Server Config
	TURN struct {
		// TODO Guest Support
		// Whether or not guests can request TURN credentials
		//AllowGuests bool `yaml:"turn_allow_guests"`
		// How long the authorization should last
		UserLifetime string `yaml:"turn_user_lifetime"`
		// The list of TURN URIs to pass to clients
		URIs []string `yaml:"turn_uris"`

		// Authorization via Shared Secret
		// The shared secret from coturn
		SharedSecret string `yaml:"turn_shared_secret"`

		// Authorization via Static Username & Password
		// Hardcoded Username and Password
		Username string `yaml:"turn_username"`
		Password string `yaml:"turn_password"`
	}

173 174 175
	// The internal addresses the components will listen on.
	// These should not be exposed externally as they expose metrics and debugging APIs.
	Listen struct {
176 177 178 179 180 181
		MediaAPI         Address `yaml:"media_api"`
		ClientAPI        Address `yaml:"client_api"`
		FederationAPI    Address `yaml:"federation_api"`
		SyncAPI          Address `yaml:"sync_api"`
		RoomServer       Address `yaml:"room_server"`
		FederationSender Address `yaml:"federation_sender"`
182
		PublicRoomsAPI   Address `yaml:"public_rooms_api"`
183
	} `yaml:"listen"`
184 185 186 187 188 189

	// The config for tracing the dendrite servers.
	Tracing struct {
		// The config for the jaeger opentracing reporter.
		Jaeger jaegerconfig.Configuration `yaml:"jaeger"`
	}
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
}

// A Path on the filesystem.
type Path string

// A DataSource for opening a postgresql database using lib/pq.
type DataSource string

// A Topic in kafka.
type Topic string

// An Address to listen on.
type Address string

// FileSizeBytes is a file size in bytes
type FileSizeBytes int64

// ThumbnailSize contains a single thumbnail size configuration
type ThumbnailSize struct {
	// Maximum width of the thumbnail image
	Width int `yaml:"width"`
	// Maximum height of the thumbnail image
	Height int `yaml:"height"`
	// ResizeMethod is one of crop or scale.
	// crop scales to fill the requested dimensions and crops the excess.
	// scale scales to fit the requested dimensions and one dimension may be smaller than requested.
	ResizeMethod string `yaml:"method,omitempty"`
}

219 220 221 222
// Load a yaml config file for a server run as multiple processes.
// Checks the config to ensure that it is valid.
// The checks are different if the server is run as a monolithic process instead
// of being split into multiple components
223 224 225 226 227 228 229 230 231 232 233
func Load(configPath string) (*Dendrite, error) {
	configData, err := ioutil.ReadFile(configPath)
	if err != nil {
		return nil, err
	}
	basePath, err := filepath.Abs(".")
	if err != nil {
		return nil, err
	}
	// Pass the current working directory and ioutil.ReadFile so that they can
	// be mocked in the tests
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
	monolithic := false
	return loadConfig(basePath, configData, ioutil.ReadFile, monolithic)
}

// LoadMonolithic loads a yaml config file for a server run as a single monolith.
// Checks the config to ensure that it is valid.
// The checks are different if the server is run as a monolithic process instead
// of being split into multiple components
func LoadMonolithic(configPath string) (*Dendrite, error) {
	configData, err := ioutil.ReadFile(configPath)
	if err != nil {
		return nil, err
	}
	basePath, err := filepath.Abs(".")
	if err != nil {
		return nil, err
	}
	// Pass the current working directory and ioutil.ReadFile so that they can
	// be mocked in the tests
	monolithic := true
	return loadConfig(basePath, configData, ioutil.ReadFile, monolithic)
255 256 257 258 259 260 261 262 263 264 265 266
}

// An Error indicates a problem parsing the config.
type Error struct {
	// List of problems encountered parsing the config.
	Problems []string
}

func loadConfig(
	basePath string,
	configData []byte,
	readFile func(string) ([]byte, error),
267
	monolithic bool,
268 269 270 271 272 273 274 275 276
) (*Dendrite, error) {
	var config Dendrite
	var err error
	if err = yaml.Unmarshal(configData, &config); err != nil {
		return nil, err
	}

	config.setDefaults()

277
	if err = config.check(monolithic); err != nil {
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
		return nil, err
	}

	privateKeyPath := absPath(basePath, config.Matrix.PrivateKeyPath)
	privateKeyData, err := readFile(privateKeyPath)
	if err != nil {
		return nil, err
	}

	if config.Matrix.KeyID, config.Matrix.PrivateKey, err = readKeyPEM(privateKeyPath, privateKeyData); err != nil {
		return nil, err
	}

	for _, certPath := range config.Matrix.FederationCertificatePaths {
		absCertPath := absPath(basePath, certPath)
		pemData, err := readFile(absCertPath)
		if err != nil {
			return nil, err
		}
		fingerprint := fingerprintPEM(pemData)
		if fingerprint == nil {
			return nil, fmt.Errorf("no certificate PEM data in %q", absCertPath)
		}
		config.Matrix.TLSFingerPrints = append(config.Matrix.TLSFingerPrints, *fingerprint)
	}

	config.Media.AbsBasePath = Path(absPath(basePath, config.Media.BasePath))

	return &config, nil
}

func (config *Dendrite) setDefaults() {
	if config.Matrix.KeyValidityPeriod == 0 {
		config.Matrix.KeyValidityPeriod = 24 * time.Hour
	}

314 315 316 317
	if config.Matrix.TrustedIDServers == nil {
		config.Matrix.TrustedIDServers = []string{}
	}

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
	if config.Media.MaxThumbnailGenerators == 0 {
		config.Media.MaxThumbnailGenerators = 10
	}

	if config.Media.MaxFileSizeBytes == nil {
		defaultMaxFileSizeBytes := FileSizeBytes(10485760)
		config.Media.MaxFileSizeBytes = &defaultMaxFileSizeBytes
	}
}

func (e Error) Error() string {
	if len(e.Problems) == 1 {
		return e.Problems[0]
	}
	return fmt.Sprintf(
		"%s (and %d other problems)", e.Problems[0], len(e.Problems)-1,
	)
}

337
func (config *Dendrite) check(monolithic bool) error {
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
	var problems []string

	if config.Version != Version {
		return Error{[]string{fmt.Sprintf(
			"unknown config version %q, expected %q", config.Version, Version,
		)}}
	}

	checkNotEmpty := func(key string, value string) {
		if value == "" {
			problems = append(problems, fmt.Sprintf("missing config key %q", key))
		}
	}

	checkNotZero := func(key string, value int64) {
		if value == 0 {
			problems = append(problems, fmt.Sprintf("missing config key %q", key))
		}
	}

	checkPositive := func(key string, value int64) {
		if value < 0 {
			problems = append(problems, fmt.Sprintf("invalid value for config key %q: %d", key, value))
		}
	}

364 365 366 367 368 369
	checkValidDuration := func(key, value string) {
		if _, err := time.ParseDuration(config.TURN.UserLifetime); err != nil {
			problems = append(problems, fmt.Sprintf("invalid duration for config key %q: %s", key, value))
		}
	}

370 371 372 373
	checkNotEmpty("matrix.server_name", string(config.Matrix.ServerName))
	checkNotEmpty("matrix.private_key", string(config.Matrix.PrivateKeyPath))
	checkNotZero("matrix.federation_certificates", int64(len(config.Matrix.FederationCertificatePaths)))

374 375 376 377
	if config.TURN.UserLifetime != "" {
		checkValidDuration("turn.turn_user_lifetime", config.TURN.UserLifetime)
	}

378 379 380 381 382 383 384
	checkNotEmpty("media.base_path", string(config.Media.BasePath))
	checkPositive("media.max_file_size_bytes", int64(*config.Media.MaxFileSizeBytes))
	checkPositive("media.max_thumbnail_generators", int64(config.Media.MaxThumbnailGenerators))
	for i, size := range config.Media.ThumbnailSizes {
		checkPositive(fmt.Sprintf("media.thumbnail_sizes[%d].width", i), int64(size.Width))
		checkPositive(fmt.Sprintf("media.thumbnail_sizes[%d].height", i), int64(size.Height))
	}
385 386 387 388 389 390 391 392 393
	if config.Kafka.UseNaffka {
		if !monolithic {
			problems = append(problems, fmt.Sprintf("naffka can only be used in a monolithic server"))
		}
	} else {
		// If we aren't using naffka then we need to have at least one kafka
		// server to talk to.
		checkNotZero("kafka.addresses", int64(len(config.Kafka.Addresses)))
	}
394
	checkNotEmpty("kafka.topics.output_room_event", string(config.Kafka.Topics.OutputRoomEvent))
395
	checkNotEmpty("kafka.topics.output_client_data", string(config.Kafka.Topics.OutputClientData))
396
	checkNotEmpty("kafka.topics.user_updates", string(config.Kafka.Topics.UserUpdates))
397 398 399 400 401 402
	checkNotEmpty("database.account", string(config.Database.Account))
	checkNotEmpty("database.device", string(config.Database.Device))
	checkNotEmpty("database.server_key", string(config.Database.ServerKey))
	checkNotEmpty("database.media_api", string(config.Database.MediaAPI))
	checkNotEmpty("database.sync_api", string(config.Database.SyncAPI))
	checkNotEmpty("database.room_server", string(config.Database.RoomServer))
403 404 405 406 407 408 409 410

	if !monolithic {
		checkNotEmpty("listen.media_api", string(config.Listen.MediaAPI))
		checkNotEmpty("listen.client_api", string(config.Listen.ClientAPI))
		checkNotEmpty("listen.federation_api", string(config.Listen.FederationAPI))
		checkNotEmpty("listen.sync_api", string(config.Listen.SyncAPI))
		checkNotEmpty("listen.room_server", string(config.Listen.RoomServer))
	}
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459

	if problems != nil {
		return Error{problems}
	}

	return nil
}

func absPath(dir string, path Path) string {
	if filepath.IsAbs(string(path)) {
		// filepath.Join cleans the path so we should clean the absolute paths as well for consistency.
		return filepath.Clean(string(path))
	}
	return filepath.Join(dir, string(path))
}

func readKeyPEM(path string, data []byte) (gomatrixserverlib.KeyID, ed25519.PrivateKey, error) {
	for {
		var keyBlock *pem.Block
		keyBlock, data = pem.Decode(data)
		if data == nil {
			return "", nil, fmt.Errorf("no matrix private key PEM data in %q", path)
		}
		if keyBlock.Type == "MATRIX PRIVATE KEY" {
			keyID := keyBlock.Headers["Key-ID"]
			if keyID == "" {
				return "", nil, fmt.Errorf("missing key ID in PEM data in %q", path)
			}
			if !strings.HasPrefix(keyID, "ed25519:") {
				return "", nil, fmt.Errorf("key ID %q doesn't start with \"ed25519:\" in %q", keyID, path)
			}
			_, privKey, err := ed25519.GenerateKey(bytes.NewReader(keyBlock.Bytes))
			if err != nil {
				return "", nil, err
			}
			return gomatrixserverlib.KeyID(keyID), privKey, nil
		}
	}
}

func fingerprintPEM(data []byte) *gomatrixserverlib.TLSFingerprint {
	for {
		var certDERBlock *pem.Block
		certDERBlock, data = pem.Decode(data)
		if data == nil {
			return nil
		}
		if certDERBlock.Type == "CERTIFICATE" {
			digest := sha256.Sum256(certDERBlock.Bytes)
E
Erik Johnston 已提交
460
			return &gomatrixserverlib.TLSFingerprint{SHA256: digest[:]}
461 462 463
		}
	}
}
464 465 466 467 468 469 470 471 472

// RoomServerURL returns an HTTP URL for where the roomserver is listening.
func (config *Dendrite) RoomServerURL() string {
	// Hard code the roomserver to talk HTTP for now.
	// If we support HTTPS we need to think of a practical way to do certificate validation.
	// People setting up servers shouldn't need to get a certificate valid for the public
	// internet for an internal API.
	return "http://" + string(config.Listen.RoomServer)
}
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494

// SetupTracing configures the opentracing using the supplied configuration.
func (config *Dendrite) SetupTracing(serviceName string) (closer io.Closer, err error) {
	return config.Tracing.Jaeger.InitGlobalTracer(
		serviceName,
		jaegerconfig.Logger(logrusLogger{logrus.StandardLogger()}),
		jaegerconfig.Metrics(jaegermetrics.NullFactory),
	)
}

// logrusLogger is a small wrapper that implements jaeger.Logger using logrus.
type logrusLogger struct {
	l *logrus.Logger
}

func (l logrusLogger) Error(msg string) {
	l.l.Error(msg)
}

func (l logrusLogger) Infof(msg string, args ...interface{}) {
	l.l.Infof(msg, args...)
}