mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
287 lines
7.1 KiB
Go
287 lines
7.1 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
l4g "github.com/alecthomas/log4go"
|
|
"github.com/gorilla/handlers"
|
|
"github.com/gorilla/mux"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/acme/autocert"
|
|
|
|
"github.com/mattermost/mattermost-server/model"
|
|
"github.com/mattermost/mattermost-server/store"
|
|
"github.com/mattermost/mattermost-server/utils"
|
|
)
|
|
|
|
type Server struct {
|
|
Store store.Store
|
|
WebSocketRouter *WebSocketRouter
|
|
Router *mux.Router
|
|
Server *http.Server
|
|
ListenAddr *net.TCPAddr
|
|
RateLimiter *RateLimiter
|
|
|
|
didFinishListen chan struct{}
|
|
}
|
|
|
|
var allowedMethods []string = []string{
|
|
"POST",
|
|
"GET",
|
|
"OPTIONS",
|
|
"PUT",
|
|
"PATCH",
|
|
"DELETE",
|
|
}
|
|
|
|
type RecoveryLogger struct {
|
|
}
|
|
|
|
func (rl *RecoveryLogger) Println(i ...interface{}) {
|
|
l4g.Error("Please check the std error output for the stack trace")
|
|
l4g.Error(i)
|
|
}
|
|
|
|
type CorsWrapper struct {
|
|
config model.ConfigFunc
|
|
router *mux.Router
|
|
}
|
|
|
|
func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if allowed := *cw.config().ServiceSettings.AllowCorsFrom; allowed != "" {
|
|
if utils.CheckOrigin(r, allowed) {
|
|
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.Header().Set(
|
|
"Access-Control-Allow-Methods",
|
|
strings.Join(allowedMethods, ", "))
|
|
|
|
w.Header().Set(
|
|
"Access-Control-Allow-Headers",
|
|
r.Header.Get("Access-Control-Request-Headers"))
|
|
}
|
|
}
|
|
}
|
|
|
|
if r.Method == "OPTIONS" {
|
|
return
|
|
}
|
|
|
|
cw.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
|
|
|
|
type VaryBy struct {
|
|
useIP bool
|
|
useAuth bool
|
|
}
|
|
|
|
func (m *VaryBy) Key(r *http.Request) string {
|
|
key := ""
|
|
|
|
if m.useAuth {
|
|
token, tokenLocation := ParseAuthTokenFromRequest(r)
|
|
if tokenLocation != TokenLocationNotFound {
|
|
key += token
|
|
} else if m.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
|
|
key += utils.GetIpAddress(r)
|
|
}
|
|
} else if m.useIP { // Only if Auth based is not enabed do we use a plain IP based
|
|
key = utils.GetIpAddress(r)
|
|
}
|
|
|
|
return key
|
|
}
|
|
|
|
func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) {
|
|
if r.Host == "" {
|
|
http.Error(w, "Not Found", http.StatusNotFound)
|
|
}
|
|
|
|
url := r.URL
|
|
url.Host = r.Host
|
|
url.Scheme = "https"
|
|
http.Redirect(w, r, url.String(), http.StatusFound)
|
|
}
|
|
|
|
func (a *App) StartServer() error {
|
|
l4g.Info(utils.T("api.server.start_server.starting.info"))
|
|
|
|
var handler http.Handler = &CorsWrapper{a.Config, a.Srv.Router}
|
|
|
|
if *a.Config().RateLimitSettings.Enable {
|
|
l4g.Info(utils.T("api.server.start_server.rate.info"))
|
|
|
|
rateLimiter, err := NewRateLimiter(&a.Config().RateLimitSettings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.Srv.RateLimiter = rateLimiter
|
|
handler = rateLimiter.RateLimitHandler(handler)
|
|
}
|
|
|
|
a.Srv.Server = &http.Server{
|
|
Handler: handlers.RecoveryHandler(handlers.RecoveryLogger(&RecoveryLogger{}), handlers.PrintRecoveryStack(true))(handler),
|
|
ReadTimeout: time.Duration(*a.Config().ServiceSettings.ReadTimeout) * time.Second,
|
|
WriteTimeout: time.Duration(*a.Config().ServiceSettings.WriteTimeout) * time.Second,
|
|
}
|
|
|
|
addr := *a.Config().ServiceSettings.ListenAddress
|
|
if addr == "" {
|
|
if *a.Config().ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
|
|
addr = ":https"
|
|
} else {
|
|
addr = ":http"
|
|
}
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
errors.Wrapf(err, utils.T("api.server.start_server.starting.critical"), err)
|
|
return err
|
|
}
|
|
a.Srv.ListenAddr = listener.Addr().(*net.TCPAddr)
|
|
|
|
l4g.Info(utils.T("api.server.start_server.listening.info"), listener.Addr().String())
|
|
|
|
// Migration from old let's encrypt library
|
|
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
|
if stat, err := os.Stat(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile); err == nil && !stat.IsDir() {
|
|
os.Remove(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile)
|
|
}
|
|
}
|
|
|
|
m := &autocert.Manager{
|
|
Cache: autocert.DirCache(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile),
|
|
Prompt: autocert.AcceptTOS,
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.Forward80To443 {
|
|
if host, _, err := net.SplitHostPort(addr); err != nil {
|
|
l4g.Error("Unable to setup forwarding: " + err.Error())
|
|
} else {
|
|
httpListenAddress := net.JoinHostPort(host, "http")
|
|
|
|
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
|
go http.ListenAndServe(httpListenAddress, m.HTTPHandler(nil))
|
|
} else {
|
|
go func() {
|
|
redirectListener, err := net.Listen("tcp", httpListenAddress)
|
|
if err != nil {
|
|
l4g.Error("Unable to setup forwarding: " + err.Error())
|
|
return
|
|
}
|
|
defer redirectListener.Close()
|
|
|
|
http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
a.Srv.didFinishListen = make(chan struct{})
|
|
go func() {
|
|
var err error
|
|
if *a.Config().ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
|
|
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
|
|
|
tlsConfig := &tls.Config{
|
|
GetCertificate: m.GetCertificate,
|
|
}
|
|
|
|
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
|
|
|
|
a.Srv.Server.TLSConfig = tlsConfig
|
|
err = a.Srv.Server.ServeTLS(listener, "", "")
|
|
} else {
|
|
err = a.Srv.Server.ServeTLS(listener, *a.Config().ServiceSettings.TLSCertFile, *a.Config().ServiceSettings.TLSKeyFile)
|
|
}
|
|
} else {
|
|
err = a.Srv.Server.Serve(listener)
|
|
}
|
|
if err != nil && err != http.ErrServerClosed {
|
|
l4g.Critical(utils.T("api.server.start_server.starting.critical"), err)
|
|
time.Sleep(time.Second)
|
|
}
|
|
close(a.Srv.didFinishListen)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
type tcpKeepAliveListener struct {
|
|
*net.TCPListener
|
|
}
|
|
|
|
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
|
tc, err := ln.AcceptTCP()
|
|
if err != nil {
|
|
return
|
|
}
|
|
tc.SetKeepAlive(true)
|
|
tc.SetKeepAlivePeriod(3 * time.Minute)
|
|
return tc, nil
|
|
}
|
|
|
|
func (a *App) Listen(addr string) (net.Listener, error) {
|
|
if addr == "" {
|
|
addr = ":http"
|
|
}
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil
|
|
}
|
|
|
|
func (a *App) StopServer() {
|
|
if a.Srv.Server != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN)
|
|
defer cancel()
|
|
didShutdown := false
|
|
for a.Srv.didFinishListen != nil && !didShutdown {
|
|
if err := a.Srv.Server.Shutdown(ctx); err != nil {
|
|
l4g.Warn(err.Error())
|
|
}
|
|
timer := time.NewTimer(time.Millisecond * 50)
|
|
select {
|
|
case <-a.Srv.didFinishListen:
|
|
didShutdown = true
|
|
case <-timer.C:
|
|
}
|
|
timer.Stop()
|
|
}
|
|
a.Srv.Server.Close()
|
|
a.Srv.Server = nil
|
|
}
|
|
}
|
|
|
|
func (a *App) OriginChecker() func(*http.Request) bool {
|
|
if allowed := *a.Config().ServiceSettings.AllowCorsFrom; allowed != "" {
|
|
return utils.OriginChecker(allowed)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// This is required to re-use the underlying connection and not take up file descriptors
|
|
func consumeAndClose(r *http.Response) {
|
|
if r.Body != nil {
|
|
io.Copy(ioutil.Discard, r.Body)
|
|
r.Body.Close()
|
|
}
|
|
}
|