Browse Source

Checkpoint

master
parent
commit
149aaf2b62
21 changed files with 612 additions and 91 deletions
  1. +9
    -0
      API.md
  2. +8
    -2
      cmd/config.go
  3. +13
    -11
      cmd/gate/main.go
  4. +12
    -3
      gate.hcl
  5. +4
    -4
      go.mod
  6. +34
    -8
      go.sum
  7. +10
    -0
      pkg/core/group.go
  8. +6
    -17
      pkg/core/identity.go
  9. +1
    -0
      pkg/core/identity/config.go
  10. +20
    -3
      pkg/core/logger/logger.go
  11. +50
    -0
      pkg/core/role.go
  12. +1
    -1
      pkg/core/service.go
  13. +11
    -0
      pkg/core/user.go
  14. +174
    -0
      pkg/net/mesh/mesh.go
  15. +5
    -2
      pkg/net/secureshell/server.go
  16. +14
    -11
      pkg/net/secureshell/transport.go
  17. +65
    -0
      pkg/net/web/auth.go
  18. +159
    -16
      pkg/net/web/server.go
  19. +12
    -11
      pkg/text/prompt/prompt.go
  20. +2
    -2
      pkg/text/recorder/recorder.go
  21. +2
    -0
      pkg/text/terminal/terminal_bsd.go

+ 9
- 0
API.md View File

@ -0,0 +1,9 @@
# Application Programming Interface specification
The Gate Application Programming Interface (API) requires authencation and is using CSRF tokens
for any POST request. The API is built on top of a RESTful service exposed over HTTPS.
## Authentication
Authentication is done based on passing JSON Web Tokens (JWT) in an `Authorization` header.

+ 8
- 2
cmd/config.go View File

