func (app *App) makeServer(addr string, handler *http.Handler) (*http.Server, error) { server := &http.Server{ Addr: addr, Handler: *handler, } if app.options.EnableTLSClientAuth { caFile := gotty.ExpandHomeDir(app.options.TLSCACrtFile) log.Printf("CA file: " + caFile) caCert, err := ioutil.ReadFile(caFile) 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, } server.TLSConfig = tlsConfig } return server, nil }
func main() { cmd := cli.NewApp() cmd.Version = "0.0.10" cmd.Name = "gotty" cmd.Usage = "Share your terminal as a web application" cmd.HideHelp = true flags := []flag{ flag{"address", "a", "IP address to listen"}, flag{"port", "p", "Port number to listen"}, flag{"permit-write", "w", "Permit clients to write to the TTY (BE CAREFUL)"}, flag{"credential", "c", "Credential for Basic Authentication (ex: user:pass, default disabled)"}, flag{"random-url", "r", "Add a random string to the URL"}, flag{"random-url-length", "", "Random URL length"}, flag{"tls", "t", "Enable TLS/SSL"}, flag{"tls-crt", "", "TLS/SSL crt file path"}, flag{"tls-key", "", "TLS/SSL key file path"}, flag{"index", "", "Custom index.html file"}, flag{"title-format", "", "Title format of browser window"}, flag{"reconnect", "", "Enable reconnection"}, flag{"reconnect-time", "", "Time to reconnect"}, flag{"once", "", "Accept only one client and exit on disconnection"}, flag{"permit-arguments", "", "Allow to send arguments"}, } mappingHint := map[string]string{ "index": "IndexFile", "tls": "EnableTLS", "tls-crt": "TLSCrtFile", "tls-key": "TLSKeyFile", "random-url": "EnableRandomUrl", "reconnect": "EnableReconnect", } cliFlags, err := generateFlags(flags, mappingHint) if err != nil { exit(err, 3) } cmd.Flags = append( cliFlags, cli.StringFlag{ Name: "config", Value: "~/.gotty", Usage: "Config file path", EnvVar: "GOTTY_CONFIG", }, ) cmd.Action = func(c *cli.Context) { if len(c.Args()) == 0 { fmt.Println("Error: No command given.\n") cli.ShowAppHelp(c) exit(err, 1) } options := app.DefaultOptions configFile := c.String("config") _, err := os.Stat(app.ExpandHomeDir(configFile)) if configFile != "~/.gotty" || !os.IsNotExist(err) { if err := app.ApplyConfigFile(&options, configFile); err != nil { exit(err, 2) } } applyFlags(&options, flags, mappingHint, c) if c.IsSet("credential") { options.EnableBasicAuth = true } app, err := app.New(c.Args(), &options) if err != nil { exit(err, 3) } registerSignals(app) err = app.Run() if err != nil { exit(err, 4) } } cli.AppHelpTemplate = helpTemplate cmd.Run(os.Args) }
func GoTTY() { flags := []struct { name string shortName string description string }{ {"address", "a", "IP address to listen"}, {"port", "p", "Port number to listen"}, {"permit-write", "w", "Permit clients to write to the TTY (BE CAREFUL)"}, {"credential", "c", "Credential for Basic Authentication (ex: user:pass, default disabled)"}, {"random-url", "r", "Add a random string to the URL"}, {"random-url-length", "", "Random URL length"}, {"tls", "t", "Enable TLS/SSL"}, {"tls-crt", "", "TLS/SSL certificate file path"}, {"tls-key", "", "TLS/SSL key file path"}, {"tls-ca-crt", "", "TLS/SSL CA certificate file for client certifications"}, {"index", "", "Custom index.html file"}, {"title-format", "", "Title format of browser window"}, {"reconnect", "", "Enable reconnection"}, {"reconnect-time", "", "Time to reconnect"}, {"once", "", "Accept only one client and exit on disconnection"}, {"permit-arguments", "", "Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)"}, {"close-signal", "", "Signal sent to the command process when gotty close it (default: SIGHUP)"}, } mappingHint := map[string]string{ "index": "IndexFile", "tls": "EnableTLS", "tls-crt": "TLSCrtFile", "tls-key": "TLSKeyFile", "tls-ca-crt": "TLSCACrtFile", "random-url": "EnableRandomUrl", "reconnect": "EnableReconnect", } options := gotty.DefaultOptions var configFile string = "~/.gotty" if v, ok := os.LookupEnv("GOTTY_CONFIG"); ok { configFile = v } args := os.Args[1:] //si := SliceIndex(len(args), func(i int) bool { return strings.Contains(args[i], "-config") }) flag.StringVar(&configFile, "config", configFile, "Config file path") //cliFlags, err := generateFlags(flags, mappingHint) //applyFlags(&options, flags, mappingHint, c) o := structs.New(&options) for _, f := range flags { fieldname := fieldName(f.name, mappingHint) field, ok := o.FieldOk(fieldname) if !ok { glog.Warningf("No such field: %s", fieldname) continue } //flagname := f.name //if f.shortName != "" { // flagname += ", " + f.shortName //} envName := "GOTTY_" + strings.ToUpper(strings.Join(strings.Split(f.name, "-"), "_")) //field := o.Field(fieldname) field.Set(os.Getenv(envName)) var val interface{} switch field.Kind() { case reflect.String: val = flag.String(f.name, field.Value().(string), f.description) case reflect.Bool: val = flag.Bool(f.name, false, f.description) case reflect.Int: val = flag.Int(f.name, field.Value().(int), f.description) } field.Set(val) } flag.Parse() _, err := os.Stat(gotty.ExpandHomeDir(configFile)) if configFile != "~/.gotty" || !os.IsNotExist(err) { if err := gotty.ApplyConfigFile(&options, configFile); err != nil { exit(err, 2) } } if options.Credential != "" { options.EnableBasicAuth = true } if options.TLSCACrtFile != "" { options.EnableTLSClientAuth = true } if err := gotty.CheckConfig(&options); err != nil { exit(err, 6) } var app *App if len(args) == 0 { app, err = NewApp([]string{"kubectl", "exec", "-ti", "netcat-simple", "ash"}, &options) } else { clientConfig := directKClientConfig(_kubeconfig, _context, _apiserver) if clientConfig == nil { exit(fmt.Errorf("kubeclient required"), 1) } pty, tty, err := openPTYTTY() if err != nil { exit(err, 1) } app, err = NewCmdExec(clientConfig, _ns, _pod, _container, _stdin, _tty, pty, tty, tty, tty, _command, &options) } if err != nil { exit(err, 3) } registerSignals(app) err = app.Run() if err != nil { exit(err, 4) } }
func (app *App) Run() error { if app.options.PermitWrite { log.Printf("Permitting clients to write input to the PTY.") } if app.options.Once { log.Printf("Once option is provided, accepting only one client") } path := "" if app.options.EnableRandomUrl { path += "/" + generateRandomString(app.options.RandomUrlLength) } endpoint := net.JoinHostPort(app.options.Address, app.options.Port) wsHandler := http.HandlerFunc(app.handleWS) customIndexHandler := http.HandlerFunc(app.handleCustomIndex) authTokenHandler := http.HandlerFunc(app.handleAuthToken) staticHandler := http.FileServer( &assetfs.AssetFS{Asset: gotty.Asset, AssetDir: gotty.AssetDir, Prefix: "static"}, ) var siteMux = http.NewServeMux() if app.options.IndexFile != "" { log.Printf("Using index file at " + app.options.IndexFile) siteMux.Handle(path+"/", customIndexHandler) } else { siteMux.Handle(path+"/", http.StripPrefix(path+"/", staticHandler)) } siteMux.Handle(path+"/auth_token.js", authTokenHandler) siteMux.Handle(path+"/js/", http.StripPrefix(path+"/", staticHandler)) siteMux.Handle(path+"/favicon.png", http.StripPrefix(path+"/", staticHandler)) siteHandler := http.Handler(siteMux) if app.options.EnableBasicAuth { log.Printf("Using Basic Authentication") siteHandler = wrapBasicAuth(siteHandler, app.options.Credential) } siteHandler = wrapHeaders(siteHandler) wsMux := http.NewServeMux() wsMux.Handle("/", siteHandler) wsMux.Handle(path+"/ws", wsHandler) siteHandler = (http.Handler(wsMux)) siteHandler = wrapLogger(siteHandler) scheme := "http" if app.options.EnableTLS { scheme = "https" } log.Printf( "Server is starting with command: %s", strings.Join(app.command, " "), ) if app.options.Address != "" { log.Printf( "URL: %s", (&url.URL{Scheme: scheme, Host: endpoint, Path: path + "/"}).String(), ) } else { for _, address := range listAddresses() { log.Printf( "URL: %s", (&url.URL{ Scheme: scheme, Host: net.JoinHostPort(address, app.options.Port), Path: path + "/", }).String(), ) } } server, err := app.makeServer(endpoint, &siteHandler) if err != nil { return errors.New("Failed to build server: " + err.Error()) } app.server = manners.NewWithServer( server, ) if app.options.EnableTLS { crtFile := gotty.ExpandHomeDir(app.options.TLSCrtFile) keyFile := gotty.ExpandHomeDir(app.options.TLSKeyFile) log.Printf("TLS crt file: " + crtFile) log.Printf("TLS key file: " + keyFile) err = app.server.ListenAndServeTLS(crtFile, keyFile) } else { err = app.server.ListenAndServe() } if err != nil { return err } log.Printf("Exiting...") return nil }
func (app *App) handleCustomIndex(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, gotty.ExpandHomeDir(app.options.IndexFile)) }