main.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "log"
  7. "log/syslog"
  8. "os"
  9. "strconv"
  10. "strings"
  11. "github.com/go-ldap/ldap/v3"
  12. "golang.org/x/crypto/ssh"
  13. )
  14. const (
  15. rootDN = "dc=example,dc=com"
  16. bindUsername = "cn=sftpgo," + rootDN
  17. bindURL = "ldap:///" // That is, the server on the default port of localhost.
  18. passwordFile = "/etc/sftpgo/admin-password.txt" // make this file readable only by the server
  19. publicDir = "/var/www/webdav/public"
  20. )
  21. type userFilters struct {
  22. DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
  23. }
  24. type minimalSFTPGoUser struct {
  25. Status int `json:"status,omitempty"`
  26. Username string `json:"username"`
  27. HomeDir string `json:"home_dir,omitempty"`
  28. UID int `json:"uid,omitempty"`
  29. GID int `json:"gid,omitempty"`
  30. Permissions map[string][]string `json:"permissions"`
  31. Filters userFilters `json:"filters"`
  32. }
  33. func exitError() {
  34. log.Printf("exitError\n")
  35. u := minimalSFTPGoUser{
  36. Username: "",
  37. }
  38. resp, _ := json.Marshal(u)
  39. fmt.Printf("%v\n", string(resp))
  40. os.Exit(1)
  41. }
  42. func printSuccessResponse(username, homeDir string, uid, gid int, permissions []string) {
  43. u := minimalSFTPGoUser{
  44. Username: username,
  45. HomeDir: homeDir,
  46. UID: uid,
  47. GID: gid,
  48. Status: 1,
  49. }
  50. u.Permissions = make(map[string][]string)
  51. u.Permissions["/"] = permissions
  52. // uncomment the next line to require publickey+password authentication
  53. //u.Filters.DeniedLoginMethods = []string{"publickey", "password", "keyboard-interactive", "publickey+keyboard-interactive"}
  54. resp, _ := json.Marshal(u)
  55. log.Printf("%v\n", string(resp))
  56. fmt.Printf("%v\n", string(resp))
  57. os.Exit(0)
  58. }
  59. func main() {
  60. logWriter, err := syslog.New(syslog.LOG_NOTICE, "sftpgo")
  61. if err == nil {
  62. log.SetOutput(logWriter)
  63. }
  64. // get credentials from env vars
  65. username := os.Getenv("SFTPGO_AUTHD_USERNAME")
  66. password := os.Getenv("SFTPGO_AUTHD_PASSWORD")
  67. publickey := os.Getenv("SFTPGO_AUTHD_PUBLIC_KEY")
  68. if strings.ToLower(username) == "anonymous" {
  69. printSuccessResponse("anonymous", publicDir, 0, 0, []string{"list", "download"})
  70. return
  71. }
  72. l, err := ldap.DialURL(bindURL)
  73. if err != nil {
  74. log.Printf("DialURL: %s\n", err.Error())
  75. exitError()
  76. }
  77. defer l.Close()
  78. // bind to the ldap server with an account that can read users
  79. bindPassword, err := os.ReadFile(passwordFile)
  80. if err != nil {
  81. log.Printf("ReadFile(%s): %s\n", passwordFile, err.Error())
  82. exitError()
  83. }
  84. err = l.Bind(bindUsername, string(bindPassword))
  85. if err != nil {
  86. log.Printf("Bind(%s): %s\n", bindUsername, err.Error())
  87. exitError()
  88. }
  89. // search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration
  90. log.Printf("username=%s\n", username)
  91. searchFilter := fmt.Sprintf("(uid=%s)", username)
  92. searchRequest := ldap.NewSearchRequest(
  93. "ou=people," + rootDN,
  94. ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
  95. searchFilter,
  96. []string{"dn", "uid", "homeDirectory", "uidNumber", "gidNumber", "nsSshPublicKey"},
  97. nil,
  98. )
  99. sr, err := l.Search(searchRequest)
  100. if err != nil {
  101. log.Printf("Search(%s): %s\n", searchFilter, err.Error())
  102. exitError()
  103. }
  104. // we expect exactly one user
  105. if len(sr.Entries) != 1 {
  106. log.Printf("Search(%s): %d entries\n", searchFilter, len(sr.Entries))
  107. exitError()
  108. }
  109. if len(publickey) > 0 {
  110. // check public key
  111. userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey))
  112. if err != nil {
  113. log.Printf("ParseAuthorizedKey(%s): %s\n", publickey, err.Error())
  114. exitError()
  115. }
  116. authOk := false
  117. for _, k := range sr.Entries[0].GetAttributeValues("nsSshPublicKey") {
  118. key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
  119. // we skip an invalid public key stored inside the LDAP server
  120. if err != nil {
  121. continue
  122. }
  123. if bytes.Equal(key.Marshal(), userKey.Marshal()) {
  124. authOk = true
  125. break
  126. }
  127. }
  128. if !authOk {
  129. log.Printf("publickey %s !authOk\n", publickey)
  130. exitError()
  131. }
  132. } else {
  133. // bind to the LDAP server with the user dn and the given password to check the password
  134. userdn := sr.Entries[0].DN
  135. // log.Printf("password=%s\n", password)
  136. err = l.Bind(userdn, password)
  137. if err != nil {
  138. log.Printf("Bind(%s): %s\n", userdn, err.Error())
  139. exitError()
  140. }
  141. }
  142. // People in the LDAP directory aren't necessarily Linux users;
  143. // so they might not have a uidNumber or gidNumber.
  144. uidNumber := sr.Entries[0].GetAttributeValue("uidNumber")
  145. uid, err := strconv.Atoi(uidNumber)
  146. if err != nil {
  147. //log.Printf("uid Atoi(%s) = %s\n", uidNumber, err.Error())
  148. uid = 0
  149. }
  150. gidNumber := sr.Entries[0].GetAttributeValue("gidNumber")
  151. gid, err := strconv.Atoi(gidNumber)
  152. if err != nil {
  153. //log.Printf("gid Atoi(%s) = %s\n", gidNumber, err.Error())
  154. gid = 0
  155. }
  156. homeDir := sr.Entries[0].GetAttributeValue("homeDirectory")
  157. if (len(homeDir) <= 0) {
  158. homeDir = publicDir // homeDir is a required attribute.
  159. }
  160. // return the authenticated user
  161. printSuccessResponse(sr.Entries[0].GetAttributeValue("uid"), homeDir, uid, gid, []string{"*"})
  162. }