mirror of https://github.com/go-gitea/gitea.git
switch to use gliderlabs/ssh for builtin server (#7250)
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>pull/7340/head
parent
c44f0b1c76
commit
d0ec940dd7
@ -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 |
||||
|
||||
[](https://godoc.org/github.com/gliderlabs/ssh) |
||||
[](https://circleci.com/gh/gliderlabs/ssh) |
||||
[](https://goreportcard.com/report/github.com/gliderlabs/ssh) |
||||
[](#sponsors) |
||||
[](http://slack.gliderlabs.com) |
||||
[](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
|
||||
|
||||
< |