diff --git a/dendrite-config.yaml b/dendrite-config.yaml index cb768a106f3be621b93b3fa6f5c768d63201b3a3..9baba8f9483204277b0b0318df80a04e106173c0 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -54,6 +54,24 @@ media: height: 600 method: scale +# The config for the TURN server +turn: + # Whether or not guests can request TURN credentials + turn_allow_guests: true + # How long the authorization should last + turn_user_lifetime: "1h" + # The list of TURN URIs to pass to clients + turn_uris: [] + + # Authorization via Shared Secret + # The shared secret from coturn + turn_shared_secret: "" + + # Authorization via Static Username & Password + # Hardcoded Username and Password + turn_username: "" + turn_password: "" + # The config for communicating with kafka kafka: # Where the kafka servers are running. diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index ebf48ad6ecf851b8f2e82627872c41b4e3900982..87f52ad09990ef11db4c6eb701159dca4a465eff 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -284,12 +284,8 @@ func Setup( ).Methods("PUT", "OPTIONS") r0mux.Handle("/voip/turnServer", - common.MakeExternalAPI("turn_server", func(req *http.Request) util.JSONResponse { - // TODO: Return credentials for a turn server if one is configured. - return util.JSONResponse{ - Code: 200, - JSON: struct{}{}, - } + common.MakeAuthAPI("turn_server", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { + return RequestTurnServer(req, device, cfg) }), ).Methods("GET") diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/voip.go b/src/github.com/matrix-org/dendrite/clientapi/routing/voip.go new file mode 100644 index 0000000000000000000000000000000000000000..e699a91f28a1a6576e2abfa3950092d6ca75c96c --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/voip.go @@ -0,0 +1,85 @@ +// Copyright 2017 Michael Telatysnki <7t3chguy@gmail.com> +// +// 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 routing + +import ( + "net/http" + + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "time" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/util" + "github.com/matrix-org/dendrite/clientapi/httputil" +) + +type turnServerResponse struct { + Username string `json:"username"` + Password string `json:"password"` + URIs []string `json:"uris"` + TTL int `json:"ttl"` +} + +// RequestTurnServer implements: +// GET /voip/turnServer +func RequestTurnServer(req *http.Request, device *authtypes.Device, cfg config.Dendrite) util.JSONResponse { + turnConfig := cfg.TURN + + // TODO Guest Support + if len(turnConfig.URIs) == 0 || turnConfig.UserLifetime == "" { + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } + } + + // Duration checked at startup, err not possible + duration, _ := time.ParseDuration(turnConfig.UserLifetime) + + resp := turnServerResponse{ + URIs: turnConfig.URIs, + TTL: int(duration.Seconds()), + } + + if turnConfig.SharedSecret != "" { + expiry := time.Now().Add(duration).Unix() + mac := hmac.New(sha1.New, []byte(turnConfig.SharedSecret)) + _, err := mac.Write([]byte(resp.Username)) + + if err != nil { + return httputil.LogThenError(req, err) + } + + resp.Username = fmt.Sprintf("%d:%s", expiry, device.UserID) + resp.Password = base64.StdEncoding.EncodeToString(mac.Sum(nil)) + } else if turnConfig.Username != "" && turnConfig.Password != "" { + resp.Username = turnConfig.Username + resp.Password = turnConfig.Password + } else { + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } + } + + return util.JSONResponse{ + Code: 200, + JSON: resp, + } +} diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index 8f80aa2bc0b27cbd90b77bb11dd3f4585daf20b3..82bdc3dcad40005838113b03b4ead74770d594c5 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -150,6 +150,26 @@ type Dendrite struct { PublicRoomsAPI DataSource `yaml:"public_rooms_api"` } `yaml:"database"` + // 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"` + } + // The internal addresses the components will listen on. // These should not be exposed externally as they expose metrics and debugging APIs. Listen struct { @@ -341,10 +361,20 @@ func (config *Dendrite) check(monolithic bool) error { } } + 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)) + } + } + 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))) + if config.TURN.UserLifetime != "" { + checkValidDuration("turn.turn_user_lifetime", config.TURN.UserLifetime) + } + 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))