You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

publickey.go 8.6 kB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "bufio"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/ioutil"
  11. "os"
  12. "os/exec"
  13. "path"
  14. "path/filepath"
  15. "strings"
  16. "sync"
  17. "time"
  18. "github.com/Unknwon/com"
  19. "github.com/gogits/gogs/modules/log"
  20. "github.com/gogits/gogs/modules/process"
  21. "github.com/gogits/gogs/modules/setting"
  22. )
  23. const (
  24. // "### autogenerated by gitgos, DO NOT EDIT\n"
  25. _TPL_PUBLICK_KEY = `command="%s serv key-%d",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
  26. )
  27. var (
  28. ErrKeyAlreadyExist = errors.New("Public key already exist")
  29. ErrKeyNotExist = errors.New("Public key does not exist")
  30. )
  31. var sshOpLocker = sync.Mutex{}
  32. var (
  33. SshPath string // SSH directory.
  34. appPath string // Execution(binary) path.
  35. )
  36. // exePath returns the executable path.
  37. func exePath() (string, error) {
  38. file, err := exec.LookPath(os.Args[0])
  39. if err != nil {
  40. return "", err
  41. }
  42. return filepath.Abs(file)
  43. }
  44. // homeDir returns the home directory of current user.
  45. func homeDir() string {
  46. home, err := com.HomeDir()
  47. if err != nil {
  48. log.Fatal(4, "Fail to get home directory: %v", err)
  49. }
  50. return home
  51. }
  52. func init() {
  53. var err error
  54. if appPath, err = exePath(); err != nil {
  55. log.Fatal(4, "fail to get app path: %v\n", err)
  56. }
  57. appPath = strings.Replace(appPath, "\\", "/", -1)
  58. // Determine and create .ssh path.
  59. SshPath = filepath.Join(homeDir(), ".ssh")
  60. if err = os.MkdirAll(SshPath, 0700); err != nil {
  61. log.Fatal(4, "fail to create SshPath(%s): %v\n", SshPath, err)
  62. }
  63. }
  64. // PublicKey represents a SSH key.
  65. type PublicKey struct {
  66. Id int64
  67. OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
  68. Name string `xorm:"UNIQUE(s) NOT NULL"`
  69. Fingerprint string
  70. Content string `xorm:"TEXT NOT NULL"`
  71. Created time.Time `xorm:"CREATED"`
  72. Updated time.Time
  73. HasRecentActivity bool `xorm:"-"`
  74. HasUsed bool `xorm:"-"`
  75. }
  76. // GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
  77. func (key *PublicKey) GetAuthorizedString() string {
  78. return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, key.Content)
  79. }
  80. var (
  81. MinimumKeySize = map[string]int{
  82. "(ED25519)": 256,
  83. "(ECDSA)": 256,
  84. "(NTRU)": 1087,
  85. "(MCE)": 1702,
  86. "(McE)": 1702,
  87. "(RSA)": 2048,
  88. }
  89. )
  90. // CheckPublicKeyString checks if the given public key string is recognized by SSH.
  91. func CheckPublicKeyString(content string) (bool, error) {
  92. if strings.ContainsAny(content, "\n\r") {
  93. return false, errors.New("Only a single line with a single key please")
  94. }
  95. // write the key to a file…
  96. tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest")
  97. if err != nil {
  98. return false, err
  99. }
  100. tmpPath := tmpFile.Name()
  101. defer os.Remove(tmpPath)
  102. tmpFile.WriteString(content)
  103. tmpFile.Close()
  104. // Check if ssh-keygen recognizes its contents.
  105. stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath)
  106. if err != nil {
  107. return false, errors.New("ssh-keygen -l -f: " + stderr)
  108. } else if len(stdout) < 2 {
  109. return false, errors.New("ssh-keygen returned not enough output to evaluate the key")
  110. }
  111. // The ssh-keygen in Windows does not print key type, so no need go further.
  112. if setting.IsWindows {
  113. return true, nil
  114. }
  115. sshKeygenOutput := strings.Split(stdout, " ")
  116. if len(sshKeygenOutput) < 4 {
  117. return false, errors.New("Not enough fields returned by ssh-keygen -l -f")
  118. }
  119. // Check if key type and key size match.
  120. keySize, err := com.StrTo(sshKeygenOutput[0]).Int()
  121. if err != nil {
  122. return false, errors.New("Cannot get key size of the given key")
  123. }
  124. keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
  125. if minimumKeySize := MinimumKeySize[keyType]; minimumKeySize == 0 {
  126. return false, errors.New("Sorry, unrecognized public key type")
  127. } else if keySize < minimumKeySize {
  128. return false, fmt.Errorf("The minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
  129. }
  130. return true, nil
  131. }
  132. // saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
  133. func saveAuthorizedKeyFile(key *PublicKey) error {
  134. sshOpLocker.Lock()
  135. defer sshOpLocker.Unlock()
  136. fpath := filepath.Join(SshPath, "authorized_keys")
  137. f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  138. if err != nil {
  139. return err
  140. }
  141. defer f.Close()
  142. finfo, err := f.Stat()
  143. if err != nil {
  144. return err
  145. }
  146. // FIXME: following command does not support in Windows.
  147. if !setting.IsWindows {
  148. if finfo.Mode().Perm() > 0600 {
  149. log.Error(4, "authorized_keys file has unusual permission flags: %s - setting to -rw-------", finfo.Mode().Perm().String())
  150. if err = f.Chmod(0600); err != nil {
  151. return err
  152. }
  153. }
  154. }
  155. _, err = f.WriteString(key.GetAuthorizedString())
  156. return err
  157. }
  158. // AddPublicKey adds new public key to database and authorized_keys file.
  159. func AddPublicKey(key *PublicKey) (err error) {
  160. has, err := x.Get(key)
  161. if err != nil {
  162. return err
  163. } else if has {
  164. return ErrKeyAlreadyExist
  165. }
  166. // Calculate fingerprint.
  167. tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
  168. "id_rsa.pub"), "\\", "/", -1)
  169. os.MkdirAll(path.Dir(tmpPath), os.ModePerm)
  170. if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil {
  171. return err
  172. }
  173. stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath)
  174. if err != nil {
  175. return errors.New("ssh-keygen -l -f: " + stderr)
  176. } else if len(stdout) < 2 {
  177. return errors.New("Not enough output for calculating fingerprint")
  178. }
  179. key.Fingerprint = strings.Split(stdout, " ")[1]
  180. // Save SSH key.
  181. if _, err = x.Insert(key); err != nil {
  182. return err
  183. } else if err = saveAuthorizedKeyFile(key); err != nil {
  184. // Roll back.
  185. if _, err2 := x.Delete(key); err2 != nil {
  186. return err2
  187. }
  188. return err
  189. }
  190. return nil
  191. }
  192. // GetPublicKeyById returns public key by given ID.
  193. func GetPublicKeyById(keyId int64) (*PublicKey, error) {
  194. key := new(PublicKey)
  195. has, err := x.Id(keyId).Get(key)
  196. if err != nil {
  197. return nil, err
  198. } else if !has {
  199. return nil, ErrKeyNotExist
  200. }
  201. return key, nil
  202. }
  203. // ListPublicKey returns a list of all public keys that user has.
  204. func ListPublicKey(uid int64) ([]*PublicKey, error) {
  205. keys := make([]*PublicKey, 0, 5)
  206. err := x.Find(&keys, &PublicKey{OwnerId: uid})
  207. if err != nil {
  208. return nil, err
  209. }
  210. for _, key := range keys {
  211. key.HasUsed = key.Updated.After(key.Created)
  212. key.HasRecentActivity = key.Updated.Add(7 * 24 * time.Hour).After(time.Now())
  213. }
  214. return keys, nil
  215. }
  216. // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file.
  217. func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
  218. sshOpLocker.Lock()
  219. defer sshOpLocker.Unlock()
  220. fr, err := os.Open(p)
  221. if err != nil {
  222. return err
  223. }
  224. defer fr.Close()
  225. fw, err := os.OpenFile(tmpP, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  226. if err != nil {
  227. return err
  228. }
  229. defer fw.Close()
  230. isFound := false
  231. keyword := fmt.Sprintf("key-%d", key.Id)
  232. buf := bufio.NewReader(fr)
  233. for {
  234. line, errRead := buf.ReadString('\n')
  235. line = strings.TrimSpace(line)
  236. if errRead != nil {
  237. if errRead != io.EOF {
  238. return errRead
  239. }
  240. // Reached end of file, if nothing to read then break,
  241. // otherwise handle the last line.
  242. if len(line) == 0 {
  243. break
  244. }
  245. }
  246. // Found the line and copy rest of file.
  247. if !isFound && strings.Contains(line, keyword) && strings.Contains(line, key.Content) {
  248. isFound = true
  249. continue
  250. }
  251. // Still finding the line, copy the line that currently read.
  252. if _, err = fw.WriteString(line + "\n"); err != nil {
  253. return err
  254. }
  255. if errRead == io.EOF {
  256. break
  257. }
  258. }
  259. return nil
  260. }
  261. // UpdatePublicKey updates given public key.
  262. func UpdatePublicKey(key *PublicKey) error {
  263. _, err := x.Id(key.Id).AllCols().Update(key)
  264. return err
  265. }
  266. // DeletePublicKey deletes SSH key information both in database and authorized_keys file.
  267. func DeletePublicKey(key *PublicKey) error {
  268. has, err := x.Get(key)
  269. if err != nil {
  270. return err
  271. } else if !has {
  272. return ErrKeyNotExist
  273. }
  274. if _, err = x.Delete(key); err != nil {
  275. return err
  276. }
  277. fpath := filepath.Join(SshPath, "authorized_keys")
  278. tmpPath := filepath.Join(SshPath, "authorized_keys.tmp")
  279. if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil {
  280. return err
  281. } else if err = os.Remove(fpath); err != nil {
  282. return err
  283. }
  284. return os.Rename(tmpPath, fpath)
  285. }