auth.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package registry
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io/ioutil"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path"
  12. "strings"
  13. "github.com/docker/docker/utils"
  14. )
  15. const (
  16. // Where we store the config file
  17. CONFIGFILE = ".dockercfg"
  18. // Only used for user auth + account creation
  19. INDEXSERVER = "https://index.docker.io/v1/"
  20. REGISTRYSERVER = "https://registry-1.docker.io/v1/"
  21. // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
  22. )
  23. var (
  24. ErrConfigFileMissing = errors.New("The Auth config file is missing")
  25. IndexServerURL *url.URL
  26. )
  27. func init() {
  28. url, err := url.Parse(INDEXSERVER)
  29. if err != nil {
  30. panic(err)
  31. }
  32. IndexServerURL = url
  33. }
  34. type AuthConfig struct {
  35. Username string `json:"username,omitempty"`
  36. Password string `json:"password,omitempty"`
  37. Auth string `json:"auth"`
  38. Email string `json:"email"`
  39. ServerAddress string `json:"serveraddress,omitempty"`
  40. }
  41. type ConfigFile struct {
  42. Configs map[string]AuthConfig `json:"configs,omitempty"`
  43. rootPath string
  44. }
  45. func IndexServerAddress() string {
  46. return INDEXSERVER
  47. }
  48. // create a base64 encoded auth string to store in config
  49. func encodeAuth(authConfig *AuthConfig) string {
  50. authStr := authConfig.Username + ":" + authConfig.Password
  51. msg := []byte(authStr)
  52. encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
  53. base64.StdEncoding.Encode(encoded, msg)
  54. return string(encoded)
  55. }
  56. // decode the auth string
  57. func decodeAuth(authStr string) (string, string, error) {
  58. decLen := base64.StdEncoding.DecodedLen(len(authStr))
  59. decoded := make([]byte, decLen)
  60. authByte := []byte(authStr)
  61. n, err := base64.StdEncoding.Decode(decoded, authByte)
  62. if err != nil {
  63. return "", "", err
  64. }
  65. if n > decLen {
  66. return "", "", fmt.Errorf("Something went wrong decoding auth config")
  67. }
  68. arr := strings.SplitN(string(decoded), ":", 2)
  69. if len(arr) != 2 {
  70. return "", "", fmt.Errorf("Invalid auth configuration file")
  71. }
  72. password := strings.Trim(arr[1], "\x00")
  73. return arr[0], password, nil
  74. }
  75. // load up the auth config information and return values
  76. // FIXME: use the internal golang config parser
  77. func LoadConfig(rootPath string) (*ConfigFile, error) {
  78. configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
  79. confFile := path.Join(rootPath, CONFIGFILE)
  80. if _, err := os.Stat(confFile); err != nil {
  81. return &configFile, nil //missing file is not an error
  82. }
  83. b, err := ioutil.ReadFile(confFile)
  84. if err != nil {
  85. return &configFile, err
  86. }
  87. if err := json.Unmarshal(b, &configFile.Configs); err != nil {
  88. arr := strings.Split(string(b), "\n")
  89. if len(arr) < 2 {
  90. return &configFile, fmt.Errorf("The Auth config file is empty")
  91. }
  92. authConfig := AuthConfig{}
  93. origAuth := strings.Split(arr[0], " = ")
  94. if len(origAuth) != 2 {
  95. return &configFile, fmt.Errorf("Invalid Auth config file")
  96. }
  97. authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
  98. if err != nil {
  99. return &configFile, err
  100. }
  101. origEmail := strings.Split(arr[1], " = ")
  102. if len(origEmail) != 2 {
  103. return &configFile, fmt.Errorf("Invalid Auth config file")
  104. }
  105. authConfig.Email = origEmail[1]
  106. authConfig.ServerAddress = IndexServerAddress()
  107. configFile.Configs[IndexServerAddress()] = authConfig
  108. } else {
  109. for k, authConfig := range configFile.Configs {
  110. authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
  111. if err != nil {
  112. return &configFile, err
  113. }
  114. authConfig.Auth = ""
  115. configFile.Configs[k] = authConfig
  116. authConfig.ServerAddress = k
  117. }
  118. }
  119. return &configFile, nil
  120. }
  121. // save the auth config
  122. func SaveConfig(configFile *ConfigFile) error {
  123. confFile := path.Join(configFile.rootPath, CONFIGFILE)
  124. if len(configFile.Configs) == 0 {
  125. os.Remove(confFile)
  126. return nil
  127. }
  128. configs := make(map[string]AuthConfig, len(configFile.Configs))
  129. for k, authConfig := range configFile.Configs {
  130. authCopy := authConfig
  131. authCopy.Auth = encodeAuth(&authCopy)
  132. authCopy.Username = ""
  133. authCopy.Password = ""
  134. authCopy.ServerAddress = ""
  135. configs[k] = authCopy
  136. }
  137. b, err := json.Marshal(configs)
  138. if err != nil {
  139. return err
  140. }
  141. err = ioutil.WriteFile(confFile, b, 0600)
  142. if err != nil {
  143. return err
  144. }
  145. return nil
  146. }
  147. // try to register/login to the registry server
  148. func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
  149. var (
  150. status string
  151. reqBody []byte
  152. err error
  153. client = &http.Client{
  154. Transport: &http.Transport{
  155. DisableKeepAlives: true,
  156. Proxy: http.ProxyFromEnvironment,
  157. },
  158. CheckRedirect: AddRequiredHeadersToRedirectedRequests,
  159. }
  160. reqStatusCode = 0
  161. serverAddress = authConfig.ServerAddress
  162. )
  163. if serverAddress == "" {
  164. serverAddress = IndexServerAddress()
  165. }
  166. loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
  167. // to avoid sending the server address to the server it should be removed before being marshalled
  168. authCopy := *authConfig
  169. authCopy.ServerAddress = ""
  170. jsonBody, err := json.Marshal(authCopy)
  171. if err != nil {
  172. return "", fmt.Errorf("Config Error: %s", err)
  173. }
  174. // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
  175. b := strings.NewReader(string(jsonBody))
  176. req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
  177. if err != nil {
  178. return "", fmt.Errorf("Server Error: %s", err)
  179. }
  180. reqStatusCode = req1.StatusCode
  181. defer req1.Body.Close()
  182. reqBody, err = ioutil.ReadAll(req1.Body)
  183. if err != nil {
  184. return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
  185. }
  186. if reqStatusCode == 201 {
  187. if loginAgainstOfficialIndex {
  188. status = "Account created. Please use the confirmation link we sent" +
  189. " to your e-mail to activate it."
  190. } else {
  191. status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
  192. }
  193. } else if reqStatusCode == 400 {
  194. if string(reqBody) == "\"Username or email already exists\"" {
  195. req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
  196. req.SetBasicAuth(authConfig.Username, authConfig.Password)
  197. resp, err := client.Do(req)
  198. if err != nil {
  199. return "", err
  200. }
  201. defer resp.Body.Close()
  202. body, err := ioutil.ReadAll(resp.Body)
  203. if err != nil {
  204. return "", err
  205. }
  206. if resp.StatusCode == 200 {
  207. return "Login Succeeded", nil
  208. } else if resp.StatusCode == 401 {
  209. return "", fmt.Errorf("Wrong login/password, please try again")
  210. } else if resp.StatusCode == 403 {
  211. if loginAgainstOfficialIndex {
  212. return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
  213. }
  214. return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
  215. }
  216. return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
  217. }
  218. return "", fmt.Errorf("Registration: %s", reqBody)
  219. } else if reqStatusCode == 401 {
  220. // This case would happen with private registries where /v1/users is
  221. // protected, so people can use `docker login` as an auth check.
  222. req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
  223. req.SetBasicAuth(authConfig.Username, authConfig.Password)
  224. resp, err := client.Do(req)
  225. if err != nil {
  226. return "", err
  227. }
  228. defer resp.Body.Close()
  229. body, err := ioutil.ReadAll(resp.Body)
  230. if err != nil {
  231. return "", err
  232. }
  233. if resp.StatusCode == 200 {
  234. return "Login Succeeded", nil
  235. } else if resp.StatusCode == 401 {
  236. return "", fmt.Errorf("Wrong login/password, please try again")
  237. } else {
  238. return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
  239. resp.StatusCode, resp.Header)
  240. }
  241. } else {
  242. return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
  243. }
  244. return status, nil
  245. }
  246. // this method matches a auth configuration to a server address or a url
  247. func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig {
  248. if hostname == IndexServerAddress() || len(hostname) == 0 {
  249. // default to the index server
  250. return config.Configs[IndexServerAddress()]
  251. }
  252. // First try the happy case
  253. if c, found := config.Configs[hostname]; found {
  254. return c
  255. }
  256. convertToHostname := func(url string) string {
  257. stripped := url
  258. if strings.HasPrefix(url, "http://") {
  259. stripped = strings.Replace(url, "http://", "", 1)
  260. } else if strings.HasPrefix(url, "https://") {
  261. stripped = strings.Replace(url, "https://", "", 1)
  262. }
  263. nameParts := strings.SplitN(stripped, "/", 2)
  264. return nameParts[0]
  265. }
  266. // Maybe they have a legacy config file, we will iterate the keys converting
  267. // them to the new format and testing
  268. normalizedHostename := convertToHostname(hostname)
  269. for registry, config := range config.Configs {
  270. if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename {
  271. return config
  272. }
  273. }
  274. // When all else fails, return an empty auth config
  275. return AuthConfig{}
  276. }