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

253 line
6.4KB

  1. // @author Couchbase <info@couchbase.com>
  2. // @copyright 2018 Couchbase, Inc.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. // Package scramsha provides implementation of client side SCRAM-SHA
  16. // via Http according to https://tools.ietf.org/html/rfc7804
  17. package scramsha
  18. import (
  19. "encoding/base64"
  20. "github.com/pkg/errors"
  21. "io"
  22. "io/ioutil"
  23. "net/http"
  24. "strings"
  25. )
  26. // consts used to parse scramsha response from target
  27. const (
  28. WWWAuthenticate = "WWW-Authenticate"
  29. AuthenticationInfo = "Authentication-Info"
  30. Authorization = "Authorization"
  31. DataPrefix = "data="
  32. SidPrefix = "sid="
  33. )
  34. // Request provides implementation of http request that can be retried
  35. type Request struct {
  36. body io.ReadSeeker
  37. // Embed an HTTP request directly. This makes a *Request act exactly
  38. // like an *http.Request so that all meta methods are supported.
  39. *http.Request
  40. }
  41. type lenReader interface {
  42. Len() int
  43. }
  44. // NewRequest creates http request that can be retried
  45. func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
  46. // Wrap the body in a noop ReadCloser if non-nil. This prevents the
  47. // reader from being closed by the HTTP client.
  48. var rcBody io.ReadCloser
  49. if body != nil {
  50. rcBody = ioutil.NopCloser(body)
  51. }
  52. // Make the request with the noop-closer for the body.
  53. httpReq, err := http.NewRequest(method, url, rcBody)
  54. if err != nil {
  55. return nil, err
  56. }
  57. // Check if we can set the Content-Length automatically.
  58. if lr, ok := body.(lenReader); ok {
  59. httpReq.ContentLength = int64(lr.Len())
  60. }
  61. return &Request{body, httpReq}, nil
  62. }
  63. func encode(str string) string {
  64. return base64.StdEncoding.EncodeToString([]byte(str))
  65. }
  66. func decode(str string) (string, error) {
  67. bytes, err := base64.StdEncoding.DecodeString(str)
  68. if err != nil {
  69. return "", errors.Errorf("Cannot base64 decode %s",
  70. str)
  71. }
  72. return string(bytes), err
  73. }
  74. func trimPrefix(s, prefix string) (string, error) {
  75. l := len(s)
  76. trimmed := strings.TrimPrefix(s, prefix)
  77. if l == len(trimmed) {
  78. return trimmed, errors.Errorf("Prefix %s not found in %s",
  79. prefix, s)
  80. }
  81. return trimmed, nil
  82. }
  83. func drainBody(resp *http.Response) {
  84. defer resp.Body.Close()
  85. io.Copy(ioutil.Discard, resp.Body)
  86. }
  87. // DoScramSha performs SCRAM-SHA handshake via Http
  88. func DoScramSha(req *Request,
  89. username string,
  90. password string,
  91. client *http.Client) (*http.Response, error) {
  92. method := "SCRAM-SHA-512"
  93. s, err := NewScramSha("SCRAM-SHA512")
  94. if err != nil {
  95. return nil, errors.Wrap(err,
  96. "Unable to initialize SCRAM-SHA handler")
  97. }
  98. message, err := s.GetStartRequest(username)
  99. if err != nil {
  100. return nil, err
  101. }
  102. encodedMessage := method + " " + DataPrefix + encode(message)
  103. req.Header.Set(Authorization, encodedMessage)
  104. res, err := client.Do(req.Request)
  105. if err != nil {
  106. return nil, errors.Wrap(err, "Problem sending SCRAM-SHA start"+
  107. "request")
  108. }
  109. if res.StatusCode != http.StatusUnauthorized {
  110. return res, nil
  111. }
  112. authHeader := res.Header.Get(WWWAuthenticate)
  113. if authHeader == "" {
  114. drainBody(res)
  115. return nil, errors.Errorf("Header %s is not populated in "+
  116. "SCRAM-SHA start response", WWWAuthenticate)
  117. }
  118. authHeader, err = trimPrefix(authHeader, method+" ")
  119. if err != nil {
  120. if strings.HasPrefix(authHeader, "Basic ") {
  121. // user not found
  122. return res, nil
  123. }
  124. drainBody(res)
  125. return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
  126. "start response %s", authHeader)
  127. }
  128. drainBody(res)
  129. sid, response, err := parseSidAndData(authHeader)
  130. if err != nil {
  131. return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
  132. "start response %s", authHeader)
  133. }
  134. err = s.HandleStartResponse(response)
  135. if err != nil {
  136. return nil, errors.Wrapf(err, "Error parsing SCRAM-SHA start "+
  137. "response %s", response)
  138. }
  139. message = s.GetFinalRequest(password)
  140. encodedMessage = method + " " + SidPrefix + sid + "," + DataPrefix +
  141. encode(message)
  142. req.Header.Set(Authorization, encodedMessage)
  143. // rewind request body so it can be resent again
  144. if req.body != nil {
  145. if _, err = req.body.Seek(0, 0); err != nil {
  146. return nil, errors.Errorf("Failed to seek body: %v",
  147. err)
  148. }
  149. }
  150. res, err = client.Do(req.Request)
  151. if err != nil {
  152. return nil, errors.Wrap(err, "Problem sending SCRAM-SHA final"+
  153. "request")
  154. }
  155. if res.StatusCode == http.StatusUnauthorized {
  156. // TODO retrieve and return error
  157. return res, nil
  158. }
  159. if res.StatusCode >= http.StatusInternalServerError {
  160. // in this case we cannot expect server to set headers properly
  161. return res, nil
  162. }
  163. authHeader = res.Header.Get(AuthenticationInfo)
  164. if authHeader == "" {
  165. drainBody(res)
  166. return nil, errors.Errorf("Header %s is not populated in "+
  167. "SCRAM-SHA final response", AuthenticationInfo)
  168. }
  169. finalSid, response, err := parseSidAndData(authHeader)
  170. if err != nil {
  171. drainBody(res)
  172. return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
  173. "final response %s", authHeader)
  174. }
  175. if finalSid != sid {
  176. drainBody(res)
  177. return nil, errors.Errorf("Sid %s returned by server "+
  178. "doesn't match the original sid %s", finalSid, sid)
  179. }
  180. err = s.HandleFinalResponse(response)
  181. if err != nil {
  182. drainBody(res)
  183. return nil, errors.Wrapf(err,
  184. "Error handling SCRAM-SHA final server response %s",
  185. response)
  186. }
  187. return res, nil
  188. }
  189. func parseSidAndData(authHeader string) (string, string, error) {
  190. sidIndex := strings.Index(authHeader, SidPrefix)
  191. if sidIndex < 0 {
  192. return "", "", errors.Errorf("Cannot find %s in %s",
  193. SidPrefix, authHeader)
  194. }
  195. sidEndIndex := strings.Index(authHeader, ",")
  196. if sidEndIndex < 0 {
  197. return "", "", errors.Errorf("Cannot find ',' in %s",
  198. authHeader)
  199. }
  200. sid := authHeader[sidIndex+len(SidPrefix) : sidEndIndex]
  201. dataIndex := strings.Index(authHeader, DataPrefix)
  202. if dataIndex < 0 {
  203. return "", "", errors.Errorf("Cannot find %s in %s",
  204. DataPrefix, authHeader)
  205. }
  206. data, err := decode(authHeader[dataIndex+len(DataPrefix):])
  207. if err != nil {
  208. return "", "", err
  209. }
  210. return sid, data, nil
  211. }
上海开阖软件有限公司 沪ICP备12045867号-1