2014-03-16 13:24:13 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2014-02-17 19:57:23 +04:00
package models
import (
2014-03-16 13:24:13 +04:00
"bufio"
2014-03-16 13:48:20 +04:00
"errors"
2014-02-17 19:57:23 +04:00
"fmt"
2014-05-07 20:09:30 +04:00
"io"
2014-03-17 22:03:58 +04:00
"io/ioutil"
2014-02-17 19:57:23 +04:00
"os"
2014-05-08 01:04:32 +04:00
"os/exec"
2014-03-16 14:16:03 +04:00
"path"
2014-02-17 19:57:23 +04:00
"path/filepath"
2014-03-16 13:24:13 +04:00
"strings"
"sync"
2014-02-17 19:57:23 +04:00
"time"
2014-03-03 00:25:09 +04:00
"github.com/Unknwon/com"
2014-03-22 22:27:03 +04:00
"github.com/gogits/gogs/modules/log"
2014-06-19 09:08:03 +04:00
"github.com/gogits/gogs/modules/process"
2014-09-16 21:34:09 +04:00
"github.com/gogits/gogs/modules/setting"
2014-02-17 19:57:23 +04:00
)
2014-03-17 22:03:58 +04:00
const (
// "### autogenerated by gitgos, DO NOT EDIT\n"
2014-05-07 20:09:30 +04:00
_TPL_PUBLICK_KEY = ` command="%s serv key-%d",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s ` + "\n"
2014-03-17 22:03:58 +04:00
)
2014-02-17 19:57:23 +04:00
var (
2014-03-21 00:04:56 +04:00
ErrKeyAlreadyExist = errors . New ( "Public key already exist" )
2014-05-07 00:28:52 +04:00
ErrKeyNotExist = errors . New ( "Public key does not exist" )
2014-10-12 05:04:42 +04:00
ErrKeyUnableVerify = errors . New ( "Unable to verify public key" )
2014-03-21 00:04:56 +04:00
)
2014-03-17 22:03:58 +04:00
2014-03-21 00:04:56 +04:00
var sshOpLocker = sync . Mutex { }
var (
2014-06-11 03:11:53 +04:00
SshPath string // SSH directory.
2014-05-07 00:28:52 +04:00
appPath string // Execution(binary) path.
2014-02-17 19:57:23 +04:00
)
2014-05-08 01:04:32 +04:00
// exePath returns the executable path.
func exePath ( ) ( string , error ) {
file , err := exec . LookPath ( os . Args [ 0 ] )
if err != nil {
return "" , err
}
return filepath . Abs ( file )
}
2014-03-17 22:03:58 +04:00
// homeDir returns the home directory of current user.
2014-02-25 14:58:55 +04:00
func homeDir ( ) string {
2014-03-03 00:25:09 +04:00
home , err := com . HomeDir ( )
2014-02-25 14:58:55 +04:00
if err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "Fail to get home directory: %v" , err )
2014-02-25 14:58:55 +04:00
}
2014-03-03 00:25:09 +04:00
return home
2014-02-25 14:58:55 +04:00
}
2014-02-25 12:13:47 +04:00
func init ( ) {
var err error
2014-03-17 22:03:58 +04:00
2014-05-08 01:04:32 +04:00
if appPath , err = exePath ( ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "fail to get app path: %v\n" , err )
2014-02-25 12:13:47 +04:00
}
2014-07-26 08:24:27 +04:00
appPath = strings . Replace ( appPath , "\\" , "/" , - 1 )
2014-02-25 14:58:55 +04:00
2014-03-17 22:03:58 +04:00
// Determine and create .ssh path.
2014-06-11 03:11:53 +04:00
SshPath = filepath . Join ( homeDir ( ) , ".ssh" )
2014-08-07 12:00:57 +04:00
if err = os . MkdirAll ( SshPath , 0700 ) ; err != nil {
2014-07-26 08:24:27 +04:00
log . Fatal ( 4 , "fail to create SshPath(%s): %v\n" , SshPath , err )
2014-03-17 22:03:58 +04:00
}
2014-02-25 12:13:47 +04:00
}
2014-05-07 20:09:30 +04:00
// PublicKey represents a SSH key.
2014-02-17 19:57:23 +04:00
type PublicKey struct {
2014-07-26 08:24:27 +04:00
Id int64
OwnerId int64 ` xorm:"UNIQUE(s) INDEX NOT NULL" `
Name string ` xorm:"UNIQUE(s) NOT NULL" `
Fingerprint string
Content string ` xorm:"TEXT NOT NULL" `
Created time . Time ` xorm:"CREATED" `
Updated time . Time
HasRecentActivity bool ` xorm:"-" `
HasUsed bool ` xorm:"-" `
2014-02-17 19:57:23 +04:00
}
2014-05-07 20:09:30 +04:00
// GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
func ( key * PublicKey ) GetAuthorizedString ( ) string {
return fmt . Sprintf ( _TPL_PUBLICK_KEY , appPath , key . Id , key . Content )
2014-02-17 19:57:23 +04:00
}
2014-07-26 08:24:27 +04:00
var (
MinimumKeySize = map [ string ] int {
"(ED25519)" : 256 ,
"(ECDSA)" : 256 ,
"(NTRU)" : 1087 ,
"(MCE)" : 1702 ,
"(McE)" : 1702 ,
"(RSA)" : 2048 ,
2014-09-29 08:37:34 +04:00
"(DSA)" : 1024 ,
2014-07-26 08:24:27 +04:00
}
)
// CheckPublicKeyString checks if the given public key string is recognized by SSH.
func CheckPublicKeyString ( content string ) ( bool , error ) {
if strings . ContainsAny ( content , "\n\r" ) {
2014-10-12 04:34:48 +04:00
return false , errors . New ( "only a single line with a single key please" )
2014-07-26 08:24:27 +04:00
}
// write the key to a file…
tmpFile , err := ioutil . TempFile ( os . TempDir ( ) , "keytest" )
if err != nil {
return false , err
}
tmpPath := tmpFile . Name ( )
defer os . Remove ( tmpPath )
tmpFile . WriteString ( content )
tmpFile . Close ( )
2014-09-16 21:34:09 +04:00
// Check if ssh-keygen recognizes its contents.
2014-07-26 08:24:27 +04:00
stdout , stderr , err := process . Exec ( "CheckPublicKeyString" , "ssh-keygen" , "-l" , "-f" , tmpPath )
if err != nil {
return false , errors . New ( "ssh-keygen -l -f: " + stderr )
} else if len ( stdout ) < 2 {
2014-10-12 04:06:35 +04:00
return false , errors . New ( "ssh-keygen returned not enough output to evaluate the key: " + stdout )
2014-07-26 08:24:27 +04:00
}
2014-09-16 21:34:09 +04:00
// The ssh-keygen in Windows does not print key type, so no need go further.
if setting . IsWindows {
return true , nil
}
2014-10-12 05:04:42 +04:00
fmt . Println ( stdout )
2014-07-26 08:24:27 +04:00
sshKeygenOutput := strings . Split ( stdout , " " )
if len ( sshKeygenOutput ) < 4 {
2014-10-12 05:04:42 +04:00
return false , ErrKeyUnableVerify
2014-07-26 08:24:27 +04:00
}
2014-09-16 21:34:09 +04:00
// Check if key type and key size match.
2014-10-12 04:34:48 +04:00
keySize := com . StrTo ( sshKeygenOutput [ 0 ] ) . MustInt ( )
if keySize == 0 {
return false , errors . New ( "cannot get key size of the given key" )
2014-07-26 08:24:27 +04:00
}
keyType := strings . TrimSpace ( sshKeygenOutput [ len ( sshKeygenOutput ) - 1 ] )
if minimumKeySize := MinimumKeySize [ keyType ] ; minimumKeySize == 0 {
2014-10-12 04:34:48 +04:00
return false , errors . New ( "sorry, unrecognized public key type" )
2014-07-26 08:24:27 +04:00
} else if keySize < minimumKeySize {
2014-10-12 04:34:48 +04:00
return false , fmt . Errorf ( "the minimum accepted size of a public key %s is %d" , keyType , minimumKeySize )
2014-07-26 08:24:27 +04:00
}
return true , nil
}
2014-05-07 20:09:30 +04:00
// saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
func saveAuthorizedKeyFile ( key * PublicKey ) error {
sshOpLocker . Lock ( )
defer sshOpLocker . Unlock ( )
2014-06-11 03:11:53 +04:00
fpath := filepath . Join ( SshPath , "authorized_keys" )
2014-05-07 20:09:30 +04:00
f , err := os . OpenFile ( fpath , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0600 )
if err != nil {
return err
}
2014-08-07 13:06:42 +04:00
defer f . Close ( )
2014-08-07 12:00:57 +04:00
finfo , err := f . Stat ( )
if err != nil {
return err
}
2014-09-16 16:32:13 +04:00
// FIXME: following command does not support in Windows.
2014-09-16 21:34:09 +04:00
if ! setting . IsWindows {
2014-09-16 16:32:13 +04:00
if finfo . Mode ( ) . Perm ( ) > 0600 {
log . Error ( 4 , "authorized_keys file has unusual permission flags: %s - setting to -rw-------" , finfo . Mode ( ) . Perm ( ) . String ( ) )
if err = f . Chmod ( 0600 ) ; err != nil {
return err
}
2014-08-07 13:06:42 +04:00
}
2014-08-07 12:00:57 +04:00
}
2014-05-07 20:09:30 +04:00
_ , err = f . WriteString ( key . GetAuthorizedString ( ) )
return err
}
// AddPublicKey adds new public key to database and authorized_keys file.
2014-03-16 14:16:03 +04:00
func AddPublicKey ( key * PublicKey ) ( err error ) {
2014-06-21 08:51:41 +04:00
has , err := x . Get ( key )
2014-03-16 14:25:16 +04:00
if err != nil {
return err
} else if has {
return ErrKeyAlreadyExist
}
2014-03-16 14:16:03 +04:00
// Calculate fingerprint.
2014-05-07 20:09:30 +04:00
tmpPath := strings . Replace ( path . Join ( os . TempDir ( ) , fmt . Sprintf ( "%d" , time . Now ( ) . Nanosecond ( ) ) ,
2014-03-22 22:27:03 +04:00
"id_rsa.pub" ) , "\\" , "/" , - 1 )
2014-03-16 14:16:03 +04:00
os . MkdirAll ( path . Dir ( tmpPath ) , os . ModePerm )
2014-03-17 22:03:58 +04:00
if err = ioutil . WriteFile ( tmpPath , [ ] byte ( key . Content ) , os . ModePerm ) ; err != nil {
2014-02-17 19:57:23 +04:00
return err
}
2014-06-19 09:08:03 +04:00
stdout , stderr , err := process . Exec ( "AddPublicKey" , "ssh-keygen" , "-l" , "-f" , tmpPath )
2014-02-17 19:57:23 +04:00
if err != nil {
2014-04-28 01:01:39 +04:00
return errors . New ( "ssh-keygen -l -f: " + stderr )
2014-03-16 14:16:03 +04:00
} else if len ( stdout ) < 2 {
2014-10-12 04:34:48 +04:00
return errors . New ( "not enough output for calculating fingerprint: " + stdout )
2014-03-16 14:16:03 +04:00
}
key . Fingerprint = strings . Split ( stdout , " " ) [ 1 ]
// Save SSH key.
2014-06-21 08:51:41 +04:00
if _ , err = x . Insert ( key ) ; err != nil {
2014-03-16 14:16:03 +04:00
return err
2014-05-07 20:09:30 +04:00
} else if err = saveAuthorizedKeyFile ( key ) ; err != nil {
// Roll back.
2014-06-21 08:51:41 +04:00
if _ , err2 := x . Delete ( key ) ; err2 != nil {
2014-03-16 14:16:03 +04:00
return err2
2014-02-17 19:57:23 +04:00
}
return err
}
return nil
}
2014-08-10 02:40:10 +04:00
// GetPublicKeyById returns public key by given ID.
func GetPublicKeyById ( keyId int64 ) ( * PublicKey , error ) {
key := new ( PublicKey )
has , err := x . Id ( keyId ) . Get ( key )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrKeyNotExist
}
return key , nil
}
2014-05-07 20:09:30 +04:00
// ListPublicKey returns a list of all public keys that user has.
2014-07-26 08:24:27 +04:00
func ListPublicKey ( uid int64 ) ( [ ] * PublicKey , error ) {
keys := make ( [ ] * PublicKey , 0 , 5 )
2014-06-21 08:51:41 +04:00
err := x . Find ( & keys , & PublicKey { OwnerId : uid } )
2014-07-26 08:24:27 +04:00
if err != nil {
return nil , err
}
for _ , key := range keys {
key . HasUsed = key . Updated . After ( key . Created )
key . HasRecentActivity = key . Updated . Add ( 7 * 24 * time . Hour ) . After ( time . Now ( ) )
}
return keys , nil
2014-05-07 20:09:30 +04:00
}
2014-05-07 00:28:52 +04:00
// rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file.
2014-03-22 22:27:03 +04:00
func rewriteAuthorizedKeys ( key * PublicKey , p , tmpP string ) error {
2014-03-16 13:24:13 +04:00
sshOpLocker . Lock ( )
defer sshOpLocker . Unlock ( )
fr , err := os . Open ( p )
if err != nil {
return err
}
defer fr . Close ( )
2014-06-24 12:53:42 +04:00
fw , err := os . OpenFile ( tmpP , os . O_CREATE | os . O_WRONLY | os . O_APPEND , 0600 )
2014-03-16 13:24:13 +04:00
if err != nil {
return err
}
defer fw . Close ( )
2014-05-07 00:28:52 +04:00
isFound := false
2014-05-07 20:09:30 +04:00
keyword := fmt . Sprintf ( "key-%d" , key . Id )
buf := bufio . NewReader ( fr )
for {
line , errRead := buf . ReadString ( '\n' )
line = strings . TrimSpace ( line )
if errRead != nil {
if errRead != io . EOF {
return errRead
}
// Reached end of file, if nothing to read then break,
// otherwise handle the last line.
if len ( line ) == 0 {
break
}
2014-03-16 13:24:13 +04:00
}
// Found the line and copy rest of file.
2014-05-07 20:09:30 +04:00
if ! isFound && strings . Contains ( line , keyword ) && strings . Contains ( line , key . Content ) {
2014-05-07 00:28:52 +04:00
isFound = true
2014-03-16 13:48:20 +04:00
continue
2014-03-16 13:24:13 +04:00
}
// Still finding the line, copy the line that currently read.
2014-05-07 20:09:30 +04:00
if _ , err = fw . WriteString ( line + "\n" ) ; err != nil {
2014-03-16 13:24:13 +04:00
return err
}
2014-05-07 00:28:52 +04:00
2014-05-07 20:09:30 +04:00
if errRead == io . EOF {
break
}
}
2014-03-22 22:27:03 +04:00
return nil
}
2014-03-16 13:24:13 +04:00
2014-08-10 02:40:10 +04:00
// UpdatePublicKey updates given public key.
func UpdatePublicKey ( key * PublicKey ) error {
_ , err := x . Id ( key . Id ) . AllCols ( ) . Update ( key )
return err
}
2014-03-22 22:27:03 +04:00
// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
2014-05-07 00:28:52 +04:00
func DeletePublicKey ( key * PublicKey ) error {
2014-06-21 08:51:41 +04:00
has , err := x . Get ( key )
2014-03-22 22:27:03 +04:00
if err != nil {
return err
} else if ! has {
2014-05-07 00:28:52 +04:00
return ErrKeyNotExist
2014-03-22 22:27:03 +04:00
}
2014-05-07 00:28:52 +04:00
2014-06-21 08:51:41 +04:00
if _ , err = x . Delete ( key ) ; err != nil {
2014-03-22 22:27:03 +04:00
return err
}
2014-06-11 03:11:53 +04:00
fpath := filepath . Join ( SshPath , "authorized_keys" )
tmpPath := filepath . Join ( SshPath , "authorized_keys.tmp" )
2014-05-07 20:09:30 +04:00
if err = rewriteAuthorizedKeys ( key , fpath , tmpPath ) ; err != nil {
2014-03-22 22:27:03 +04:00
return err
2014-05-07 20:09:30 +04:00
} else if err = os . Remove ( fpath ) ; err != nil {
2014-03-16 13:24:13 +04:00
return err
}
2014-05-07 20:09:30 +04:00
return os . Rename ( tmpPath , fpath )
2014-02-17 19:57:23 +04:00
}