TUN-3472: Set up rolling logger with zerolog and lumberjack
This commit is contained in:
		
							parent
							
								
									870f5fa907
								
							
						
					
					
						commit
						9bc1c0c70b
					
				
							
								
								
									
										8
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										8
									
								
								go.mod
								
								
								
								
							|  | @ -4,9 +4,10 @@ go 1.15 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	github.com/DATA-DOG/go-sqlmock v1.3.3 | 	github.com/DATA-DOG/go-sqlmock v1.3.3 | ||||||
| 	github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 | 	github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 // indirect | ||||||
| 	github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d | 	github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect | ||||||
| 	github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect | 	github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect | ||||||
|  | 	github.com/aws/aws-sdk-go v1.34.19 // indirect | ||||||
| 	github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect | 	github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect | ||||||
| 	github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 | 	github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 | ||||||
| 	github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc | 	github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc | ||||||
|  | @ -23,7 +24,7 @@ require ( | ||||||
| 	github.com/fsnotify/fsnotify v1.4.9 | 	github.com/fsnotify/fsnotify v1.4.9 | ||||||
| 	github.com/gdamore/tcell v1.3.0 | 	github.com/gdamore/tcell v1.3.0 | ||||||
| 	github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 | 	github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 | ||||||
| 	github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 | 	github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 // indirect | ||||||
| 	github.com/go-sql-driver/mysql v1.5.0 | 	github.com/go-sql-driver/mysql v1.5.0 | ||||||
| 	github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect | 	github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect | ||||||
| 	github.com/gobwas/pool v0.2.1 // indirect | 	github.com/gobwas/pool v0.2.1 // indirect | ||||||
|  | @ -63,6 +64,7 @@ require ( | ||||||
| 	google.golang.org/grpc v1.32.0 // indirect | 	google.golang.org/grpc v1.32.0 // indirect | ||||||
| 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect | 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect | ||||||
| 	gopkg.in/coreos/go-oidc.v2 v2.1.0 | 	gopkg.in/coreos/go-oidc.v2 v2.1.0 | ||||||
|  | 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 | ||||||
| 	gopkg.in/square/go-jose.v2 v2.4.0 // indirect | 	gopkg.in/square/go-jose.v2 v2.4.0 // indirect | ||||||
| 	gopkg.in/yaml.v2 v2.3.0 | 	gopkg.in/yaml.v2 v2.3.0 | ||||||
| 	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect | 	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										2
									
								
								go.sum
								
								
								
								
							|  | @ -100,6 +100,7 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ | ||||||
| github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | ||||||
| github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | ||||||
| github.com/aws/aws-sdk-go v1.32.1/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= | github.com/aws/aws-sdk-go v1.32.1/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= | ||||||
|  | github.com/aws/aws-sdk-go v1.34.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= | ||||||
| github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= | ||||||
| github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= | ||||||
| github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | ||||||
|  | @ -987,6 +988,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | ||||||
| gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= | gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= | ||||||
|  | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= | ||||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | ||||||
| gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= | gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= | ||||||
| gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= | gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= | ||||||
|  |  | ||||||
|  | @ -3,10 +3,12 @@ package logger | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
| 
 | 
 | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| 	fallbacklog "github.com/rs/zerolog/log" | 	fallbacklog "github.com/rs/zerolog/log" | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
|  | 	"gopkg.in/natefinch/lumberjack.v2" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -22,25 +24,34 @@ const ( | ||||||
| 	LogSSHLevelFlag     = "log-level" | 	LogSSHLevelFlag     = "log-level" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func fallbackLogger(err error) *zerolog.Logger { | ||||||
|  | 	failLog := fallbacklog.With().Logger() | ||||||
|  | 	fallbacklog.Error().Msgf("Falling back to a default logger due to logger setup failure: %s", err) | ||||||
|  | 
 | ||||||
|  | 	return &failLog | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func newZerolog(loggerConfig *Config) *zerolog.Logger { | func newZerolog(loggerConfig *Config) *zerolog.Logger { | ||||||
| 	var writers []io.Writer | 	var writers []io.Writer | ||||||
| 
 | 
 | ||||||
| 	if loggerConfig.ConsoleConfig != nil { | 	if loggerConfig.ConsoleConfig != nil { | ||||||
| 		writers = append(writers, zerolog.ConsoleWriter{ | 		writers = append(writers, createConsoleLogger(*loggerConfig.ConsoleConfig)) | ||||||
| 			Out:     os.Stderr, |  | ||||||
| 			NoColor: loggerConfig.ConsoleConfig.noColor, |  | ||||||
| 		}) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// TODO TUN-3472: Support file writer and log rotation
 | 	if loggerConfig.RollingConfig != nil { | ||||||
|  | 		rollingLogger, err := createRollingLogger(*loggerConfig.RollingConfig) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fallbackLogger(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		writers = append(writers, rollingLogger) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	multi := zerolog.MultiLevelWriter(writers...) | 	multi := zerolog.MultiLevelWriter(writers...) | ||||||
| 
 | 
 | ||||||
| 	level, err := zerolog.ParseLevel(loggerConfig.MinLevel) | 	level, err := zerolog.ParseLevel(loggerConfig.MinLevel) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		failLog := fallbacklog.With().Logger() | 		return fallbackLogger(err) | ||||||
| 		fallbacklog.Error().Msgf("Falling back to a default logger due to logger setup failure: %s", err) |  | ||||||
| 		return &failLog |  | ||||||
| 	} | 	} | ||||||
| 	log := zerolog.New(multi).With().Timestamp().Logger().Level(level) | 	log := zerolog.New(multi).With().Timestamp().Logger().Level(level) | ||||||
| 
 | 
 | ||||||
|  | @ -87,3 +98,23 @@ func Create(loggerConfig *Config) *zerolog.Logger { | ||||||
| 
 | 
 | ||||||
| 	return newZerolog(loggerConfig) | 	return newZerolog(loggerConfig) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func createConsoleLogger(config ConsoleConfig) io.Writer { | ||||||
|  | 	return zerolog.ConsoleWriter{ | ||||||
|  | 		Out:     os.Stderr, | ||||||
|  | 		NoColor: config.noColor, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func createRollingLogger(config RollingConfig) (io.Writer, error) { | ||||||
|  | 	if err := os.MkdirAll(config.Directory, 0744); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &lumberjack.Logger{ | ||||||
|  | 		Filename:   path.Join(config.Directory, config.Filename), | ||||||
|  | 		MaxBackups: config.maxBackups, | ||||||
|  | 		MaxSize:    config.maxSize, | ||||||
|  | 		MaxAge:     config.maxAge, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,40 +0,0 @@ | ||||||
| Skittles |  | ||||||
| ======== |  | ||||||
| 
 |  | ||||||
| Miminal package for terminal colors/ANSI escape code. |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
| 
 |  | ||||||
| ## Install |  | ||||||
| 
 |  | ||||||
| `go get github.com/acmacalister/skittles` |  | ||||||
| 
 |  | ||||||
| `import "github.com/acmacalister/skittles"` |  | ||||||
| 
 |  | ||||||
| ## Example |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
|   "fmt" |  | ||||||
|   "github.com/acmacalister/skittles" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
|   fmt.Println(skittles.Red("Red's my favorite color")) |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Supported Platforms |  | ||||||
| 
 |  | ||||||
| Only tested on OS X terminal app, but I would expect it to work with any unix based terminal. |  | ||||||
| 
 |  | ||||||
| ## Docs |  | ||||||
| 
 |  | ||||||
| * [GoDoc](http://godoc.org/github.com/acmacalister/skittles) |  | ||||||
| 
 |  | ||||||
| ## Help |  | ||||||
| 
 |  | ||||||
| * [Github](https://github.com/acmacalister) |  | ||||||
| * [Twitter](http://twitter.com/acmacalister) |  | ||||||
|  | @ -1,153 +0,0 @@ | ||||||
| // Miminal package for terminal colors/ANSI escape code.
 |  | ||||||
| // Check out the source here https://github.com/acmacalister/skittles.
 |  | ||||||
| // Also see the example directory for another example on how to use skittles.
 |  | ||||||
| //
 |  | ||||||
| //  package main
 |  | ||||||
| //
 |  | ||||||
| //  import (
 |  | ||||||
| //    "fmt"
 |  | ||||||
| //    "github.com/acmacalister/skittles"
 |  | ||||||
| //  )
 |  | ||||||
| //
 |  | ||||||
| //  func main() {
 |  | ||||||
| //    fmt.Println(skittles.Red("Red's my favorite color"))
 |  | ||||||
| //  }
 |  | ||||||
| package skittles |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // source: http://www.termsys.demon.co.uk/vtansi.htm
 |  | ||||||
| // source: http://ascii-table.com/ansi-escape-sequences.php
 |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	nofmt             = "0" |  | ||||||
| 	bold              = "1" |  | ||||||
| 	underline         = "4" |  | ||||||
| 	blink             = "5" |  | ||||||
| 	inverse           = "7"  // attributes end at 7
 |  | ||||||
| 	black             = "30" // colors start at 30
 |  | ||||||
| 	red               = "31" |  | ||||||
| 	green             = "32" |  | ||||||
| 	yellow            = "33" |  | ||||||
| 	blue              = "34" |  | ||||||
| 	magenta           = "35" |  | ||||||
| 	cyan              = "36" |  | ||||||
| 	white             = "37" // colors end at 37
 |  | ||||||
| 	blackBackground   = "40" // background colors start at 40
 |  | ||||||
| 	redBackground     = "41" |  | ||||||
| 	greenBackground   = "42" |  | ||||||
| 	yellowBackground  = "43" |  | ||||||
| 	blueBackground    = "44" |  | ||||||
| 	magentaBackground = "45" |  | ||||||
| 	cyanBackground    = "46" |  | ||||||
| 	whiteBackground   = "47" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // makeFunction returns a function that formats some text with the provided list
 |  | ||||||
| // of ANSI escape codes.
 |  | ||||||
| func makeFunction(attributes []string) func(interface{}) string { |  | ||||||
| 	return func(text interface{}) string { |  | ||||||
| 		return fmt.Sprintf("\033[%sm%s\033[0m", strings.Join(attributes, ";"), text) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// Reset resets all formatting.
 |  | ||||||
| 	Reset = makeFunction([]string{nofmt}) |  | ||||||
| 	// Bold makes terminal text bold and doesn't add any color.
 |  | ||||||
| 	Bold = makeFunction([]string{bold}) |  | ||||||
| 	// Underline makes terminal text underlined and doesn't add any color.
 |  | ||||||
| 	Underline = makeFunction([]string{underline}) |  | ||||||
| 	// Blink makes terminal text blink and doesn't add any color.
 |  | ||||||
| 	Blink = makeFunction([]string{blink}) |  | ||||||
| 	// Inverse inverts terminal text and doesn't add any color.
 |  | ||||||
| 	Inverse = makeFunction([]string{inverse}) |  | ||||||
| 
 |  | ||||||
| 	// Black makes terminal text black.
 |  | ||||||
| 	Black = makeFunction([]string{black}) |  | ||||||
| 	// Red makes terminal text red.
 |  | ||||||
| 	Red = makeFunction([]string{red}) |  | ||||||
| 	// Green makes terminal text green.
 |  | ||||||
| 	Green = makeFunction([]string{green}) |  | ||||||
| 	// Yellow makes terminal text yellow.
 |  | ||||||
| 	Yellow = makeFunction([]string{yellow}) |  | ||||||
| 	// Blue makes terminal text blue.
 |  | ||||||
| 	Blue = makeFunction([]string{blue}) |  | ||||||
| 	// Magenta makes terminal text magenta.
 |  | ||||||
| 	Magenta = makeFunction([]string{magenta}) |  | ||||||
| 	// Cyan makes terminal text cyan.
 |  | ||||||
| 	Cyan = makeFunction([]string{cyan}) |  | ||||||
| 	// White makes terminal text white.
 |  | ||||||
| 	White = makeFunction([]string{white}) |  | ||||||
| 
 |  | ||||||
| 	// BoldBlack makes terminal text bold and black.
 |  | ||||||
| 	BoldBlack = makeFunction([]string{black, bold}) |  | ||||||
| 	// BoldRed makes terminal text bold and red.
 |  | ||||||
| 	BoldRed = makeFunction([]string{red, bold}) |  | ||||||
| 	// BoldGreen makes terminal text bold and green.
 |  | ||||||
| 	BoldGreen = makeFunction([]string{green, bold}) |  | ||||||
| 	// BoldYellow makes terminal text bold and yellow.
 |  | ||||||
| 	BoldYellow = makeFunction([]string{yellow, bold}) |  | ||||||
| 	// BoldBlue makes terminal text bold and blue.
 |  | ||||||
| 	BoldBlue = makeFunction([]string{blue, bold}) |  | ||||||
| 	// BoldMagenta makes terminal text bold and magenta.
 |  | ||||||
| 	BoldMagenta = makeFunction([]string{magenta, bold}) |  | ||||||
| 	// BoldCyan makes terminal text bold and cyan.
 |  | ||||||
| 	BoldCyan = makeFunction([]string{cyan, bold}) |  | ||||||
| 	// BoldWhite makes terminal text bold and white.
 |  | ||||||
| 	BoldWhite = makeFunction([]string{white, bold}) |  | ||||||
| 
 |  | ||||||
| 	// BlinkBlack makes terminal text blink and black.
 |  | ||||||
| 	BlinkBlack = makeFunction([]string{black, blink}) |  | ||||||
| 	// BlinkRed makes terminal text blink and red.
 |  | ||||||
| 	BlinkRed = makeFunction([]string{red, blink}) |  | ||||||
| 	// BlinkGreen makes terminal text blink and green.
 |  | ||||||
| 	BlinkGreen = makeFunction([]string{green, blink}) |  | ||||||
| 	// BlinkYellow makes terminal text blink and yellow.
 |  | ||||||
| 	BlinkYellow = makeFunction([]string{yellow, blink}) |  | ||||||
| 	// BlinkBlue makes terminal text blink and blue.
 |  | ||||||
| 	BlinkBlue = makeFunction([]string{blue, blink}) |  | ||||||
| 	// BlinkMagenta makes terminal text blink and magenta.
 |  | ||||||
| 	BlinkMagenta = makeFunction([]string{magenta, blink}) |  | ||||||
| 	// BlinkCyan makes terminal text blink and cyan.
 |  | ||||||
| 	BlinkCyan = makeFunction([]string{cyan, blink}) |  | ||||||
| 	// BlinkWhite makes terminal text blink and white.
 |  | ||||||
| 	BlinkWhite = makeFunction([]string{white, blink}) |  | ||||||
| 
 |  | ||||||
| 	// UnderlineBlack makes terminal text underlined and black.
 |  | ||||||
| 	UnderlineBlack = makeFunction([]string{black, underline}) |  | ||||||
| 	// UnderlineRed makes terminal text underlined and red.
 |  | ||||||
| 	UnderlineRed = makeFunction([]string{red, underline}) |  | ||||||
| 	// UnderlineGreen makes terminal text underlined and green.
 |  | ||||||
| 	UnderlineGreen = makeFunction([]string{green, underline}) |  | ||||||
| 	// UnderlineYellow makes terminal text underlined and yellow.
 |  | ||||||
| 	UnderlineYellow = makeFunction([]string{yellow, underline}) |  | ||||||
| 	// UnderlineBlue makes terminal text underlined and blue.
 |  | ||||||
| 	UnderlineBlue = makeFunction([]string{blue, underline}) |  | ||||||
| 	// UnderlineMagenta makes terminal text underlined and magenta.
 |  | ||||||
| 	UnderlineMagenta = makeFunction([]string{magenta, underline}) |  | ||||||
| 	// UnderlineCyan makes terminal text underlined and cyan.
 |  | ||||||
| 	UnderlineCyan = makeFunction([]string{cyan, underline}) |  | ||||||
| 	// UnderlineWhite makes terminal text underlined and white.
 |  | ||||||
| 	UnderlineWhite = makeFunction([]string{white, underline}) |  | ||||||
| 
 |  | ||||||
| 	// InverseBlack makes terminal text inverted and black.
 |  | ||||||
| 	InverseBlack = makeFunction([]string{black, inverse}) |  | ||||||
| 	// InverseRed makes terminal text inverted and red.
 |  | ||||||
| 	InverseRed = makeFunction([]string{red, inverse}) |  | ||||||
| 	// InverseGreen makes terminal text inverted and green.
 |  | ||||||
| 	InverseGreen = makeFunction([]string{green, inverse}) |  | ||||||
| 	// InverseYellow makes terminal text inverted and yellow.
 |  | ||||||
| 	InverseYellow = makeFunction([]string{yellow, inverse}) |  | ||||||
| 	// InverseBlue makes terminal text inverted and blue.
 |  | ||||||
| 	InverseBlue = makeFunction([]string{blue, inverse}) |  | ||||||
| 	// InverseMagenta makes terminal text inverted and magenta.
 |  | ||||||
| 	InverseMagenta = makeFunction([]string{magenta, inverse}) |  | ||||||
| 	// InverseCyan makes terminal text inverted and cyan.
 |  | ||||||
| 	InverseCyan = makeFunction([]string{cyan, inverse}) |  | ||||||
| 	// InverseWhite makes terminal text inverted and white.
 |  | ||||||
| 	InverseWhite = makeFunction([]string{white, inverse}) |  | ||||||
| ) |  | ||||||
|  | @ -1,19 +0,0 @@ | ||||||
| Copyright (C) 2014 Alec Thomas |  | ||||||
| 
 |  | ||||||
| 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. |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| # Units - Helpful unit multipliers and functions for Go |  | ||||||
| 
 |  | ||||||
| The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package. |  | ||||||
| 
 |  | ||||||
| It allows for code like this: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| n, err := ParseBase2Bytes("1KB") |  | ||||||
| // n == 1024 |  | ||||||
| n = units.Mebibyte * 512 |  | ||||||
| ``` |  | ||||||
|  | @ -1,85 +0,0 @@ | ||||||
| package units |  | ||||||
| 
 |  | ||||||
| // Base2Bytes is the old non-SI power-of-2 byte scale (1024 bytes in a kilobyte,
 |  | ||||||
| // etc.).
 |  | ||||||
| type Base2Bytes int64 |  | ||||||
| 
 |  | ||||||
| // Base-2 byte units.
 |  | ||||||
| const ( |  | ||||||
| 	Kibibyte Base2Bytes = 1024 |  | ||||||
| 	KiB                 = Kibibyte |  | ||||||
| 	Mebibyte            = Kibibyte * 1024 |  | ||||||
| 	MiB                 = Mebibyte |  | ||||||
| 	Gibibyte            = Mebibyte * 1024 |  | ||||||
| 	GiB                 = Gibibyte |  | ||||||
| 	Tebibyte            = Gibibyte * 1024 |  | ||||||
| 	TiB                 = Tebibyte |  | ||||||
| 	Pebibyte            = Tebibyte * 1024 |  | ||||||
| 	PiB                 = Pebibyte |  | ||||||
| 	Exbibyte            = Pebibyte * 1024 |  | ||||||
| 	EiB                 = Exbibyte |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	bytesUnitMap    = MakeUnitMap("iB", "B", 1024) |  | ||||||
| 	oldBytesUnitMap = MakeUnitMap("B", "B", 1024) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB
 |  | ||||||
| // and KiB are both 1024.
 |  | ||||||
| // However "kB", which is the correct SI spelling of 1000 Bytes, is rejected.
 |  | ||||||
| func ParseBase2Bytes(s string) (Base2Bytes, error) { |  | ||||||
| 	n, err := ParseUnit(s, bytesUnitMap) |  | ||||||
| 	if err != nil { |  | ||||||
| 		n, err = ParseUnit(s, oldBytesUnitMap) |  | ||||||
| 	} |  | ||||||
| 	return Base2Bytes(n), err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b Base2Bytes) String() string { |  | ||||||
| 	return ToString(int64(b), 1024, "iB", "B") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	metricBytesUnitMap = MakeUnitMap("B", "B", 1000) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MetricBytes are SI byte units (1000 bytes in a kilobyte).
 |  | ||||||
| type MetricBytes SI |  | ||||||
| 
 |  | ||||||
| // SI base-10 byte units.
 |  | ||||||
| const ( |  | ||||||
| 	Kilobyte MetricBytes = 1000 |  | ||||||
| 	KB                   = Kilobyte |  | ||||||
| 	Megabyte             = Kilobyte * 1000 |  | ||||||
| 	MB                   = Megabyte |  | ||||||
| 	Gigabyte             = Megabyte * 1000 |  | ||||||
| 	GB                   = Gigabyte |  | ||||||
| 	Terabyte             = Gigabyte * 1000 |  | ||||||
| 	TB                   = Terabyte |  | ||||||
| 	Petabyte             = Terabyte * 1000 |  | ||||||
| 	PB                   = Petabyte |  | ||||||
| 	Exabyte              = Petabyte * 1000 |  | ||||||
| 	EB                   = Exabyte |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // ParseMetricBytes parses base-10 metric byte units. That is, KB is 1000 bytes.
 |  | ||||||
| func ParseMetricBytes(s string) (MetricBytes, error) { |  | ||||||
| 	n, err := ParseUnit(s, metricBytesUnitMap) |  | ||||||
| 	return MetricBytes(n), err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TODO: represents 1000B as uppercase "KB", while SI standard requires "kB".
 |  | ||||||
| func (m MetricBytes) String() string { |  | ||||||
| 	return ToString(int64(m), 1000, "B", "B") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ParseStrictBytes supports both iB and B suffixes for base 2 and metric,
 |  | ||||||
| // respectively. That is, KiB represents 1024 and kB, KB represent 1000.
 |  | ||||||
| func ParseStrictBytes(s string) (int64, error) { |  | ||||||
| 	n, err := ParseUnit(s, bytesUnitMap) |  | ||||||
| 	if err != nil { |  | ||||||
| 		n, err = ParseUnit(s, metricBytesUnitMap) |  | ||||||
| 	} |  | ||||||
| 	return int64(n), err |  | ||||||
| } |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| // Package units provides helpful unit multipliers and functions for Go.
 |  | ||||||
| //
 |  | ||||||
| // The goal of this package is to have functionality similar to the time [1] package.
 |  | ||||||
| //
 |  | ||||||
| //
 |  | ||||||
| // [1] http://golang.org/pkg/time/
 |  | ||||||
| //
 |  | ||||||
| // It allows for code like this:
 |  | ||||||
| //
 |  | ||||||
| //     n, err := ParseBase2Bytes("1KB")
 |  | ||||||
| //     // n == 1024
 |  | ||||||
| //     n = units.Mebibyte * 512
 |  | ||||||
| package units |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| module github.com/alecthomas/units |  | ||||||
| 
 |  | ||||||
| require github.com/stretchr/testify v1.4.0 |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= |  | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |  | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |  | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |  | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |  | ||||||
| github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |  | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |  | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |  | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |  | ||||||
| gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= |  | ||||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |  | ||||||
|  | @ -1,50 +0,0 @@ | ||||||
| package units |  | ||||||
| 
 |  | ||||||
| // SI units.
 |  | ||||||
| type SI int64 |  | ||||||
| 
 |  | ||||||
| // SI unit multiples.
 |  | ||||||
| const ( |  | ||||||
| 	Kilo SI = 1000 |  | ||||||
| 	Mega    = Kilo * 1000 |  | ||||||
| 	Giga    = Mega * 1000 |  | ||||||
| 	Tera    = Giga * 1000 |  | ||||||
| 	Peta    = Tera * 1000 |  | ||||||
| 	Exa     = Peta * 1000 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 { |  | ||||||
| 	res := map[string]float64{ |  | ||||||
| 		shortSuffix: 1, |  | ||||||
| 		// see below for "k" / "K"
 |  | ||||||
| 		"M" + suffix: float64(scale * scale), |  | ||||||
| 		"G" + suffix: float64(scale * scale * scale), |  | ||||||
| 		"T" + suffix: float64(scale * scale * scale * scale), |  | ||||||
| 		"P" + suffix: float64(scale * scale * scale * scale * scale), |  | ||||||
| 		"E" + suffix: float64(scale * scale * scale * scale * scale * scale), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Standard SI prefixes use lowercase "k" for kilo = 1000.
 |  | ||||||
| 	// For compatibility, and to be fool-proof, we accept both "k" and "K" in metric mode.
 |  | ||||||
| 	//
 |  | ||||||
| 	// However, official binary prefixes are always capitalized - "KiB" -
 |  | ||||||
| 	// and we specifically never parse "kB" as 1024B because:
 |  | ||||||
| 	//
 |  | ||||||
| 	// (1) people pedantic enough to use lowercase according to SI unlikely to abuse "k" to mean 1024 :-)
 |  | ||||||
| 	//
 |  | ||||||
| 	// (2) Use of capital K for 1024 was an informal tradition predating IEC prefixes:
 |  | ||||||
| 	//     "The binary meaning of the kilobyte for 1024 bytes typically uses the symbol KB, with an
 |  | ||||||
| 	//     uppercase letter K."
 |  | ||||||
| 	//     -- https://en.wikipedia.org/wiki/Kilobyte#Base_2_(1024_bytes)
 |  | ||||||
| 	//     "Capitalization of the letter K became the de facto standard for binary notation, although this
 |  | ||||||
| 	//     could not be extended to higher powers, and use of the lowercase k did persist.[13][14][15]"
 |  | ||||||
| 	//     -- https://en.wikipedia.org/wiki/Binary_prefix#History
 |  | ||||||
| 	//     See also the extensive https://en.wikipedia.org/wiki/Timeline_of_binary_prefixes.
 |  | ||||||
| 	if scale == 1024 { |  | ||||||
| 		res["K"+suffix] = float64(scale) |  | ||||||
| 	} else { |  | ||||||
| 		res["k"+suffix] = float64(scale) |  | ||||||
| 		res["K"+suffix] = float64(scale) |  | ||||||
| 	} |  | ||||||
| 	return res |  | ||||||
| } |  | ||||||
|  | @ -1,138 +0,0 @@ | ||||||
| package units |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	siUnits = []string{"", "K", "M", "G", "T", "P", "E"} |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func ToString(n int64, scale int64, suffix, baseSuffix string) string { |  | ||||||
| 	mn := len(siUnits) |  | ||||||
| 	out := make([]string, mn) |  | ||||||
| 	for i, m := range siUnits { |  | ||||||
| 		if n%scale != 0 || i == 0 && n == 0 { |  | ||||||
| 			s := suffix |  | ||||||
| 			if i == 0 { |  | ||||||
| 				s = baseSuffix |  | ||||||
| 			} |  | ||||||
| 			out[mn-1-i] = fmt.Sprintf("%d%s%s", n%scale, m, s) |  | ||||||
| 		} |  | ||||||
| 		n /= scale |  | ||||||
| 		if n == 0 { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return strings.Join(out, "") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Below code ripped straight from http://golang.org/src/pkg/time/format.go?s=33392:33438#L1123
 |  | ||||||
| var errLeadingInt = errors.New("units: bad [0-9]*") // never printed
 |  | ||||||
| 
 |  | ||||||
| // leadingInt consumes the leading [0-9]* from s.
 |  | ||||||
| func leadingInt(s string) (x int64, rem string, err error) { |  | ||||||
| 	i := 0 |  | ||||||
| 	for ; i < len(s); i++ { |  | ||||||
| 		c := s[i] |  | ||||||
| 		if c < '0' || c > '9' { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		if x >= (1<<63-10)/10 { |  | ||||||
| 			// overflow
 |  | ||||||
| 			return 0, "", errLeadingInt |  | ||||||
| 		} |  | ||||||
| 		x = x*10 + int64(c) - '0' |  | ||||||
| 	} |  | ||||||
| 	return x, s[i:], nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ParseUnit(s string, unitMap map[string]float64) (int64, error) { |  | ||||||
| 	// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
 |  | ||||||
| 	orig := s |  | ||||||
| 	f := float64(0) |  | ||||||
| 	neg := false |  | ||||||
| 
 |  | ||||||
| 	// Consume [-+]?
 |  | ||||||
| 	if s != "" { |  | ||||||
| 		c := s[0] |  | ||||||
| 		if c == '-' || c == '+' { |  | ||||||
| 			neg = c == '-' |  | ||||||
| 			s = s[1:] |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	// Special case: if all that is left is "0", this is zero.
 |  | ||||||
| 	if s == "0" { |  | ||||||
| 		return 0, nil |  | ||||||
| 	} |  | ||||||
| 	if s == "" { |  | ||||||
| 		return 0, errors.New("units: invalid " + orig) |  | ||||||
| 	} |  | ||||||
| 	for s != "" { |  | ||||||
| 		g := float64(0) // this element of the sequence
 |  | ||||||
| 
 |  | ||||||
| 		var x int64 |  | ||||||
| 		var err error |  | ||||||
| 
 |  | ||||||
| 		// The next character must be [0-9.]
 |  | ||||||
| 		if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) { |  | ||||||
| 			return 0, errors.New("units: invalid " + orig) |  | ||||||
| 		} |  | ||||||
| 		// Consume [0-9]*
 |  | ||||||
| 		pl := len(s) |  | ||||||
| 		x, s, err = leadingInt(s) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return 0, errors.New("units: invalid " + orig) |  | ||||||
| 		} |  | ||||||
| 		g = float64(x) |  | ||||||
| 		pre := pl != len(s) // whether we consumed anything before a period
 |  | ||||||
| 
 |  | ||||||
| 		// Consume (\.[0-9]*)?
 |  | ||||||
| 		post := false |  | ||||||
| 		if s != "" && s[0] == '.' { |  | ||||||
| 			s = s[1:] |  | ||||||
| 			pl := len(s) |  | ||||||
| 			x, s, err = leadingInt(s) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return 0, errors.New("units: invalid " + orig) |  | ||||||
| 			} |  | ||||||
| 			scale := 1.0 |  | ||||||
| 			for n := pl - len(s); n > 0; n-- { |  | ||||||
| 				scale *= 10 |  | ||||||
| 			} |  | ||||||
| 			g += float64(x) / scale |  | ||||||
| 			post = pl != len(s) |  | ||||||
| 		} |  | ||||||
| 		if !pre && !post { |  | ||||||
| 			// no digits (e.g. ".s" or "-.s")
 |  | ||||||
| 			return 0, errors.New("units: invalid " + orig) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Consume unit.
 |  | ||||||
| 		i := 0 |  | ||||||
| 		for ; i < len(s); i++ { |  | ||||||
| 			c := s[i] |  | ||||||
| 			if c == '.' || ('0' <= c && c <= '9') { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		u := s[:i] |  | ||||||
| 		s = s[i:] |  | ||||||
| 		unit, ok := unitMap[u] |  | ||||||
| 		if !ok { |  | ||||||
| 			return 0, errors.New("units: unknown unit " + u + " in " + orig) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		f += g * unit |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if neg { |  | ||||||
| 		f = -f |  | ||||||
| 	} |  | ||||||
| 	if f < float64(-1<<63) || f > float64(1<<63-1) { |  | ||||||
| 		return 0, errors.New("units: overflow parsing unit") |  | ||||||
| 	} |  | ||||||
| 	return int64(f), nil |  | ||||||
| } |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| shlex.test |  | ||||||
|  | @ -1,20 +0,0 @@ | ||||||
| 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. |  | ||||||
|  | @ -1,38 +0,0 @@ | ||||||
| # 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 |  | ||||||
| 
 |  | ||||||
|  | @ -1,193 +0,0 @@ | ||||||
| // 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 |  | ||||||
| } |  | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| 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. |  | ||||||
|  | @ -1,96 +0,0 @@ | ||||||
| # 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 |  | ||||||
|  | @ -1,83 +0,0 @@ | ||||||
| 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) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,26 +0,0 @@ | ||||||
| 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 |  | ||||||
|  | @ -1,55 +0,0 @@ | ||||||
| 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) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,152 +0,0 @@ | ||||||
| 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) |  | ||||||
| } |  | ||||||
|  | @ -1,45 +0,0 @@ | ||||||
| /* |  | ||||||
| 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 |  | ||||||
|  | @ -1,77 +0,0 @@ | ||||||
| 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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,395 +0,0 @@ | ||||||
| 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) { |  | ||||||
| 			applyConnMetadata(ctx, conn) |  | ||||||
| 			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) { |  | ||||||
| 	ctx, cancel := newContext(srv) |  | ||||||
| 	if srv.ConnCallback != nil { |  | ||||||
| 		cbConn := srv.ConnCallback(ctx, newConn) |  | ||||||
| 		if cbConn == nil { |  | ||||||
| 			newConn.Close() |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		newConn = cbConn |  | ||||||
| 	} |  | ||||||
| 	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() |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,308 +0,0 @@ | ||||||
| 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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,123 +0,0 @@ | ||||||
| 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(ctx Context, 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) |  | ||||||
| } |  | ||||||
|  | @ -1,193 +0,0 @@ | ||||||
| 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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,83 +0,0 @@ | ||||||
| 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 |  | ||||||
| } |  | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| 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) |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||||
|  | *.o | ||||||
|  | *.a | ||||||
|  | *.so | ||||||
|  | 
 | ||||||
|  | # Folders | ||||||
|  | _obj | ||||||
|  | _test | ||||||
|  | 
 | ||||||
|  | # Architecture specific extensions/prefixes | ||||||
|  | *.[568vq] | ||||||
|  | [568vq].out | ||||||
|  | 
 | ||||||
|  | *.cgo1.go | ||||||
|  | *.cgo2.c | ||||||
|  | _cgo_defun.c | ||||||
|  | _cgo_gotypes.go | ||||||
|  | _cgo_export.* | ||||||
|  | 
 | ||||||
|  | _testmain.go | ||||||
|  | 
 | ||||||
|  | *.exe | ||||||
|  | *.test | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | language: go | ||||||
|  | 
 | ||||||
|  | go: | ||||||
|  |   - 1.8 | ||||||
|  |   - 1.7 | ||||||
|  |   - 1.6 | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| The MIT License (MIT) | The MIT License (MIT) | ||||||
| 
 | 
 | ||||||
| Copyright (c) 2016 Austin Cherry | Copyright (c) 2014 Nate Finch  | ||||||
| 
 | 
 | ||||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -0,0 +1,179 @@ | ||||||
|  | # lumberjack  [](https://godoc.org/gopkg.in/natefinch/lumberjack.v2) [](https://travis-ci.org/natefinch/lumberjack) [](https://ci.appveyor.com/project/natefinch/lumberjack) [](https://coveralls.io/r/natefinch/lumberjack?branch=v2.0) | ||||||
|  | 
 | ||||||
|  | ### Lumberjack is a Go package for writing logs to rolling files. | ||||||
|  | 
 | ||||||
|  | Package lumberjack provides a rolling logger. | ||||||
|  | 
 | ||||||
|  | Note that this is v2.0 of lumberjack, and should be imported using gopkg.in | ||||||
|  | thusly: | ||||||
|  | 
 | ||||||
|  |     import "gopkg.in/natefinch/lumberjack.v2" | ||||||
|  | 
 | ||||||
|  | The package name remains simply lumberjack, and the code resides at | ||||||
|  | https://github.com/natefinch/lumberjack under the v2.0 branch. | ||||||
|  | 
 | ||||||
|  | Lumberjack is intended to be one part of a logging infrastructure. | ||||||
|  | It is not an all-in-one solution, but instead is a pluggable | ||||||
|  | component at the bottom of the logging stack that simply controls the files | ||||||
|  | to which logs are written. | ||||||
|  | 
 | ||||||
|  | Lumberjack plays well with any logging package that can write to an | ||||||
|  | io.Writer, including the standard library's log package. | ||||||
|  | 
 | ||||||
|  | Lumberjack assumes that only one process is writing to the output files. | ||||||
|  | Using the same lumberjack configuration from multiple processes on the same | ||||||
|  | machine will result in improper behavior. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | **Example** | ||||||
|  | 
 | ||||||
|  | To use lumberjack with the standard library's log package, just pass it into the SetOutput function when your application starts. | ||||||
|  | 
 | ||||||
|  | Code: | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | log.SetOutput(&lumberjack.Logger{ | ||||||
|  |     Filename:   "/var/log/myapp/foo.log", | ||||||
|  |     MaxSize:    500, // megabytes | ||||||
|  |     MaxBackups: 3, | ||||||
|  |     MaxAge:     28, //days | ||||||
|  |     Compress:   true, // disabled by default | ||||||
|  | }) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## type Logger | ||||||
|  | ``` go | ||||||
|  | type Logger struct { | ||||||
|  |     // Filename is the file to write logs to.  Backup log files will be retained | ||||||
|  |     // in the same directory.  It uses <processname>-lumberjack.log in | ||||||
|  |     // os.TempDir() if empty. | ||||||
|  |     Filename string `json:"filename" yaml:"filename"` | ||||||
|  | 
 | ||||||
|  |     // MaxSize is the maximum size in megabytes of the log file before it gets | ||||||
|  |     // rotated. It defaults to 100 megabytes. | ||||||
|  |     MaxSize int `json:"maxsize" yaml:"maxsize"` | ||||||
|  | 
 | ||||||
|  |     // MaxAge is the maximum number of days to retain old log files based on the | ||||||
|  |     // timestamp encoded in their filename.  Note that a day is defined as 24 | ||||||
|  |     // hours and may not exactly correspond to calendar days due to daylight | ||||||
|  |     // savings, leap seconds, etc. The default is not to remove old log files | ||||||
|  |     // based on age. | ||||||
|  |     MaxAge int `json:"maxage" yaml:"maxage"` | ||||||
|  | 
 | ||||||
|  |     // MaxBackups is the maximum number of old log files to retain.  The default | ||||||
|  |     // is to retain all old log files (though MaxAge may still cause them to get | ||||||
|  |     // deleted.) | ||||||
|  |     MaxBackups int `json:"maxbackups" yaml:"maxbackups"` | ||||||
|  | 
 | ||||||
|  |     // LocalTime determines if the time used for formatting the timestamps in | ||||||
|  |     // backup files is the computer's local time.  The default is to use UTC | ||||||
|  |     // time. | ||||||
|  |     LocalTime bool `json:"localtime" yaml:"localtime"` | ||||||
|  | 
 | ||||||
|  |     // Compress determines if the rotated log files should be compressed | ||||||
|  |     // using gzip. The default is not to perform compression. | ||||||
|  |     Compress bool `json:"compress" yaml:"compress"` | ||||||
|  |     // contains filtered or unexported fields | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | Logger is an io.WriteCloser that writes to the specified filename. | ||||||
|  | 
 | ||||||
|  | Logger opens or creates the logfile on first Write.  If the file exists and | ||||||
|  | is less than MaxSize megabytes, lumberjack will open and append to that file. | ||||||
|  | If the file exists and its size is >= MaxSize megabytes, the file is renamed | ||||||
|  | by putting the current time in a timestamp in the name immediately before the | ||||||
|  | file's extension (or the end of the filename if there's no extension). A new | ||||||
|  | log file is then created using original filename. | ||||||
|  | 
 | ||||||
|  | Whenever a write would cause the current log file exceed MaxSize megabytes, | ||||||
|  | the current file is closed, renamed, and a new log file created with the | ||||||
|  | original name. Thus, the filename you give Logger is always the "current" log | ||||||
|  | file. | ||||||
|  | 
 | ||||||
|  | Backups use the log file name given to Logger, in the form `name-timestamp.ext` | ||||||
|  | where name is the filename without the extension, timestamp is the time at which | ||||||
|  | the log was rotated formatted with the time.Time format of | ||||||
|  | `2006-01-02T15-04-05.000` and the extension is the original extension.  For | ||||||
|  | example, if your Logger.Filename is `/var/log/foo/server.log`, a backup created | ||||||
|  | at 6:30pm on Nov 11 2016 would use the filename | ||||||
|  | `/var/log/foo/server-2016-11-04T18-30-00.000.log` | ||||||
|  | 
 | ||||||
|  | ### Cleaning Up Old Log Files | ||||||
|  | Whenever a new logfile gets created, old log files may be deleted.  The most | ||||||
|  | recent files according to the encoded timestamp will be retained, up to a | ||||||
|  | number equal to MaxBackups (or all of them if MaxBackups is 0).  Any files | ||||||
|  | with an encoded timestamp older than MaxAge days are deleted, regardless of | ||||||
|  | MaxBackups.  Note that the time encoded in the timestamp is the rotation | ||||||
|  | time, which may differ from the last time that file was written to. | ||||||
|  | 
 | ||||||
|  | If MaxBackups and MaxAge are both 0, no old log files will be deleted. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### func (\*Logger) Close | ||||||
|  | ``` go | ||||||
|  | func (l *Logger) Close() error | ||||||
|  | ``` | ||||||
|  | Close implements io.Closer, and closes the current logfile. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### func (\*Logger) Rotate | ||||||
|  | ``` go | ||||||
|  | func (l *Logger) Rotate() error | ||||||
|  | ``` | ||||||
|  | Rotate causes Logger to close the existing log file and immediately create a | ||||||
|  | new one.  This is a helper function for applications that want to initiate | ||||||
|  | rotations outside of the normal rotation rules, such as in response to | ||||||
|  | SIGHUP.  After rotating, this initiates a cleanup of old log files according | ||||||
|  | to the normal rules. | ||||||
|  | 
 | ||||||
|  | **Example** | ||||||
|  | 
 | ||||||
|  | Example of how to rotate in response to SIGHUP. | ||||||
|  | 
 | ||||||
|  | Code: | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | l := &lumberjack.Logger{} | ||||||
|  | log.SetOutput(l) | ||||||
|  | c := make(chan os.Signal, 1) | ||||||
|  | signal.Notify(c, syscall.SIGHUP) | ||||||
|  | 
 | ||||||
|  | go func() { | ||||||
|  |     for { | ||||||
|  |         <-c | ||||||
|  |         l.Rotate() | ||||||
|  |     } | ||||||
|  | }() | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### func (\*Logger) Write | ||||||
|  | ``` go | ||||||
|  | func (l *Logger) Write(p []byte) (n int, err error) | ||||||
|  | ``` | ||||||
|  | Write implements io.Writer.  If a write would cause the log file to be larger | ||||||
|  | than MaxSize, the file is closed, renamed to include a timestamp of the | ||||||
|  | current time, and a new log file is created using the original log file name. | ||||||
|  | If the length of the write is greater than MaxSize, an error is returned. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | - - - | ||||||
|  | Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | // +build !linux
 | ||||||
|  | 
 | ||||||
|  | package lumberjack | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func chown(_ string, _ os.FileInfo) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | package lumberjack | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"syscall" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // os_Chown is a var so we can mock it out during tests.
 | ||||||
|  | var os_Chown = os.Chown | ||||||
|  | 
 | ||||||
|  | func chown(name string, info os.FileInfo) error { | ||||||
|  | 	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	f.Close() | ||||||
|  | 	stat := info.Sys().(*syscall.Stat_t) | ||||||
|  | 	return os_Chown(name, int(stat.Uid), int(stat.Gid)) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,541 @@ | ||||||
|  | // Package lumberjack provides a rolling logger.
 | ||||||
|  | //
 | ||||||
|  | // Note that this is v2.0 of lumberjack, and should be imported using gopkg.in
 | ||||||
|  | // thusly:
 | ||||||
|  | //
 | ||||||
|  | //   import "gopkg.in/natefinch/lumberjack.v2"
 | ||||||
|  | //
 | ||||||
|  | // The package name remains simply lumberjack, and the code resides at
 | ||||||
|  | // https://github.com/natefinch/lumberjack under the v2.0 branch.
 | ||||||
|  | //
 | ||||||
|  | // Lumberjack is intended to be one part of a logging infrastructure.
 | ||||||
|  | // It is not an all-in-one solution, but instead is a pluggable
 | ||||||
|  | // component at the bottom of the logging stack that simply controls the files
 | ||||||
|  | // to which logs are written.
 | ||||||
|  | //
 | ||||||
|  | // Lumberjack plays well with any logging package that can write to an
 | ||||||
|  | // io.Writer, including the standard library's log package.
 | ||||||
|  | //
 | ||||||
|  | // Lumberjack assumes that only one process is writing to the output files.
 | ||||||
|  | // Using the same lumberjack configuration from multiple processes on the same
 | ||||||
|  | // machine will result in improper behavior.
 | ||||||
|  | package lumberjack | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"compress/gzip" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	backupTimeFormat = "2006-01-02T15-04-05.000" | ||||||
|  | 	compressSuffix   = ".gz" | ||||||
|  | 	defaultMaxSize   = 100 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ensure we always implement io.WriteCloser
 | ||||||
|  | var _ io.WriteCloser = (*Logger)(nil) | ||||||
|  | 
 | ||||||
|  | // Logger is an io.WriteCloser that writes to the specified filename.
 | ||||||
|  | //
 | ||||||
|  | // Logger opens or creates the logfile on first Write.  If the file exists and
 | ||||||
|  | // is less than MaxSize megabytes, lumberjack will open and append to that file.
 | ||||||
|  | // If the file exists and its size is >= MaxSize megabytes, the file is renamed
 | ||||||
|  | // by putting the current time in a timestamp in the name immediately before the
 | ||||||
|  | // file's extension (or the end of the filename if there's no extension). A new
 | ||||||
|  | // log file is then created using original filename.
 | ||||||
|  | //
 | ||||||
|  | // Whenever a write would cause the current log file exceed MaxSize megabytes,
 | ||||||
|  | // the current file is closed, renamed, and a new log file created with the
 | ||||||
|  | // original name. Thus, the filename you give Logger is always the "current" log
 | ||||||
|  | // file.
 | ||||||
|  | //
 | ||||||
|  | // Backups use the log file name given to Logger, in the form
 | ||||||
|  | // `name-timestamp.ext` where name is the filename without the extension,
 | ||||||
|  | // timestamp is the time at which the log was rotated formatted with the
 | ||||||
|  | // time.Time format of `2006-01-02T15-04-05.000` and the extension is the
 | ||||||
|  | // original extension.  For example, if your Logger.Filename is
 | ||||||
|  | // `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would
 | ||||||
|  | // use the filename `/var/log/foo/server-2016-11-04T18-30-00.000.log`
 | ||||||
|  | //
 | ||||||
|  | // Cleaning Up Old Log Files
 | ||||||
|  | //
 | ||||||
|  | // Whenever a new logfile gets created, old log files may be deleted.  The most
 | ||||||
|  | // recent files according to the encoded timestamp will be retained, up to a
 | ||||||
|  | // number equal to MaxBackups (or all of them if MaxBackups is 0).  Any files
 | ||||||
|  | // with an encoded timestamp older than MaxAge days are deleted, regardless of
 | ||||||
|  | // MaxBackups.  Note that the time encoded in the timestamp is the rotation
 | ||||||
|  | // time, which may differ from the last time that file was written to.
 | ||||||
|  | //
 | ||||||
|  | // If MaxBackups and MaxAge are both 0, no old log files will be deleted.
 | ||||||
|  | type Logger struct { | ||||||
|  | 	// Filename is the file to write logs to.  Backup log files will be retained
 | ||||||
|  | 	// in the same directory.  It uses <processname>-lumberjack.log in
 | ||||||
|  | 	// os.TempDir() if empty.
 | ||||||
|  | 	Filename string `json:"filename" yaml:"filename"` | ||||||
|  | 
 | ||||||
|  | 	// MaxSize is the maximum size in megabytes of the log file before it gets
 | ||||||
|  | 	// rotated. It defaults to 100 megabytes.
 | ||||||
|  | 	MaxSize int `json:"maxsize" yaml:"maxsize"` | ||||||
|  | 
 | ||||||
|  | 	// MaxAge is the maximum number of days to retain old log files based on the
 | ||||||
|  | 	// timestamp encoded in their filename.  Note that a day is defined as 24
 | ||||||
|  | 	// hours and may not exactly correspond to calendar days due to daylight
 | ||||||
|  | 	// savings, leap seconds, etc. The default is not to remove old log files
 | ||||||
|  | 	// based on age.
 | ||||||
|  | 	MaxAge int `json:"maxage" yaml:"maxage"` | ||||||
|  | 
 | ||||||
|  | 	// MaxBackups is the maximum number of old log files to retain.  The default
 | ||||||
|  | 	// is to retain all old log files (though MaxAge may still cause them to get
 | ||||||
|  | 	// deleted.)
 | ||||||
|  | 	MaxBackups int `json:"maxbackups" yaml:"maxbackups"` | ||||||
|  | 
 | ||||||
|  | 	// LocalTime determines if the time used for formatting the timestamps in
 | ||||||
|  | 	// backup files is the computer's local time.  The default is to use UTC
 | ||||||
|  | 	// time.
 | ||||||
|  | 	LocalTime bool `json:"localtime" yaml:"localtime"` | ||||||
|  | 
 | ||||||
|  | 	// Compress determines if the rotated log files should be compressed
 | ||||||
|  | 	// using gzip. The default is not to perform compression.
 | ||||||
|  | 	Compress bool `json:"compress" yaml:"compress"` | ||||||
|  | 
 | ||||||
|  | 	size int64 | ||||||
|  | 	file *os.File | ||||||
|  | 	mu   sync.Mutex | ||||||
|  | 
 | ||||||
|  | 	millCh    chan bool | ||||||
|  | 	startMill sync.Once | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// currentTime exists so it can be mocked out by tests.
 | ||||||
|  | 	currentTime = time.Now | ||||||
|  | 
 | ||||||
|  | 	// os_Stat exists so it can be mocked out by tests.
 | ||||||
|  | 	os_Stat = os.Stat | ||||||
|  | 
 | ||||||
|  | 	// megabyte is the conversion factor between MaxSize and bytes.  It is a
 | ||||||
|  | 	// variable so tests can mock it out and not need to write megabytes of data
 | ||||||
|  | 	// to disk.
 | ||||||
|  | 	megabyte = 1024 * 1024 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Write implements io.Writer.  If a write would cause the log file to be larger
 | ||||||
|  | // than MaxSize, the file is closed, renamed to include a timestamp of the
 | ||||||
|  | // current time, and a new log file is created using the original log file name.
 | ||||||
|  | // If the length of the write is greater than MaxSize, an error is returned.
 | ||||||
|  | func (l *Logger) Write(p []byte) (n int, err error) { | ||||||
|  | 	l.mu.Lock() | ||||||
|  | 	defer l.mu.Unlock() | ||||||
|  | 
 | ||||||
|  | 	writeLen := int64(len(p)) | ||||||
|  | 	if writeLen > l.max() { | ||||||
|  | 		return 0, fmt.Errorf( | ||||||
|  | 			"write length %d exceeds maximum file size %d", writeLen, l.max(), | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if l.file == nil { | ||||||
|  | 		if err = l.openExistingOrNew(len(p)); err != nil { | ||||||
|  | 			return 0, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if l.size+writeLen > l.max() { | ||||||
|  | 		if err := l.rotate(); err != nil { | ||||||
|  | 			return 0, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	n, err = l.file.Write(p) | ||||||
|  | 	l.size += int64(n) | ||||||
|  | 
 | ||||||
|  | 	return n, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Close implements io.Closer, and closes the current logfile.
 | ||||||
|  | func (l *Logger) Close() error { | ||||||
|  | 	l.mu.Lock() | ||||||
|  | 	defer l.mu.Unlock() | ||||||
|  | 	return l.close() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // close closes the file if it is open.
 | ||||||
|  | func (l *Logger) close() error { | ||||||
|  | 	if l.file == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	err := l.file.Close() | ||||||
|  | 	l.file = nil | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Rotate causes Logger to close the existing log file and immediately create a
 | ||||||
|  | // new one.  This is a helper function for applications that want to initiate
 | ||||||
|  | // rotations outside of the normal rotation rules, such as in response to
 | ||||||
|  | // SIGHUP.  After rotating, this initiates compression and removal of old log
 | ||||||
|  | // files according to the configuration.
 | ||||||
|  | func (l *Logger) Rotate() error { | ||||||
|  | 	l.mu.Lock() | ||||||
|  | 	defer l.mu.Unlock() | ||||||
|  | 	return l.rotate() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // rotate closes the current file, moves it aside with a timestamp in the name,
 | ||||||
|  | // (if it exists), opens a new file with the original filename, and then runs
 | ||||||
|  | // post-rotation processing and removal.
 | ||||||
|  | func (l *Logger) rotate() error { | ||||||
|  | 	if err := l.close(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err := l.openNew(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	l.mill() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // openNew opens a new log file for writing, moving any old log file out of the
 | ||||||
|  | // way.  This methods assumes the file has already been closed.
 | ||||||
|  | func (l *Logger) openNew() error { | ||||||
|  | 	err := os.MkdirAll(l.dir(), 0744) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("can't make directories for new logfile: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	name := l.filename() | ||||||
|  | 	mode := os.FileMode(0644) | ||||||
|  | 	info, err := os_Stat(name) | ||||||
|  | 	if err == nil { | ||||||
|  | 		// Copy the mode off the old logfile.
 | ||||||
|  | 		mode = info.Mode() | ||||||
|  | 		// move the existing file
 | ||||||
|  | 		newname := backupName(name, l.LocalTime) | ||||||
|  | 		if err := os.Rename(name, newname); err != nil { | ||||||
|  | 			return fmt.Errorf("can't rename log file: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// this is a no-op anywhere but linux
 | ||||||
|  | 		if err := chown(name, info); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// we use truncate here because this should only get called when we've moved
 | ||||||
|  | 	// the file ourselves. if someone else creates the file in the meantime,
 | ||||||
|  | 	// just wipe out the contents.
 | ||||||
|  | 	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("can't open new logfile: %s", err) | ||||||
|  | 	} | ||||||
|  | 	l.file = f | ||||||
|  | 	l.size = 0 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // backupName creates a new filename from the given name, inserting a timestamp
 | ||||||
|  | // between the filename and the extension, using the local time if requested
 | ||||||
|  | // (otherwise UTC).
 | ||||||
|  | func backupName(name string, local bool) string { | ||||||
|  | 	dir := filepath.Dir(name) | ||||||
|  | 	filename := filepath.Base(name) | ||||||
|  | 	ext := filepath.Ext(filename) | ||||||
|  | 	prefix := filename[:len(filename)-len(ext)] | ||||||
|  | 	t := currentTime() | ||||||
|  | 	if !local { | ||||||
|  | 		t = t.UTC() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	timestamp := t.Format(backupTimeFormat) | ||||||
|  | 	return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // openExistingOrNew opens the logfile if it exists and if the current write
 | ||||||
|  | // would not put it over MaxSize.  If there is no such file or the write would
 | ||||||
|  | // put it over the MaxSize, a new file is created.
 | ||||||
|  | func (l *Logger) openExistingOrNew(writeLen int) error { | ||||||
|  | 	l.mill() | ||||||
|  | 
 | ||||||
|  | 	filename := l.filename() | ||||||
|  | 	info, err := os_Stat(filename) | ||||||
|  | 	if os.IsNotExist(err) { | ||||||
|  | 		return l.openNew() | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error getting log file info: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if info.Size()+int64(writeLen) >= l.max() { | ||||||
|  | 		return l.rotate() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// if we fail to open the old log file for some reason, just ignore
 | ||||||
|  | 		// it and open a new log file.
 | ||||||
|  | 		return l.openNew() | ||||||
|  | 	} | ||||||
|  | 	l.file = file | ||||||
|  | 	l.size = info.Size() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // genFilename generates the name of the logfile from the current time.
 | ||||||
|  | func (l *Logger) filename() string { | ||||||
|  | 	if l.Filename != "" { | ||||||
|  | 		return l.Filename | ||||||
|  | 	} | ||||||
|  | 	name := filepath.Base(os.Args[0]) + "-lumberjack.log" | ||||||
|  | 	return filepath.Join(os.TempDir(), name) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // millRunOnce performs compression and removal of stale log files.
 | ||||||
|  | // Log files are compressed if enabled via configuration and old log
 | ||||||
|  | // files are removed, keeping at most l.MaxBackups files, as long as
 | ||||||
|  | // none of them are older than MaxAge.
 | ||||||
|  | func (l *Logger) millRunOnce() error { | ||||||
|  | 	if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	files, err := l.oldLogFiles() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var compress, remove []logInfo | ||||||
|  | 
 | ||||||
|  | 	if l.MaxBackups > 0 && l.MaxBackups < len(files) { | ||||||
|  | 		preserved := make(map[string]bool) | ||||||
|  | 		var remaining []logInfo | ||||||
|  | 		for _, f := range files { | ||||||
|  | 			// Only count the uncompressed log file or the
 | ||||||
|  | 			// compressed log file, not both.
 | ||||||
|  | 			fn := f.Name() | ||||||
|  | 			if strings.HasSuffix(fn, compressSuffix) { | ||||||
|  | 				fn = fn[:len(fn)-len(compressSuffix)] | ||||||
|  | 			} | ||||||
|  | 			preserved[fn] = true | ||||||
|  | 
 | ||||||
|  | 			if len(preserved) > l.MaxBackups { | ||||||
|  | 				remove = append(remove, f) | ||||||
|  | 			} else { | ||||||
|  | 				remaining = append(remaining, f) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		files = remaining | ||||||
|  | 	} | ||||||
|  | 	if l.MaxAge > 0 { | ||||||
|  | 		diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge)) | ||||||
|  | 		cutoff := currentTime().Add(-1 * diff) | ||||||
|  | 
 | ||||||
|  | 		var remaining []logInfo | ||||||
|  | 		for _, f := range files { | ||||||
|  | 			if f.timestamp.Before(cutoff) { | ||||||
|  | 				remove = append(remove, f) | ||||||
|  | 			} else { | ||||||
|  | 				remaining = append(remaining, f) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		files = remaining | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if l.Compress { | ||||||
|  | 		for _, f := range files { | ||||||
|  | 			if !strings.HasSuffix(f.Name(), compressSuffix) { | ||||||
|  | 				compress = append(compress, f) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, f := range remove { | ||||||
|  | 		errRemove := os.Remove(filepath.Join(l.dir(), f.Name())) | ||||||
|  | 		if err == nil && errRemove != nil { | ||||||
|  | 			err = errRemove | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for _, f := range compress { | ||||||
|  | 		fn := filepath.Join(l.dir(), f.Name()) | ||||||
|  | 		errCompress := compressLogFile(fn, fn+compressSuffix) | ||||||
|  | 		if err == nil && errCompress != nil { | ||||||
|  | 			err = errCompress | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // millRun runs in a goroutine to manage post-rotation compression and removal
 | ||||||
|  | // of old log files.
 | ||||||
|  | func (l *Logger) millRun() { | ||||||
|  | 	for _ = range l.millCh { | ||||||
|  | 		// what am I going to do, log this?
 | ||||||
|  | 		_ = l.millRunOnce() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // mill performs post-rotation compression and removal of stale log files,
 | ||||||
|  | // starting the mill goroutine if necessary.
 | ||||||
|  | func (l *Logger) mill() { | ||||||
|  | 	l.startMill.Do(func() { | ||||||
|  | 		l.millCh = make(chan bool, 1) | ||||||
|  | 		go l.millRun() | ||||||
|  | 	}) | ||||||
|  | 	select { | ||||||
|  | 	case l.millCh <- true: | ||||||
|  | 	default: | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // oldLogFiles returns the list of backup log files stored in the same
 | ||||||
|  | // directory as the current log file, sorted by ModTime
 | ||||||
|  | func (l *Logger) oldLogFiles() ([]logInfo, error) { | ||||||
|  | 	files, err := ioutil.ReadDir(l.dir()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("can't read log file directory: %s", err) | ||||||
|  | 	} | ||||||
|  | 	logFiles := []logInfo{} | ||||||
|  | 
 | ||||||
|  | 	prefix, ext := l.prefixAndExt() | ||||||
|  | 
 | ||||||
|  | 	for _, f := range files { | ||||||
|  | 		if f.IsDir() { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil { | ||||||
|  | 			logFiles = append(logFiles, logInfo{t, f}) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil { | ||||||
|  | 			logFiles = append(logFiles, logInfo{t, f}) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		// error parsing means that the suffix at the end was not generated
 | ||||||
|  | 		// by lumberjack, and therefore it's not a backup file.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sort.Sort(byFormatTime(logFiles)) | ||||||
|  | 
 | ||||||
|  | 	return logFiles, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // timeFromName extracts the formatted time from the filename by stripping off
 | ||||||
|  | // the filename's prefix and extension. This prevents someone's filename from
 | ||||||
|  | // confusing time.parse.
 | ||||||
|  | func (l *Logger) timeFromName(filename, prefix, ext string) (time.Time, error) { | ||||||
|  | 	if !strings.HasPrefix(filename, prefix) { | ||||||
|  | 		return time.Time{}, errors.New("mismatched prefix") | ||||||
|  | 	} | ||||||
|  | 	if !strings.HasSuffix(filename, ext) { | ||||||
|  | 		return time.Time{}, errors.New("mismatched extension") | ||||||
|  | 	} | ||||||
|  | 	ts := filename[len(prefix) : len(filename)-len(ext)] | ||||||
|  | 	return time.Parse(backupTimeFormat, ts) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // max returns the maximum size in bytes of log files before rolling.
 | ||||||
|  | func (l *Logger) max() int64 { | ||||||
|  | 	if l.MaxSize == 0 { | ||||||
|  | 		return int64(defaultMaxSize * megabyte) | ||||||
|  | 	} | ||||||
|  | 	return int64(l.MaxSize) * int64(megabyte) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // dir returns the directory for the current filename.
 | ||||||
|  | func (l *Logger) dir() string { | ||||||
|  | 	return filepath.Dir(l.filename()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // prefixAndExt returns the filename part and extension part from the Logger's
 | ||||||
|  | // filename.
 | ||||||
|  | func (l *Logger) prefixAndExt() (prefix, ext string) { | ||||||
|  | 	filename := filepath.Base(l.filename()) | ||||||
|  | 	ext = filepath.Ext(filename) | ||||||
|  | 	prefix = filename[:len(filename)-len(ext)] + "-" | ||||||
|  | 	return prefix, ext | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // compressLogFile compresses the given log file, removing the
 | ||||||
|  | // uncompressed log file if successful.
 | ||||||
|  | func compressLogFile(src, dst string) (err error) { | ||||||
|  | 	f, err := os.Open(src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to open log file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer f.Close() | ||||||
|  | 
 | ||||||
|  | 	fi, err := os_Stat(src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to stat log file: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := chown(dst, fi); err != nil { | ||||||
|  | 		return fmt.Errorf("failed to chown compressed log file: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If this file already exists, we presume it was created by
 | ||||||
|  | 	// a previous attempt to compress the log file.
 | ||||||
|  | 	gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to open compressed log file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer gzf.Close() | ||||||
|  | 
 | ||||||
|  | 	gz := gzip.NewWriter(gzf) | ||||||
|  | 
 | ||||||
|  | 	defer func() { | ||||||
|  | 		if err != nil { | ||||||
|  | 			os.Remove(dst) | ||||||
|  | 			err = fmt.Errorf("failed to compress log file: %v", err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	if _, err := io.Copy(gz, f); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err := gz.Close(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err := gzf.Close(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := f.Close(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err := os.Remove(src); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // logInfo is a convenience struct to return the filename and its embedded
 | ||||||
|  | // timestamp.
 | ||||||
|  | type logInfo struct { | ||||||
|  | 	timestamp time.Time | ||||||
|  | 	os.FileInfo | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // byFormatTime sorts by newest time formatted in the name.
 | ||||||
|  | type byFormatTime []logInfo | ||||||
|  | 
 | ||||||
|  | func (b byFormatTime) Less(i, j int) bool { | ||||||
|  | 	return b[i].timestamp.After(b[j].timestamp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (b byFormatTime) Swap(i, j int) { | ||||||
|  | 	b[i], b[j] = b[j], b[i] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (b byFormatTime) Len() int { | ||||||
|  | 	return len(b) | ||||||
|  | } | ||||||
|  | @ -5,13 +5,12 @@ github.com/BurntSushi/toml | ||||||
| github.com/DATA-DOG/go-sqlmock | github.com/DATA-DOG/go-sqlmock | ||||||
| # github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 | # github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 | ||||||
| ## explicit | ## explicit | ||||||
| github.com/acmacalister/skittles |  | ||||||
| # github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d | # github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d | ||||||
| ## explicit | ## explicit | ||||||
| github.com/alecthomas/units |  | ||||||
| # github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 | # github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 | ||||||
| ## explicit | ## explicit | ||||||
| github.com/anmitsu/go-shlex | # github.com/aws/aws-sdk-go v1.34.19 | ||||||
|  | ## explicit | ||||||
| # github.com/beorn7/perks v1.0.1 | # github.com/beorn7/perks v1.0.1 | ||||||
| github.com/beorn7/perks/quantile | github.com/beorn7/perks/quantile | ||||||
| # github.com/caddyserver/caddy v1.0.5 | # github.com/caddyserver/caddy v1.0.5 | ||||||
|  | @ -156,7 +155,6 @@ github.com/gdamore/tcell/terminfo/x/xterm_kitty | ||||||
| github.com/getsentry/raven-go | github.com/getsentry/raven-go | ||||||
| # github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 | # github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 | ||||||
| ## explicit | ## explicit | ||||||
| github.com/gliderlabs/ssh |  | ||||||
| # github.com/go-sql-driver/mysql v1.5.0 | # github.com/go-sql-driver/mysql v1.5.0 | ||||||
| ## explicit | ## explicit | ||||||
| github.com/go-sql-driver/mysql | github.com/go-sql-driver/mysql | ||||||
|  | @ -452,6 +450,9 @@ google.golang.org/protobuf/types/known/timestamppb | ||||||
| # gopkg.in/coreos/go-oidc.v2 v2.1.0 | # gopkg.in/coreos/go-oidc.v2 v2.1.0 | ||||||
| ## explicit | ## explicit | ||||||
| gopkg.in/coreos/go-oidc.v2 | gopkg.in/coreos/go-oidc.v2 | ||||||
|  | # gopkg.in/natefinch/lumberjack.v2 v2.0.0 | ||||||
|  | ## explicit | ||||||
|  | gopkg.in/natefinch/lumberjack.v2 | ||||||
| # gopkg.in/square/go-jose.v2 v2.4.0 | # gopkg.in/square/go-jose.v2 v2.4.0 | ||||||
| ## explicit | ## explicit | ||||||
| gopkg.in/square/go-jose.v2 | gopkg.in/square/go-jose.v2 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue