gotty/server/server.go

270 lines
7.6 KiB
Go
Raw Permalink Normal View History

2017-02-25 22:37:07 +00:00
package server
import (
"context"
"crypto/tls"
"crypto/x509"
"html/template"
"io/fs"
2017-02-25 22:37:07 +00:00
"log"
"net"
"net/http"
2023-10-21 15:49:19 +00:00
"os"
"regexp"
2022-08-27 07:42:49 +00:00
"strings"
2017-02-25 22:37:07 +00:00
noesctmpl "text/template"
2017-08-13 04:40:00 +00:00
"time"
2017-02-25 22:37:07 +00:00
2017-08-22 08:31:27 +00:00
"github.com/NYTimes/gziphandler"
2017-02-25 22:37:07 +00:00
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/sorenisanerd/gotty/bindata"
"github.com/sorenisanerd/gotty/pkg/homedir"
"github.com/sorenisanerd/gotty/pkg/randomstring"
"github.com/sorenisanerd/gotty/webtty"
2017-02-25 22:37:07 +00:00
)
// Server provides a webtty HTTP endpoint.
type Server struct {
factory Factory
options *Options
upgrader *websocket.Upgrader
indexTemplate *template.Template
titleTemplate *noesctmpl.Template
manifestTemplate *template.Template
2017-02-25 22:37:07 +00:00
}
// New creates a new instance of Server.
// Server will use the New() of the factory provided to handle each request.
func New(factory Factory, options *Options) (*Server, error) {
indexData, err := bindata.Fs.ReadFile("static/index.html")
2017-02-25 22:37:07 +00:00
if err != nil {
panic("index not found") // must be in bindata
}
if options.IndexFile != "" {
path := homedir.Expand(options.IndexFile)
2023-10-21 15:49:19 +00:00
indexData, err = os.ReadFile(path)
2017-02-25 22:37:07 +00:00
if err != nil {
return nil, errors.Wrapf(err, "failed to read custom index file at `%s`", path)
}
}
indexTemplate, err := template.New("index").Parse(string(indexData))
if err != nil {
panic("index template parse failed") // must be valid
}
manifestData, err := bindata.Fs.ReadFile("static/manifest.json")
if err != nil {
panic("manifest not found") // must be in bindata
}
manifestTemplate, err := template.New("manifest").Parse(string(manifestData))
if err != nil {
panic("manifest template parse failed") // must be valid
}
2017-02-25 22:37:07 +00:00
titleTemplate, err := noesctmpl.New("title").Parse(options.TitleFormat)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse window title format `%s`", options.TitleFormat)
}
var originChekcer func(r *http.Request) bool
if options.WSOrigin != "" {
matcher, err := regexp.Compile(options.WSOrigin)
if err != nil {
return nil, errors.Wrapf(err, "failed to compile regular expression of Websocket Origin: %s", options.WSOrigin)
}
originChekcer = func(r *http.Request) bool {
return matcher.MatchString(r.Header.Get("Origin"))
}
}
2017-02-25 22:37:07 +00:00
return &Server{
factory: factory,
options: options,
upgrader: &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
Subprotocols: webtty.Protocols,
CheckOrigin: originChekcer,
2017-02-25 22:37:07 +00:00
},
indexTemplate: indexTemplate,
titleTemplate: titleTemplate,
manifestTemplate: manifestTemplate,
2017-02-25 22:37:07 +00:00
}, nil
}
// Run starts the main process of the Server.
// The cancelation of ctx will shutdown the server immediately with aborting
// existing connections. Use WithGracefullContext() to support gracefull shutdown.
func (server *Server) Run(ctx context.Context, options ...RunOption) error {
cctx, cancel := context.WithCancel(ctx)
opts := &RunOptions{gracefullCtx: context.Background()}
for _, opt := range options {
opt(opts)
}
2017-08-13 04:40:00 +00:00
counter := newCounter(time.Duration(server.options.Timeout) * time.Second)
2021-04-19 09:53:24 +00:00
path := server.options.Path
if server.options.EnableRandomUrl {
path = "/" + randomstring.Generate(server.options.RandomUrlLength) + "/"
}
2022-08-22 04:59:12 +00:00
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
handlers := server.setupHandlers(cctx, cancel, path, counter)
srv, err := server.setupHTTPServer(handlers)
2017-02-25 22:37:07 +00:00
if err != nil {
return errors.Wrapf(err, "failed to setup an HTTP server")
}
if server.options.PermitWrite {
log.Printf("Permitting clients to write input to the PTY.")
}
if server.options.Once {
log.Printf("Once option is provided, accepting only one client")
}
if server.options.Port == "0" {
log.Printf("Port number configured to `0`, choosing a random port")
}
hostPort := net.JoinHostPort(server.options.Address, server.options.Port)
listener, err := net.Listen("tcp", hostPort)
if err != nil {
return errors.Wrapf(err, "failed to listen at `%s`", hostPort)
}
scheme := "http"
if server.options.EnableTLS {
scheme = "https"
}
host, port, _ := net.SplitHostPort(listener.Addr().String())
2019-12-30 12:55:36 +00:00
log.Printf("HTTP server is listening at: %s", scheme+"://"+net.JoinHostPort(host, port)+path)
if server.options.Address == "0.0.0.0" {
for _, address := range listAddresses() {
2019-12-30 12:55:36 +00:00
log.Printf("Alternative URL: %s", scheme+"://"+net.JoinHostPort(address, port)+path)
}
}
srvErr := make(chan error, 1)
2017-02-25 22:37:07 +00:00
go func() {
if server.options.EnableTLS {
crtFile := homedir.Expand(server.options.TLSCrtFile)
keyFile := homedir.Expand(server.options.TLSKeyFile)
log.Printf("TLS crt file: " + crtFile)
log.Printf("TLS key file: " + keyFile)
err = srv.ServeTLS(listener, crtFile, keyFile)
2017-02-25 22:37:07 +00:00
} else {
err = srv.Serve(listener)
2017-02-25 22:37:07 +00:00
}
if err != nil {
srvErr <- err
2017-02-25 22:37:07 +00:00
}
}()
go func() {
select {
case <-opts.gracefullCtx.Done():
srv.Shutdown(context.Background())
case <-cctx.Done():
}
}()
select {
case err = <-srvErr:
2017-02-25 22:37:07 +00:00
if err == http.ErrServerClosed { // by gracefull ctx
err = nil
} else {
cancel()
}
case <-cctx.Done():
srv.Close()
err = cctx.Err()
}
2017-08-13 04:40:00 +00:00
conn := counter.count()
2017-02-25 22:37:07 +00:00
if conn > 0 {
log.Printf("Waiting for %d connections to be closed", conn)
}
2017-08-13 04:40:00 +00:00
counter.wait()
2017-02-25 22:37:07 +00:00
return err
}
func (server *Server) setupHandlers(ctx context.Context, cancel context.CancelFunc, pathPrefix string, counter *counter) http.Handler {
fs, err := fs.Sub(bindata.Fs, "static")
if err != nil {
log.Fatalf("failed to open static/ subdirectory of embedded filesystem: %v", err)
}
staticFileHandler := http.FileServer(http.FS(fs))
2017-02-25 22:37:07 +00:00
var siteMux = http.NewServeMux()
siteMux.HandleFunc(pathPrefix, server.handleIndex)
siteMux.Handle(pathPrefix+"js/", http.StripPrefix(pathPrefix, staticFileHandler))
2021-04-25 09:25:41 +00:00
siteMux.Handle(pathPrefix+"favicon.ico", http.StripPrefix(pathPrefix, staticFileHandler))
siteMux.Handle(pathPrefix+"icon.svg", http.StripPrefix(pathPrefix, staticFileHandler))
siteMux.Handle(pathPrefix+"css/", http.StripPrefix(pathPrefix, staticFileHandler))
siteMux.Handle(pathPrefix+"icon_192.png", http.StripPrefix(pathPrefix, staticFileHandler))
siteMux.HandleFunc(pathPrefix+"manifest.json", server.handleManifest)
siteMux.HandleFunc(pathPrefix+"auth_token.js", server.handleAuthToken)
siteMux.HandleFunc(pathPrefix+"config.js", server.handleConfig)
2017-02-25 22:37:07 +00:00
siteHandler := http.Handler(siteMux)
if server.options.EnableBasicAuth {
log.Printf("Using Basic Authentication")
siteHandler = server.wrapBasicAuth(siteHandler, server.options.Credential)
}
2017-08-22 08:31:27 +00:00
withGz := gziphandler.GzipHandler(server.wrapHeaders(siteHandler))
siteHandler = server.wrapLogger(withGz)
2017-02-25 22:37:07 +00:00
wsMux := http.NewServeMux()
wsMux.Handle("/", siteHandler)
wsMux.HandleFunc(pathPrefix+"ws", server.generateHandleWS(ctx, cancel, counter))
2017-02-25 22:37:07 +00:00
siteHandler = http.Handler(wsMux)
2017-08-13 05:00:51 +00:00
return siteHandler
2017-02-25 22:37:07 +00:00
}
func (server *Server) setupHTTPServer(handler http.Handler) (*http.Server, error) {
2017-02-25 22:37:07 +00:00
srv := &http.Server{
Handler: handler,
}
if server.options.EnableTLSClientAuth {
tlsConfig, err := server.tlsConfig()
if err != nil {
return nil, errors.Wrapf(err, "failed to setup TLS configuration")
}
srv.TLSConfig = tlsConfig
}
return srv, nil
}
func (server *Server) tlsConfig() (*tls.Config, error) {
caFile := homedir.Expand(server.options.TLSCACrtFile)
2023-10-21 15:49:19 +00:00
caCert, err := os.ReadFile(caFile)
2017-02-25 22:37:07 +00:00
if err != nil {
return nil, errors.New("could not open CA crt file " + caFile)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, errors.New("could not parse CA crt file data in " + caFile)
}
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
return tlsConfig, nil
}