You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
15 KiB
506 lines
15 KiB
/* |
|
* Spreed WebRTC. |
|
* Copyright (C) 2013-2014 struktur AG |
|
* |
|
* This file is part of Spreed WebRTC. |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
*/ |
|
|
|
package main |
|
|
|
import ( |
|
"app/spreed-webrtc-server/sleepy" |
|
"bytes" |
|
"crypto/rand" |
|
"encoding/hex" |
|
"flag" |
|
"fmt" |
|
"github.com/gorilla/mux" |
|
"github.com/strukturag/goacceptlanguageparser" |
|
"github.com/strukturag/httputils" |
|
"github.com/strukturag/phoenix" |
|
"html/template" |
|
"log" |
|
"net/http" |
|
_ "net/http/pprof" |
|
"os" |
|
"path" |
|
goruntime "runtime" |
|
"strconv" |
|
"strings" |
|
"syscall" |
|
"time" |
|
) |
|
|
|
var version = "unreleased" |
|
var defaultConfig = "./server.conf" |
|
|
|
var templates *template.Template |
|
var config *Config |
|
|
|
// Helper to retrieve languages from request. |
|
func getRequestLanguages(r *http.Request, supportedLanguages []string) []string { |
|
|
|
acceptLanguageHeader, ok := r.Header["Accept-Language"] |
|
var langs []string |
|
if ok { |
|
langs = goacceptlanguageparser.ParseAcceptLanguage(acceptLanguageHeader[0], supportedLanguages) |
|
} |
|
return langs |
|
|
|
} |
|
|
|
// Helper function to clean up string arrays. |
|
func trimAndRemoveDuplicates(data *[]string) { |
|
found := make(map[string]bool) |
|
j := 0 |
|
for i, x := range *data { |
|
x = strings.TrimSpace(x) |
|
if len(x) > 0 && !found[x] { |
|
found[x] = true |
|
(*data)[j] = (*data)[i] |
|
j++ |
|
} |
|
} |
|
*data = (*data)[:j] |
|
} |
|
|
|
func mainHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
handleRoomView("", w, r) |
|
|
|
} |
|
|
|
func roomHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
vars := mux.Vars(r) |
|
handleRoomView(vars["room"], w, r) |
|
|
|
} |
|
|
|
func makeImageHandler(buddyImages ImageCache, expires time.Duration) http.HandlerFunc { |
|
|
|
return func(w http.ResponseWriter, r *http.Request) { |
|
|
|
vars := mux.Vars(r) |
|
image := buddyImages.Get(vars["imageid"]) |
|
if image == nil { |
|
http.Error(w, "Unknown image", http.StatusNotFound) |
|
return |
|
} |
|
|
|
w.Header().Set("Content-Type", image.mimetype) |
|
w.Header().Set("ETag", image.lastChangeId) |
|
age := time.Now().Sub(image.lastChange) |
|
if age >= time.Second { |
|
w.Header().Set("Age", strconv.Itoa(int(age.Seconds()))) |
|
} |
|
if expires >= time.Second { |
|
w.Header().Set("Expires", time.Now().Add(expires).Format(time.RFC1123)) |
|
w.Header().Set("Cache-Control", "public, no-transform, max-age="+strconv.Itoa(int(expires.Seconds()))) |
|
} |
|
http.ServeContent(w, r, "", image.lastChange, bytes.NewReader(image.data)) |
|
} |
|
|
|
} |
|
|
|
func handleRoomView(room string, w http.ResponseWriter, r *http.Request) { |
|
|
|
var err error |
|
|
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8") |
|
w.Header().Set("Expires", "-1") |
|
w.Header().Set("Cache-Control", "private, max-age=0") |
|
|
|
scheme := "http" |
|
|
|
// Detect if the request was made with SSL. |
|
ssl := r.TLS != nil |
|
proto, ok := r.Header["X-Forwarded-Proto"] |
|
if ok { |
|
ssl = proto[0] == "https" |
|
scheme = "https" |
|
} |
|
|
|
// Get languages from request. |
|
langs := getRequestLanguages(r, []string{}) |
|
if len(langs) == 0 { |
|
langs = append(langs, "en") |
|
} |
|
|
|
// Prepare context to deliver to HTML.. |
|
context := &Context{Cfg: config, App: "main", Host: r.Host, Scheme: scheme, Ssl: ssl, Languages: langs, Room: room} |
|
|
|
// Get URL parameters. |
|
r.ParseForm() |
|
|
|
// Check if incoming request is a crawler which supports AJAX crawling. |
|
// See https://developers.google.com/webmasters/ajax-crawling/docs/getting-started for details. |
|
if _, ok := r.Form["_escaped_fragment_"]; ok { |
|
// Render crawlerPage template.. |
|
err = templates.ExecuteTemplate(w, "crawlerPage", &context) |
|
} else { |
|
// Render mainPage template. |
|
err = templates.ExecuteTemplate(w, "mainPage", &context) |
|
} |
|
|
|
if err != nil { |
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
} |
|
|
|
} |
|
|
|
func runner(runtime phoenix.Runtime) error { |
|
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds) |
|
|
|
rootFolder, err := runtime.GetString("http", "root") |
|
if err != nil { |
|
cwd, err2 := os.Getwd() |
|
if err2 != nil { |
|
return fmt.Errorf("Error while getting current directory: %s", err) |
|
} |
|
rootFolder = cwd |
|
} |
|
|
|
if !httputils.HasDirPath(rootFolder) { |
|
return fmt.Errorf("Configured root '%s' is not a directory.", rootFolder) |
|
} |
|
|
|
if !httputils.HasFilePath(path.Join(rootFolder, "static", "css", "main.min.css")) { |
|
return fmt.Errorf("Unable to find client. Path correct and compiled css?") |
|
} |
|
|
|
// Read base path from config and make sure it ends with a slash. |
|
basePath, err := runtime.GetString("http", "basePath") |
|
if err != nil { |
|
basePath = "/" |
|
} else { |
|
if !strings.HasSuffix(basePath, "/") { |
|
basePath = fmt.Sprintf("%s/", basePath) |
|
} |
|
log.Printf("Using '%s' base base path.", basePath) |
|
} |
|
|
|
statsEnabled, err := runtime.GetBool("http", "stats") |
|
if err != nil { |
|
statsEnabled = false |
|
} |
|
|
|
pprofListen, err := runtime.GetString("http", "pprofListen") |
|
if err == nil && pprofListen != "" { |
|
log.Printf("Starting pprof HTTP server on %s", pprofListen) |
|
go func() { |
|
log.Println(http.ListenAndServe(pprofListen, nil)) |
|
}() |
|
} |
|
|
|
var sessionSecret []byte |
|
sessionSecretString, err := runtime.GetString("app", "sessionSecret") |
|
if err != nil { |
|
return fmt.Errorf("No sessionSecret in config file.") |
|
} else { |
|
sessionSecret, err = hex.DecodeString(sessionSecretString) |
|
if err != nil { |
|
log.Println("Warning: sessionSecret value is not a hex encoded", err) |
|
sessionSecret = []byte(sessionSecretString) |
|
} |
|
if len(sessionSecret) < 32 { |
|
return fmt.Errorf("Length of sessionSecret must be at least 32 bytes.") |
|
} |
|
} |
|
|
|
if len(sessionSecret) < 32 { |
|
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(sessionSecret)) |
|
} |
|
|
|
var encryptionSecret []byte |
|
encryptionSecretString, err := runtime.GetString("app", "encryptionSecret") |
|
if err != nil { |
|
return fmt.Errorf("No encryptionSecret in config file.") |
|
} else { |
|
encryptionSecret, err = hex.DecodeString(encryptionSecretString) |
|
if err != nil { |
|
log.Println("Warning: encryptionSecret value is not a hex encoded", err) |
|
encryptionSecret = []byte(encryptionSecretString) |
|
} |
|
switch l := len(encryptionSecret); { |
|
case l == 16: |
|
case l == 24: |
|
case l == 32: |
|
default: |
|
return fmt.Errorf("Length of encryptionSecret must be exactly 16, 24 or 32 bytes to select AES-128, AES-192 or AES-256.") |
|
} |
|
} |
|
|
|
tokenFile, err := runtime.GetString("app", "tokenFile") |
|
if err == nil { |
|
if !httputils.HasFilePath(path.Clean(tokenFile)) { |
|
return fmt.Errorf("Unable to find token file at %s", tokenFile) |
|
} |
|
} |
|
|
|
title, err := runtime.GetString("app", "title") |
|
if err != nil { |
|
title = "Spreed WebRTC" |
|
} |
|
|
|
ver, err := runtime.GetString("app", "ver") |
|
if err != nil { |
|
ver = "" |
|
} |
|
|
|
runtimeVersion := version |
|
if version != "unreleased" { |
|
ver1 := ver |
|
if err != nil { |
|
ver1 = "" |
|
} |
|
ver = fmt.Sprintf("%s%s", ver1, strings.Replace(version, ".", "", -1)) |
|
} else { |
|
ts := fmt.Sprintf("%d", time.Now().Unix()) |
|
if err != nil { |
|
ver = ts |
|
} |
|
runtimeVersion = fmt.Sprintf("unreleased.%s", ts) |
|
} |
|
|
|
turnURIsString, err := runtime.GetString("app", "turnURIs") |
|
if err != nil { |
|
turnURIsString = "" |
|
} |
|
turnURIs := strings.Split(turnURIsString, " ") |
|
trimAndRemoveDuplicates(&turnURIs) |
|
|
|
var turnSecret []byte |
|
turnSecretString, err := runtime.GetString("app", "turnSecret") |
|
if err == nil { |
|
turnSecret = []byte(turnSecretString) |
|
} |
|
|
|
stunURIsString, err := runtime.GetString("app", "stunURIs") |
|
if err != nil { |
|
stunURIsString = "" |
|
} |
|
stunURIs := strings.Split(stunURIsString, " ") |
|
trimAndRemoveDuplicates(&stunURIs) |
|
|
|
globalRoomid, err := runtime.GetString("app", "globalRoom") |
|
if err != nil { |
|
// Global room is disabled. |
|
globalRoomid = "" |
|
} |
|
|
|
plugin, err := runtime.GetString("app", "plugin") |
|
if err != nil { |
|
plugin = "" |
|
} |
|
|
|
defaultRoomEnabled := true |
|
defaultRoomEnabledString, err := runtime.GetString("app", "defaultRoomEnabled") |
|
if err == nil { |
|
defaultRoomEnabled = defaultRoomEnabledString == "true" |
|
} |
|
|
|
usersEnabled := false |
|
usersEnabledString, err := runtime.GetString("users", "enabled") |
|
if err == nil { |
|
usersEnabled = usersEnabledString == "true" |
|
} |
|
|
|
usersAllowRegistration := false |
|
usersAllowRegistrationString, err := runtime.GetString("users", "allowRegistration") |
|
if err == nil { |
|
usersAllowRegistration = usersAllowRegistrationString == "true" |
|
} |
|
|
|
serverToken, err := runtime.GetString("app", "serverToken") |
|
if err != nil { |
|
//TODO(longsleep): When we have a database, generate this once from random source and store it. |
|
serverToken = "i-did-not-change-the-public-token-boo" |
|
} |
|
|
|
serverRealm, err := runtime.GetString("app", "serverRealm") |
|
if err != nil { |
|
serverRealm = "local" |
|
} |
|
|
|
usersMode, _ := runtime.GetString("users", "mode") |
|
|
|
// Create token provider. |
|
var tokenProvider TokenProvider |
|
if tokenFile != "" { |
|
log.Printf("Using token authorization from %s\n", tokenFile) |
|
tokenProvider = TokenFileProvider(tokenFile) |
|
} |
|
|
|
// Create configuration data structure. |
|
config = NewConfig(title, ver, runtimeVersion, basePath, serverToken, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, usersEnabled, usersAllowRegistration, usersMode, plugin) |
|
|
|
// Load templates. |
|
tt := template.New("") |
|
tt.Delims("<%", "%>") |
|
|
|
templates, err = tt.ParseGlob(path.Join(rootFolder, "html", "*.html")) |
|
if err != nil { |
|
return fmt.Errorf("Failed to load templates: %s", err) |
|
} |
|
|
|
// Load extra templates folder |
|
extraFolder, err := runtime.GetString("app", "extra") |
|
if err == nil { |
|
if !httputils.HasDirPath(extraFolder) { |
|
return fmt.Errorf("Configured extra '%s' is not a directory.", extraFolder) |
|
} |
|
templates, err = templates.ParseGlob(path.Join(extraFolder, "*.html")) |
|
if err != nil { |
|
return fmt.Errorf("Failed to load extra templates: %s", err) |
|
} |
|
log.Printf("Loaded extra templates from: %s", extraFolder) |
|
} |
|
|
|
// Create realm string from config. |
|
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken) |
|
|
|
// Set number of go routines if it is 1 |
|
if goruntime.GOMAXPROCS(0) == 1 { |
|
nCPU := goruntime.NumCPU() |
|
goruntime.GOMAXPROCS(nCPU) |
|
log.Printf("Using the number of CPU's (%d) as GOMAXPROCS\n", nCPU) |
|
} |
|
|
|
// Get current number of max open files. |
|
var rLimit syscall.Rlimit |
|
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) |
|
if err != nil { |
|
log.Println("Error getting max numer of open files", err) |
|
} else { |
|
log.Printf("Max open files are %d\n", rLimit.Max) |
|
} |
|
|
|
// Try to increase number of file open files. This only works as root. |
|
maxfd, err := runtime.GetInt("http", "maxfd") |
|
if err == nil { |
|
rLimit.Max = uint64(maxfd) |
|
rLimit.Cur = uint64(maxfd) |
|
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) |
|
if err != nil { |
|
log.Println("Error setting max open files", err) |
|
} else { |
|
log.Printf("Set max open files successfully to %d\n", uint64(maxfd)) |
|
} |
|
} |
|
|
|
// Create router. |
|
router := mux.NewRouter() |
|
r := router.PathPrefix(basePath).Subrouter().StrictSlash(true) |
|
|
|
// HTTP listener support. |
|
if _, err = runtime.GetString("http", "listen"); err == nil { |
|
runtime.DefaultHTTPHandler(r) |
|
} |
|
|
|
// Native HTTPS listener support. |
|
if _, err = runtime.GetString("https", "listen"); err == nil { |
|
// Setup TLS. |
|
tlsConfig, err := runtime.TLSConfig() |
|
if err != nil { |
|
return fmt.Errorf("TLS configuration error: %s", err) |
|
} |
|
// Explicitly set random to use. |
|
tlsConfig.Rand = rand.Reader |
|
log.Println("Native TLS configuration intialized") |
|
runtime.DefaultHTTPSHandler(r) |
|
} |
|
|
|
// Add handlers. |
|
buddyImages := NewImageCache() |
|
codec := NewCodec() |
|
roomManager := NewRoomManager(config, codec) |
|
hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec) |
|
tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm) |
|
sessionManager := NewSessionManager(config, tickets, sessionSecret) |
|
statsManager := NewStatsManager(hub, roomManager, sessionManager) |
|
channellingAPI := NewChannellingAPI(runtimeVersion, config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, roomManager, buddyImages) |
|
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler)) |
|
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(buddyImages, time.Duration(24)*time.Hour))) |
|
r.Handle("/static/{path:.*}", http.StripPrefix(basePath, httputils.FileStaticServer(http.Dir(rootFolder)))) |
|
r.Handle("/robots.txt", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static"))))) |
|
r.Handle("/favicon.ico", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img"))))) |
|
r.Handle("/ws", makeWSHandler(statsManager, sessionManager, codec, channellingAPI)) |
|
r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler)) |
|
|
|
// Add API end points. |
|
api := sleepy.NewAPI() |
|
api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) |
|
api.AddResource(&Rooms{}, "/rooms") |
|
api.AddResource(config, "/config") |
|
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") |
|
if usersEnabled { |
|
// Create Users handler. |
|
users := NewUsers(hub, tickets, sessionManager, usersMode, serverRealm, runtime) |
|
api.AddResource(&Sessions{tickets, hub, users}, "/sessions/{id}/") |
|
if usersAllowRegistration { |
|
api.AddResource(users, "/users") |
|
} |
|
} |
|
if statsEnabled { |
|
api.AddResourceWithWrapper(&Stats{statsManager}, httputils.MakeGzipHandler, "/stats") |
|
log.Println("Stats are enabled!") |
|
} |
|
|
|
// Add extra/static support if configured and exists. |
|
if extraFolder != "" { |
|
extraFolderStatic := path.Join(extraFolder, "static") |
|
if _, err = os.Stat(extraFolderStatic); err == nil { |
|
r.Handle("/extra/static/{path:.*}", http.StripPrefix(fmt.Sprintf("%sextra", basePath), httputils.FileStaticServer(http.Dir(extraFolder)))) |
|
log.Printf("Added URL handler /extra/static/... for static files in %s/...\n", extraFolderStatic) |
|
} |
|
} |
|
|
|
return runtime.Start() |
|
} |
|
|
|
func boot() error { |
|
configPath := flag.String("c", defaultConfig, "Configuration file.") |
|
logPath := flag.String("l", "", "Log file, defaults to stderr.") |
|
showVersion := flag.Bool("v", false, "Display version number and exit.") |
|
memprofile := flag.String("memprofile", "", "Write memory profile to this file.") |
|
cpuprofile := flag.String("cpuprofile", "", "Write cpu profile to file.") |
|
showHelp := flag.Bool("h", false, "Show this usage information and exit.") |
|
flag.Parse() |
|
|
|
if *showHelp { |
|
flag.Usage() |
|
return nil |
|
} else if *showVersion { |
|
fmt.Printf("Version %s\n", version) |
|
return nil |
|
} |
|
|
|
return phoenix.NewServer("server", ""). |
|
Config(configPath). |
|
Log(logPath). |
|
CpuProfile(cpuprofile). |
|
MemProfile(memprofile). |
|
Run(runner) |
|
} |
|
|
|
func main() { |
|
if boot() != nil { |
|
os.Exit(-1) |
|
} |
|
}
|
|
|