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 @@
-
+

@@ -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)))
+ }
+}