initial commit
This commit is contained in:
commit
adbd4da93a
56
options.go
Normal file
56
options.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import "io/ioutil"
|
||||||
|
|
||||||
|
func PasswordAuth(fn PasswordHandler) Option {
|
||||||
|
return func(srv *Server) error {
|
||||||
|
srv.PasswordHandler = fn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PublicKeyAuth(fn PublicKeyHandler) Option {
|
||||||
|
return func(srv *Server) error {
|
||||||
|
srv.PublicKeyHandler = fn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HostKeyFile(filepath string) Option {
|
||||||
|
return func(srv *Server) error {
|
||||||
|
pemBytes, err := ioutil.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, block := range decodePemBlocks(pemBytes) {
|
||||||
|
signer, err := signerFromBlock(block)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.HostSigners = append(srv.HostSigners, signer)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HostKeyPEM(bytes []byte) Option {
|
||||||
|
return func(srv *Server) error {
|
||||||
|
for _, block := range decodePemBlocks(bytes) {
|
||||||
|
signer, err := signerFromBlock(block)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.HostSigners = append(srv.HostSigners, signer)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoPty() Option {
|
||||||
|
return func(srv *Server) error {
|
||||||
|
srv.PtyCallback = func(user string, permissions *Permissions) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
151
server.go
Normal file
151
server.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Addr string
|
||||||
|
Handler Handler
|
||||||
|
HostSigners []Signer
|
||||||
|
PasswordHandler PasswordHandler
|
||||||
|
PublicKeyHandler PublicKeyHandler
|
||||||
|
PermissionsCallback PermissionsCallback
|
||||||
|
PtyCallback PtyCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) makeConfig() (*gossh.ServerConfig, error) {
|
||||||
|
config := &gossh.ServerConfig{}
|
||||||
|
if len(srv.HostSigners) == 0 {
|
||||||
|
signer, err := generateSigner()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
srv.HostSigners = append(srv.HostSigners, signer)
|
||||||
|
}
|
||||||
|
for _, signer := range srv.HostSigners {
|
||||||
|
config.AddHostKey(signer)
|
||||||
|
}
|
||||||
|
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
|
||||||
|
config.NoClientAuth = true
|
||||||
|
}
|
||||||
|
if srv.PasswordHandler != nil {
|
||||||
|
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
||||||
|
perms := &gossh.Permissions{}
|
||||||
|
if ok := srv.PasswordHandler(conn.User(), string(password)); !ok {
|
||||||
|
return perms, fmt.Errorf("permission denied")
|
||||||
|
}
|
||||||
|
if srv.PermissionsCallback != nil {
|
||||||
|
srv.PermissionsCallback(conn.User(), &Permissions{perms})
|
||||||
|
}
|
||||||
|
return perms, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if srv.PublicKeyHandler != nil {
|
||||||
|
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||||
|
perms := &gossh.Permissions{}
|
||||||
|
if ok := srv.PublicKeyHandler(conn.User(), key); !ok {
|
||||||
|
return perms, fmt.Errorf("permission denied")
|
||||||
|
}
|
||||||
|
perms.Extensions = map[string]string{
|
||||||
|
"_publickey": string(key.Marshal()),
|
||||||
|
}
|
||||||
|
if srv.PermissionsCallback != nil {
|
||||||
|
srv.PermissionsCallback(conn.User(), &Permissions{perms})
|
||||||
|
}
|
||||||
|
return perms, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) Handle(fn Handler) {
|
||||||
|
srv.Handler = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) Serve(l net.Listener) error {
|
||||||
|
defer l.Close()
|
||||||
|
config, err := srv.makeConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if srv.Handler == nil {
|
||||||
|
srv.Handler = defaultHandler
|
||||||
|
}
|
||||||
|
var tempDelay time.Duration
|
||||||
|
for {
|
||||||
|
conn, e := l.Accept()
|
||||||
|
if e != nil {
|
||||||
|
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
||||||
|
if tempDelay == 0 {
|
||||||
|
tempDelay = 5 * time.Millisecond
|
||||||
|
} else {
|
||||||
|
tempDelay *= 2
|
||||||
|
}
|
||||||
|
if max := 1 * time.Second; tempDelay > max {
|
||||||
|
tempDelay = max
|
||||||
|
}
|
||||||
|
//srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
|
||||||
|
time.Sleep(tempDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
go srv.handleConn(conn, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) handleConn(conn net.Conn, conf *gossh.ServerConfig) {
|
||||||
|
defer conn.Close()
|
||||||
|
sshConn, chans, reqs, err := gossh.NewServerConn(conn, conf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go gossh.DiscardRequests(reqs)
|
||||||
|
for ch := range chans {
|
||||||
|
if ch.ChannelType() != "session" {
|
||||||
|
ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go srv.handleChannel(sshConn, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) handleChannel(conn *gossh.ServerConn, newChan gossh.NewChannel) {
|
||||||
|
ch, reqs, err := newChan.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sess := srv.newSession(conn, ch)
|
||||||
|
sess.handleRequests(reqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) newSession(conn *gossh.ServerConn, ch gossh.Channel) *session {
|
||||||
|
sess := &session{
|
||||||
|
Channel: ch,
|
||||||
|
conn: conn,
|
||||||
|
handler: srv.Handler,
|
||||||
|
ptyCb: srv.PtyCallback,
|
||||||
|
}
|
||||||
|
return sess
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) ListenAndServe() error {
|
||||||
|
addr := srv.Addr
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":22"
|
||||||
|
}
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) SetOption(option Option) error {
|
||||||
|
return option(srv)
|
||||||
|
}
|
149
session.go
Normal file
149
session.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/anmitsu/go-shlex"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session interface {
|
||||||
|
gossh.Channel
|
||||||
|
User() string
|
||||||
|
RemoteAddr() net.Addr
|
||||||
|
Environ() []string
|
||||||
|
Exit(code int) error
|
||||||
|
Command() []string
|
||||||
|
//Signals(c chan<- Signal)
|
||||||
|
PublicKey() PublicKey
|
||||||
|
Pty() (Pty, <-chan Window, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
gossh.Channel
|
||||||
|
conn *gossh.ServerConn
|
||||||
|
handler Handler
|
||||||
|
handled bool
|
||||||
|
pty *Pty
|
||||||
|
winch chan Window
|
||||||
|
env []string
|
||||||
|
ptyCb PtyCallback
|
||||||
|
cmd []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) Write(p []byte) (n int, err error) {
|
||||||
|
if sess.pty != nil {
|
||||||
|
// normalize \n to \r\n when pty is accepted
|
||||||
|
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
|
||||||
|
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
|
||||||
|
}
|
||||||
|
return sess.Channel.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) PublicKey() PublicKey {
|
||||||
|
if sess.conn.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s, ok := sess.conn.Permissions.Extensions["_publickey"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key, err := ParsePublicKey([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) Exit(code int) error {
|
||||||
|
status := struct{ Status uint32 }{uint32(code)}
|
||||||
|
_, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sess.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) User() string {
|
||||||
|
return sess.conn.User()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) RemoteAddr() net.Addr {
|
||||||
|
return sess.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) Environ() []string {
|
||||||
|
return append([]string(nil), sess.env...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) Command() []string {
|
||||||
|
return append([]string(nil), sess.cmd...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) Pty() (Pty, <-chan Window, bool) {
|
||||||
|
if sess.pty != nil {
|
||||||
|
return *sess.pty, sess.winch, true
|
||||||
|
}
|
||||||
|
return Pty{}, sess.winch, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
|
||||||
|
for req := range reqs {
|
||||||
|
var width, height int
|
||||||
|
var ok bool
|
||||||
|
switch req.Type {
|
||||||
|
case "shell", "exec":
|
||||||
|
if sess.handled {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var payload = struct{ Value string }{}
|
||||||
|
gossh.Unmarshal(req.Payload, &payload)
|
||||||
|
sess.cmd, _ = shlex.Split(payload.Value, true)
|
||||||
|
go func() {
|
||||||
|
sess.handler(sess)
|
||||||
|
sess.Exit(0)
|
||||||
|
}()
|
||||||
|
sess.handled = true
|
||||||
|
req.Reply(true, nil)
|
||||||
|
case "env":
|
||||||
|
if sess.handled {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var kv = struct{ Key, Value string }{}
|
||||||
|
gossh.Unmarshal(req.Payload, &kv)
|
||||||
|
sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
|
||||||
|
case "pty-req":
|
||||||
|
if sess.handled {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sess.ptyCb != nil {
|
||||||
|
ok := sess.ptyCb(sess.conn.User(), &Permissions{sess.conn.Permissions})
|
||||||
|
if !ok {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width, height, ok = parsePtyRequest(req.Payload)
|
||||||
|
if ok {
|
||||||
|
sess.pty = &Pty{Window{width, height}}
|
||||||
|
sess.winch = make(chan Window)
|
||||||
|
req.Reply(true, nil)
|
||||||
|
}
|
||||||
|
case "window-change":
|
||||||
|
if sess.pty == nil {
|
||||||
|
req.Reply(false, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
width, height, ok = parseWinchRequest(req.Payload)
|
||||||
|
if ok {
|
||||||
|
sess.pty.Window = Window{width, height}
|
||||||
|
sess.winch <- sess.pty.Window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
ssh.go
Normal file
78
ssh.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Signal string
|
||||||
|
|
||||||
|
// POSIX signals as listed in RFC 4254 Section 6.10.
|
||||||
|
const (
|
||||||
|
SIGABRT Signal = "ABRT"
|
||||||
|
SIGALRM Signal = "ALRM"
|
||||||
|
SIGFPE Signal = "FPE"
|
||||||
|
SIGHUP Signal = "HUP"
|
||||||
|
SIGILL Signal = "ILL"
|
||||||
|
SIGINT Signal = "INT"
|
||||||
|
SIGKILL Signal = "KILL"
|
||||||
|
SIGPIPE Signal = "PIPE"
|
||||||
|
SIGQUIT Signal = "QUIT"
|
||||||
|
SIGSEGV Signal = "SEGV"
|
||||||
|
SIGTERM Signal = "TERM"
|
||||||
|
SIGUSR1 Signal = "USR1"
|
||||||
|
SIGUSR2 Signal = "USR2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultHandler Handler
|
||||||
|
|
||||||
|
type Option func(*Server) error
|
||||||
|
type Handler func(Session)
|
||||||
|
|
||||||
|
type PublicKeyHandler func(user string, key PublicKey) bool
|
||||||
|
type PasswordHandler func(user, password string) bool
|
||||||
|
|
||||||
|
type PermissionsCallback func(user string, permissions *Permissions) error
|
||||||
|
type PtyCallback func(user string, permissions *Permissions) bool
|
||||||
|
|
||||||
|
type Window struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pty struct {
|
||||||
|
Window Window
|
||||||
|
}
|
||||||
|
|
||||||
|
func Serve(l net.Listener, handler Handler, options ...Option) error {
|
||||||
|
srv := &Server{Handler: handler}
|
||||||
|
for _, option := range options {
|
||||||
|
if err := srv.SetOption(option); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return srv.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenAndServe(addr string, handler Handler, options ...Option) error {
|
||||||
|
srv := &Server{Addr: addr, Handler: handler}
|
||||||
|
for _, option := range options {
|
||||||
|
if err := srv.SetOption(option); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return srv.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handle(handler Handler) {
|
||||||
|
defaultHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeysEqual is constant time compare of the keys to avoid timing attacks
|
||||||
|
func KeysEqual(ak, bk PublicKey) bool {
|
||||||
|
a := gossh.Marshal(ak)
|
||||||
|
b := gossh.Marshal(bk)
|
||||||
|
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
|
||||||
|
}
|
118
util.go
Normal file
118
util.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func signerFromBlock(block *pem.Block) (ssh.Signer, error) {
|
||||||
|
var key interface{}
|
||||||
|
var err error
|
||||||
|
switch block.Type {
|
||||||
|
case "RSA PRIVATE KEY":
|
||||||
|
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
case "EC PRIVATE KEY":
|
||||||
|
key, err = x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
case "DSA PRIVATE KEY":
|
||||||
|
key, err = ssh.ParseDSAPrivateKey(block.Bytes)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported key type %q", block.Type)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signer, err := ssh.NewSignerFromKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePemBlocks(pemData []byte) []*pem.Block {
|
||||||
|
var blocks []*pem.Block
|
||||||
|
var block *pem.Block
|
||||||
|
for {
|
||||||
|
block, pemData = pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
blocks = append(blocks, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSigner() (ssh.Signer, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 768)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ssh.NewSignerFromKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePtyRequest(s []byte) (width, height int, ok bool) {
|
||||||
|
_, s, ok = parseString(s)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
width32, s, ok := parseUint32(s)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
height32, _, ok := parseUint32(s)
|
||||||
|
width = int(width32)
|
||||||
|
height = int(height32)
|
||||||
|
if width < 1 {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if height < 1 {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWinchRequest(s []byte) (width, height int, ok bool) {
|
||||||
|
width32, _, ok := parseUint32(s)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
height32, _, ok := parseUint32(s)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
width = int(width32)
|
||||||
|
height = int(height32)
|
||||||
|
if width < 1 {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if height < 1 {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseString(in []byte) (out string, rest []byte, ok bool) {
|
||||||
|
if len(in) < 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint32(in)
|
||||||
|
if uint32(len(in)) < 4+length {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out = string(in[4 : 4+length])
|
||||||
|
rest = in[4+length:]
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint32(in []byte) (uint32, []byte, bool) {
|
||||||
|
if len(in) < 4 {
|
||||||
|
return 0, nil, false
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(in), in[4:], true
|
||||||
|
}
|
23
wrap.go
Normal file
23
wrap.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import gossh "golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
type PublicKey interface {
|
||||||
|
gossh.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permissions struct {
|
||||||
|
*gossh.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signer interface {
|
||||||
|
gossh.Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
|
||||||
|
return gossh.ParseAuthorizedKey(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePublicKey(in []byte) (out PublicKey, err error) {
|
||||||
|
return gossh.ParsePublicKey(in)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user