2017-09-30 19:12:43 +08:00
package main
import (
2017-11-08 02:40:14 +08:00
"errors"
2017-09-30 19:12:43 +08:00
"fmt"
"log"
2017-11-14 08:29:25 +08:00
"math/rand"
2017-09-30 19:12:43 +08:00
"os"
"path"
2017-11-08 02:40:14 +08:00
"strings"
2017-11-14 08:29:25 +08:00
"time"
2017-09-30 19:12:43 +08:00
"github.com/gliderlabs/ssh"
2017-10-30 23:48:14 +08:00
"github.com/jinzhu/gorm"
2017-10-31 00:12:04 +08:00
_ "github.com/jinzhu/gorm/dialects/mysql"
2017-10-30 23:48:14 +08:00
_ "github.com/jinzhu/gorm/dialects/sqlite"
2017-09-30 19:12:43 +08:00
"github.com/urfave/cli"
2017-11-04 05:54:16 +08:00
gossh "golang.org/x/crypto/ssh"
2017-09-30 19:12:43 +08:00
)
2017-11-14 07:38:23 +08:00
var (
// VERSION should be updated by hand at each release
2017-11-24 22:22:50 +08:00
VERSION = "1.4.0+dev"
2017-11-14 07:38:23 +08:00
// GIT_TAG will be overwritten automatically by the build system
2017-11-14 08:13:51 +08:00
GIT_TAG string
2017-11-14 07:38:23 +08:00
// GIT_SHA will be overwritten automatically by the build system
2017-11-14 08:13:51 +08:00
GIT_SHA string
2017-11-14 07:38:23 +08:00
// GIT_BRANCH will be overwritten automatically by the build system
2017-11-14 08:13:51 +08:00
GIT_BRANCH string
2017-11-14 07:38:23 +08:00
)
2017-11-02 05:09:08 +08:00
2017-11-03 08:46:55 +08:00
type sshportalContextKey string
2017-11-08 02:40:14 +08:00
var (
userContextKey = sshportalContextKey ( "user" )
messageContextKey = sshportalContextKey ( "message" )
errorContextKey = sshportalContextKey ( "error" )
)
2017-11-03 08:46:55 +08:00
2017-09-30 19:12:43 +08:00
func main ( ) {
2017-11-14 08:29:25 +08:00
rand . Seed ( time . Now ( ) . UnixNano ( ) )
2017-09-30 19:12:43 +08:00
app := cli . NewApp ( )
app . Name = path . Base ( os . Args [ 0 ] )
app . Author = "Manfred Touron"
2017-11-14 08:13:51 +08:00
app . Version = VERSION + " (" + GIT_SHA + ")"
2017-09-30 19:12:43 +08:00
app . Email = "https://github.com/moul/sshportal"
app . Flags = [ ] cli . Flag {
cli . StringFlag {
Name : "bind-address, b" ,
EnvVar : "SSHPORTAL_BIND" ,
Value : ":2222" ,
Usage : "SSH server bind address" ,
} ,
2017-11-14 08:28:18 +08:00
/ * cli . StringFlag {
2017-10-30 23:48:14 +08:00
Name : "db-driver" ,
Value : "sqlite3" ,
2017-11-14 08:28:18 +08:00
Usage : "GORM driver (sqlite3)" ,
} , * /
2017-10-30 23:48:14 +08:00
cli . StringFlag {
Name : "db-conn" ,
Value : "./sshportal.db" ,
2017-10-31 00:12:04 +08:00
Usage : "GORM connection string" ,
} ,
cli . BoolFlag {
Name : "debug, D" ,
Usage : "Display debug information" ,
2017-10-30 23:48:14 +08:00
} ,
2017-11-02 05:11:46 +08:00
cli . StringFlag {
Name : "config-user" ,
Usage : "SSH user that spawns a configuration shell" ,
Value : "admin" ,
} ,
2017-11-24 21:29:41 +08:00
cli . StringFlag {
Name : "aes-key" ,
Usage : "Encrypt sensitive data in database (length: 16, 24 or 32)" ,
} ,
2017-09-30 19:12:43 +08:00
}
app . Action = server
2017-11-02 17:32:35 +08:00
if err := app . Run ( os . Args ) ; err != nil {
log . Fatalf ( "error: %v" , err )
}
2017-09-30 19:12:43 +08:00
}
func server ( c * cli . Context ) error {
2017-11-24 21:29:41 +08:00
switch len ( c . String ( "aes-key" ) ) {
case 0 , 16 , 24 , 32 :
default :
return fmt . Errorf ( "invalid aes key size, should be 16 or 24, 32" )
}
2017-11-19 08:18:17 +08:00
// db
2017-11-14 08:28:18 +08:00
db , err := gorm . Open ( "sqlite3" , c . String ( "db-conn" ) )
2017-10-30 23:48:14 +08:00
if err != nil {
return err
}
defer db . Close ( )
2017-11-19 08:18:17 +08:00
if err = db . DB ( ) . Ping ( ) ; err != nil {
return err
}
2017-10-31 00:12:04 +08:00
if c . Bool ( "debug" ) {
db . LogMode ( true )
}
2017-10-30 23:48:14 +08:00
if err := dbInit ( db ) ; err != nil {
return err
}
2017-11-19 08:18:17 +08:00
// ssh server
2017-09-30 19:12:43 +08:00
ssh . Handle ( func ( s ssh . Session ) {
2017-11-04 04:47:54 +08:00
currentUser := s . Context ( ) . Value ( userContextKey ) . ( User )
log . Printf ( "New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s" , s . User ( ) , s . RemoteAddr ( ) , s . LocalAddr ( ) , s . Command ( ) , currentUser . ID , currentUser . Email )
2017-11-08 02:40:14 +08:00
if err := s . Context ( ) . Value ( errorContextKey ) ; err != nil {
fmt . Fprintf ( s , "error: %v\n" , err )
2017-11-04 04:47:54 +08:00
return
}
2017-09-30 19:12:43 +08:00
2017-11-08 02:40:14 +08:00
if msg := s . Context ( ) . Value ( messageContextKey ) ; msg != nil {
fmt . Fprint ( s , msg . ( string ) )
}
switch username := s . User ( ) ; {
2017-11-24 00:23:20 +08:00
case username == currentUser . Name || username == currentUser . Email || username == c . String ( "config-user" ) :
2017-11-02 00:00:34 +08:00
if err := shell ( c , s , s . Command ( ) , db ) ; err != nil {
2017-11-04 04:47:54 +08:00
fmt . Fprintf ( s , "error: %v\n" , err )
2017-10-31 17:17:06 +08:00
}
2017-11-08 02:40:14 +08:00
case strings . HasPrefix ( username , "invite:" ) :
return
2017-09-30 19:12:43 +08:00
default :
2017-10-31 16:24:18 +08:00
host , err := RemoteHostFromSession ( s , db )
2017-09-30 19:12:43 +08:00
if err != nil {
2017-11-04 04:47:54 +08:00
fmt . Fprintf ( s , "error: %v\n" , err )
2017-10-31 16:24:18 +08:00
// FIXME: print available hosts
2017-09-30 19:12:43 +08:00
return
}
2017-11-13 17:13:17 +08:00
// load up-to-date objects
// FIXME: cache them or try not to load them
var tmpUser User
if err := db . Preload ( "Groups" ) . Preload ( "Groups.ACLs" ) . Where ( "id = ?" , currentUser . ID ) . First ( & tmpUser ) . Error ; err != nil {
fmt . Fprintf ( s , "error: %v\n" , err )
return
}
var tmpHost Host
if err := db . Preload ( "Groups" ) . Preload ( "Groups.ACLs" ) . Where ( "id = ?" , host . ID ) . First ( & tmpHost ) . Error ; err != nil {
2017-11-04 04:47:54 +08:00
fmt . Fprintf ( s , "error: %v\n" , err )
2017-11-13 17:13:17 +08:00
return
2017-09-30 19:12:43 +08:00
}
2017-11-13 17:13:17 +08:00
action , err := CheckACLs ( tmpUser , tmpHost )
if err != nil {
fmt . Fprintf ( s , "error: %v\n" , err )
return
}
2017-11-24 21:29:41 +08:00
// decrypt key and password
HostDecrypt ( c . String ( "aes-key" ) , host )
SSHKeyDecrypt ( c . String ( "aes-key" ) , host . SSHKey )
2017-11-13 17:13:17 +08:00
switch action {
case "allow" :
2017-11-27 15:22:13 +08:00
sess := Session {
UserID : currentUser . ID ,
HostID : host . ID ,
Status : SessionStatusActive ,
}
if err := db . Create ( & sess ) . Error ; err != nil {
fmt . Fprintf ( s , "error: %v\n" , err )
return
}
err := proxy ( s , host )
sessUpdate := Session { }
if err != nil {
2017-11-13 17:13:17 +08:00
fmt . Fprintf ( s , "error: %v\n" , err )
2017-11-27 15:22:13 +08:00
sessUpdate . ErrMsg = fmt . Sprintf ( "%v" , err )
2017-11-13 17:13:17 +08:00
}
2017-11-27 15:22:13 +08:00
sessUpdate . Status = SessionStatusClosed
sessUpdate . StoppedAt = time . Now ( )
db . Model ( & sess ) . Updates ( & sessUpdate )
2017-11-13 17:13:17 +08:00
case "deny" :
fmt . Fprintf ( s , "You don't have permission to that host.\n" )
default :
fmt . Fprintf ( s , "error: %v\n" , err )
}
2017-09-30 19:12:43 +08:00
}
} )
opts := [ ] ssh . Option { }
2017-11-03 08:46:55 +08:00
opts = append ( opts , ssh . PublicKeyAuth ( func ( ctx ssh . Context , key ssh . PublicKey ) bool {
var (
2017-11-08 02:40:14 +08:00
userKey UserKey
user User
username = ctx . User ( )
2017-11-03 08:46:55 +08:00
)
// lookup user by key
db . Where ( "key = ?" , key . Marshal ( ) ) . First ( & userKey )
if userKey . UserID > 0 {
2017-11-23 23:22:23 +08:00
db . Preload ( "Roles" ) . Where ( "id = ?" , userKey . UserID ) . First ( & user )
2017-11-08 02:40:14 +08:00
if strings . HasPrefix ( username , "invite:" ) {
ctx . SetValue ( errorContextKey , fmt . Errorf ( "invites are only supported for ney SSH keys; your ssh key is already associated with the user %q." , user . Email ) )
}
2017-11-03 08:46:55 +08:00
ctx . SetValue ( userContextKey , user )
return true
}
2017-11-08 02:40:14 +08:00
// handle invite "links"
if strings . HasPrefix ( username , "invite:" ) {
inputToken := strings . Split ( username , ":" ) [ 1 ]
2017-11-15 18:24:48 +08:00
if len ( inputToken ) > 0 {
2017-11-08 02:40:14 +08:00
db . Where ( "invite_token = ?" , inputToken ) . First ( & user )
2017-11-03 08:46:55 +08:00
}
2017-11-08 02:40:14 +08:00
if user . ID > 0 {
userKey = UserKey {
UserID : user . ID ,
Key : key . Marshal ( ) ,
Comment : "created by sshportal" ,
}
db . Create ( & userKey )
// token is only usable once
user . InviteToken = ""
db . Update ( & user )
ctx . SetValue ( messageContextKey , fmt . Sprintf ( "Welcome %s!\n\nYour key is now associated with the user %q.\n" , user . Name , user . Email ) )
ctx . SetValue ( userContextKey , user )
} else {
ctx . SetValue ( userContextKey , User { Name : "Anonymous" } )
ctx . SetValue ( errorContextKey , errors . New ( "your token is invalid or expired" ) )
2017-11-03 08:46:55 +08:00
}
return true
}
2017-11-08 02:40:14 +08:00
// fallback
ctx . SetValue ( errorContextKey , errors . New ( "unknown ssh key" ) )
ctx . SetValue ( userContextKey , User { Name : "Anonymous" } )
2017-11-04 04:47:54 +08:00
return true
2017-11-03 08:46:55 +08:00
} ) )
2017-11-04 05:54:16 +08:00
opts = append ( opts , func ( srv * ssh . Server ) error {
2017-11-23 16:58:32 +08:00
var key SSHKey
if err := SSHKeysByIdentifiers ( db , [ ] string { "host" } ) . First ( & key ) . Error ; err != nil {
2017-11-04 05:54:16 +08:00
return err
}
signer , err := gossh . ParsePrivateKey ( [ ] byte ( key . PrivKey ) )
if err != nil {
return err
}
srv . AddHostKey ( signer )
return nil
} )
2017-09-30 19:12:43 +08:00
log . Printf ( "SSH Server accepting connections on %s" , c . String ( "bind-address" ) )
return ssh . ListenAndServe ( c . String ( "bind-address" ) , nil , opts ... )
}