resolves git conflicts from #3896 (credit to @belak, in case github doesn't keep original author during squash) Co-Authored-By: Matti Ranta <techknowlogick@gitea.io>tags/v1.10.0-rc1
@@ -40,7 +40,7 @@ require ( | |||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect | |||
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 // indirect | |||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect | |||
github.com/gliderlabs/ssh v0.1.4 // indirect | |||
github.com/gliderlabs/ssh v0.2.2 | |||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd // indirect | |||
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e // indirect | |||
github.com/go-macaron/binding v0.0.0-20160711225916-9440f336b443 | |||
@@ -110,11 +110,11 @@ require ( | |||
github.com/yohcop/openid-go v0.0.0-20160914080427-2c050d2dae53 | |||
go.etcd.io/bbolt v1.3.2 // indirect | |||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 | |||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 | |||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b | |||
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 | |||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f | |||
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45 | |||
golang.org/x/text v0.3.2 | |||
golang.org/x/tools v0.0.0-20190618163018-fdf1049a943a // indirect | |||
golang.org/x/tools v0.0.0-20190620154339-431033348dd0 // indirect | |||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | |||
gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect | |||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect |
@@ -100,8 +100,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI | |||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= | |||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | |||
github.com/gliderlabs/ssh v0.1.3/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= | |||
github.com/gliderlabs/ssh v0.1.4 h1:5N8AYXpaQAPy0L7linKa5aI+WRfyYagAhjksVzxh+mI= | |||
github.com/gliderlabs/ssh v0.1.4/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= | |||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= | |||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= | |||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd h1:r04MMPyLHj/QwZuMJ5+7tJcBr1AQjpiAK/rZWRrQT7o= | |||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= | |||
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e h1:SiEs4J3BKVIeaWrH3tKaz3QLZhJ68iJ/A4xrzIoE5+Y= | |||
@@ -369,8 +369,8 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r | |||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |||
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= | |||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b h1:lkjdUzSyJ5P1+eal9fxXX9Xg2BTfswsonKUse48C0uE= | |||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 h1:TMrx+Qdx7uJAeUbv15N72h5Hmyb5+VDjEiMufAEAM04= | |||
@@ -395,15 +395,15 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqY | |||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f h1:dHNZYIYdq2QuU6w73vZ/DzesPbVlZVYZTtTZmrnsbQ8= | |||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45 h1:Dl2hc890lrizvUppGbRWhnIh2f8jOTCQpY5IKWRS0oM= | |||
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | |||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= | |||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | |||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |||
golang.org/x/tools v0.0.0-20190618163018-fdf1049a943a h1:aQmaYPOmKItb96VioBrTlYay5tSNUdKAFEhPCWMeLSM= | |||
golang.org/x/tools v0.0.0-20190618163018-fdf1049a943a/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | |||
golang.org/x/tools v0.0.0-20190620154339-431033348dd0 h1:qUGDNmGEM+ZBtwF9vuzEv+9nQQPL+l/oNBZ+DCDTAyo= | |||
golang.org/x/tools v0.0.0-20190620154339-431033348dd0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | |||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | |||
google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24= | |||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-mssql | |||
OFFLINE_MODE = false | |||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | |||
APP_DATA_PATH = integrations/gitea-integration-mssql/data | |||
BUILTIN_SSH_SERVER_USER = git | |||
[mailer] | |||
ENABLED = true |
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-mysql | |||
OFFLINE_MODE = false | |||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | |||
APP_DATA_PATH = integrations/gitea-integration-mysql/data | |||
BUILTIN_SSH_SERVER_USER = git | |||
[mailer] | |||
ENABLED = true |
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-mysql8 | |||
OFFLINE_MODE = false | |||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | |||
APP_DATA_PATH = integrations/gitea-integration-mysql8/data | |||
BUILTIN_SSH_SERVER_USER = git | |||
[mailer] | |||
ENABLED = false |
@@ -34,6 +34,7 @@ LFS_CONTENT_PATH = data/lfs-pgsql | |||
OFFLINE_MODE = false | |||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | |||
APP_DATA_PATH = integrations/gitea-integration-pgsql/data | |||
BUILTIN_SSH_SERVER_USER = git | |||
[mailer] | |||
ENABLED = true |
@@ -73,7 +73,7 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { | |||
assert.Equal(t, setting.AppURL+"user2/repo1.git", link) | |||
link, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link") | |||
assert.True(t, exists, "The template has changed") | |||
sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.RunUser, setting.SSH.Domain, setting.SSH.Port) | |||
sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.BuiltinServerUser, setting.SSH.Domain, setting.SSH.Port) | |||
assert.Equal(t, sshURL, link) | |||
} | |||
@@ -31,6 +31,7 @@ OFFLINE_MODE = false | |||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | |||
APP_DATA_PATH = integrations/gitea-integration-sqlite/data | |||
ENABLE_GZIP = true | |||
BUILTIN_SSH_SERVER_USER = git | |||
[mailer] | |||
ENABLED = true |
@@ -1,4 +1,3 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Copyright 2017 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
@@ -10,178 +9,157 @@ import ( | |||
"crypto/rsa" | |||
"crypto/x509" | |||
"encoding/pem" | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"net" | |||
"os" | |||
"os/exec" | |||
"path/filepath" | |||
"strings" | |||
"github.com/Unknwon/com" | |||
"golang.org/x/crypto/ssh" | |||
"sync" | |||
"syscall" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"github.com/Unknwon/com" | |||
"github.com/gliderlabs/ssh" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
func cleanCommand(cmd string) string { | |||
i := strings.Index(cmd, "git") | |||
if i == -1 { | |||
return cmd | |||
type contextKey string | |||
const giteaKeyID = contextKey("gitea-key-id") | |||
func getExitStatusFromError(err error) int { | |||
if err == nil { | |||
return 0 | |||
} | |||
return cmd[i:] | |||
} | |||
func handleServerConn(keyID string, chans <-chan ssh.NewChannel) { | |||
for newChan := range chans { | |||
if newChan.ChannelType() != "session" { | |||
err := newChan.Reject(ssh.UnknownChannelType, "unknown channel type") | |||
if err != nil { | |||
log.Error("Error rejecting channel: %v", err) | |||
} | |||
continue | |||
} | |||
exitErr, ok := err.(*exec.ExitError) | |||
if !ok { | |||
return 1 | |||
} | |||
ch, reqs, err := newChan.Accept() | |||
if err != nil { | |||
log.Error("Error accepting channel: %v", err) | |||
continue | |||
waitStatus, ok := exitErr.Sys().(syscall.WaitStatus) | |||
if !ok { | |||
// This is a fallback and should at least let us return something useful | |||
// when running on Windows, even if it isn't completely accurate. | |||
if exitErr.Success() { | |||
return 0 | |||
} | |||
go func(in <-chan *ssh.Request) { | |||
defer func() { | |||
if err = ch.Close(); err != nil { | |||
log.Error("Close: %v", err) | |||
} | |||
}() | |||
for req := range in { | |||
payload := cleanCommand(string(req.Payload)) | |||
switch req.Type { | |||
case "exec": | |||
cmdName := strings.TrimLeft(payload, "'()") | |||
log.Trace("SSH: Payload: %v", cmdName) | |||
args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf} | |||
log.Trace("SSH: Arguments: %v", args) | |||
cmd := exec.Command(setting.AppPath, args...) | |||
cmd.Env = append( | |||
os.Environ(), | |||
"SSH_ORIGINAL_COMMAND="+cmdName, | |||
"SKIP_MINWINSVC=1", | |||
) | |||
stdout, err := cmd.StdoutPipe() | |||
if err != nil { | |||
log.Error("SSH: StdoutPipe: %v", err) | |||
return | |||
} | |||
stderr, err := cmd.StderrPipe() | |||
if err != nil { | |||
log.Error("SSH: StderrPipe: %v", err) | |||
return | |||
} | |||
input, err := cmd.StdinPipe() | |||
if err != nil { | |||
log.Error("SSH: StdinPipe: %v", err) | |||
return | |||
} | |||
// FIXME: check timeout | |||
if err = cmd.Start(); err != nil { | |||
log.Error("SSH: Start: %v", err) | |||
return | |||
} | |||
err = req.Reply(true, nil) | |||
if err != nil { | |||
log.Error("SSH: Reply: %v", err) | |||
} | |||
go func() { | |||
_, err = io.Copy(input, ch) | |||
if err != nil { | |||
log.Error("SSH: Copy: %v", err) | |||
} | |||
}() | |||
_, err = io.Copy(ch, stdout) | |||
if err != nil { | |||
log.Error("SSH: Copy: %v", err) | |||
} | |||
_, err = io.Copy(ch.Stderr(), stderr) | |||
if err != nil { | |||
log.Error("SSH: Copy: %v", err) | |||
} | |||
if err = cmd.Wait(); err != nil { | |||
log.Error("SSH: Wait: %v", err) | |||
return | |||
} | |||
_, err = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) | |||
if err != nil { | |||
log.Error("SSH: SendRequest: %v", err) | |||
} | |||
return | |||
default: | |||
} | |||
} | |||
}(reqs) | |||
return 1 | |||
} | |||
return waitStatus.ExitStatus() | |||
} | |||
func listen(config *ssh.ServerConfig, host string, port int) { | |||
listener, err := net.Listen("tcp", host+":"+com.ToStr(port)) | |||
func sessionHandler(session ssh.Session) { | |||
keyID := session.Context().Value(giteaKeyID).(int64) | |||
command := session.RawCommand() | |||
log.Trace("SSH: Payload: %v", command) | |||
args := []string{"serv", "key-" + com.ToStr(keyID), "--config=" + setting.CustomConf} | |||
log.Trace("SSH: Arguments: %v", args) | |||
cmd := exec.Command(setting.AppPath, args...) | |||
cmd.Env = append( | |||
os.Environ(), | |||
"SSH_ORIGINAL_COMMAND="+command, | |||
"SKIP_MINWINSVC=1", | |||
) | |||
stdout, err := cmd.StdoutPipe() | |||
if err != nil { | |||
log.Fatal("Failed to start SSH server: %v", err) | |||
log.Error("SSH: StdoutPipe: %v", err) | |||
return | |||
} | |||
for { | |||
// Once a ServerConfig has been configured, connections can be accepted. | |||
conn, err := listener.Accept() | |||
if err != nil { | |||
log.Error("SSH: Error accepting incoming connection: %v", err) | |||
continue | |||
stderr, err := cmd.StderrPipe() | |||
if err != nil { | |||
log.Error("SSH: StderrPipe: %v", err) | |||
return | |||
} | |||
stdin, err := cmd.StdinPipe() | |||
if err != nil { | |||
log.Error("SSH: StdinPipe: %v", err) | |||
return | |||
} | |||
wg := &sync.WaitGroup{} | |||
wg.Add(2) | |||
if err = cmd.Start(); err != nil { | |||
log.Error("SSH: Start: %v", err) | |||
return | |||
} | |||
go func() { | |||
defer stdin.Close() | |||
if _, err := io.Copy(stdin, session); err != nil { | |||
log.Error("Failed to write session to stdin. %s", err) | |||
} | |||
}() | |||
go func() { | |||
defer wg.Done() | |||
if _, err := io.Copy(session, stdout); err != nil { | |||
log.Error("Failed to write stdout to session. %s", err) | |||
} | |||
}() | |||
go func() { | |||
defer wg.Done() | |||
if _, err := io.Copy(session.Stderr(), stderr); err != nil { | |||
log.Error("Failed to write stderr to session. %s", err) | |||
} | |||
}() | |||
// Ensure all the output has been written before we wait on the command | |||
// to exit. | |||
wg.Wait() | |||
// Wait for the command to exit and log any errors we get | |||
err = cmd.Wait() | |||
if err != nil { | |||
log.Error("SSH: Wait: %v", err) | |||
} | |||
if err := session.Exit(getExitStatusFromError(err)); err != nil { | |||
log.Error("Session failed to exit. %s", err) | |||
} | |||
} | |||
func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { | |||
if ctx.User() != setting.SSH.BuiltinServerUser { | |||
return false | |||
} | |||
// Before use, a handshake must be performed on the incoming net.Conn. | |||
// It must be handled in a separate goroutine, | |||
// otherwise one user could easily block entire loop. | |||
// For example, user could be asked to trust server key fingerprint and hangs. | |||
go func() { | |||
log.Trace("SSH: Handshaking for %s", conn.RemoteAddr()) | |||
sConn, chans, reqs, err := ssh.NewServerConn(conn, config) | |||
if err != nil { | |||
if err == io.EOF { | |||
log.Warn("SSH: Handshaking with %s was terminated: %v", conn.RemoteAddr(), err) | |||
} else { | |||
log.Error("SSH: Error on handshaking with %s: %v", conn.RemoteAddr(), err) | |||
} | |||
return | |||
} | |||
log.Trace("SSH: Connection from %s (%s)", sConn.RemoteAddr(), sConn.ClientVersion()) | |||
// The incoming Request channel must be serviced. | |||
go ssh.DiscardRequests(reqs) | |||
go handleServerConn(sConn.Permissions.Extensions["key-id"], chans) | |||
}() | |||
pkey, err := models.SearchPublicKeyByContent(strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key)))) | |||
if err != nil { | |||
log.Error("SearchPublicKeyByContent: %v", err) | |||
return false | |||
} | |||
ctx.SetValue(giteaKeyID, pkey.ID) | |||
return true | |||
} | |||
// Listen starts a SSH server listens on given port. | |||
func Listen(host string, port int, ciphers []string, keyExchanges []string, macs []string) { | |||
config := &ssh.ServerConfig{ | |||
Config: ssh.Config{ | |||
Ciphers: ciphers, | |||
KeyExchanges: keyExchanges, | |||
MACs: macs, | |||
}, | |||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | |||
pkey, err := models.SearchPublicKeyByContent(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))) | |||
if err != nil { | |||
log.Error("SearchPublicKeyByContent: %v", err) | |||
return nil, err | |||
} | |||
return &ssh.Permissions{Extensions: map[string]string{"key-id": com.ToStr(pkey.ID)}}, nil | |||
// TODO: Handle ciphers, keyExchanges, and macs | |||
srv := ssh.Server{ | |||
Addr: fmt.Sprintf("%s:%d", host, port), | |||
PublicKeyHandler: publicKeyHandler, | |||
Handler: sessionHandler, | |||
// We need to explicitly disable the PtyCallback so text displays | |||
// properly. | |||
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool { | |||
return false | |||
}, | |||
} | |||
@@ -197,20 +175,21 @@ func Listen(host string, port int, ciphers []string, keyExchanges []string, macs | |||
if err != nil { | |||
log.Fatal("Failed to generate private key: %v", err) | |||
} | |||
log.Trace("SSH: New private key is generateed: %s", keyPath) | |||
log.Trace("New private key is generated: %s", keyPath) | |||
} | |||
privateBytes, err := ioutil.ReadFile(keyPath) | |||
err := srv.SetOption(ssh.HostKeyFile(keyPath)) | |||
if err != nil { | |||
log.Fatal("SSH: Failed to load private key") | |||
log.Error("Failed to set Host Key. %s", err) | |||
} | |||
private, err := ssh.ParsePrivateKey(privateBytes) | |||
if err != nil { | |||
log.Fatal("SSH: Failed to parse private key") | |||
} | |||
config.AddHostKey(private) | |||
go listen(config, host, port) | |||
go func() { | |||
err := srv.ListenAndServe() | |||
if err != nil { | |||
log.Error("Failed to serve with builtin SSH server. %s", err) | |||
} | |||
}() | |||
} | |||
// GenKeyPair make a pair of public and private keys for SSH access. | |||
@@ -238,12 +217,12 @@ func GenKeyPair(keyPath string) error { | |||
} | |||
// generate public key | |||
pub, err := ssh.NewPublicKey(&privateKey.PublicKey) | |||
pub, err := gossh.NewPublicKey(&privateKey.PublicKey) | |||
if err != nil { | |||
return err | |||
} | |||
public := ssh.MarshalAuthorizedKey(pub) | |||
public := gossh.MarshalAuthorizedKey(pub) | |||
p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) | |||
if err != nil { | |||
return err |
@@ -0,0 +1 @@ | |||
shlex.test |
@@ -0,0 +1,20 @@ | |||
Copyright (c) anmitsu <anmitsu.s@gmail.com> | |||
Permission is hereby granted, free of charge, to any person obtaining | |||
a copy of this software and associated documentation files (the | |||
"Software"), to deal in the Software without restriction, including | |||
without limitation the rights to use, copy, modify, merge, publish, | |||
distribute, sublicense, and/or sell copies of the Software, and to | |||
permit persons to whom the Software is furnished to do so, subject to | |||
the following conditions: | |||
The above copyright notice and this permission notice shall be | |||
included in all copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@@ -0,0 +1,38 @@ | |||
# go-shlex | |||
go-shlex is a library to make a lexical analyzer like Unix shell for | |||
Go. | |||
## Install | |||
go get -u "github.com/anmitsu/go-shlex" | |||
## Usage | |||
```go | |||
package main | |||
import ( | |||
"fmt" | |||
"log" | |||
"github.com/anmitsu/go-shlex" | |||
) | |||
func main() { | |||
cmd := `cp -Rdp "file name" 'file name2' dir\ name` | |||
words, err := shlex.Split(cmd, true) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
for _, w := range words { | |||
fmt.Println(w) | |||
} | |||
} | |||
``` | |||
## Documentation | |||
http://godoc.org/github.com/anmitsu/go-shlex | |||
@@ -0,0 +1,193 @@ | |||
// Package shlex provides a simple lexical analysis like Unix shell. | |||
package shlex | |||
import ( | |||
"bufio" | |||
"errors" | |||
"io" | |||
"strings" | |||
"unicode" | |||
) | |||
var ( | |||
ErrNoClosing = errors.New("No closing quotation") | |||
ErrNoEscaped = errors.New("No escaped character") | |||
) | |||
// Tokenizer is the interface that classifies a token according to | |||
// words, whitespaces, quotations, escapes and escaped quotations. | |||
type Tokenizer interface { | |||
IsWord(rune) bool | |||
IsWhitespace(rune) bool | |||
IsQuote(rune) bool | |||
IsEscape(rune) bool | |||
IsEscapedQuote(rune) bool | |||
} | |||
// DefaultTokenizer implements a simple tokenizer like Unix shell. | |||
type DefaultTokenizer struct{} | |||
func (t *DefaultTokenizer) IsWord(r rune) bool { | |||
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r) | |||
} | |||
func (t *DefaultTokenizer) IsQuote(r rune) bool { | |||
switch r { | |||
case '\'', '"': | |||
return true | |||
default: | |||
return false | |||
} | |||
} | |||
func (t *DefaultTokenizer) IsWhitespace(r rune) bool { | |||
return unicode.IsSpace(r) | |||
} | |||
func (t *DefaultTokenizer) IsEscape(r rune) bool { | |||
return r == '\\' | |||
} | |||
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool { | |||
return r == '"' | |||
} | |||
// Lexer represents a lexical analyzer. | |||
type Lexer struct { | |||
reader *bufio.Reader | |||
tokenizer Tokenizer | |||
posix bool | |||
whitespacesplit bool | |||
} | |||
// NewLexer creates a new Lexer reading from io.Reader. This Lexer | |||
// has a DefaultTokenizer according to posix and whitespacesplit | |||
// rules. | |||
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer { | |||
return &Lexer{ | |||
reader: bufio.NewReader(r), | |||
tokenizer: &DefaultTokenizer{}, | |||
posix: posix, | |||
whitespacesplit: whitespacesplit, | |||
} | |||
} | |||
// NewLexerString creates a new Lexer reading from a string. This | |||
// Lexer has a DefaultTokenizer according to posix and whitespacesplit | |||
// rules. | |||
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer { | |||
return NewLexer(strings.NewReader(s), posix, whitespacesplit) | |||
} | |||
// Split splits a string according to posix or non-posix rules. | |||
func Split(s string, posix bool) ([]string, error) { | |||
return NewLexerString(s, posix, true).Split() | |||
} | |||
// SetTokenizer sets a Tokenizer. | |||
func (l *Lexer) SetTokenizer(t Tokenizer) { | |||
l.tokenizer = t | |||
} | |||
func (l *Lexer) Split() ([]string, error) { | |||
result := make([]string, 0) | |||
for { | |||
token, err := l.readToken() | |||
if token != "" { | |||
result = append(result, token) | |||
} | |||
if err == io.EOF { | |||
break | |||
} else if err != nil { | |||
return result, err | |||
} | |||
} | |||
return result, nil | |||
} | |||
func (l *Lexer) readToken() (string, error) { | |||
t := l.tokenizer | |||
token := "" | |||
quoted := false | |||
state := ' ' | |||
escapedstate := ' ' | |||
scanning: | |||
for { | |||
next, _, err := l.reader.ReadRune() | |||
if err != nil { | |||
if t.IsQuote(state) { | |||
return token, ErrNoClosing | |||
} else if t.IsEscape(state) { | |||
return token, ErrNoEscaped | |||
} | |||
return token, err | |||
} | |||
switch { | |||
case t.IsWhitespace(state): | |||
switch { | |||
case t.IsWhitespace(next): | |||
break scanning | |||
case l.posix && t.IsEscape(next): | |||
escapedstate = 'a' | |||
state = next | |||
case t.IsWord(next): | |||
token += string(next) | |||
state = 'a' | |||
case t.IsQuote(next): | |||
if !l.posix { | |||
token += string(next) | |||
} | |||
state = next | |||
default: | |||
token = string(next) | |||
if l.whitespacesplit { | |||
state = 'a' | |||
} else if token != "" || (l.posix && quoted) { | |||
break scanning | |||
} | |||
} | |||
case t.IsQuote(state): | |||
quoted = true | |||
switch { | |||
case next == state: | |||
if !l.posix { | |||
token += string(next) | |||
break scanning | |||
} else { | |||
state = 'a' | |||
} | |||
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state): | |||
escapedstate = state | |||
state = next | |||
default: | |||
token += string(next) | |||
} | |||
case t.IsEscape(state): | |||
if t.IsQuote(escapedstate) && next != state && next != escapedstate { | |||
token += string(state) | |||
} | |||
token += string(next) | |||
state = escapedstate | |||
case t.IsWord(state): | |||
switch { | |||
case t.IsWhitespace(next): | |||
if token != "" || (l.posix && quoted) { | |||
break scanning | |||
} | |||
case l.posix && t.IsQuote(next): | |||
state = next | |||
case l.posix && t.IsEscape(next): | |||
escapedstate = 'a' | |||
state = next | |||
case t.IsWord(next) || t.IsQuote(next): | |||
token += string(next) | |||
default: | |||
if l.whitespacesplit { | |||
token += string(next) | |||
} else if token != "" { | |||
l.reader.UnreadRune() | |||
break scanning | |||
} | |||
} | |||
} | |||
} | |||
return token, nil | |||
} |
@@ -0,0 +1,27 @@ | |||
Copyright (c) 2016 Glider Labs. All rights reserved. | |||
Redistribution and use in source and binary forms, with or without | |||
modification, are permitted provided that the following conditions are | |||
met: | |||
* Redistributions of source code must retain the above copyright | |||
notice, this list of conditions and the following disclaimer. | |||
* Redistributions in binary form must reproduce the above | |||
copyright notice, this list of conditions and the following disclaimer | |||
in the documentation and/or other materials provided with the | |||
distribution. | |||
* Neither the name of Glider Labs nor the names of its | |||
contributors may be used to endorse or promote products derived from | |||
this software without specific prior written permission. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,96 @@ | |||
# gliderlabs/ssh | |||
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh) | |||
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh) | |||
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh) | |||
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors) | |||
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com) | |||
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312) | |||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member | |||
This Go package wraps the [crypto/ssh | |||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for | |||
building SSH servers. The goal of the API was to make it as simple as using | |||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar: | |||
```go | |||
package main | |||
import ( | |||
"github.com/gliderlabs/ssh" | |||
"io" | |||
"log" | |||
) | |||
func main() { | |||
ssh.Handle(func(s ssh.Session) { | |||
io.WriteString(s, "Hello world\n") | |||
}) | |||
log.Fatal(ssh.ListenAndServe(":2222", nil)) | |||
} | |||
``` | |||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)). | |||
## Examples | |||
A bunch of great examples are in the `_examples` directory. | |||
## Usage | |||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh) | |||
## Contributing | |||
Pull requests are welcome! However, since this project is very much about API | |||
design, please submit API changes as issues to discuss before submitting PRs. | |||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well. | |||
## Roadmap | |||
* Non-session channel handlers | |||
* Cleanup callback API | |||
* 1.0 release | |||
* High-level client? | |||
## Sponsors | |||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)] | |||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a> | |||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a> | |||
## License | |||
BSD |
@@ -0,0 +1,83 @@ | |||
package ssh | |||
import ( | |||
"io" | |||
"io/ioutil" | |||
"net" | |||
"path" | |||
"sync" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
const ( | |||
agentRequestType = "auth-agent-req@openssh.com" | |||
agentChannelType = "auth-agent@openssh.com" | |||
agentTempDir = "auth-agent" | |||
agentListenFile = "listener.sock" | |||
) | |||
// contextKeyAgentRequest is an internal context key for storing if the | |||
// client requested agent forwarding | |||
var contextKeyAgentRequest = &contextKey{"auth-agent-req"} | |||
// SetAgentRequested sets up the session context so that AgentRequested | |||
// returns true. | |||
func SetAgentRequested(ctx Context) { | |||
ctx.SetValue(contextKeyAgentRequest, true) | |||
} | |||
// AgentRequested returns true if the client requested agent forwarding. | |||
func AgentRequested(sess Session) bool { | |||
return sess.Context().Value(contextKeyAgentRequest) == true | |||
} | |||
// NewAgentListener sets up a temporary Unix socket that can be communicated | |||
// to the session environment and used for forwarding connections. | |||
func NewAgentListener() (net.Listener, error) { | |||
dir, err := ioutil.TempDir("", agentTempDir) | |||
if err != nil { | |||
return nil, err | |||
} | |||
l, err := net.Listen("unix", path.Join(dir, agentListenFile)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return l, nil | |||
} | |||
// ForwardAgentConnections takes connections from a listener to proxy into the | |||
// session on the OpenSSH channel for agent connections. It blocks and services | |||
// connections until the listener stop accepting. | |||
func ForwardAgentConnections(l net.Listener, s Session) { | |||
sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn) | |||
for { | |||
conn, err := l.Accept() | |||
if err != nil { | |||
return | |||
} | |||
go func(conn net.Conn) { | |||
defer conn.Close() | |||
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil) | |||
if err != nil { | |||
return | |||
} | |||
defer channel.Close() | |||
go gossh.DiscardRequests(reqs) | |||
var wg sync.WaitGroup | |||
wg.Add(2) | |||
go func() { | |||
io.Copy(conn, channel) | |||
conn.(*net.UnixConn).CloseWrite() | |||
wg.Done() | |||
}() | |||
go func() { | |||
io.Copy(channel, conn) | |||
channel.CloseWrite() | |||
wg.Done() | |||
}() | |||
wg.Wait() | |||
}(conn) | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
version: 2 | |||
jobs: | |||
build-go-latest: | |||
docker: | |||
- image: golang:latest | |||
working_directory: /go/src/github.com/gliderlabs/ssh | |||
steps: | |||
- checkout | |||
- run: go get | |||
- run: go test -v -race | |||
build-go-1.9: | |||
docker: | |||
- image: golang:1.9 | |||
working_directory: /go/src/github.com/gliderlabs/ssh | |||
steps: | |||
- checkout | |||
- run: go get | |||
- run: go test -v -race | |||
workflows: | |||
version: 2 | |||
build: | |||
jobs: | |||
- build-go-latest | |||
- build-go-1.9 |
@@ -0,0 +1,55 @@ | |||
package ssh | |||
import ( | |||
"context" | |||
"net" | |||
"time" | |||
) | |||
type serverConn struct { | |||
net.Conn | |||
idleTimeout time.Duration | |||
maxDeadline time.Time | |||
closeCanceler context.CancelFunc | |||
} | |||
func (c *serverConn) Write(p []byte) (n int, err error) { | |||
c.updateDeadline() | |||
n, err = c.Conn.Write(p) | |||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil { | |||
c.closeCanceler() | |||
} | |||
return | |||
} | |||
func (c *serverConn) Read(b []byte) (n int, err error) { | |||
c.updateDeadline() | |||
n, err = c.Conn.Read(b) | |||
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil { | |||
c.closeCanceler() | |||
} | |||
return | |||
} | |||
func (c *serverConn) Close() (err error) { | |||
err = c.Conn.Close() | |||
if c.closeCanceler != nil { | |||
c.closeCanceler() | |||
} | |||
return | |||
} | |||
func (c *serverConn) updateDeadline() { | |||
switch { | |||
case c.idleTimeout > 0: | |||
idleDeadline := time.Now().Add(c.idleTimeout) | |||
if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() { | |||
c.Conn.SetDeadline(idleDeadline) | |||
return | |||
} | |||
fallthrough | |||
default: | |||
c.Conn.SetDeadline(c.maxDeadline) | |||
} | |||
} |
@@ -0,0 +1,152 @@ | |||
package ssh | |||
import ( | |||
"context" | |||
"encoding/hex" | |||
"net" | |||
"sync" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
// contextKey is a value for use with context.WithValue. It's used as | |||
// a pointer so it fits in an interface{} without allocation. | |||
type contextKey struct { | |||
name string | |||
} | |||
var ( | |||
// ContextKeyUser is a context key for use with Contexts in this package. | |||
// The associated value will be of type string. | |||
ContextKeyUser = &contextKey{"user"} | |||
// ContextKeySessionID is a context key for use with Contexts in this package. | |||
// The associated value will be of type string. | |||
ContextKeySessionID = &contextKey{"session-id"} | |||
// ContextKeyPermissions is a context key for use with Contexts in this package. | |||
// The associated value will be of type *Permissions. | |||
ContextKeyPermissions = &contextKey{"permissions"} | |||
// ContextKeyClientVersion is a context key for use with Contexts in this package. | |||
// The associated value will be of type string. | |||
ContextKeyClientVersion = &contextKey{"client-version"} | |||
// ContextKeyServerVersion is a context key for use with Contexts in this package. | |||
// The associated value will be of type string. | |||
ContextKeyServerVersion = &contextKey{"server-version"} | |||
// ContextKeyLocalAddr is a context key for use with Contexts in this package. | |||
// The associated value will be of type net.Addr. | |||
ContextKeyLocalAddr = &contextKey{"local-addr"} | |||
// ContextKeyRemoteAddr is a context key for use with Contexts in this package. | |||
// The associated value will be of type net.Addr. | |||
ContextKeyRemoteAddr = &contextKey{"remote-addr"} | |||
// ContextKeyServer is a context key for use with Contexts in this package. | |||
// The associated value will be of type *Server. | |||
ContextKeyServer = &contextKey{"ssh-server"} | |||
// ContextKeyConn is a context key for use with Contexts in this package. | |||
// The associated value will be of type gossh.ServerConn. | |||
ContextKeyConn = &contextKey{"ssh-conn"} | |||
// ContextKeyPublicKey is a context key for use with Contexts in this package. | |||
// The associated value will be of type PublicKey. | |||
ContextKeyPublicKey = &contextKey{"public-key"} | |||
) | |||
// Context is a package specific context interface. It exposes connection | |||
// metadata and allows new values to be easily written to it. It's used in | |||
// authentication handlers and callbacks, and its underlying context.Context is | |||
// exposed on Session in the session Handler. A connection-scoped lock is also | |||
// embedded in the context to make it easier to limit operations per-connection. | |||
type Context interface { | |||
context.Context | |||
sync.Locker | |||
// User returns the username used when establishing the SSH connection. | |||
User() string | |||
// SessionID returns the session hash. | |||
SessionID() string | |||
// ClientVersion returns the version reported by the client. | |||
ClientVersion() string | |||
// ServerVersion returns the version reported by the server. | |||
ServerVersion() string | |||
// RemoteAddr returns the remote address for this connection. | |||
RemoteAddr() net.Addr | |||
// LocalAddr returns the local address for this connection. | |||
LocalAddr() net.Addr | |||
// Permissions returns the Permissions object used for this connection. | |||
Permissions() *Permissions | |||
// SetValue allows you to easily write new values into the underlying context. | |||
SetValue(key, value interface{}) | |||
} | |||
type sshContext struct { | |||
context.Context | |||
*sync.Mutex | |||
} | |||
func newContext(srv *Server) (*sshContext, context.CancelFunc) { | |||
innerCtx, cancel := context.WithCancel(context.Background()) | |||
ctx := &sshContext{innerCtx, &sync.Mutex{}} | |||
ctx.SetValue(ContextKeyServer, srv) | |||
perms := &Permissions{&gossh.Permissions{}} | |||
ctx.SetValue(ContextKeyPermissions, perms) | |||
return ctx, cancel | |||
} | |||
// this is separate from newContext because we will get ConnMetadata | |||
// at different points so it needs to be applied separately | |||
func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) { | |||
if ctx.Value(ContextKeySessionID) != nil { | |||
return | |||
} | |||
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID())) | |||
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion())) | |||
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion())) | |||
ctx.SetValue(ContextKeyUser, conn.User()) | |||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr()) | |||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr()) | |||
} | |||
func (ctx *sshContext) SetValue(key, value interface{}) { | |||
ctx.Context = context.WithValue(ctx.Context, key, value) | |||
} | |||
func (ctx *sshContext) User() string { | |||
return ctx.Value(ContextKeyUser).(string) | |||
} | |||
func (ctx *sshContext) SessionID() string { | |||
return ctx.Value(ContextKeySessionID).(string) | |||
} | |||
func (ctx *sshContext) ClientVersion() string { | |||
return ctx.Value(ContextKeyClientVersion).(string) | |||
} | |||
func (ctx *sshContext) ServerVersion() string { | |||
return ctx.Value(ContextKeyServerVersion).(string) | |||
} | |||
func (ctx *sshContext) RemoteAddr() net.Addr { | |||
return ctx.Value(ContextKeyRemoteAddr).(net.Addr) | |||
} | |||
func (ctx *sshContext) LocalAddr() net.Addr { | |||
return ctx.Value(ContextKeyLocalAddr).(net.Addr) | |||
} | |||
func (ctx *sshContext) Permissions() *Permissions { | |||
return ctx.Value(ContextKeyPermissions).(*Permissions) | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
Package ssh wraps the crypto/ssh package with a higher-level API for building | |||
SSH servers. The goal of the API was to make it as simple as using net/http, so | |||
the API is very similar. | |||
You should be able to build any SSH server using only this package, which wraps | |||
relevant types and some functions from crypto/ssh. However, you still need to | |||
use crypto/ssh for building SSH clients. | |||
ListenAndServe starts an SSH server with a given address, handler, and options. The | |||
handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler: | |||
ssh.Handle(func(s ssh.Session) { | |||
io.WriteString(s, "Hello world\n") | |||
}) | |||
log.Fatal(ssh.ListenAndServe(":2222", nil)) | |||
If you don't specify a host key, it will generate one every time. This is convenient | |||
except you'll have to deal with clients being confused that the host key is different. | |||
It's a better idea to generate or point to an existing key on your system: | |||
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa"))) | |||
Although all options have functional option helpers, another way to control the | |||
server's behavior is by creating a custom Server: | |||
s := &ssh.Server{ | |||
Addr: ":2222", | |||
Handler: sessionHandler, | |||
PublicKeyHandler: authHandler, | |||
} | |||
s.AddHostKey(hostKeySigner) | |||
log.Fatal(s.ListenAndServe()) | |||
This package automatically handles basic SSH requests like setting environment | |||
variables, requesting PTY, and changing window size. These requests are | |||
processed, responded to, and any relevant state is updated. This state is then | |||
exposed to you via the Session interface. | |||
The one big feature missing from the Session abstraction is signals. This was | |||
started, but not completed. Pull Requests welcome! | |||
*/ | |||
package ssh |
@@ -0,0 +1,77 @@ | |||
package ssh | |||
import ( | |||
"io/ioutil" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
// PasswordAuth returns a functional option that sets PasswordHandler on the server. | |||
func PasswordAuth(fn PasswordHandler) Option { | |||
return func(srv *Server) error { | |||
srv.PasswordHandler = fn | |||
return nil | |||
} | |||
} | |||
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server. | |||
func PublicKeyAuth(fn PublicKeyHandler) Option { | |||
return func(srv *Server) error { | |||
srv.PublicKeyHandler = fn | |||
return nil | |||
} | |||
} | |||
// HostKeyFile returns a functional option that adds HostSigners to the server | |||
// from a PEM file at filepath. | |||
func HostKeyFile(filepath string) Option { | |||
return func(srv *Server) error { | |||
pemBytes, err := ioutil.ReadFile(filepath) | |||
if err != nil { | |||
return err | |||
} | |||
signer, err := gossh.ParsePrivateKey(pemBytes) | |||
if err != nil { | |||
return err | |||
} | |||
srv.AddHostKey(signer) | |||
return nil | |||
} | |||
} | |||
// HostKeyPEM returns a functional option that adds HostSigners to the server | |||
// from a PEM file as bytes. | |||
func HostKeyPEM(bytes []byte) Option { | |||
return func(srv *Server) error { | |||
signer, err := gossh.ParsePrivateKey(bytes) | |||
if err != nil { | |||
return err | |||
} | |||
srv.AddHostKey(signer) | |||
return nil | |||
} | |||
} | |||
// NoPty returns a functional option that sets PtyCallback to return false, | |||
// denying PTY requests. | |||
func NoPty() Option { | |||
return func(srv *Server) error { | |||
srv.PtyCallback = func(ctx Context, pty Pty) bool { | |||
return false | |||
} | |||
return nil | |||
} | |||
} | |||
// WrapConn returns a functional option that sets ConnCallback on the server. | |||
func WrapConn(fn ConnCallback) Option { | |||
return func(srv *Server) error { | |||
srv.ConnCallback = fn | |||
return nil | |||
} | |||
} |
@@ -0,0 +1,394 @@ | |||
package ssh | |||
import ( | |||
"context" | |||
"errors" | |||
"fmt" | |||
"net" | |||
"sync" | |||
"time" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
// ErrServerClosed is returned by the Server's Serve, ListenAndServe, | |||
// and ListenAndServeTLS methods after a call to Shutdown or Close. | |||
var ErrServerClosed = errors.New("ssh: Server closed") | |||
type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte) | |||
var DefaultRequestHandlers = map[string]RequestHandler{} | |||
type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) | |||
var DefaultChannelHandlers = map[string]ChannelHandler{ | |||
"session": DefaultSessionHandler, | |||
} | |||
// Server defines parameters for running an SSH server. The zero value for | |||
// Server is a valid configuration. When both PasswordHandler and | |||
// PublicKeyHandler are nil, no client authentication is performed. | |||
type Server struct { | |||
Addr string // TCP address to listen on, ":22" if empty | |||
Handler Handler // handler to invoke, ssh.DefaultHandler if nil | |||
HostSigners []Signer // private keys for the host key, must have at least one | |||
Version string // server version to be sent before the initial handshake | |||
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler | |||
PasswordHandler PasswordHandler // password authentication handler | |||
PublicKeyHandler PublicKeyHandler // public key authentication handler | |||
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil | |||
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling | |||
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil | |||
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil | |||
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options | |||
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions | |||
IdleTimeout time.Duration // connection timeout when no activity, none if empty | |||
MaxTimeout time.Duration // absolute connection timeout, none if empty | |||
// ChannelHandlers allow overriding the built-in session handlers or provide | |||
// extensions to the protocol, such as tcpip forwarding. By default only the | |||
// "session" handler is enabled. | |||
ChannelHandlers map[string]ChannelHandler | |||
// RequestHandlers allow overriding the server-level request handlers or | |||
// provide extensions to the protocol, such as tcpip forwarding. By default | |||
// no handlers are enabled. | |||
RequestHandlers map[string]RequestHandler | |||
listenerWg sync.WaitGroup | |||
mu sync.Mutex | |||
listeners map[net.Listener]struct{} | |||
conns map[*gossh.ServerConn]struct{} | |||
connWg sync.WaitGroup | |||
doneChan chan struct{} | |||
} | |||
func (srv *Server) ensureHostSigner() error { | |||
if len(srv.HostSigners) == 0 { | |||
signer, err := generateSigner() | |||
if err != nil { | |||
return err | |||
} | |||
srv.HostSigners = append(srv.HostSigners, signer) | |||
} | |||
return nil | |||
} | |||
func (srv *Server) ensureHandlers() { | |||
srv.mu.Lock() | |||
defer srv.mu.Unlock() | |||
if srv.RequestHandlers == nil { | |||
srv.RequestHandlers = map[string]RequestHandler{} | |||
for k, v := range DefaultRequestHandlers { | |||
srv.RequestHandlers[k] = v | |||
} | |||
} | |||
if srv.ChannelHandlers == nil { | |||
srv.ChannelHandlers = map[string]ChannelHandler{} | |||
for k, v := range DefaultChannelHandlers { | |||
srv.ChannelHandlers[k] = v | |||
} | |||
} | |||
} | |||
func (srv *Server) config(ctx Context) *gossh.ServerConfig { | |||
var config *gossh.ServerConfig | |||
if srv.ServerConfigCallback == nil { | |||
config = &gossh.ServerConfig{} | |||
} else { | |||
config = srv.ServerConfigCallback(ctx) | |||
} | |||
for _, signer := range srv.HostSigners { | |||
config.AddHostKey(signer) | |||
} | |||
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil { | |||
config.NoClientAuth = true | |||
} | |||
if srv.Version != "" { | |||
config.ServerVersion = "SSH-2.0-" + srv.Version | |||
} | |||
if srv.PasswordHandler != nil { | |||
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) { | |||
applyConnMetadata(ctx, conn) | |||
if ok := srv.PasswordHandler(ctx, string(password)); !ok { | |||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied") | |||
} | |||
return ctx.Permissions().Permissions, nil | |||
} | |||
} | |||
if srv.PublicKeyHandler != nil { | |||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) { | |||
applyConnMetadata(ctx, conn) | |||
if ok := srv.PublicKeyHandler(ctx, key); !ok { | |||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied") | |||
} | |||
ctx.SetValue(ContextKeyPublicKey, key) | |||
return ctx.Permissions().Permissions, nil | |||
} | |||
} | |||
if srv.KeyboardInteractiveHandler != nil { | |||
config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) { | |||
if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok { | |||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied") | |||
} | |||
return ctx.Permissions().Permissions, nil | |||
} | |||
} | |||
return config | |||
} | |||
// Handle sets the Handler for the server. | |||
func (srv *Server) Handle(fn Handler) { | |||
srv.Handler = fn | |||
} | |||
// Close immediately closes all active listeners and all active | |||
// connections. | |||
// | |||
// Close returns any error returned from closing the Server's | |||
// underlying Listener(s). | |||
func (srv *Server) Close() error { | |||
srv.mu.Lock() | |||
defer srv.mu.Unlock() | |||
srv.closeDoneChanLocked() | |||
err := srv.closeListenersLocked() | |||
for c := range srv.conns { | |||
c.Close() | |||
delete(srv.conns, c) | |||
} | |||
return err | |||
} | |||
// Shutdown gracefully shuts down the server without interrupting any | |||
// active connections. Shutdown works by first closing all open | |||
// listeners, and then waiting indefinitely for connections to close. | |||
// If the provided context expires before the shutdown is complete, | |||
// then the context's error is returned. | |||
func (srv *Server) Shutdown(ctx context.Context) error { | |||
srv.mu.Lock() | |||
lnerr := srv.closeListenersLocked() | |||
srv.closeDoneChanLocked() | |||
srv.mu.Unlock() | |||
finished := make(chan struct{}, 1) | |||
go func() { | |||
srv.listenerWg.Wait() | |||
srv.connWg.Wait() | |||
finished <- struct{}{} | |||
}() | |||
select { | |||
case <-ctx.Done(): | |||
return ctx.Err() | |||
case <-finished: | |||
return lnerr | |||
} | |||
} | |||
// Serve accepts incoming connections on the Listener l, creating a new | |||
// connection goroutine for each. The connection goroutines read requests and then | |||
// calls srv.Handler to handle sessions. | |||
// | |||
// Serve always returns a non-nil error. | |||
func (srv *Server) Serve(l net.Listener) error { | |||
srv.ensureHandlers() | |||
defer l.Close() | |||
if err := srv.ensureHostSigner(); err != nil { | |||
return err | |||
} | |||
if srv.Handler == nil { | |||
srv.Handler = DefaultHandler | |||
} | |||
var tempDelay time.Duration | |||
srv.trackListener(l, true) | |||
defer srv.trackListener(l, false) | |||
for { | |||
conn, e := l.Accept() | |||
if e != nil { | |||
select { | |||
case <-srv.getDoneChan(): | |||
return ErrServerClosed | |||
default: | |||
} | |||
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 | |||
} | |||
time.Sleep(tempDelay) | |||
continue | |||
} | |||
return e | |||
} | |||
go srv.handleConn(conn) | |||
} | |||
} | |||
func (srv *Server) handleConn(newConn net.Conn) { | |||
if srv.ConnCallback != nil { | |||
cbConn := srv.ConnCallback(newConn) | |||
if cbConn == nil { | |||
newConn.Close() | |||
return | |||
} | |||
newConn = cbConn | |||
} | |||
ctx, cancel := newContext(srv) | |||
conn := &serverConn{ | |||
Conn: newConn, | |||
idleTimeout: srv.IdleTimeout, | |||
closeCanceler: cancel, | |||
} | |||
if srv.MaxTimeout > 0 { | |||
conn.maxDeadline = time.Now().Add(srv.MaxTimeout) | |||
} | |||
defer conn.Close() | |||
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx)) | |||
if err != nil { | |||
// TODO: trigger event callback | |||
return | |||
} | |||
srv.trackConn(sshConn, true) | |||
defer srv.trackConn(sshConn, false) | |||
ctx.SetValue(ContextKeyConn, sshConn) | |||
applyConnMetadata(ctx, sshConn) | |||
//go gossh.DiscardRequests(reqs) | |||
go srv.handleRequests(ctx, reqs) | |||
for ch := range chans { | |||
handler := srv.ChannelHandlers[ch.ChannelType()] | |||
if handler == nil { | |||
handler = srv.ChannelHandlers["default"] | |||
} | |||
if handler == nil { | |||
ch.Reject(gossh.UnknownChannelType, "unsupported channel type") | |||
continue | |||
} | |||
go handler(srv, sshConn, ch, ctx) | |||
} | |||
} | |||
func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) { | |||
for req := range in { | |||
handler := srv.RequestHandlers[req.Type] | |||
if handler == nil { | |||
handler = srv.RequestHandlers["default"] | |||
} | |||
if handler == nil { | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
/*reqCtx, cancel := context.WithCancel(ctx) | |||
defer cancel() */ | |||
ret, payload := handler(ctx, srv, req) | |||
req.Reply(ret, payload) | |||
} | |||
} | |||
// ListenAndServe listens on the TCP network address srv.Addr and then calls | |||
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used. | |||
// ListenAndServe always returns a non-nil error. | |||
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) | |||
} | |||
// AddHostKey adds a private key as a host key. If an existing host key exists | |||
// with the same algorithm, it is overwritten. Each server config must have at | |||
// least one host key. | |||
func (srv *Server) AddHostKey(key Signer) { | |||
// these are later added via AddHostKey on ServerConfig, which performs the | |||
// check for one of every algorithm. | |||
srv.HostSigners = append(srv.HostSigners, key) | |||
} | |||
// SetOption runs a functional option against the server. | |||
func (srv *Server) SetOption(option Option) error { | |||
return option(srv) | |||
} | |||
func (srv *Server) getDoneChan() <-chan struct{} { | |||
srv.mu.Lock() | |||
defer srv.mu.Unlock() | |||
return srv.getDoneChanLocked() | |||
} | |||
func (srv *Server) getDoneChanLocked() chan struct{} { | |||
if srv.doneChan == nil { | |||
srv.doneChan = make(chan struct{}) | |||
} | |||
return srv.doneChan | |||
} | |||
func (srv *Server) closeDoneChanLocked() { | |||
ch := srv.getDoneChanLocked() | |||
select { | |||
case <-ch: | |||
// Already closed. Don't close again. | |||
default: | |||
// Safe to close here. We're the only closer, guarded | |||
// by srv.mu. | |||
close(ch) | |||
} | |||
} | |||
func (srv *Server) closeListenersLocked() error { | |||
var err error | |||
for ln := range srv.listeners { | |||
if cerr := ln.Close(); cerr != nil && err == nil { | |||
err = cerr | |||
} | |||
delete(srv.listeners, ln) | |||
} | |||
return err | |||
} | |||
func (srv *Server) trackListener(ln net.Listener, add bool) { | |||
srv.mu.Lock() | |||
defer srv.mu.Unlock() | |||
if srv.listeners == nil { | |||
srv.listeners = make(map[net.Listener]struct{}) | |||
} | |||
if add { | |||
// If the *Server is being reused after a previous | |||
// Close or Shutdown, reset its doneChan: | |||
if len(srv.listeners) == 0 && len(srv.conns) == 0 { | |||
srv.doneChan = nil | |||
} | |||
srv.listeners[ln] = struct{}{} | |||
srv.listenerWg.Add(1) | |||
} else { | |||
delete(srv.listeners, ln) | |||
srv.listenerWg.Done() | |||
} | |||
} | |||
func (srv *Server) trackConn(c *gossh.ServerConn, add bool) { | |||
srv.mu.Lock() | |||
defer srv.mu.Unlock() | |||
if srv.conns == nil { | |||
srv.conns = make(map[*gossh.ServerConn]struct{}) | |||
} | |||
if add { | |||
srv.conns[c] = struct{}{} | |||
srv.connWg.Add(1) | |||
} else { | |||
delete(srv.conns, c) | |||
srv.connWg.Done() | |||
} | |||
} |
@@ -0,0 +1,308 @@ | |||
package ssh | |||
import ( | |||
"bytes" | |||
"context" | |||
"errors" | |||
"fmt" | |||
"net" | |||
"sync" | |||
"github.com/anmitsu/go-shlex" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
// Session provides access to information about an SSH session and methods | |||
// to read and write to the SSH channel with an embedded Channel interface from | |||
// cypto/ssh. | |||
// | |||
// When Command() returns an empty slice, the user requested a shell. Otherwise | |||
// the user is performing an exec with those command arguments. | |||
// | |||
// TODO: Signals | |||
type Session interface { | |||
gossh.Channel | |||
// User returns the username used when establishing the SSH connection. | |||
User() string | |||
// RemoteAddr returns the net.Addr of the client side of the connection. | |||
RemoteAddr() net.Addr | |||
// LocalAddr returns the net.Addr of the server side of the connection. | |||
LocalAddr() net.Addr | |||
// Environ returns a copy of strings representing the environment set by the | |||
// user for this session, in the form "key=value". | |||
Environ() []string | |||
// Exit sends an exit status and then closes the session. | |||
Exit(code int) error | |||
// Command returns a shell parsed slice of arguments that were provided by the | |||
// user. Shell parsing splits the command string according to POSIX shell rules, | |||
// which considers quoting not just whitespace. | |||
Command() []string | |||
// RawCommand returns the exact command that was provided by the user. | |||
RawCommand() string | |||
// PublicKey returns the PublicKey used to authenticate. If a public key was not | |||
// used it will return nil. | |||
PublicKey() PublicKey | |||
// Context returns the connection's context. The returned context is always | |||
// non-nil and holds the same data as the Context passed into auth | |||
// handlers and callbacks. | |||
// | |||
// The context is canceled when the client's connection closes or I/O | |||
// operation fails. | |||
Context() context.Context | |||
// Permissions returns a copy of the Permissions object that was available for | |||
// setup in the auth handlers via the Context. | |||
Permissions() Permissions | |||
// Pty returns PTY information, a channel of window size changes, and a boolean | |||
// of whether or not a PTY was accepted for this session. | |||
Pty() (Pty, <-chan Window, bool) | |||
// Signals registers a channel to receive signals sent from the client. The | |||
// channel must handle signal sends or it will block the SSH request loop. | |||
// Registering nil will unregister the channel from signal sends. During the | |||
// time no channel is registered signals are buffered up to a reasonable amount. | |||
// If there are buffered signals when a channel is registered, they will be | |||
// sent in order on the channel immediately after registering. | |||
Signals(c chan<- Signal) | |||
} | |||
// maxSigBufSize is how many signals will be buffered | |||
// when there is no signal channel specified | |||
const maxSigBufSize = 128 | |||
func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) { | |||
ch, reqs, err := newChan.Accept() | |||
if err != nil { | |||
// TODO: trigger event callback | |||
return | |||
} | |||
sess := &session{ | |||
Channel: ch, | |||
conn: conn, | |||
handler: srv.Handler, | |||
ptyCb: srv.PtyCallback, | |||
sessReqCb: srv.SessionRequestCallback, | |||
ctx: ctx, | |||
} | |||
sess.handleRequests(reqs) | |||
} | |||
type session struct { | |||
sync.Mutex | |||
gossh.Channel | |||
conn *gossh.ServerConn | |||
handler Handler | |||
handled bool | |||
exited bool | |||
pty *Pty | |||
winch chan Window | |||
env []string | |||
ptyCb PtyCallback | |||
sessReqCb SessionRequestCallback | |||
rawCmd string | |||
ctx Context | |||
sigCh chan<- Signal | |||
sigBuf []Signal | |||
} | |||
func (sess *session) Write(p []byte) (n int, err error) { | |||
if sess.pty != nil { | |||
m := len(p) | |||
// normalize \n to \r\n when pty is accepted. | |||
// this is a hardcoded shortcut since we don't support terminal modes. | |||
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1) | |||
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1) | |||
n, err = sess.Channel.Write(p) | |||
if n > m { | |||
n = m | |||
} | |||
return | |||
} | |||
return sess.Channel.Write(p) | |||
} | |||
func (sess *session) PublicKey() PublicKey { | |||
sessionkey := sess.ctx.Value(ContextKeyPublicKey) | |||
if sessionkey == nil { | |||
return nil | |||
} | |||
return sessionkey.(PublicKey) | |||
} | |||
func (sess *session) Permissions() Permissions { | |||
// use context permissions because its properly | |||
// wrapped and easier to dereference | |||
perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions) | |||
return *perms | |||
} | |||
func (sess *session) Context() context.Context { | |||
return sess.ctx | |||
} | |||
func (sess *session) Exit(code int) error { | |||
sess.Lock() | |||
defer sess.Unlock() | |||
if sess.exited { | |||
return errors.New("Session.Exit called multiple times") | |||
} | |||
sess.exited = true | |||
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) LocalAddr() net.Addr { | |||
return sess.conn.LocalAddr() | |||
} | |||
func (sess *session) Environ() []string { | |||
return append([]string(nil), sess.env...) | |||
} | |||
func (sess *session) RawCommand() string { | |||
return sess.rawCmd | |||
} | |||
func (sess *session) Command() []string { | |||
cmd, _ := shlex.Split(sess.rawCmd, true) | |||
return append([]string(nil), 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) Signals(c chan<- Signal) { | |||
sess.Lock() | |||
defer sess.Unlock() | |||
sess.sigCh = c | |||
if len(sess.sigBuf) > 0 { | |||
go func() { | |||
for _, sig := range sess.sigBuf { | |||
sess.sigCh <- sig | |||
} | |||
}() | |||
} | |||
} | |||
func (sess *session) handleRequests(reqs <-chan *gossh.Request) { | |||
for req := range reqs { | |||
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.rawCmd = payload.Value | |||
// If there's a session policy callback, we need to confirm before | |||
// accepting the session. | |||
if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) { | |||
sess.rawCmd = "" | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
sess.handled = true | |||
req.Reply(true, nil) | |||
go func() { | |||
sess.handler(sess) | |||
sess.Exit(0) | |||
}() | |||
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)) | |||
req.Reply(true, nil) | |||
case "signal": | |||
var payload struct{ Signal string } | |||
gossh.Unmarshal(req.Payload, &payload) | |||
sess.Lock() | |||
if sess.sigCh != nil { | |||
sess.sigCh <- Signal(payload.Signal) | |||
} else { | |||
if len(sess.sigBuf) < maxSigBufSize { | |||
sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal)) | |||
} | |||
} | |||
sess.Unlock() | |||
case "pty-req": | |||
if sess.handled || sess.pty != nil { | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
ptyReq, ok := parsePtyRequest(req.Payload) | |||
if !ok { | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
if sess.ptyCb != nil { | |||
ok := sess.ptyCb(sess.ctx, ptyReq) | |||
if !ok { | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
} | |||
sess.pty = &ptyReq | |||
sess.winch = make(chan Window, 1) | |||
sess.winch <- ptyReq.Window | |||
defer func() { | |||
// when reqs is closed | |||
close(sess.winch) | |||
}() | |||
req.Reply(ok, nil) | |||
case "window-change": | |||
if sess.pty == nil { | |||
req.Reply(false, nil) | |||
continue | |||
} | |||
win, ok := parseWinchRequest(req.Payload) | |||
if ok { | |||
sess.pty.Window = win | |||
sess.winch <- win | |||
} | |||
req.Reply(ok, nil) | |||
case agentRequestType: | |||
// TODO: option/callback to allow agent forwarding | |||
SetAgentRequested(sess.ctx) | |||
req.Reply(true, nil) | |||
default: | |||
// TODO: debug log | |||
req.Reply(false, nil) | |||
} | |||
} | |||
} |
@@ -0,0 +1,123 @@ | |||
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" | |||
) | |||
// DefaultHandler is the default Handler used by Serve. | |||
var DefaultHandler Handler | |||
// Option is a functional option handler for Server. | |||
type Option func(*Server) error | |||
// Handler is a callback for handling established SSH sessions. | |||
type Handler func(Session) | |||
// PublicKeyHandler is a callback for performing public key authentication. | |||
type PublicKeyHandler func(ctx Context, key PublicKey) bool | |||
// PasswordHandler is a callback for performing password authentication. | |||
type PasswordHandler func(ctx Context, password string) bool | |||
// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication. | |||
type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool | |||
// PtyCallback is a hook for allowing PTY sessions. | |||
type PtyCallback func(ctx Context, pty Pty) bool | |||
// SessionRequestCallback is a callback for allowing or denying SSH sessions. | |||
type SessionRequestCallback func(sess Session, requestType string) bool | |||
// ConnCallback is a hook for new connections before handling. | |||
// It allows wrapping for timeouts and limiting by returning | |||
// the net.Conn that will be used as the underlying connection. | |||
type ConnCallback func(conn net.Conn) net.Conn | |||
// LocalPortForwardingCallback is a hook for allowing port forwarding | |||
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool | |||
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding | |||
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool | |||
// ServerConfigCallback is a hook for creating custom default server configs | |||
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig | |||
// Window represents the size of a PTY window. | |||
type Window struct { | |||
Width int | |||
Height int | |||
} | |||
// Pty represents a PTY request and configuration. | |||
type Pty struct { | |||
Term string | |||
Window Window | |||
// HELP WANTED: terminal modes! | |||
} | |||
// Serve accepts incoming SSH connections on the listener l, creating a new | |||
// connection goroutine for each. The connection goroutines read requests and | |||
// then calls handler to handle sessions. Handler is typically nil, in which | |||
// case the DefaultHandler is used. | |||
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) | |||
} | |||
// ListenAndServe listens on the TCP network address addr and then calls Serve | |||
// with handler to handle sessions on incoming connections. Handler is typically | |||
// nil, in which case the DefaultHandler is used. | |||
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() | |||
} | |||
// Handle registers the handler as the DefaultHandler. | |||
func Handle(handler Handler) { | |||
DefaultHandler = handler | |||
} | |||
// KeysEqual is constant time compare of the keys to avoid timing attacks. | |||
func KeysEqual(ak, bk PublicKey) bool { | |||
//avoid panic if one of the keys is nil, return false instead | |||
if ak == nil || bk == nil { | |||
return false | |||
} | |||
a := ak.Marshal() | |||
b := bk.Marshal() | |||
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1) | |||
} |
@@ -0,0 +1,193 @@ | |||
package ssh | |||
import ( | |||
"io" | |||
"log" | |||
"net" | |||
"strconv" | |||
"sync" | |||
gossh "golang.org/x/crypto/ssh" | |||
) | |||
const ( | |||
forwardedTCPChannelType = "forwarded-tcpip" | |||
) | |||
// direct-tcpip data struct as specified in RFC4254, Section 7.2 | |||
type localForwardChannelData struct { | |||
DestAddr string | |||
DestPort uint32 | |||
OriginAddr string | |||
OriginPort uint32 | |||
} | |||
// DirectTCPIPHandler can be enabled by adding it to the server's | |||
// ChannelHandlers under direct-tcpip. | |||
func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) { | |||
d := localForwardChannelData{} | |||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil { | |||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error()) | |||
return | |||
} | |||
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) { | |||
newChan.Reject(gossh.Prohibited, "port forwarding is disabled") | |||
return | |||
} | |||
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10)) | |||
var dialer net.Dialer | |||
dconn, err := dialer.DialContext(ctx, "tcp", dest) | |||
if err != nil { | |||
newChan.Reject(gossh.ConnectionFailed, err.Error()) | |||
return | |||
} | |||
ch, reqs, err := newChan.Accept() | |||
if err != nil { | |||
dconn.Close() | |||
return | |||
} | |||
go gossh.DiscardRequests(reqs) | |||
go func() { | |||
defer ch.Close() | |||
defer dconn.Close() | |||
io.Copy(ch, dconn) | |||
}() | |||
go func() { | |||
defer ch.Close() | |||
defer dconn.Close() | |||
io.Copy(dconn, ch) | |||
}() | |||
} | |||
type remoteForwardRequest struct { | |||
BindAddr string | |||
BindPort uint32 | |||
} | |||
type remoteForwardSuccess struct { | |||
BindPort uint32 | |||
} | |||
type remoteForwardCancelRequest struct { | |||
BindAddr string | |||
BindPort uint32 | |||
} | |||
type remoteForwardChannelData struct { | |||
DestAddr string | |||
DestPort uint32 | |||
OriginAddr string | |||
OriginPort uint32 | |||
} | |||
// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and | |||
// adding the HandleSSHRequest callback to the server's RequestHandlers under | |||
// tcpip-forward and cancel-tcpip-forward. | |||
type ForwardedTCPHandler struct { | |||
forwards map[string]net.Listener | |||
sync.Mutex | |||
} | |||
func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) { | |||
h.Lock() | |||
if h.forwards == nil { | |||
h.forwards = make(map[string]net.Listener) | |||
} | |||
h.Unlock() | |||
conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn) | |||
switch req.Type { | |||
case "tcpip-forward": | |||
var reqPayload remoteForwardRequest | |||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { | |||
// TODO: log parse failure | |||
return false, []byte{} | |||
} | |||
if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) { | |||
return false, []byte("port forwarding is disabled") | |||
} | |||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) | |||
ln, err := net.Listen("tcp", addr) | |||
if err != nil { | |||
// TODO: log listen failure | |||
return false, []byte{} | |||
} | |||
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String()) | |||
destPort, _ := strconv.Atoi(destPortStr) | |||
h.Lock() | |||
h.forwards[addr] = ln | |||
h.Unlock() | |||
go func() { | |||
<-ctx.Done() | |||
h.Lock() | |||
ln, ok := h.forwards[addr] | |||
h.Unlock() | |||
if ok { | |||
ln.Close() | |||
} | |||
}() | |||
go func() { | |||
for { | |||
c, err := ln.Accept() | |||
if err != nil { | |||
// TODO: log accept failure | |||
break | |||
} | |||
originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String()) | |||
originPort, _ := strconv.Atoi(orignPortStr) | |||
payload := gossh.Marshal(&remoteForwardChannelData{ | |||
DestAddr: reqPayload.BindAddr, | |||
DestPort: uint32(destPort), | |||
OriginAddr: originAddr, | |||
OriginPort: uint32(originPort), | |||
}) | |||
go func() { | |||
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload) | |||
if err != nil { | |||
// TODO: log failure to open channel | |||
log.Println(err) | |||
c.Close() | |||
return | |||
} | |||
go gossh.DiscardRequests(reqs) | |||
go func() { | |||
defer ch.Close() | |||
defer c.Close() | |||
io.Copy(ch, c) | |||
}() | |||
go func() { | |||
defer ch.Close() | |||
defer c.Close() | |||
io.Copy(c, ch) | |||
}() | |||
}() | |||
} | |||
h.Lock() | |||
delete(h.forwards, addr) | |||
h.Unlock() | |||
}() | |||
return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)}) | |||
case "cancel-tcpip-forward": | |||
var reqPayload remoteForwardCancelRequest | |||
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { | |||
// TODO: log parse failure | |||
return false, []byte{} | |||
} | |||
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) | |||
h.Lock() | |||
ln, ok := h.forwards[addr] | |||
h.Unlock() | |||
if ok { | |||
ln.Close() | |||
} | |||
return true, nil | |||
default: | |||
return false, nil | |||
} | |||
} |
@@ -0,0 +1,83 @@ | |||
package ssh | |||
import ( | |||
"crypto/rand" | |||
"crypto/rsa" | |||
"encoding/binary" | |||
"golang.org/x/crypto/ssh" | |||
) | |||
func generateSigner() (ssh.Signer, error) { | |||
key, err := rsa.GenerateKey(rand.Reader, 2048) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return ssh.NewSignerFromKey(key) | |||
} | |||
func parsePtyRequest(s []byte) (pty Pty, ok bool) { | |||
term, s, ok := parseString(s) | |||
if !ok { | |||
return | |||
} | |||
width32, s, ok := parseUint32(s) | |||
if !ok { | |||
return | |||
} | |||
height32, _, ok := parseUint32(s) | |||
if !ok { | |||
return | |||
} | |||
pty = Pty{ | |||
Term: term, | |||
Window: Window{ | |||
Width: int(width32), | |||
Height: int(height32), | |||
}, | |||
} | |||
return | |||
} | |||
func parseWinchRequest(s []byte) (win Window, ok bool) { | |||
width32, s, ok := parseUint32(s) | |||
if width32 < 1 { | |||
ok = false | |||
} | |||
if !ok { | |||
return | |||
} | |||
height32, _, ok := parseUint32(s) | |||
if height32 < 1 { | |||
ok = false | |||
} | |||
if !ok { | |||
return | |||
} | |||
win = Window{ | |||
Width: int(width32), | |||
Height: int(height32), | |||
} | |||
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 | |||
} |
@@ -0,0 +1,33 @@ | |||
package ssh | |||
import gossh "golang.org/x/crypto/ssh" | |||
// PublicKey is an abstraction of different types of public keys. | |||
type PublicKey interface { | |||
gossh.PublicKey | |||
} | |||
// The Permissions type holds fine-grained permissions that are specific to a | |||
// user or a specific authentication method for a user. Permissions, except for | |||
// "source-address", must be enforced in the server application layer, after | |||
// successful authentication. | |||
type Permissions struct { | |||
*gossh.Permissions | |||
} | |||
// A Signer can create signatures that verify against a public key. | |||
type Signer interface { | |||
gossh.Signer | |||
} | |||
// ParseAuthorizedKey parses a public key from an authorized_keys file used in | |||
// OpenSSH according to the sshd(8) manual page. | |||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) { | |||
return gossh.ParseAuthorizedKey(in) | |||
} | |||
// ParsePublicKey parses an SSH public key formatted for use in | |||
// the SSH wire protocol according to RFC 4253, section 6.6. | |||
func ParsePublicKey(in []byte) (out PublicKey, err error) { | |||
return gossh.ParsePublicKey(in) | |||
} |
@@ -13,7 +13,6 @@ package unix | |||
import ( | |||
"encoding/binary" | |||
"net" | |||
"runtime" | |||
"syscall" | |||
"unsafe" | |||
@@ -765,7 +764,7 @@ const px_proto_oe = 0 | |||
type SockaddrPPPoE struct { | |||
SID uint16 | |||
Remote net.HardwareAddr | |||
Remote []byte | |||
Dev string | |||
raw RawSockaddrPPPoX | |||
} | |||
@@ -916,7 +915,7 @@ func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) { | |||
} | |||
sa := &SockaddrPPPoE{ | |||
SID: binary.BigEndian.Uint16(pp[6:8]), | |||
Remote: net.HardwareAddr(pp[8:14]), | |||
Remote: pp[8:14], | |||
} | |||
for i := 14; i < 14+IFNAMSIZ; i++ { | |||
if pp[i] == 0 { |
@@ -254,6 +254,7 @@ type Ptmget C.struct_ptmget | |||
const ( | |||
AT_FDCWD = C.AT_FDCWD | |||
AT_SYMLINK_FOLLOW = C.AT_SYMLINK_FOLLOW | |||
AT_SYMLINK_NOFOLLOW = C.AT_SYMLINK_NOFOLLOW | |||
) | |||
@@ -241,6 +241,7 @@ type Winsize C.struct_winsize | |||
const ( | |||
AT_FDCWD = C.AT_FDCWD | |||
AT_SYMLINK_FOLLOW = C.AT_SYMLINK_FOLLOW | |||
AT_SYMLINK_NOFOLLOW = C.AT_SYMLINK_NOFOLLOW | |||
) | |||
@@ -411,6 +411,7 @@ type Ptmget struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x400 | |||
AT_SYMLINK_NOFOLLOW = 0x200 | |||
) | |||
@@ -418,6 +418,7 @@ type Ptmget struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x400 | |||
AT_SYMLINK_NOFOLLOW = 0x200 | |||
) | |||
@@ -416,6 +416,7 @@ type Ptmget struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x400 | |||
AT_SYMLINK_NOFOLLOW = 0x200 | |||
) | |||
@@ -418,6 +418,7 @@ type Ptmget struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x400 | |||
AT_SYMLINK_NOFOLLOW = 0x200 | |||
) | |||
@@ -436,6 +436,7 @@ type Winsize struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x4 | |||
AT_SYMLINK_NOFOLLOW = 0x2 | |||
) | |||
@@ -436,6 +436,7 @@ type Winsize struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x4 | |||
AT_SYMLINK_NOFOLLOW = 0x2 | |||
) | |||
@@ -437,6 +437,7 @@ type Winsize struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x4 | |||
AT_SYMLINK_NOFOLLOW = 0x2 | |||
) | |||
@@ -430,6 +430,7 @@ type Winsize struct { | |||
const ( | |||
AT_FDCWD = -0x64 | |||
AT_SYMLINK_FOLLOW = 0x4 | |||
AT_SYMLINK_NOFOLLOW = 0x2 | |||
) | |||
@@ -294,7 +294,7 @@ func NewCallbackCDecl(fn interface{}) uintptr { | |||
//sys clsidFromString(lpsz *uint16, pclsid *GUID) (ret error) = ole32.CLSIDFromString | |||
//sys stringFromGUID2(rguid *GUID, lpsz *uint16, cchMax int32) (chars int32) = ole32.StringFromGUID2 | |||
//sys coCreateGuid(pguid *GUID) (ret error) = ole32.CoCreateGuid | |||
//sys coTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree | |||
//sys CoTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree | |||
//sys rtlGetVersion(info *OsVersionInfoEx) (ret error) = ntdll.RtlGetVersion | |||
// syscall interface implementation for other packages | |||
@@ -1302,7 +1302,7 @@ func (t Token) KnownFolderPath(folderID *KNOWNFOLDERID, flags uint32) (string, e | |||
if err != nil { | |||
return "", err | |||
} | |||
defer coTaskMemFree(unsafe.Pointer(p)) | |||
defer CoTaskMemFree(unsafe.Pointer(p)) | |||
return UTF16ToString((*[(1 << 30) - 1]uint16)(unsafe.Pointer(p))[:]), nil | |||
} | |||
@@ -2517,7 +2517,7 @@ func coCreateGuid(pguid *GUID) (ret error) { | |||
return | |||
} | |||
func coTaskMemFree(address unsafe.Pointer) { | |||
func CoTaskMemFree(address unsafe.Pointer) { | |||
syscall.Syscall(procCoTaskMemFree.Addr(), 1, uintptr(address), 0, 0) | |||
return | |||
} |
@@ -15,6 +15,8 @@ github.com/Unknwon/i18n | |||
github.com/Unknwon/paginater | |||
# github.com/andybalholm/cascadia v0.0.0-20161224141413-349dd0209470 | |||
github.com/andybalholm/cascadia | |||
# github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 | |||
github.com/anmitsu/go-shlex | |||
# github.com/beorn7/perks v1.0.0 | |||
github.com/beorn7/perks/quantile | |||
# github.com/blevesearch/bleve v0.0.0-20190214220507-05d86ea8f6e3 | |||
@@ -112,6 +114,8 @@ github.com/facebookgo/grace/gracenet | |||
github.com/facebookgo/httpdown | |||
# github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 | |||
github.com/facebookgo/stats | |||
# github.com/gliderlabs/ssh v0.2.2 | |||
github.com/gliderlabs/ssh | |||
# github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd | |||
github.com/glycerine/go-unsnap-stream | |||
# github.com/go-macaron/binding v0.0.0-20160711225916-9440f336b443 | |||
@@ -353,7 +357,7 @@ golang.org/x/crypto/cast5 | |||
golang.org/x/crypto/openpgp/elgamal | |||
golang.org/x/crypto/ssh/knownhosts | |||
golang.org/x/crypto/ssh/agent | |||
# golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 | |||
# golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b | |||
golang.org/x/net/html/charset | |||
golang.org/x/net/html | |||
golang.org/x/net/html/atom | |||
@@ -365,7 +369,7 @@ golang.org/x/net/internal/socks | |||
# golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 | |||
golang.org/x/oauth2 | |||
golang.org/x/oauth2/internal | |||
# golang.org/x/sys v0.0.0-20190618155005-516e3c20635f | |||
# golang.org/x/sys v0.0.0-20190620070143-6f217b454f45 | |||
golang.org/x/sys/windows | |||
golang.org/x/sys/windows/svc | |||
golang.org/x/sys/unix |