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>tokarchuk/v1.17
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 |
||||||
|
|
||||||
|
[![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) |
||||||
|
} |
Loading…
Reference in new issue