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.
365 lines
10 KiB
365 lines
10 KiB
/* |
|
* Spreed Speak Freely. |
|
* Copyright (C) 2013-2014 struktur AG |
|
* |
|
* This file is part of Spreed Speak Freely. |
|
* |
|
* 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-speakfreely-server/sleepy" |
|
"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" |
|
"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, supported_languages []string) []string { |
|
|
|
accept_language_header, ok := r.Header["Accept-Language"] |
|
var langs []string |
|
if ok { |
|
langs = goacceptlanguageparser.ParseAcceptLanguage(accept_language_header[0], supported_languages) |
|
} |
|
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 handleRoomView(room string, w http.ResponseWriter, r *http.Request) { |
|
|
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8") |
|
w.Header().Set("Expires", "-1") |
|
w.Header().Set("Cache-Control", "private, max-age=0") |
|
|
|
// Detect if the request was made with SSL. |
|
ssl := false |
|
proto, ok := r.Header["X-Forwarded-Proto"] |
|
if ok { |
|
ssl = proto[0] == "https" |
|
} |
|
|
|
// Get languages from request. |
|
// TODO(longsleep): Added supported and default language to configuration. |
|
langs := getRequestLanguages(r, []string{"en", "de"}) |
|
if len(langs) == 0 { |
|
langs = append(langs, "en") |
|
} |
|
|
|
// Prepare context to deliver to Javascript. |
|
context := &Context{Cfg: config, App: "main", Host: r.Host, Ssl: ssl, Languages: langs} |
|
|
|
// Render the 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)) |
|
}() |
|
} |
|
|
|
sessionSecret, err := runtime.GetString("app", "sessionSecret") |
|
if err != nil { |
|
return fmt.Errorf("No sessionSecret in config file.") |
|
} |
|
|
|
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 Speak Freely" |
|
} |
|
|
|
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) |
|
|
|
turnSecret, err := runtime.GetString("app", "turnSecret") |
|
if err != nil { |
|
turnSecret = "" |
|
} |
|
|
|
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" |
|
} |
|
|
|
// 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, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, 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) |
|
} else { |
|
log.Printf("Loaded extra templates from: %s", extraFolder) |
|
} |
|
} |
|
|
|
// Create our hub instance. |
|
hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) |
|
|
|
// 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) |
|
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler)) |
|
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", makeWsHubHandler(hub)) |
|
r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler)) |
|
|
|
// Add API end points. |
|
api := sleepy.NewAPI(r.PathPrefix("/api/v1/").Subrouter()) |
|
api.AddResource(&Rooms{}, "/rooms") |
|
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") |
|
if statsEnabled { |
|
api.AddResourceWithWrapper(&Stats{hub: hub}, 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) |
|
} |
|
} |
|
|
|
runtime.DefaultHTTPHandler(r) |
|
|
|
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) |
|
} |
|
}
|
|
|