Created
March 28, 2015 22:17
-
-
Save vito/dbf30553d158bd7a67bd to your computer and use it in GitHub Desktop.
golang ssh+http listener
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"io/ioutil" | |
"log" | |
"net" | |
"net/http" | |
"os" | |
"syscall" | |
"golang.org/x/crypto/ssh" | |
"golang.org/x/crypto/ssh/terminal" | |
) | |
type SSHTTPListener interface { | |
SSHListener() net.Listener | |
HTTPListener() net.Listener | |
} | |
type listener struct { | |
net.Listener | |
sshConns chan net.Conn | |
sshErrs chan error | |
httpConns chan net.Conn | |
httpErrs chan error | |
} | |
type fileConn interface { | |
File() (*os.File, error) | |
} | |
func (l *listener) Dispatch() error { | |
for { | |
c, err := l.Listener.Accept() | |
if err != nil { | |
l.sshErrs <- err | |
l.httpErrs <- err | |
continue | |
} | |
var connFile *os.File | |
if fileConn, ok := c.(fileConn); ok { | |
var err error | |
connFile, err = fileConn.File() | |
if err != nil { | |
l.sshErrs <- err | |
l.httpErrs <- err | |
continue | |
} | |
} else { | |
return fmt.Errorf("connection type cannot provide file: %T", c) | |
} | |
buf := make([]byte, 4) | |
n, addr, err := syscall.Recvfrom(int(connFile.Fd()), buf, syscall.MSG_PEEK) | |
if err != nil { | |
log.Println("peek conn type failed:", err) | |
continue | |
} | |
log.Println("RECVD", n, addr, string(buf[:n])) | |
if string(buf[:n]) == "SSH-" { | |
l.sshConns <- c | |
} else { | |
l.httpConns <- c | |
} | |
} | |
} | |
func (l *listener) HTTPListener() net.Listener { | |
return &chanListener{ | |
Listener: l.Listener, | |
conns: l.httpConns, | |
errs: l.httpErrs, | |
} | |
} | |
func (l *listener) SSHListener() net.Listener { | |
return &chanListener{ | |
Listener: l.Listener, | |
conns: l.sshConns, | |
errs: l.sshErrs, | |
} | |
} | |
type chanListener struct { | |
net.Listener | |
conns <-chan net.Conn | |
errs <-chan error | |
} | |
func (l *chanListener) Accept() (net.Conn, error) { | |
select { | |
case c := <-l.conns: | |
return c, nil | |
case e := <-l.errs: | |
return nil, e | |
} | |
} | |
func main() { | |
l, err := net.Listen("tcp", ":8081") | |
if err != nil { | |
log.Fatalln("failed to listen:", err) | |
} | |
sshttpListener := &listener{ | |
Listener: l, | |
sshConns: make(chan net.Conn), | |
sshErrs: make(chan error), | |
httpConns: make(chan net.Conn), | |
httpErrs: make(chan error), | |
} | |
go http.Serve(sshttpListener.HTTPListener(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintln(w, "hello from http!") | |
})) | |
sshL := sshttpListener.SSHListener() | |
config := &ssh.ServerConfig{ | |
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { | |
// Should use constant-time compare (or better, salt+hash) in | |
// a production setting. | |
if c.User() == "testuser" && string(pass) == "tiger" { | |
return nil, nil | |
} | |
return nil, fmt.Errorf("password rejected for %q", c.User()) | |
}, | |
} | |
privateBytes, err := ioutil.ReadFile("/home/alex/.ssh/id_rsa") | |
if err != nil { | |
panic("Failed to load private key") | |
} | |
private, err := ssh.ParsePrivateKey(privateBytes) | |
if err != nil { | |
panic("Failed to parse private key") | |
} | |
config.AddHostKey(private) | |
go func() { | |
for { | |
c, err := sshL.Accept() | |
if err != nil { | |
log.Println("SSH ACCEPT ERR", err) | |
break | |
} | |
_, chans, reqs, err := ssh.NewServerConn(c, config) | |
if err != nil { | |
log.Println("failed to handshake:", err) | |
continue | |
} | |
go ssh.DiscardRequests(reqs) | |
// Service the incoming Channel channel. | |
for newChannel := range chans { | |
// Channels have a type, depending on the application level | |
// protocol intended. In the case of a shell, the type is | |
// "session" and ServerShell may be used to present a simple | |
// terminal interface. | |
if newChannel.ChannelType() != "session" { | |
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") | |
continue | |
} | |
channel, requests, err := newChannel.Accept() | |
if err != nil { | |
panic("could not accept channel.") | |
} | |
// Sessions have out-of-band requests such as "shell", | |
// "pty-req" and "env". Here we handle only the | |
// "shell" request. | |
go func(in <-chan *ssh.Request) { | |
for req := range in { | |
ok := false | |
switch req.Type { | |
case "shell": | |
ok = true | |
if len(req.Payload) > 0 { | |
// We don't accept any | |
// commands, only the | |
// default shell. | |
ok = false | |
} | |
} | |
req.Reply(ok, nil) | |
} | |
}(requests) | |
term := terminal.NewTerminal(channel, "> ") | |
go func() { | |
defer channel.Close() | |
for { | |
line, err := term.ReadLine() | |
if err != nil { | |
break | |
} | |
fmt.Println(line) | |
} | |
}() | |
} | |
} | |
}() | |
err = sshttpListener.Dispatch() | |
if err != nil { | |
log.Fatalln("failed to dispatch:", err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment