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.
|
|
||||||
|
|
||||||
![alt tag](https://raw.githubusercontent.com/acmacalister/skittles/master/pictures/terminal-colors.png)
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
|
|
||||||
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
|
||||||
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
|
|
||||||
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
|
|
||||||
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
|
|
||||||
|
|
||||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
|
||||||
|
|
||||||
This Go package wraps the [crypto/ssh
|
|
||||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
|
||||||
building SSH servers. The goal of the API was to make it as simple as using
|
|
||||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gliderlabs/ssh"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ssh.Handle(func(s ssh.Session) {
|
|
||||||
io.WriteString(s, "Hello world\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
A bunch of great examples are in the `_examples` directory.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Pull requests are welcome! However, since this project is very much about API
|
|
||||||
design, please submit API changes as issues to discuss before submitting PRs.
|
|
||||||
|
|
||||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
* Non-session channel handlers
|
|
||||||
* Cleanup callback API
|
|
||||||
* 1.0 release
|
|
||||||
* High-level client?
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
BSD
|
|
|
@ -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 [![GoDoc](https://godoc.org/gopkg.in/natefinch/lumberjack.v2?status.png)](https://godoc.org/gopkg.in/natefinch/lumberjack.v2) [![Build Status](https://travis-ci.org/natefinch/lumberjack.svg?branch=v2.0)](https://travis-ci.org/natefinch/lumberjack) [![Build status](https://ci.appveyor.com/api/projects/status/00gchpxtg4gkrt5d)](https://ci.appveyor.com/project/natefinch/lumberjack) [![Coverage Status](https://coveralls.io/repos/natefinch/lumberjack/badge.svg?branch=v2.0)](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