From d71e2fcfa8437ab63a0acacd9cad3eb6ad887b73 Mon Sep 17 00:00:00 2001 From: zlji Date: Mon, 9 Jan 2017 22:06:46 +0800 Subject: [PATCH] generate falgs based on struct options instead of defining them externally --- app/app.go | 111 +++++++++++------------------------------- flags.go | 101 --------------------------------------- main.go | 61 +++++------------------- utils/default.go | 41 ++++++++++++++++ utils/flags.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++ utils/path.go | 13 +++++ 6 files changed, 218 insertions(+), 231 deletions(-) delete mode 100644 flags.go create mode 100644 utils/default.go create mode 100644 utils/flags.go create mode 100644 utils/path.go diff --git a/app/app.go b/app/app.go index 78a7772..a9fb2d6 100644 --- a/app/app.go +++ b/app/app.go @@ -13,7 +13,6 @@ import ( "net" "net/http" "net/url" - "os" "os/exec" "strconv" "strings" @@ -22,11 +21,12 @@ import ( "text/template" "time" + "github.com/yudai/gotty/utils" + "github.com/braintree/manners" "github.com/elazarl/go-bindata-assetfs" "github.com/gorilla/websocket" "github.com/kr/pty" - "github.com/yudai/hcl" "github.com/yudai/umutex" ) @@ -53,60 +53,35 @@ type App struct { } type Options struct { - Address string `hcl:"address"` - Port string `hcl:"port"` - PermitWrite bool `hcl:"permit_write"` - EnableBasicAuth bool `hcl:"enable_basic_auth"` - Credential string `hcl:"credential"` - EnableRandomUrl bool `hcl:"enable_random_url"` - RandomUrlLength int `hcl:"random_url_length"` - IndexFile string `hcl:"index_file"` - EnableTLS bool `hcl:"enable_tls"` - TLSCrtFile string `hcl:"tls_crt_file"` - TLSKeyFile string `hcl:"tls_key_file"` - EnableTLSClientAuth bool `hcl:"enable_tls_client_auth"` - TLSCACrtFile string `hcl:"tls_ca_crt_file"` - TitleFormat string `hcl:"title_format"` - EnableReconnect bool `hcl:"enable_reconnect"` - ReconnectTime int `hcl:"reconnect_time"` - MaxConnection int `hcl:"max_connection"` - Once bool `hcl:"once"` - Timeout int `hcl:"timeout"` - PermitArguments bool `hcl:"permit_arguments"` - CloseSignal int `hcl:"close_signal"` + Address string `hcl:"address" flagName:"address" flagSName:"a" flagDescribe:"IP address to listen" default:""` + Port string `hcl:"port" flagName:"port" flagSName:"p" flagDescribe:"Port number to liten" default:"8080"` + PermitWrite bool `hcl:"permit_write" flagName:"permit-write" flagSName:"w" flagDescribe:"Permit clients to write to the TTY (BE CAREFUL)" default:"false"` + EnableBasicAuth bool `hcl:"enable_basic_auth" default:"false"` + Credential string `hcl:"credential" flagName:"credential" flagSName:"c" flagDescribe:"Credential for Basic Authentication (ex: user:pass, default disabled)" default:""` + EnableRandomUrl bool `hcl:"enable_random_url flagName:"random-url" flagSName:"r" flagDescribe:"Add a random string to the URL"" default:"false"` + RandomUrlLength int `hcl:"random_url_length" flagName:"random-url-length" flagDescribe:"Random URL length" default:"8"` + IndexFile string `hcl:"index_file" flagName:"index" flagDescribe:"Custom index.html file" default:""` + EnableTLS bool `hcl:"enable_tls" flagName:"tls" flagSName:"t" flagDescribe:"Enable TLS/SSL" default:"false"` + TLSCrtFile string `hcl:"tls_crt_file" flagName:"tls-crt" flagDescribe:"TLS/SSL certificate file path" default:"~/.gotty.crt"` + TLSKeyFile string `hcl:"tls_key_file" flagName:"tls-key" flagDescribe:"TLS/SSL key file path" default:"~/.gotty.key"` + EnableTLSClientAuth bool `hcl:"enable_tls_client_auth" default:"false"` + TLSCACrtFile string `hcl:"tls_ca_crt_file" flagName:"tls-ca-crt" flagDescribe:"TLS/SSL CA certificate file for client certifications" default:"~/.gotty.ca.crt"` + TitleFormat string `hcl:"title_format" flagName:"title-format" flagDescribe:"Title format of browser window" default:"GoTTY - {{ .Command }} ({{ .Hostname }})"` + EnableReconnect bool `hcl:"enable_reconnect" flagName:"reconnect" flagDescribe:"Enable reconnection" default:"false"` + ReconnectTime int `hcl:"reconnect_time" flagName:"reconnect-time" flagDescribe:"Time to reconnect" default:"10"` + MaxConnection int `hcl:"max_connection" flagName:"max-connection" flagDescribe:"Maximum connection to gotty" default:"0"` + Once bool `hcl:"once" flagName:"once" flagDescribe:"Accept only one client and exit on disconnection" default:"false"` + Timeout int `hcl:"timeout" flagName:"timeout" flagDescribe:"Timeout seconds for waiting a client(0 to disable)" default:"0"` + PermitArguments bool `hcl:"permit_arguments" flagName:"permit-arguments" flagDescribe:"Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)" default:"true"` + CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"` Preferences HtermPrefernces `hcl:"preferences"` RawPreferences map[string]interface{} `hcl:"preferences"` - Width int `hcl:"width"` - Height int `hcl:"height"` + Width int `hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0"` + Height int `hcl:"height" flagName:"height" flagDescribe:"Static height of the screen, 0(default) means dynamically resize" default:"0"` } var Version = "1.0.0" -var DefaultOptions = Options{ - Address: "", - Port: "8080", - PermitWrite: false, - EnableBasicAuth: false, - Credential: "", - EnableRandomUrl: false, - RandomUrlLength: 8, - IndexFile: "", - EnableTLS: false, - TLSCrtFile: "~/.gotty.crt", - TLSKeyFile: "~/.gotty.key", - EnableTLSClientAuth: false, - TLSCACrtFile: "~/.gotty.ca.crt", - TitleFormat: "GoTTY - {{ .Command }} ({{ .Hostname }})", - EnableReconnect: false, - ReconnectTime: 10, - MaxConnection: 0, - Once: false, - CloseSignal: 1, // syscall.SIGHUP - Preferences: HtermPrefernces{}, - Width: 0, - Height: 0, -} - func New(command []string, options *Options) (*App, error) { titleTemplate, err := template.New("title").Parse(options.TitleFormat) if err != nil { @@ -132,26 +107,6 @@ func New(command []string, options *Options) (*App, error) { }, nil } -func ApplyConfigFile(options *Options, filePath string) error { - filePath = ExpandHomeDir(filePath) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - return err - } - - fileString := []byte{} - log.Printf("Loading config file at: %s", filePath) - fileString, err := ioutil.ReadFile(filePath) - if err != nil { - return err - } - - if err := hcl.Decode(options, string(fileString)); err != nil { - return err - } - - return nil -} - func CheckConfig(options *Options) error { if options.EnableTLSClientAuth && !options.EnableTLS { return errors.New("TLS client authentication is enabled, but TLS is not enabled") @@ -253,8 +208,8 @@ func (app *App) Run() error { } if app.options.EnableTLS { - crtFile := ExpandHomeDir(app.options.TLSCrtFile) - keyFile := ExpandHomeDir(app.options.TLSKeyFile) + crtFile := utils.ExpandHomeDir(app.options.TLSCrtFile) + keyFile := utils.ExpandHomeDir(app.options.TLSKeyFile) log.Printf("TLS crt file: " + crtFile) log.Printf("TLS key file: " + keyFile) @@ -278,7 +233,7 @@ func (app *App) makeServer(addr string, handler *http.Handler) (*http.Server, er } if app.options.EnableTLSClientAuth { - caFile := ExpandHomeDir(app.options.TLSCACrtFile) + caFile := utils.ExpandHomeDir(app.options.TLSCACrtFile) log.Printf("CA file: " + caFile) caCert, err := ioutil.ReadFile(caFile) if err != nil { @@ -410,7 +365,7 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) { } func (app *App) handleCustomIndex(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, ExpandHomeDir(app.options.IndexFile)) + http.ServeFile(w, r, utils.ExpandHomeDir(app.options.IndexFile)) } func (app *App) handleAuthToken(w http.ResponseWriter, r *http.Request) { @@ -501,11 +456,3 @@ func listAddresses() (addresses []string) { return } - -func ExpandHomeDir(path string) string { - if path[0:2] == "~/" { - return os.Getenv("HOME") + path[1:] - } else { - return path - } -} diff --git a/flags.go b/flags.go deleted file mode 100644 index bbd6274..0000000 --- a/flags.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "errors" - "reflect" - "strings" - - "github.com/codegangsta/cli" - "github.com/fatih/structs" - - "github.com/yudai/gotty/app" -) - -type flag struct { - name string - shortName string - description string -} - -func generateFlags(flags []flag, hint map[string]string) ([]cli.Flag, error) { - o := structs.New(app.DefaultOptions) - - results := make([]cli.Flag, len(flags)) - - for i, flag := range flags { - fieldName := fieldName(flag.name, hint) - - field, ok := o.FieldOk(fieldName) - if !ok { - return nil, errors.New("No such field: " + fieldName) - } - - flagName := flag.name - if flag.shortName != "" { - flagName += ", " + flag.shortName - } - envName := "GOTTY_" + strings.ToUpper(strings.Join(strings.Split(flag.name, "-"), "_")) - - switch field.Kind() { - case reflect.String: - results[i] = cli.StringFlag{ - Name: flagName, - Value: field.Value().(string), - Usage: flag.description, - EnvVar: envName, - } - case reflect.Bool: - results[i] = cli.BoolFlag{ - Name: flagName, - Usage: flag.description, - EnvVar: envName, - } - case reflect.Int: - results[i] = cli.IntFlag{ - Name: flagName, - Value: field.Value().(int), - Usage: flag.description, - EnvVar: envName, - } - default: - return nil, errors.New("Unsupported type: " + fieldName) - } - } - - return results, nil -} - -func applyFlags( - options *app.Options, - flags []flag, - mappingHint map[string]string, - c *cli.Context, -) { - o := structs.New(options) - for _, flag := range flags { - if c.IsSet(flag.name) { - field := o.Field(fieldName(flag.name, mappingHint)) - var val interface{} - switch field.Kind() { - case reflect.String: - val = c.String(flag.name) - case reflect.Bool: - val = c.Bool(flag.name) - case reflect.Int: - val = c.Int(flag.name) - } - field.Set(val) - } - } -} - -func fieldName(name string, hint map[string]string) string { - if fieldName, ok := hint[name]; ok { - return fieldName - } - nameParts := strings.Split(name, "-") - for i, part := range nameParts { - nameParts[i] = strings.ToUpper(part[0:1]) + part[1:] - } - return strings.Join(nameParts, "") -} diff --git a/main.go b/main.go index cffd38c..32a37a5 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/codegangsta/cli" "github.com/yudai/gotty/app" + "github.com/yudai/gotty/utils" ) func main() { @@ -18,41 +19,12 @@ func main() { 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 certificate file path"}, - flag{"tls-key", "", "TLS/SSL key file path"}, - flag{"tls-ca-crt", "", "TLS/SSL CA certificate file for client certifications"}, - 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{"timeout", "", "Timeout seconds for waiting a client (0 to disable)"}, - flag{"max-connection", "", "Maximum connection to gotty, 0(default) means no limit"}, - flag{"once", "", "Accept only one client and exit on disconnection"}, - flag{"permit-arguments", "", "Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)"}, - flag{"close-signal", "", "Signal sent to the command process when gotty close it (default: SIGHUP)"}, - flag{"width", "", "Static width of the screen, 0(default) means dynamically resize"}, - flag{"height", "", "Static height of the screen, 0(default) means dynamically resize"}, + options := &app.Options{} + if err := utils.ApplyDefaultValues(options); err != nil { + exit(err, 1) } - mappingHint := map[string]string{ - "index": "IndexFile", - "tls": "EnableTLS", - "tls-crt": "TLSCrtFile", - "tls-key": "TLSKeyFile", - "tls-ca-crt": "TLSCACrtFile", - "random-url": "EnableRandomUrl", - "reconnect": "EnableReconnect", - } - - cliFlags, err := generateFlags(flags, mappingHint) + cliFlags, flagMappings, err := utils.GenerateFlags(options) if err != nil { exit(err, 3) } @@ -69,35 +41,28 @@ func main() { cmd.Action = func(c *cli.Context) { if len(c.Args()) == 0 { - fmt.Println("Error: No command given.\n") cli.ShowAppHelp(c) - exit(err, 1) + exit(fmt.Errorf("Error: No command given."), 1) } - options := app.DefaultOptions - configFile := c.String("config") - _, err := os.Stat(app.ExpandHomeDir(configFile)) + _, err := os.Stat(utils.ExpandHomeDir(configFile)) if configFile != "~/.gotty" || !os.IsNotExist(err) { - if err := app.ApplyConfigFile(&options, configFile); err != nil { + if err := utils.ApplyConfigFile(configFile, options); err != nil { exit(err, 2) } } - applyFlags(&options, flags, mappingHint, c) + utils.ApplyFlags(cliFlags, flagMappings, c, options) - if c.IsSet("credential") { - options.EnableBasicAuth = true - } - if c.IsSet("tls-ca-crt") { - options.EnableTLSClientAuth = true - } + options.EnableBasicAuth = c.IsSet("credential") + options.EnableTLSClientAuth = c.IsSet("tls-ca-crt") - if err := app.CheckConfig(&options); err != nil { + if err := app.CheckConfig(options); err != nil { exit(err, 6) } - app, err := app.New(c.Args(), &options) + app, err := app.New(c.Args(), options) if err != nil { exit(err, 3) } diff --git a/utils/default.go b/utils/default.go new file mode 100644 index 0000000..e813b3b --- /dev/null +++ b/utils/default.go @@ -0,0 +1,41 @@ +package utils + +import ( + "fmt" + "github.com/fatih/structs" + "reflect" + "strconv" +) + +func ApplyDefaultValues(struct_ interface{}) (err error) { + o := structs.New(struct_) + + for _, field := range o.Fields() { + defaultValue := field.Tag("default") + if defaultValue == "" { + continue + } + var val interface{} + switch field.Kind() { + case reflect.String: + val = defaultValue + case reflect.Bool: + if defaultValue == "true" { + val = true + } else if defaultValue == "false" { + val = false + } else { + return fmt.Errorf("invalid bool expression: %v, use true/false", defaultValue) + } + case reflect.Int: + val, err = strconv.Atoi(defaultValue) + if err != nil { + return err + } + default: + val = field.Value() + } + field.Set(val) + } + return nil +} diff --git a/utils/flags.go b/utils/flags.go new file mode 100644 index 0000000..de98dff --- /dev/null +++ b/utils/flags.go @@ -0,0 +1,122 @@ +package utils + +import ( + "io/ioutil" + "log" + "os" + "reflect" + "strings" + + "github.com/codegangsta/cli" + "github.com/fatih/structs" + "github.com/yudai/hcl" +) + +func GenerateFlags(options ...interface{}) (flags []cli.Flag, mappings map[string]string, err error) { + mappings = make(map[string]string) + + for _, struct_ := range options { + o := structs.New(struct_) + for _, field := range o.Fields() { + flagName := field.Tag("flagName") + if flagName == "" { + continue + } + envName := "GOTTY_" + strings.ToUpper(strings.Join(strings.Split(flagName, "-"), "_")) + mappings[flagName] = field.Name() + + flagShortName := field.Tag("flagSName") + if flagShortName != "" { + flagName += ", " + flagShortName + } + + flagDescription := field.Tag("flagDescribe") + + switch field.Kind() { + case reflect.String: + flags = append(flags, cli.StringFlag{ + Name: flagName, + Value: field.Value().(string), + Usage: flagDescription, + EnvVar: envName, + }) + case reflect.Bool: + flags = append(flags, cli.BoolFlag{ + Name: flagName, + Usage: flagDescription, + EnvVar: envName, + }) + case reflect.Int: + flags = append(flags, cli.IntFlag{ + Name: flagName, + Value: field.Value().(int), + Usage: flagDescription, + EnvVar: envName, + }) + } + } + } + + return +} + +func ApplyFlags( + flags []cli.Flag, + mappingHint map[string]string, + c *cli.Context, + options ...interface{}, +) { + objects := make([]*structs.Struct, len(options)) + for i, struct_ := range options { + objects[i] = structs.New(struct_) + } + + for flagName, fieldName := range mappingHint { + if !c.IsSet(flagName) { + continue + } + var field *structs.Field + var ok bool + for _, o := range objects { + field, ok = o.FieldOk(fieldName) + if ok { + break + } + } + if field == nil { + continue + } + var val interface{} + switch field.Kind() { + case reflect.String: + val = c.String(flagName) + case reflect.Bool: + val = c.Bool(flagName) + case reflect.Int: + val = c.Int(flagName) + } + field.Set(val) + } +} + +func ApplyConfigFile(filePath string, options ...interface{}) error { + filePath = ExpandHomeDir(filePath) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return err + } + + fileString := []byte{} + log.Printf("Loading config file at: %s", filePath) + fileString, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + for _, object := range options { + if err := hcl.Decode(object, string(fileString)); err != nil { + return err + } + } + + return nil +} diff --git a/utils/path.go b/utils/path.go new file mode 100644 index 0000000..4230593 --- /dev/null +++ b/utils/path.go @@ -0,0 +1,13 @@ +package utils + +import ( + "os" +) + +func ExpandHomeDir(path string) string { + if path[0:2] == "~/" { + return os.Getenv("HOME") + path[1:] + } else { + return path + } +}