diff --git a/HISTORY.md b/HISTORY.md index d9b13d0c..84e4da0f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,14 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras/iris` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 4.0.0 -> 4.1.0 + +- **NEW FEATURE**: Basic remote control through SSH, example [here](https://github.com/iris-contrib/examples/blob/master/ssh/main.go) +- **NEW FEATURE**: Optionally `OnError` foreach Party (by prefix, use it with your own risk), example [here](https://github.com/iris-contrib/examples/blob/master/httperrors/main.go#L37) +- **FIX**: Sessions + SetFlash on same handler strange behavior[*](https://github.com/kataras/iris/issues/351) +- **FIX**: Multi websocket servers client-side source route panic[*](https://github.com/kataras/iris/issues/365) +- Better gzip response managment + ## 4.0.0-alpha.5 -> 4.0.0 @@ -27,7 +35,7 @@ Notes: if you compare it with previous releases (13+ versions before v3 stable), **The important** , is that the [book](https://kataras.gitbooks.io/iris/content/) is finally updated! - + If you're **willing to donate** click [here](DONATIONS.md)! @@ -49,17 +57,17 @@ If you're **willing to donate** click [here](DONATIONS.md)! **New** -A **Response Engine** gives you the freedom to create/change the render/response writer for +A **Response Engine** gives you the freedom to create/change the render/response writer for - `context.JSON` -- `context.JSONP` -- `context.XML` -- `context.Text` +- `context.JSONP` +- `context.XML` +- `context.Text` - `context.Markdown` -- `context.Data` +- `context.Data` - `context.Render("my_custom_type",mystructOrData{}, iris.RenderOptions{"gzip":false,"charset":"UTF-8"})` - `context.MarkdownString` -- `iris.ResponseString(...)` +- `iris.ResponseString(...)` **Fix** @@ -70,7 +78,7 @@ A **Response Engine** gives you the freedom to create/change the render/response **Small changes** - `iris.Config.Charset`, before alpha.3 was `iris.Config.Rest.Charset` & `iris.Config.Render.Template.Charset`, but you can override it at runtime by passinth a map `iris.RenderOptions` on the `context.Render` call . -- `iris.Config.IsDevelopment`, before alpha.1 was `iris.Config.Render.Template.IsDevelopment` +- `iris.Config.IsDevelopment`, before alpha.1 was `iris.Config.Render.Template.IsDevelopment` **Websockets changes** diff --git a/README.md b/README.md index a9cf330f..06964679 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ License -Releases +Releases Practical Guide/Docs
@@ -70,6 +70,7 @@ Features - Focus on high performance - Robust routing, static, wildcard subdomains and routes. - Websocket API, Sessions support out of the box +- Remote control through [SSH](https://github.com/iris-contrib/examples/blob/master/ssh/main.go) - View system supporting [6+](https://kataras.gitbooks.io/iris/content/template-engines.html) template engines - Highly scalable response engines - Live reload @@ -143,7 +144,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v4.0.0** +Current: **v4.1.0** > Iris is an active project @@ -178,7 +179,7 @@ License can be found [here](LICENSE). [Travis]: http://travis-ci.org/kataras/iris [License Widget]: https://img.shields.io/badge/license-MIT%20%20License%20-E91E63.svg?style=flat-square [License]: https://github.com/kataras/iris/blob/master/LICENSE -[Release Widget]: https://img.shields.io/badge/release-v4.0.0-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-v4.1.0-blue.svg?style=flat-square [Release]: https://github.com/kataras/iris/releases [Chat Widget]: https://img.shields.io/badge/community-chat-00BCD4.svg?style=flat-square [Chat]: https://kataras.rocket.chat/channel/iris @@ -193,4 +194,3 @@ License can be found [here](LICENSE). [Language Widget]: https://img.shields.io/badge/powered_by-Go-3362c2.svg?style=flat-square [Language]: http://golang.org [Platform Widget]: https://img.shields.io/badge/platform-Any--OS-gray.svg?style=flat-square - diff --git a/http.go b/http.go index 14eb75d9..ef9fc3b2 100644 --- a/http.go +++ b/http.go @@ -1440,7 +1440,7 @@ func (mux *serveMux) register(method []byte, subdomain string, path string, midd // build collects all routes info and adds them to the registry in order to be served from the request handler // this happens once when server is setting the mux's handler. func (mux *serveMux) build() { - + mux.tree = nil sort.Sort(bySubdomain(mux.lookups)) for _, r := range mux.lookups { // add to the registry tree diff --git a/iris.go b/iris.go index 17b67596..cea09385 100644 --- a/iris.go +++ b/iris.go @@ -85,7 +85,7 @@ import ( const ( // Version of the iris - Version = "4.0.0" + Version = "4.1.0" banner = ` _____ _ |_ _| (_) @@ -102,7 +102,10 @@ var ( Logger *logger.Logger Plugins PluginContainer Websocket WebsocketServer - Servers *ServerList + // Look ssh.go for this field's configuration + // example: https://github.com/iris-contrib/examples/blob/master/ssh/main.go + SSH *SSHServer + Servers *ServerList // Available is a channel type of bool, fired to true when the server is opened and all plugins ran // never fires false, if the .Close called then the channel is re-allocating. // the channel is closed only when .ListenVirtual is used, otherwise it remains open until you close it. @@ -121,6 +124,7 @@ func initDefault() { Logger = Default.Logger Plugins = Default.Plugins Websocket = Default.Websocket + SSH = Default.SSH Servers = Default.Servers Available = Default.Available } @@ -180,6 +184,7 @@ type ( Logger *logger.Logger Plugins PluginContainer Websocket WebsocketServer + SSH *SSHServer Available chan bool // this is setted once when .Tester(t) is called testFramework *httpexpect.Expect @@ -201,6 +206,7 @@ func New(cfg ...config.Iris) *Framework { Config: &c, responses: &responseEngines{}, Available: make(chan bool), + SSH: &SSHServer{}, } { ///NOTE: set all with s.Config pointer @@ -283,6 +289,11 @@ func (s *Framework) initialize() { if debugPath := s.Config.ProfilePath; debugPath != "" { s.Handle(MethodGet, debugPath+"/*action", profileMiddleware(debugPath)...) } + + // ssh + if s.SSH != nil && s.SSH.Enabled() { + s.SSH.bindTo(s) + } } func (s *Framework) acquireCtx(reqCtx *fasthttp.RequestCtx) *Context { diff --git a/ssh.go b/ssh.go new file mode 100644 index 00000000..6b2010b4 --- /dev/null +++ b/ssh.go @@ -0,0 +1,708 @@ +package iris + +// Minimal managment over SSH for your Iris & Q web server +// +// Declaration: +// +// iris.SSH.Host = "0.0.0.0:22" +// iris.SSH.KeyPath = "./iris_rsa" // it's auto-generated if not exists +// iris.SSH.Users = iris.Users{"kataras", []byte("pass")} +// +// +// Usage: +// via interactive command shell: +// +// $ ssh kataras@localhost +// +// or via standalone command and exit: +// +// $ ssh kataras@localhost stop +// +// +// Commands available: +// +// stop +// start +// restart +// log +// help +// exit + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "text/template" + "time" + + "github.com/iris-contrib/errors" + "github.com/iris-contrib/logger" + "github.com/kardianos/osext" + "github.com/kardianos/service" + "github.com/kataras/iris/utils" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" +) + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------------------------Iris+SSH------------------------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +func _output(format string, a ...interface{}) func(io.Writer) { + if format[len(format)-3:] != "\n" { + format += "\n" + } + msgBytes := []byte(fmt.Sprintf(format, a...)) + + return func(w io.Writer) { + w.Write(msgBytes) + } +} + +type systemServiceWrapper struct{} + +func (w *systemServiceWrapper) Start(s service.Service) error { + return nil +} + +func (w *systemServiceWrapper) Stop(s service.Service) error { + return nil +} + +func (s *SSHServer) bindTo(station *Framework) { + if s.Enabled() && !s.IsListening() { // check if not listening because on restart this block will re-executing,but we don't want to start ssh again, ssh will never stops. + + if station.Config.IsDevelopment && s.Logger == nil { + s.Logger = station.Logger + } + + // cache the messages to be sent to the channel, no need to produce memory allocations here + statusRunningMsg := _output("The HTTP Server is running.") + statusNotRunningMsg := _output("The HTTP Server is NOT running. ") + + serverStoppedMsg := _output("The HTTP Server has been stopped.") + errServerNotReadyMsg := _output("Error: HTTP Server is not even builded yet!") + + serverStartedMsg := _output("The HTTP Server has been started.") + serverRestartedMsg := _output("The HTTP Server has been restarted.") + + loggerStartedMsg := _output("Logger has been registered to the HTTP Server.\nNew Requests will be printed here.\nYou can still type 'exit' to close this SSH Session.\n\n") + // + + sshCommands := Commands{ + Command{Name: "status", Description: "Prompts the status of the HTTP Server, is listening(started) or not(stopped).", Action: func(conn ssh.Channel) { + if station.Servers.Main() != nil && station.Servers.Main().IsListening() { + statusRunningMsg(conn) + } else { + statusNotRunningMsg(conn) + } + execPath, err := osext.Executable() // this works fine, if the developer builded the go app, if just go run main.go then prints the temporary path which the go tool creates + if err == nil { + conn.Write([]byte("[EXEC] " + execPath + "\n")) + } + }}, + // Note for stop If you have opened a tab with Q route: + // in order to see that the http listener has closed you have to close your browser and re-navigate(browsers caches the tcp connection) + Command{Name: "stop", Description: "Stops the HTTP Server.", Action: func(conn ssh.Channel) { + srv := station.Servers.Main() + if srv != nil && srv.IsListening() { + srv.Close() + srv.listener = nil + serverStoppedMsg(conn) + } else { + errServerNotReadyMsg(conn) + } + }}, + Command{Name: "start", Description: "Starts the HTTP Server.", Action: func(conn ssh.Channel) { + srv := station.Servers.Main() + if !srv.IsListening() { + go srv.Open(srv.Handler) + } + serverStartedMsg(conn) + }}, + Command{Name: "restart", Description: "Restarts the HTTP Server.", Action: func(conn ssh.Channel) { + srv := station.Servers.Main() + if srv != nil && srv.IsListening() { + srv.Close() + srv.listener = nil + } + go srv.Open(srv.Handler) + serverRestartedMsg(conn) + }}, + /* not ready yet + Command{Name: "service", Description: "[REQUIRES HTTP SERVER's ADMIN PRIVILEGE] Adds the web server to the system services, use it when you want to make your server to autorun on reboot", Action: func(conn ssh.Channel) { + ///TODO: + // 1. Unistall service and change the 'service' to 'install service' + // 2. Fix, this current implementation doesn't works on windows 10 it says that the service is not responding to request and start... + // 2.1 the fix is maybe add these and change the s.Install to s.Run to the $DESKTOP/some/q/main.go I will try this + // as the example shows. + + // remember: run command line as administrator > sc delete "Iris Web Server - $DATETIME" to delete the service, do it on each test. + svcConfig := &service.Config{ + Name: "Iris Web Server - " + time.Now().Format(q.TimeFormat), + DisplayName: "Iris Web Server - " + time.Now().Format(q.TimeFormat), + Description: "The web server which has been registered by SSH interface.", + } + + prg := &systemServiceWrapper{} + s, err := service.New(prg, svcConfig) + + if err != nil { + conn.Write([]byte(err.Error() + "\n")) + return + } + + err = s.Install() + if err != nil { + conn.Write([]byte(err.Error() + "\n")) + return + } + conn.Write([]byte("Service has been registered.\n")) + + }},*/ + Command{Name: "log", Description: "Adds a logger to the HTTP Server, waits for requests and prints them here.", Action: func(conn ssh.Channel) { + // the ssh user can still write commands, this is not blocking anything. + loggerMiddleware := NewLoggerHandler(conn, true) + station.UseGlobalFunc(loggerMiddleware) + + // register to the errors also + errorLoggerHandler := NewLoggerHandler(conn, false) + + for k, v := range station.mux.errorHandlers { + errorH := v + // wrap the error handler with the ssh logger middleware + station.mux.errorHandlers[k] = HandlerFunc(func(ctx *Context) { + errorH.Serve(ctx) + errorLoggerHandler(ctx) // after the error handler because that is setting the status code. + }) + } + + station.mux.build() // rebuild the mux in order the UseGlobalFunc to work at runtime + + loggerStartedMsg(conn) + // the middleware will still to run, we could remove it on exit but exit is general command I dont want to touch that + // we could make a command like 'log stop' or on 'stop' to remove the middleware...I will think about it. + }}, + } + + for _, cmd := range sshCommands { + if _, found := s.Commands.ByName(cmd.Name); !found { // yes, the user can add custom commands too, I will cover this on docs some day, it's not too hard if you see the code. + s.Commands.Add(cmd) + } + } + + go func() { + station.Must(s.Listen()) + }() + } +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------------------------SSH implementation--------------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +var ( + // SSHBanner is the banner goes on top of the 'ssh help message' + // it can be changed, defaults is the Iris's banner + SSHBanner = banner + + helpMessage = SSHBanner + ` + +COMMANDS: + {{ range $index, $cmd := .Commands }} + {{- $cmd.Name }} - {{ $cmd.Description }} + {{ end }} +USAGE: + ssh myusername@{{ .Hostname}} -p {{ .Port }} {{ first .Commands}} + or just write the command below +VERSION: + {{ .Version }} + +` + helpTmpl *template.Template +) + +func init() { + var err error + helpTmpl = template.New("help_message").Funcs(template.FuncMap{"first": func(cmds Commands) string { + if len(cmds) > 0 { + return cmds[0].Name + } + + return "" + }}) + + helpTmpl, err = helpTmpl.Parse(helpMessage) + if err != nil { + panic(err.Error()) + } +} + +//no need of SSH prefix on these types, we don't have other commands +// use of struct and no global variables because we want each Iris instance to have its own SSH interface. + +// Action the command's handler +type Action func(ssh.Channel) + +// Command contains the registered SSH commands +// contains a Name which is the payload string +// Description which is the description of the command shows to the admin/user +// Action is the particular command's handler +type Command struct { + Name string + Description string + Action Action +} + +// Commands the SSH Commands, it's just a type of []Command +type Commands []Command + +// Add adds command(s) to the commands list +func (c *Commands) Add(cmd ...Command) { + pCommands := *c + *c = append(pCommands, cmd...) +} + +// ByName returns the command by its Name +// if not found returns a zero-value Command and false as the second output parameter. +func (c *Commands) ByName(commandName string) (cmd Command, found bool) { + pCommands := *c + for _, cmd = range pCommands { + if cmd.Name == commandName { + found = true + return + } + } + return +} + +// Users SSH.Users field, it's just map[string][]byte (username:password) +type Users map[string][]byte + +func (m Users) exists(username string, pass []byte) bool { + for k, v := range m { + if k == username && bytes.Equal(v, pass) { + return true + } + } + return false + +} + +// DefaultSSHKeyPath used if SSH.KeyPath is empty. Defaults to: "iris_rsa". It can be changed. +var DefaultSSHKeyPath = "iris_rsa" + +var errSSHExecutableNotFound = errors.New(`Cannot generate ssh private key: ssh-keygen couldn't be found. Please specify the ssh[.exe] and ssh-keygen[.exe] + path on your operating system's environment's $PATH or set the configuration field 'Bin'.\n For example, on windows, the path is: C:\\Program Files\\Git\usr\\bin. Error Trace: %q`) + +func generateSigner(keypath string, sshKeygenBin string) (ssh.Signer, error) { + if keypath == "" { + keypath = DefaultSSHKeyPath + } + if sshKeygenBin != "" { + // if empty then the user should specify the ssh-keygen bin path (if not setted already) + // on the $PATH system environment, otherwise it will panic. + if sshKeygenBin[len(sshKeygenBin)-1] != os.PathSeparator { + sshKeygenBin += string(os.PathSeparator) + } + sshKeygenBin += "ssh-keygen" + if isWindows { + sshKeygenBin += ".exe" + } + } else { + sshKeygenBin = "ssh-keygen" + } + if !utils.DirectoryExists(keypath) { + os.MkdirAll(filepath.Dir(keypath), os.ModePerm) + keygenCmd := exec.Command(sshKeygenBin, "-f", keypath, "-t", "rsa", "-N", "") + _, err := keygenCmd.Output() + if err != nil { + panic(errSSHExecutableNotFound.Format(err.Error())) + } + } + + pemBytes, err := ioutil.ReadFile(keypath) + if err != nil { + return nil, err + } + + return ssh.ParsePrivateKey(pemBytes) +} + +func validChannel(ch ssh.NewChannel) bool { + if typ := ch.ChannelType(); typ != "session" { + ch.Reject(ssh.UnknownChannelType, typ) + return false + } + return true +} + +func execCmd(cmd *exec.Cmd, ch ssh.Channel) error { + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + input, err := cmd.StdinPipe() + if err != nil { + return err + } + + if err = cmd.Start(); err != nil { + return err + } + + go io.Copy(input, ch) + io.Copy(ch, stdout) + io.Copy(ch.Stderr(), stderr) + + if err = cmd.Wait(); err != nil { + return err + } + return nil +} + +func sendExitStatus(ch ssh.Channel) { + ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) +} + +var errInvalidSSHCommand = errors.New("Invalid Command: '%s'") + +func parsePayload(payload string, prefix string) (string, error) { + payloadUTF8 := strings.Map(func(r rune) rune { + if r >= 32 && r < 127 { + return r + } + return -1 + }, payload) + + if prefIdx := strings.Index(payloadUTF8, prefix); prefIdx != -1 { + p := strings.TrimSpace(payloadUTF8[prefIdx+len(prefix):]) + return p, nil + } + return "", errInvalidSSHCommand.Format(payload) +} + +const ( + isWindows = runtime.GOOS == "windows" + isMac = runtime.GOOS == "darwin" +) + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------------------------SSH Server----------------------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +// SSHServer : Simple SSH interface for Iris web framework, does not implements the most secure options and code, +// but its should works +// use it at your own risk. +type SSHServer struct { + Bin string // windows: C:/Program Files/Git/usr/bin, it's the ssh[.exe] and ssh-keygen[.exe], we only need the ssh-keygen. + KeyPath string // C:/Users/kataras/.ssh/iris_rsa + Host string // host:port + listener net.Listener + Users Users // map[string][]byte]{ "username":[]byte("password"), "my_second_username" : []byte("my_second_password")} + Commands Commands // Commands{Command{Name: "restart", Description:"restarts & rebuild the server", Action: func(ssh.Channel){}}} + // note for Commands field: + // the default Iris's commands are defined at the end of this file, I tried to make this file as standalone as I can, because it will be used for Iris web framework also. + Shell bool // Set it to true to enable execute terminal's commands(system commands) via ssh if no other command is found from the Commands field. Defaults to false for security reasons + Logger *logger.Logger // log.New(...)/ $qinstance.Logger, fill it when you want to receive debug and info/warnings messages +} + +// Enabled returns true if SSH can be started, if Host != "" +func (s *SSHServer) Enabled() bool { + if s == nil { + return false + } + return s.Host != "" +} + +// IsListening returns true if ssh server has been started +func (s *SSHServer) IsListening() bool { + return s.Enabled() && s.listener != nil +} + +func (s *SSHServer) logf(format string, a ...interface{}) { + if s.Logger != nil { + s.Logger.Printf(format, a...) + } +} + +// parseHostname receives an addr of form host[:port] and returns the hostname part of it +// ex: localhost:8080 will return the `localhost`, mydomain.com:22 will return the 'mydomain' +func parseHostname(addr string) string { + idx := strings.IndexByte(addr, ':') + if idx == 0 { + // only port, then return 0.0.0.0 + return "0.0.0.0" + } else if idx > 0 { + return addr[0:idx] + } + // it's already hostname + return addr +} + +// parsePort receives an addr of form host[:port] and returns the port part of it +// ex: localhost:8080 will return the `8080`, mydomain.com will return the '22' +func parsePort(addr string) int { + if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { + afP := addr[portIdx+1:] + p, err := strconv.Atoi(afP) + if err == nil { + return p + } + } + return 22 +} + +// commands that exists on all ssh interfaces, both Q and Iris +var standardCommands = Commands{Command{Name: "help", Description: "Opens up the assistance"}, + Command{Name: "exit", Description: "Exits from the terminal (if interactive shell)"}} + +func (s *SSHServer) writeHelp(wr io.Writer) { + port := parsePort(s.Host) + hostname := parseHostname(s.Host) + + data := map[string]interface{}{ + "Hostname": hostname, "Port": port, + "Commands": append(s.Commands, standardCommands...), + "Version": Version, + } + + helpTmpl.Execute(wr, data) +} + +var ( + errUserInvalid = errors.New("Username or Password rejected for: %q") + errServerListen = errors.New("Cannot listen to: %s, Trace: %s") +) + +// Listen starts the SSH Server +func (s *SSHServer) Listen() error { + + // get the key + privateKey, err := generateSigner(s.KeyPath, s.Bin) + if err != nil { + return err + } + // prepare the server's configuration + cfg := &ssh.ServerConfig{ + // NoClientAuth: true to allow anyone to login, nooo + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + username := c.User() + if !s.Users.exists(username, pass) { + return nil, errUserInvalid.Format(username) + } + return nil, nil + }} + + cfg.AddHostKey(privateKey) + + // start the server with the configuration we just made. + var lerr error + s.listener, lerr = net.Listen("tcp", s.Host) + if lerr != nil { + return errServerListen.Format(s.Host, lerr.Error()) + } + + // ready to accept incoming requests + s.logf("SSH Server is running") + for { + conn, err := s.listener.Accept() + if err != nil { + s.logf(err.Error()) + continue + } + // handshake first + + sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg) + if err != nil { + s.logf(err.Error()) + continue + } + + s.logf("New SSH Connection has been enstablish from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) + + // discard all global requests + go ssh.DiscardRequests(reqs) + // accept all current chanels + go s.handleChannels(chans) + } + +} + +func (s *SSHServer) handleChannels(chans <-chan ssh.NewChannel) { + for ch := range chans { + go s.handleChannel(ch) + } +} + +var errUnsupportedReqType = errors.New("Unsupported request type: %q") + +func (s *SSHServer) handleChannel(newChannel ssh.NewChannel) { + // we working from terminal, so only type of "session" is allowed. + if !validChannel(newChannel) { + return + } + + conn, reqs, err := newChannel.Accept() + if err != nil { + s.logf(err.Error()) + return + } + + go func(in <-chan *ssh.Request) { + defer func() { + conn.Close() + //debug + s.logf("Session closed") + }() + + for req := range in { + var err error + defer func() { + if err != nil { + conn.Write([]byte(err.Error())) + } + sendExitStatus(conn) + }() + + switch req.Type { + case "pty-req": + { + s.writeHelp(conn) + req.Reply(true, nil) + } + + case "shell": + { + // comes after pty-req, this is when the user just use this form: ssh kataras@mydomain.com -p 22 + // then we want interactive shell which will execute the commands: + term := terminal.NewTerminal(conn, "> ") + + for { + line, lerr := term.ReadLine() + if lerr == io.EOF { + return + } + if lerr != nil { + err = lerr + s.logf(lerr.Error()) + continue + } + + payload, perr := parsePayload(line, "") + if perr != nil { + err = perr + return + } + + if payload == "help" { + s.writeHelp(conn) + continue + } else if payload == "exit" { + return + } + + if cmd, found := s.Commands.ByName(payload); found { + cmd.Action(conn) + } else if s.Shell { + // yes every time check that + if isWindows { + execCmd(exec.Command("cmd", "/C", payload), conn) + } else { + execCmd(exec.Command("sh", "-c", payload), conn) + } + } else { + conn.Write([]byte(errInvalidSSHCommand.Format(payload).Error() + "\n")) + } + + //s.logf(line) + } + } + + case "exec": + { + // this is the place which the user executed something like that: ssh kataras@mydomain.com -p 22 stop + // a direct command, we don' t open the interactive shell, just execute the command and exit. + payload, perr := parsePayload(string(req.Payload), "") + if perr != nil { + err = perr + return + } + + if cmd, found := s.Commands.ByName(payload); found { + cmd.Action(conn) + } else if payload == "help" { + s.writeHelp(conn) + } else if s.Shell { + // yes every time check that + if isWindows { + execCmd(exec.Command("cmd", "/C", payload), conn) + } else { + execCmd(exec.Command("sh", "-c", payload), conn) + } + } else { + err = errInvalidSSHCommand.Format(payload) + } + return + } + default: + { + err = errUnsupportedReqType.Format(req.Type) + return + } + } + } + + }(reqs) + +} + +// NewLoggerHandler is a basic Logger middleware/Handler (not an Entry Parser) +func NewLoggerHandler(writer io.Writer, calculateLatency ...bool) HandlerFunc { + shouldNext := false + if len(calculateLatency) > 0 { + shouldNext = calculateLatency[0] + } + return func(ctx *Context) { + var date, status, ip, method, path string + var latency time.Duration + var startTime, endTime time.Time + path = ctx.PathString() + method = ctx.MethodString() + + startTime = time.Now() + if shouldNext { + ctx.Next() + } + + endTime = time.Now() + latency = endTime.Sub(startTime) + date = endTime.Format("01/02 - 15:04:05") + + status = strconv.Itoa(ctx.Response.StatusCode()) + ip = ctx.RemoteAddr() + + //finally print the logs to the ssh + writer.Write([]byte(fmt.Sprintf("%s %v %4v %s %s %s \n", date, status, latency, ip, method, path))) + } +}