@ -9,12 +9,14 @@ import (
"maze.io/gate/pkg/core"
"maze.io/gate/pkg/core/identity"
"maze.io/gate/pkg/core/logger"
"maze.io/gate/pkg/net/mesh"
"maze.io/gate/pkg/net/secureshell"
"maze.io/gate/pkg/net/web"
)
// Config is the top configuration structure.
type Config struct {
Mesh *mesh.Mesh `hcl:"mesh"`
SecureShell *secureshell.Server `hcl:"sshd"`
Web *web.Server `hcl:"httpd"`
Log logger.Config `hcl:"log"`
@ -51,12 +53,16 @@ func (config *Config) IdentityProvider() (core.IdentityProvider, error) {
}
// Services returns all configured services.
func (config *Config) Services() (httpd *web.Server, services []core.Service) {
func (config *Config) Services() (services []core.Service) {
if config.Mesh != nil {
services = append(services, config.Mesh)
}
if config.SecureShell != nil {
services = append(services, config.SecureShell)
}
if config.Web != nil {
httpd = config.Web
config.Web.AddService(services...)
config.Web.SetMesh(config.Mesh)
services = append(services, config.Web)
}
return


+ 13
- 11
cmd/gate/main.go View File

@ -3,8 +3,7 @@ package main
import (
"flag"
"os"
"maze.io/gate/pkg/net/web"
"os/signal"
"maze.io/gate/cmd"
"maze.io/gate/pkg/core"
@ -32,11 +31,10 @@ func main() {
log.WithError(err).Fatal("failed to setup identity provider")
}
httpd, services := config.Services()
run(log, httpd, services, provider)
run(log, config.Services(), provider)
}
func run(log *logger.Logger, httpd *web.Server, services []core.Service, provider core.IdentityProvider) {
func run(log *logger.Logger, services []core.Service, provider core.IdentityProvider) {
log.Info("services starting")
errors := make(chan error, 16)
for i, service := range services {
@ -59,14 +57,18 @@ func run(log *logger.Logger, httpd *web.Server, services []core.Service, provide
return
}
if httpd != nil {
for _, service := range services {
httpd.AddService(service)
}
log.Info("all services were started")
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, os.Kill)
select {
case sig := <-signals:
log.WithField("signal", sig.String()).Error("service interrupt")
case err := <-errors:
log.WithError(err).Error("service error")
}
for err := range errors {
log.WithError(err).Error("service error")
for _, service := range services {
_ = service.Close()
}
log.Info("services stopped")
}

+ 12
- 3
gate.hcl View File

@ -27,6 +27,12 @@ sshd {
idle_timeout = "5m"
}
# Recorder can record SSH sessions to a file.
recorder {
path = "testdata/recording"
name = "%Y/%m/%d/%s.cast"
}
banner = <<EOF
Welcome to the Gate SSH server.
EOF
@ -49,9 +55,12 @@ identity {
}
}
recorder {
type = "disk"
path = "testdata/recording/%Y/%m/%d/%s.cast"
# mesh clustering
# ===============
mesh {
network {
peers = ["127.0.0.123"]
}
}
log {


+ 4
- 4
go.mod View File

@ -5,17 +5,16 @@ go 1.13
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/c-bata/go-prompt v0.2.3
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cloudian/readline v1.4.4
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/memberlist v0.1.4
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0
github.com/labstack/gommon v0.3.0 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect
github.com/shinichy/go-wcwidth v0.0.0-20140219061058-b202ee861e25
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
@ -23,5 +22,6 @@ require (
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a
golang.org/x/text v0.3.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
maze.io/x/asciicast v1.0.1
maze.io/x/secureshell v1.0.3
)

+ 34
- 8
go.sum View File

@ -1,20 +1,34 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s=
github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudian/readline v1.4.4 h1:B7AzvPDzcscnnBDstHaKpiN37NlelgHB7bPZqyKGYgc=
github.com/cloudian/readline v1.4.4/go.mod h1:RP0woDmS6tvrgKZRa3HyMZIyzmPY+/e3g+LEsqhZINY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/memberlist v0.1.4 h1:gkyML/r71w3FL8gUi74Vk76avkj/9lYAY9lvg0OcoGs=
github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
@ -30,17 +44,24 @@ github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE=
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
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/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shinichy/go-wcwidth v0.0.0-20140219061058-b202ee861e25 h1:8RgNB2/qHJ5bzBd6b7yjLxnE8rSJnv5mGVp0kijd3FY=
github.com/shinichy/go-wcwidth v0.0.0-20140219061058-b202ee861e25/go.mod h1:jaaU71/7CKovUsOskljAdTI37gElMSQZpHOxE7EEfI0=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -48,17 +69,20 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -70,5 +94,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXL
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maze.io/x/asciicast v1.0.1 h1:oYekeRlWiFu4PSxrCLC5hBDjgRm3KMFOi57YmFMjbg4=
maze.io/x/asciicast v1.0.1/go.mod h1:QvbmGlYtHr01i+18bKessxvxwMnRbPIhIru09wk8jeY=
maze.io/x/secureshell v1.0.3 h1:5fb1WGQykrhBeodmtUvROCJTYQacISRd6WsBoaZbCo8=
maze.io/x/secureshell v1.0.3/go.mod h1:jD54Uod5w7PCNrQgoP87iHQF8JyyB7E9XRymFKrXmsU=

+ 10
- 0
pkg/core/group.go View File

@ -0,0 +1,10 @@
package core
import "maze.io/gate/pkg/util/compact"
// Group entity.
type Group interface {
ID() compact.ID
Name() string
Members() []User
}

+ 6
- 17
pkg/core/identity.go View File

@ -4,10 +4,14 @@ import (
"golang.org/x/crypto/ssh"
"maze.io/gate/pkg/core/logger"
"maze.io/gate/pkg/util/compact"
)
// Authentication contains authentication providers.
type Authentication struct {
IdentityProvider
Roles
}
// IdentityProvider provides user and groups.
type IdentityProvider interface {
// Setup the IDP with a logger.
@ -32,18 +36,3 @@ type IdentityProvider interface {
// Permissions.Extensions entry.
PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
}
// User entity.
type User interface {
ID() compact.ID
Login() string
Name() string
Groups() []Group
}
// Group entity.
type Group interface {
ID() compact.ID
Name() string
Members() []User
}

+ 1
- 0
pkg/core/identity/config.go View File

@ -11,6 +11,7 @@ var DefaultProvider = &systemIdentityProvider{}
type Config struct {
System *systemIdentityProvider
Roles core.Roles
}
func (config *Config) Provider() (core.IdentityProvider, error) {


+ 20
- 3
pkg/core/logger/logger.go View File

@ -1,14 +1,15 @@
package logger
import (
"bytes"
"context"
"io"
"net"
"net/http"
"os"
"github.com/labstack/echo/middleware"
"github.com/labstack/echo"
echo "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/natefinch/lumberjack"
"github.com/sirupsen/logrus"
@ -144,6 +145,22 @@ func (logger *Logger) WitTargetAddr(addr net.Addr) *Logger {
return logger.WithField(TargetAddr, addr.String())
}
func (logger *Logger) WithRequest(r *http.Request) *Logger {
return logger.WithFields(Fields{
"http_method": r.Method,
"http_url": r.URL.String(),
SourceAddr: r.RemoteAddr,
})
}
func (logger *Logger) Write(p []byte) (n int, err error) {
var buf bytes.Buffer
if n, err = buf.Write(p); err == nil {
logger.Info(buf.String())
}
return
}
var std = New(os.Stdout)
// Configure the standard logger.


+ 50
- 0
pkg/core/role.go View File

@ -0,0 +1,50 @@
package core
// Role entity.
type Role struct {
// Name of the role.
Name string
// Users is a slice of user login belonging to the role.
Users []string
// Groups is a slice of group name belonging to the role.
Groups []string
}
// Contains checks if the provided user has the provided role.
func (role Role) Contains(user User) bool {
if user == nil || len(role.Users) == 0 {
// Fast path
return false
}
if contains(role.Users, user.Login()) {
return true
}
for _, group := range user.Groups() {
if contains(role.Groups, group.Name()) {
return true
}
}
return false
}
// Roles is a map containing name to Role mappings.
type Roles map[string]Role
func (roles Roles) Setup() {
for name, role := range roles {
if role.Name == "" {
role.Name = name
}
}
}
func contains(haystack []string, needle string) bool {
for _, value := range haystack {
if value == needle {
return true
}
}
return false
}

+ 1
- 1
pkg/core/service.go View File

@ -13,7 +13,7 @@ type Service interface {
Component() string
// Setup the service.
Setup(provider IdentityProvider) error
Setup(IdentityProvider) error
// Start the service.
Start(*logger.Logger, chan<- error) error


+ 11
- 0
pkg/core/user.go View File

@ -0,0 +1,11 @@
package core
import "maze.io/gate/pkg/util/compact"
// User entity.
type User interface {
ID() compact.ID
Login() string
Name() string
Groups() []Group
}

+ 174
- 0
pkg/net/mesh/mesh.go View File

@ -0,0 +1,174 @@
package mesh
import (
"net"
"os"
"time"
"github.com/hashicorp/memberlist"
"maze.io/gate/pkg/core"
"maze.io/gate/pkg/core/logger"
)
const DefaultPort = 7946
var defaultKey = []byte("")
// Mesh of Gate servers.
type Mesh struct {
// WAN mode.
WAN bool
// Port for gossip talk.
Port int
// Peers to bootstrap the mesh.
Peers []string
// Network specific configuration.
Network memberlist.Config
log *logger.Logger
list *memberlist.Memberlist
node *memberlist.Node
events chan memberlist.NodeEvent
}
func (Mesh) Component() string {
return "mesh"
}
func (Mesh) Transports() []core.Transport {
return nil
}
func (mesh *Mesh) Close() error {
if mesh.list != nil {
err := mesh.list.Leave(time.Second)
mesh.list.Shutdown()
mesh.list = nil
mesh.node = nil
return err
}
return nil
}
func (mesh *Mesh) Setup(_ core.IdentityProvider) (err error) {
mesh.log = logger.Default.WithField(logger.Component, mesh.Component())
if mesh.Network.ProtocolVersion == 0 {
mesh.Network.ProtocolVersion = 4
}
if mesh.Network.Name == "" {
if mesh.Network.Name, err = os.Hostname(); err != nil {
return
}
}
if mesh.Port == 0 {
mesh.Port = DefaultPort
}
if mesh.Network.AdvertisePort == 0 {
mesh.Network.AdvertisePort = mesh.Port
}
if mesh.Network.BindPort == 0 {
mesh.Network.BindPort = mesh.Port
}
if mesh.WAN {
mesh.setupWAN()
} else {
mesh.setupLAN()
}
for _, peer := range mesh.Peers {
if _, _, err := net.SplitHostPort(peer); err != nil {
mesh.log.WithError(err).WithField("peer", peer).Warn("invalid peer")
} else {
mesh.log.WithField("peer", peer).Debug("mesh peer")
}
}
mesh.events = make(chan memberlist.NodeEvent, 128)
mesh.Network.Events = &memberlist.ChannelEventDelegate{mesh.events}
go mesh.processEvents()
mesh.Network.Logger = nil
return
}
func (mesh Mesh) setupWAN() {
mesh.setupLAN()
mesh.log.Debug("WAN mode")
mesh.Network.TCPTimeout = 30 * time.Second
mesh.Network.SuspicionMult = 6
mesh.Network.PushPullInterval = 60 * time.Second
mesh.Network.ProbeTimeout = 3 * time.Second
mesh.Network.ProbeInterval = 5 * time.Second
mesh.Network.GossipNodes = 3
mesh.Network.GossipInterval = 500 * time.Millisecond
mesh.Network.GossipToTheDeadTime = 60 * time.Second
}
func (mesh Mesh) setupLAN() {
mesh.log.Debug("LAN mode")
mesh.Network.BindAddr = "0.0.0.0"
mesh.Network.TCPTimeout = 10 * time.Second // Timeout after 10 seconds
mesh.Network.IndirectChecks = 1 // Use 3 nodes for the indirect ping
mesh.Network.RetransmitMult = 4 // Retransmit a message 4 * log(N+1) nodes
mesh.Network.SuspicionMult = 4 // Suspect a node for 4 * log(N+1) * Interval
mesh.Network.SuspicionMaxTimeoutMult = 6 // For 10k nodes this will give a max timeout of 120 seconds
mesh.Network.PushPullInterval = 30 * time.Second // Low frequency
mesh.Network.ProbeTimeout = 500 * time.Millisecond // Reasonable RTT time for LAN
mesh.Network.ProbeInterval = 1 * time.Second // Failure check every second
mesh.Network.DisableTcpPings = false // TCP pings are safe, even with mixed versions
mesh.Network.AwarenessMaxMultiplier = 8 // Probe interval backs off to 8 seconds
mesh.Network.GossipNodes = 1 // Gossip to 3 nodes
mesh.Network.GossipInterval = 300 * time.Millisecond // Gossip more rapidly
mesh.Network.GossipToTheDeadTime = 30 * time.Second // Same as push/pull
mesh.Network.GossipVerifyIncoming = true
mesh.Network.GossipVerifyOutgoing = true
mesh.Network.EnableCompression = false // Enable compression by default
mesh.Network.DNSConfigPath = "/etc/resolv.conf"
mesh.Network.HandoffQueueDepth = 1024
mesh.Network.UDPBufferSize = 1400
}
func (mesh *Mesh) processEvents() {
for event := range mesh.events {
mesh.log.WithField("event", event).Debug("event")
}
}
func (mesh *Mesh) Start(log *logger.Logger, errs chan<- error) (err error) {
mesh.log = log.WithField(logger.Component, mesh.Component())
mesh.Network.LogOutput = mesh.log
if mesh.list, err = memberlist.Create(&mesh.Network); err != nil {
return
}
go mesh.join()
return
}
func (mesh *Mesh) join() {
for {
if _, err := mesh.list.Join(mesh.Peers); err != nil {
mesh.log.WithError(err).Warn("mesh join failed")
time.Sleep(500 * time.Millisecond)
}
mesh.node = mesh.list.LocalNode()
mesh.log = mesh.log.WithFields(logger.Fields{
"node": mesh.node.Name,
})
mesh.log.WithField("members", len(mesh.list.Members())).Info("joined mesh")
if len(mesh.list.Members()) > 1 {
return
}
time.Sleep(500 * time.Millisecond)
}
}
var _ core.Service = (*Mesh)(nil)

+ 5
- 2
pkg/net/secureshell/server.go View File

@ -122,11 +122,14 @@ func (sshd *Server) wrapPublicKeyCallback(metadata ssh.ConnMetadata, key ssh.Pub
}
func (sshd *Server) serve(errors chan<- error) {
sshd.log.Info("start server")
defer sshd.log.Warn("server stopped")
defer sshd.log.Warn("stopped")
sshd.log.WithField("addr", sshd.Addr).Info("start listener")
if err := sshd.ListenAndServe(); err != nil {
sshd.log.WithError(err).Error("start failed")
errors <- err
}
sshd.log.Info("started")
}
func (sshd *Server) authLog(metadata ssh.ConnMetadata, method string, err error) {


+ 14
- 11
pkg/net/secureshell/transport.go View File

@ -13,22 +13,25 @@ import (
)
type transport struct {
ssh.ConnMetadata
*logger.Logger
id compact.ID
conn ssh.Conn
connTime time.Time
user core.User
mutex sync.Mutex
tunnels map[string]tunnel
id compact.ID
conn ssh.Conn
connTime time.Time
user core.User
mutex sync.Mutex
passedAuth []string
tunnels map[string]tunnel
}
func newTransport(conn ssh.Conn, user core.User) *transport {
tr := &transport{
id: compact.Bytes(conn.SessionID()),
conn: conn,
connTime: time.Now(),
user: user,
tunnels: make(map[string]tunnel),
ConnMetadata: conn,
id: compact.Bytes(conn.SessionID()),
conn: conn,
connTime: time.Now(),
user: user,
tunnels: make(map[string]tunnel),
}
tr.setupLogger(logger.Default)
return tr


+ 65
- 0
pkg/net/web/auth.go View File

@ -0,0 +1,65 @@
package web
import (
"net/http"
"time"
"maze.io/gate/pkg/core/logger"
"github.com/dgrijalva/jwt-go"
echo "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func (httpd *Server) apiAuthGet(ctx echo.Context) error {
if token, ok := ctx.Get(middleware.DefaultCSRFConfig.ContextKey).(string); ok {
ctx.Response().Header().Set(echo.HeaderXCSRFToken, token)
}
return ctx.NoContent(http.StatusOK)
}
func (httpd *Server) apiAuthPasswordPost(ctx echo.Context) error {
var (
username = ctx.FormValue("username")
user, err = httpd.IdentityProvider.LookupUser(username)
// password = ctx.FormValue("password")
isAdmin bool
)
if err != nil {
return err
}
// Check for admin.
if isAdmin = contains(httpd.Admins, user.Login()); !isAdmin {
for _, group := range user.Groups() {
if isAdmin = contains(httpd.AdminGroups, group.Name()); isAdmin {
break
}
}
}
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["login"] = user.Login()
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString(httpd.secret)
if err != nil {
return err
}
log := httpd.log.WithRequest(ctx.Request()).WithFields(logger.Fields{
logger.Component: httpd.Component() + ".api",
logger.User: user.Login(),
"admin": isAdmin,
})
log.Info("user login")
return ctx.JSON(http.StatusOK, map[string]string{
"token": t,
})
}

+ 159
- 16
pkg/net/web/server.go View File

@ -1,18 +1,24 @@
package web
import (
"crypto/rand"
"crypto/tls"
"encoding/hex"
"io"
"log"
"net"
"net/http"
"github.com/labstack/echo/middleware"
"github.com/dgrijalva/jwt-go"
"maze.io/gate/pkg/core"
"github.com/labstack/echo"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
echo "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"maze.io/gate/pkg/core"
"maze.io/gate/pkg/core/logger"
"maze.io/gate/pkg/net/mesh"
)
// DefaultServer is our default server configuration.
@ -31,12 +37,24 @@ type Server struct {
// Static files directory.
Static string
// IdentityProvider
// IdentityProvider.
IdentityProvider core.IdentityProvider
// Admins is a slice of admin login names.
Admins []string
// AdminGroups is a slice of admin group names.
AdminGroups []string
// Secret for encrypting cookies.
Secret string
log *logger.Logger
listener net.Listener
secret []byte
store sessions.Store
services []core.Service
mesh *mesh.Mesh
err chan error
}
@ -54,10 +72,27 @@ func (httpd *Server) setup(log *logger.Logger) {
if httpd.Static == "" {
httpd.Static = "static"
}
if httpd.Secret == "" {
httpd.log.Warn("no secret configured, using ephemeral key")
}
}
func (httpd *Server) Setup(provider core.IdentityProvider) error {
httpd.IdentityProvider = provider
if httpd.Secret != "" {
var err error
if httpd.secret, err = hex.DecodeString(httpd.Secret); err != nil {
return err
}
} else {
httpd.secret = make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, httpd.secret); err != nil {
return err
}
}
return nil
}
@ -98,19 +133,54 @@ func (httpd *Server) serve(l net.Listener, errors chan<- error) {
httpd.log.Debug("start server")
httpd.listener = l
// Middleware used below.
var (
middlewareJWT = middleware.JWT(httpd.secret)
//middlewareCSRF = middleware.CSRFWithConfig(middleware.CSRFConfig{
// CookiePath: "/",
//})
)
// TODO(maze): pluggable authentication
// TODO(maze): dashboard that shows active connections (+ option to terminate?)
// TODO(maze): management page to view sessions
// TODO(maze): management page to view recordings
router := echo.New()
router.Use(logger.Middleware(httpd.AccessLog))
//router.Use(middleware.Recover())
router.Use(middleware.Recover())
router.Use(middleware.Static(httpd.Static))
api := router.Group("/api")
api.GET("/service", httpd.apiServiceList)
api.GET("/transport", httpd.apiTransportList)
api.GET("/transport/:id", httpd.apiTransportGet)
router.Use(session.Middleware(sessions.NewCookieStore(httpd.secret)))
//router.Use(middlewareCSRF)
// /api/v1/...
api := router.Group("/api/v1")
// TODO(maze): auth users (password + mfa)
// TODO(maze): list users
// TODO(maze): list transports for a user
// TODO(maze): list tunnels for a user
// TODO(maze): list recordings for a user
// TODO(maze): kill transport
// TODO(maze): kill tunnel
// TODO(maze): view transport (recording/tail)
// TODO(maze): view tunnel (recording/tail)
api.GET("/auth", httpd.apiAuthGet) // to get CSRF token
api.POST("/auth/password", httpd.apiAuthPasswordPost) // to get JWT based on username + password
// /api/v1/my/...
userAPI := api.Group("/my")
userAPI.Use(middlewareJWT)
userAPI.GET("/transport", httpd.myTransportList)
// /api/v1/admin/...
adminAPI := api.Group("/admin")
adminAPI.Use(middlewareJWT)
adminAPI.Use(httpd.middlewareAdminRequired)
adminAPI.GET("/service", httpd.adminServiceList)
adminAPI.GET("/transport", httpd.adminTransportList)
adminAPI.GET("/transport/:id", httpd.adminTransportGet)
adminAPI.DELETE("/transport/:id", httpd.adminTransportDelete)
adminAPI.DELETE("/tunnel/:id", httpd.adminTunnelDelete)
adminAPI.GET("/user/:login/transport", httpd.adminUserTransportList)
server := &http.Server{
Handler: router,
@ -128,8 +198,12 @@ func (httpd *Server) Close() error {
return nil
}
func (httpd *Server) AddService(s core.Service) {
httpd.services = append(httpd.services, s)
func (httpd *Server) AddService(services ...core.Service) {
httpd.services = append(httpd.services, services...)
}
func (httpd *Server) SetMesh(instance *mesh.Mesh) {
httpd.mesh = instance
}
type apiResponse struct {
@ -143,7 +217,44 @@ func (res *apiResponse) WriteTo(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, res)
}
func (httpd *Server) apiServiceList(ctx echo.Context) error {
func (httpd *Server) myTransportList(ctx echo.Context) error {
var (
tokenUser = ctx.Get("user").(*jwt.Token)
claims = tokenUser.Claims.(jwt.MapClaims)
user, err = httpd.IdentityProvider.LookupUser(claims["user"].(string))
res apiResponse
)
if err != nil {
return err
}
for _, service := range httpd.services {
for _, transport := range service.Transports() {
if transport.User() == user {
res.Data = append(res.Data, transportMap(transport))
}
}
}
return res.WriteTo(ctx)
}
func (httpd *Server) middlewareAdminRequired(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
var (
request = ctx.Request()
tokenUser = ctx.Get("user").(*jwt.Token)
claims = tokenUser.Claims.(jwt.MapClaims)
isAdmin, _ = claims["admin"].(bool)
)
if !isAdmin {
httpd.log.WithField(logger.User, claims["user"]).WithRequest(request).Warn("admin access reject")
return ctx.NoContent(http.StatusForbidden)
}
httpd.log.WithRequest(request).Debug("admin access permit")
return next(ctx)
}
}
func (httpd *Server) adminServiceList(ctx echo.Context) error {
var res apiResponse
for _, service := range httpd.services {
res.Data = append(res.Data, service.Component())
@ -151,7 +262,7 @@ func (httpd *Server) apiServiceList(ctx echo.Context) error {
return res.WriteTo(ctx)
}
func (httpd *Server) apiTransportList(ctx echo.Context) error {
func (httpd *Server) adminTransportList(ctx echo.Context) error {
var res apiResponse
for _, service := range httpd.services {
for _, transport := range service.Transports() {
@ -161,7 +272,7 @@ func (httpd *Server) apiTransportList(ctx echo.Context) error {
return res.WriteTo(ctx)
}
func (httpd *Server) apiTransportGet(ctx echo.Context) error {
func (httpd *Server) adminTransportGet(ctx echo.Context) error {
id := ctx.Param("id")
for _, service := range httpd.services {
for _, transport := range service.Transports() {
@ -173,6 +284,29 @@ func (httpd *Server) apiTransportGet(ctx echo.Context) error {
return ctx.JSON(http.StatusNotFound, nil)
}
func (httpd *Server) adminTransportDelete(ctx echo.Context) error {
return ctx.NoContent(http.StatusNotImplemented)
}
func (httpd *Server) adminTunnelDelete(ctx echo.Context) error {
return ctx.NoContent(http.StatusNotImplemented)
}
func (httpd *Server) adminUserTransportList(ctx echo.Context) error {
var (
login = ctx.Param("login")
res apiResponse
)
for _, service := range httpd.services {
for _, transport := range service.Transports() {
if transport.User().Login() == login {
res.Data = append(res.Data, transportMap(transport))
}
}
}
return res.WriteTo(ctx)
}
func transportMap(transport core.Transport) map[string]interface{} {
return map[string]interface{}{
"id": transport.TransportID().String(),
@ -201,3 +335,12 @@ func tunnelMap(tunnel core.Tunnel) map[string]interface{} {
"user": tunnel.User().Login(),
}
}
func contains(haystack []string, needle string) bool {
for _, value := range haystack {
if value == needle {
return true
}
}
return false
}

+ 12
- 11
pkg/text/prompt/prompt.go View File

@ -1,7 +1,8 @@
package prompt
import (
promptx "github.com/c-bata/go-prompt"
"github.com/c-bata/go-prompt"
"maze.io/gate/pkg/text/terminal"
)
@ -13,8 +14,8 @@ type Prompt struct {
term terminal.Terminal
prefix string
completer *Completer
cParser promptx.ConsoleParser
cWriter promptx.ConsoleWriter
cParser prompt.ConsoleParser
cWriter prompt.ConsoleWriter
history []string
}
@ -31,11 +32,11 @@ func New(prompt string, term terminal.Terminal) (*Prompt, error) {
}
func (p *Prompt) Readline() (string, error) {
line := promptx.Input(p.prefix,
line := prompt.Input(p.prefix,
p.completer.Complete,
promptx.OptionParser(p.cParser),
promptx.OptionHistory(p.history),
promptx.OptionWriter(p.cWriter),
prompt.OptionParser(p.cParser),
prompt.OptionHistory(p.history),
prompt.OptionWriter(p.cWriter),
)
return line, nil
}
@ -59,13 +60,13 @@ func (p *Prompt) SetPrompt(prompt string) error {
}
type Completer struct {
suggests []promptx.Suggest
suggests []prompt.Suggest
}
func NewCompleter(suggests []promptx.Suggest) *Completer {
func NewCompleter(suggests []prompt.Suggest) *Completer {
return &Completer{suggests}
}
func (c *Completer) Complete(d promptx.Document) []promptx.Suggest {
return promptx.FilterHasPrefix(c.suggests, d.Text, false)
func (c *Completer) Complete(d prompt.Document) []prompt.Suggest {
return prompt.FilterHasPrefix(c.suggests, d.Text, false)
}

+ 2
- 2
pkg/text/recorder/recorder.go View File

@ -51,8 +51,8 @@ func (r *Recorder) RecordTerminal(name string, term terminal.Terminal, env map[s
var (
width, height = term.Size()
header = asciicast.Header{
Width: width,
Height: height,
Width: int(width),
Height: int(height),
Timestamp: time.Now().Unix(),
Env: env,
}


+ 2
- 0
pkg/text/terminal/terminal_bsd.go View File

@ -1,3 +1,5 @@
// +build !linux darwin freebsd netbsd openbsd
package terminal
import "golang.org/x/sys/unix"


Loading…
Cancel
Save