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.

450 lines
10 KiB

9 years ago
2 years ago
10 years ago
FEATURE: add JWT to HTTP endpoints of Filer and use them in S3 Client - one JWT for reading and one for writing, analogous to how the JWT between Master and Volume Server works - I did not implement IP `whiteList` parameter on the filer Additionally, because http_util.DownloadFile now sets the JWT, the `download` command should now work when `jwt.signing.read` is configured. By looking at the code, I think this case did not work before. ## Docs to be adjusted after a release Page `Amazon-S3-API`: ``` # Authentication with Filer You can use mTLS for the gRPC connection between S3-API-Proxy and the filer, as explained in [Security-Configuration](Security-Configuration) - controlled by the `grpc.*` configuration in `security.toml`. Starting with version XX, it is also possible to authenticate the HTTP operations between the S3-API-Proxy and the Filer (especially uploading new files). This is configured by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. With both configurations (gRPC and JWT), it is possible to have Filer and S3 communicate in fully authenticated fashion; so Filer will reject any unauthenticated communication. ``` Page `Security Overview`: ``` The following items are not covered, yet: - master server http REST services Starting with version XX, the Filer HTTP REST services can be secured with a JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. ... Before version XX: "weed filer -disableHttp", disable http operations, only gRPC operations are allowed. This works with "weed mount" by FUSE. It does **not work** with the [S3 Gateway](Amazon S3 API), as this does HTTP calls to the Filer. Starting with version XX: secured by JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. **This now works with the [S3 Gateway](Amazon S3 API).** ... # Securing Filer HTTP with JWT To enable JWT-based access control for the Filer, 1. generate `security.toml` file by `weed scaffold -config=security` 2. set `filer_jwt.signing.key` to a secret string - and optionally filer_jwt.signing.read.key` as well to a secret string 3. copy the same `security.toml` file to the filers and all S3 proxies. If `filer_jwt.signing.key` is configured: When sending upload/update/delete HTTP operations to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.key`. If `filer_jwt.signing.read.key` is configured: When sending GET or HEAD requests to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.read.key`. The S3 API Gateway reads the above JWT keys and sends authenticated HTTP requests to the filer. ``` Page `Security Configuration`: ``` (update scaffold file) ... [filer_jwt.signing] key = "blahblahblahblah" [filer_jwt.signing.read] key = "blahblahblahblah" ``` Resolves: #158
3 years ago
FEATURE: add JWT to HTTP endpoints of Filer and use them in S3 Client - one JWT for reading and one for writing, analogous to how the JWT between Master and Volume Server works - I did not implement IP `whiteList` parameter on the filer Additionally, because http_util.DownloadFile now sets the JWT, the `download` command should now work when `jwt.signing.read` is configured. By looking at the code, I think this case did not work before. ## Docs to be adjusted after a release Page `Amazon-S3-API`: ``` # Authentication with Filer You can use mTLS for the gRPC connection between S3-API-Proxy and the filer, as explained in [Security-Configuration](Security-Configuration) - controlled by the `grpc.*` configuration in `security.toml`. Starting with version XX, it is also possible to authenticate the HTTP operations between the S3-API-Proxy and the Filer (especially uploading new files). This is configured by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. With both configurations (gRPC and JWT), it is possible to have Filer and S3 communicate in fully authenticated fashion; so Filer will reject any unauthenticated communication. ``` Page `Security Overview`: ``` The following items are not covered, yet: - master server http REST services Starting with version XX, the Filer HTTP REST services can be secured with a JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. ... Before version XX: "weed filer -disableHttp", disable http operations, only gRPC operations are allowed. This works with "weed mount" by FUSE. It does **not work** with the [S3 Gateway](Amazon S3 API), as this does HTTP calls to the Filer. Starting with version XX: secured by JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. **This now works with the [S3 Gateway](Amazon S3 API).** ... # Securing Filer HTTP with JWT To enable JWT-based access control for the Filer, 1. generate `security.toml` file by `weed scaffold -config=security` 2. set `filer_jwt.signing.key` to a secret string - and optionally filer_jwt.signing.read.key` as well to a secret string 3. copy the same `security.toml` file to the filers and all S3 proxies. If `filer_jwt.signing.key` is configured: When sending upload/update/delete HTTP operations to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.key`. If `filer_jwt.signing.read.key` is configured: When sending GET or HEAD requests to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.read.key`. The S3 API Gateway reads the above JWT keys and sends authenticated HTTP requests to the filer. ``` Page `Security Configuration`: ``` (update scaffold file) ... [filer_jwt.signing] key = "blahblahblahblah" [filer_jwt.signing.read] key = "blahblahblahblah" ``` Resolves: #158
3 years ago
5 years ago
2 years ago
3 years ago
2 years ago
5 years ago
5 years ago
FEATURE: add JWT to HTTP endpoints of Filer and use them in S3 Client - one JWT for reading and one for writing, analogous to how the JWT between Master and Volume Server works - I did not implement IP `whiteList` parameter on the filer Additionally, because http_util.DownloadFile now sets the JWT, the `download` command should now work when `jwt.signing.read` is configured. By looking at the code, I think this case did not work before. ## Docs to be adjusted after a release Page `Amazon-S3-API`: ``` # Authentication with Filer You can use mTLS for the gRPC connection between S3-API-Proxy and the filer, as explained in [Security-Configuration](Security-Configuration) - controlled by the `grpc.*` configuration in `security.toml`. Starting with version XX, it is also possible to authenticate the HTTP operations between the S3-API-Proxy and the Filer (especially uploading new files). This is configured by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. With both configurations (gRPC and JWT), it is possible to have Filer and S3 communicate in fully authenticated fashion; so Filer will reject any unauthenticated communication. ``` Page `Security Overview`: ``` The following items are not covered, yet: - master server http REST services Starting with version XX, the Filer HTTP REST services can be secured with a JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. ... Before version XX: "weed filer -disableHttp", disable http operations, only gRPC operations are allowed. This works with "weed mount" by FUSE. It does **not work** with the [S3 Gateway](Amazon S3 API), as this does HTTP calls to the Filer. Starting with version XX: secured by JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. **This now works with the [S3 Gateway](Amazon S3 API).** ... # Securing Filer HTTP with JWT To enable JWT-based access control for the Filer, 1. generate `security.toml` file by `weed scaffold -config=security` 2. set `filer_jwt.signing.key` to a secret string - and optionally filer_jwt.signing.read.key` as well to a secret string 3. copy the same `security.toml` file to the filers and all S3 proxies. If `filer_jwt.signing.key` is configured: When sending upload/update/delete HTTP operations to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.key`. If `filer_jwt.signing.read.key` is configured: When sending GET or HEAD requests to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.read.key`. The S3 API Gateway reads the above JWT keys and sends authenticated HTTP requests to the filer. ``` Page `Security Configuration`: ``` (update scaffold file) ... [filer_jwt.signing] key = "blahblahblahblah" [filer_jwt.signing.read] key = "blahblahblahblah" ``` Resolves: #158
3 years ago
FEATURE: add JWT to HTTP endpoints of Filer and use them in S3 Client - one JWT for reading and one for writing, analogous to how the JWT between Master and Volume Server works - I did not implement IP `whiteList` parameter on the filer Additionally, because http_util.DownloadFile now sets the JWT, the `download` command should now work when `jwt.signing.read` is configured. By looking at the code, I think this case did not work before. ## Docs to be adjusted after a release Page `Amazon-S3-API`: ``` # Authentication with Filer You can use mTLS for the gRPC connection between S3-API-Proxy and the filer, as explained in [Security-Configuration](Security-Configuration) - controlled by the `grpc.*` configuration in `security.toml`. Starting with version XX, it is also possible to authenticate the HTTP operations between the S3-API-Proxy and the Filer (especially uploading new files). This is configured by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. With both configurations (gRPC and JWT), it is possible to have Filer and S3 communicate in fully authenticated fashion; so Filer will reject any unauthenticated communication. ``` Page `Security Overview`: ``` The following items are not covered, yet: - master server http REST services Starting with version XX, the Filer HTTP REST services can be secured with a JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. ... Before version XX: "weed filer -disableHttp", disable http operations, only gRPC operations are allowed. This works with "weed mount" by FUSE. It does **not work** with the [S3 Gateway](Amazon S3 API), as this does HTTP calls to the Filer. Starting with version XX: secured by JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. **This now works with the [S3 Gateway](Amazon S3 API).** ... # Securing Filer HTTP with JWT To enable JWT-based access control for the Filer, 1. generate `security.toml` file by `weed scaffold -config=security` 2. set `filer_jwt.signing.key` to a secret string - and optionally filer_jwt.signing.read.key` as well to a secret string 3. copy the same `security.toml` file to the filers and all S3 proxies. If `filer_jwt.signing.key` is configured: When sending upload/update/delete HTTP operations to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.key`. If `filer_jwt.signing.read.key` is configured: When sending GET or HEAD requests to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.read.key`. The S3 API Gateway reads the above JWT keys and sends authenticated HTTP requests to the filer. ``` Page `Security Configuration`: ``` (update scaffold file) ... [filer_jwt.signing] key = "blahblahblahblah" [filer_jwt.signing.read] key = "blahblahblahblah" ``` Resolves: #158
3 years ago
  1. package util
  2. import (
  3. "compress/gzip"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "github.com/seaweedfs/seaweedfs/weed/util/mem"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "github.com/seaweedfs/seaweedfs/weed/glog"
  13. )
  14. var (
  15. client *http.Client
  16. Transport *http.Transport
  17. )
  18. func init() {
  19. Transport = &http.Transport{
  20. MaxIdleConns: 1024,
  21. MaxIdleConnsPerHost: 1024,
  22. DisableCompression: true,
  23. }
  24. client = &http.Client{
  25. Transport: Transport,
  26. }
  27. }
  28. func Post(url string, values url.Values) ([]byte, error) {
  29. r, err := client.PostForm(url, values)
  30. if err != nil {
  31. return nil, err
  32. }
  33. defer r.Body.Close()
  34. b, err := io.ReadAll(r.Body)
  35. if r.StatusCode >= 400 {
  36. if err != nil {
  37. return nil, fmt.Errorf("%s: %d - %s", url, r.StatusCode, string(b))
  38. } else {
  39. return nil, fmt.Errorf("%s: %s", url, r.Status)
  40. }
  41. }
  42. if err != nil {
  43. return nil, err
  44. }
  45. return b, nil
  46. }
  47. // github.com/seaweedfs/seaweedfs/unmaintained/repeated_vacuum/repeated_vacuum.go
  48. // may need increasing http.Client.Timeout
  49. func Get(url string) ([]byte, bool, error) {
  50. request, err := http.NewRequest("GET", url, nil)
  51. request.Header.Add("Accept-Encoding", "gzip")
  52. response, err := client.Do(request)
  53. if err != nil {
  54. return nil, true, err
  55. }
  56. defer CloseResponse(response)
  57. var reader io.ReadCloser
  58. switch response.Header.Get("Content-Encoding") {
  59. case "gzip":
  60. reader, err = gzip.NewReader(response.Body)
  61. defer reader.Close()
  62. default:
  63. reader = response.Body
  64. }
  65. b, err := io.ReadAll(reader)
  66. if response.StatusCode >= 400 {
  67. retryable := response.StatusCode >= 500
  68. return nil, retryable, fmt.Errorf("%s: %s", url, response.Status)
  69. }
  70. if err != nil {
  71. return nil, false, err
  72. }
  73. return b, false, nil
  74. }
  75. func Head(url string) (http.Header, error) {
  76. r, err := client.Head(url)
  77. if err != nil {
  78. return nil, err
  79. }
  80. defer CloseResponse(r)
  81. if r.StatusCode >= 400 {
  82. return nil, fmt.Errorf("%s: %s", url, r.Status)
  83. }
  84. return r.Header, nil
  85. }
  86. func Delete(url string, jwt string) error {
  87. req, err := http.NewRequest("DELETE", url, nil)
  88. if jwt != "" {
  89. req.Header.Set("Authorization", "BEARER "+string(jwt))
  90. }
  91. if err != nil {
  92. return err
  93. }
  94. resp, e := client.Do(req)
  95. if e != nil {
  96. return e
  97. }
  98. defer resp.Body.Close()
  99. body, err := io.ReadAll(resp.Body)
  100. if err != nil {
  101. return err
  102. }
  103. switch resp.StatusCode {
  104. case http.StatusNotFound, http.StatusAccepted, http.StatusOK:
  105. return nil
  106. }
  107. m := make(map[string]interface{})
  108. if e := json.Unmarshal(body, &m); e == nil {
  109. if s, ok := m["error"].(string); ok {
  110. return errors.New(s)
  111. }
  112. }
  113. return errors.New(string(body))
  114. }
  115. func DeleteProxied(url string, jwt string) (body []byte, httpStatus int, err error) {
  116. req, err := http.NewRequest("DELETE", url, nil)
  117. if jwt != "" {
  118. req.Header.Set("Authorization", "BEARER "+string(jwt))
  119. }
  120. if err != nil {
  121. return
  122. }
  123. resp, err := client.Do(req)
  124. if err != nil {
  125. return
  126. }
  127. defer resp.Body.Close()
  128. body, err = io.ReadAll(resp.Body)
  129. if err != nil {
  130. return
  131. }
  132. httpStatus = resp.StatusCode
  133. return
  134. }
  135. func GetBufferStream(url string, values url.Values, allocatedBytes []byte, eachBuffer func([]byte)) error {
  136. r, err := client.PostForm(url, values)
  137. if err != nil {
  138. return err
  139. }
  140. defer CloseResponse(r)
  141. if r.StatusCode != 200 {
  142. return fmt.Errorf("%s: %s", url, r.Status)
  143. }
  144. for {
  145. n, err := r.Body.Read(allocatedBytes)
  146. if n > 0 {
  147. eachBuffer(allocatedBytes[:n])
  148. }
  149. if err != nil {
  150. if err == io.EOF {
  151. return nil
  152. }
  153. return err
  154. }
  155. }
  156. }
  157. func GetUrlStream(url string, values url.Values, readFn func(io.Reader) error) error {
  158. r, err := client.PostForm(url, values)
  159. if err != nil {
  160. return err
  161. }
  162. defer CloseResponse(r)
  163. if r.StatusCode != 200 {
  164. return fmt.Errorf("%s: %s", url, r.Status)
  165. }
  166. return readFn(r.Body)
  167. }
  168. func DownloadFile(fileUrl string, jwt string) (filename string, header http.Header, resp *http.Response, e error) {
  169. req, err := http.NewRequest("GET", fileUrl, nil)
  170. if err != nil {
  171. return "", nil, nil, err
  172. }
  173. if len(jwt) > 0 {
  174. req.Header.Set("Authorization", "BEARER "+jwt)
  175. }
  176. response, err := client.Do(req)
  177. if err != nil {
  178. return "", nil, nil, err
  179. }
  180. header = response.Header
  181. contentDisposition := response.Header["Content-Disposition"]
  182. if len(contentDisposition) > 0 {
  183. idx := strings.Index(contentDisposition[0], "filename=")
  184. if idx != -1 {
  185. filename = contentDisposition[0][idx+len("filename="):]
  186. filename = strings.Trim(filename, "\"")
  187. }
  188. }
  189. resp = response
  190. return
  191. }
  192. func Do(req *http.Request) (resp *http.Response, err error) {
  193. return client.Do(req)
  194. }
  195. func NormalizeUrl(url string) string {
  196. if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
  197. return url
  198. }
  199. return "http://" + url
  200. }
  201. func ReadUrl(fileUrl string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, buf []byte) (int64, error) {
  202. if cipherKey != nil {
  203. var n int
  204. _, err := readEncryptedUrl(fileUrl, cipherKey, isContentCompressed, isFullChunk, offset, size, func(data []byte) {
  205. n = copy(buf, data)
  206. })
  207. return int64(n), err
  208. }
  209. req, err := http.NewRequest("GET", fileUrl, nil)
  210. if err != nil {
  211. return 0, err
  212. }
  213. if !isFullChunk {
  214. req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
  215. } else {
  216. req.Header.Set("Accept-Encoding", "gzip")
  217. }
  218. r, err := client.Do(req)
  219. if err != nil {
  220. return 0, err
  221. }
  222. defer CloseResponse(r)
  223. if r.StatusCode >= 400 {
  224. return 0, fmt.Errorf("%s: %s", fileUrl, r.Status)
  225. }
  226. var reader io.ReadCloser
  227. contentEncoding := r.Header.Get("Content-Encoding")
  228. switch contentEncoding {
  229. case "gzip":
  230. reader, err = gzip.NewReader(r.Body)
  231. defer reader.Close()
  232. default:
  233. reader = r.Body
  234. }
  235. var (
  236. i, m int
  237. n int64
  238. )
  239. // refers to https://github.com/golang/go/blob/master/src/bytes/buffer.go#L199
  240. // commit id c170b14c2c1cfb2fd853a37add92a82fd6eb4318
  241. for {
  242. m, err = reader.Read(buf[i:])
  243. i += m
  244. n += int64(m)
  245. if err == io.EOF {
  246. return n, nil
  247. }
  248. if err != nil {
  249. return n, err
  250. }
  251. if n == int64(len(buf)) {
  252. break
  253. }
  254. }
  255. // drains the response body to avoid memory leak
  256. data, _ := io.ReadAll(reader)
  257. if len(data) != 0 {
  258. glog.V(1).Infof("%s reader has remaining %d bytes", contentEncoding, len(data))
  259. }
  260. return n, err
  261. }
  262. func ReadUrlAsStream(fileUrl string, cipherKey []byte, isContentGzipped bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (retryable bool, err error) {
  263. if cipherKey != nil {
  264. return readEncryptedUrl(fileUrl, cipherKey, isContentGzipped, isFullChunk, offset, size, fn)
  265. }
  266. req, err := http.NewRequest("GET", fileUrl, nil)
  267. if err != nil {
  268. return false, err
  269. }
  270. if isFullChunk {
  271. req.Header.Add("Accept-Encoding", "gzip")
  272. } else {
  273. req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
  274. }
  275. r, err := client.Do(req)
  276. if err != nil {
  277. return true, err
  278. }
  279. defer CloseResponse(r)
  280. if r.StatusCode >= 400 {
  281. retryable = r.StatusCode == http.StatusNotFound || r.StatusCode >= 500
  282. return retryable, fmt.Errorf("%s: %s", fileUrl, r.Status)
  283. }
  284. var reader io.ReadCloser
  285. contentEncoding := r.Header.Get("Content-Encoding")
  286. switch contentEncoding {
  287. case "gzip":
  288. reader, err = gzip.NewReader(r.Body)
  289. defer reader.Close()
  290. default:
  291. reader = r.Body
  292. }
  293. var (
  294. m int
  295. )
  296. buf := mem.Allocate(64 * 1024)
  297. defer mem.Free(buf)
  298. for {
  299. m, err = reader.Read(buf)
  300. if m > 0 {
  301. fn(buf[:m])
  302. }
  303. if err == io.EOF {
  304. return false, nil
  305. }
  306. if err != nil {
  307. return true, err
  308. }
  309. }
  310. }
  311. func readEncryptedUrl(fileUrl string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (bool, error) {
  312. encryptedData, retryable, err := Get(fileUrl)
  313. if err != nil {
  314. return retryable, fmt.Errorf("fetch %s: %v", fileUrl, err)
  315. }
  316. decryptedData, err := Decrypt(encryptedData, CipherKey(cipherKey))
  317. if err != nil {
  318. return false, fmt.Errorf("decrypt %s: %v", fileUrl, err)
  319. }
  320. if isContentCompressed {
  321. decryptedData, err = DecompressData(decryptedData)
  322. if err != nil {
  323. glog.V(0).Infof("unzip decrypt %s: %v", fileUrl, err)
  324. }
  325. }
  326. if len(decryptedData) < int(offset)+size {
  327. return false, fmt.Errorf("read decrypted %s size %d [%d, %d)", fileUrl, len(decryptedData), offset, int(offset)+size)
  328. }
  329. if isFullChunk {
  330. fn(decryptedData)
  331. } else {
  332. fn(decryptedData[int(offset) : int(offset)+size])
  333. }
  334. return false, nil
  335. }
  336. func ReadUrlAsReaderCloser(fileUrl string, jwt string, rangeHeader string) (*http.Response, io.ReadCloser, error) {
  337. req, err := http.NewRequest("GET", fileUrl, nil)
  338. if err != nil {
  339. return nil, nil, err
  340. }
  341. if rangeHeader != "" {
  342. req.Header.Add("Range", rangeHeader)
  343. } else {
  344. req.Header.Add("Accept-Encoding", "gzip")
  345. }
  346. if len(jwt) > 0 {
  347. req.Header.Set("Authorization", "BEARER "+jwt)
  348. }
  349. r, err := client.Do(req)
  350. if err != nil {
  351. return nil, nil, err
  352. }
  353. if r.StatusCode >= 400 {
  354. CloseResponse(r)
  355. return nil, nil, fmt.Errorf("%s: %s", fileUrl, r.Status)
  356. }
  357. var reader io.ReadCloser
  358. contentEncoding := r.Header.Get("Content-Encoding")
  359. switch contentEncoding {
  360. case "gzip":
  361. reader, err = gzip.NewReader(r.Body)
  362. default:
  363. reader = r.Body
  364. }
  365. return r, reader, nil
  366. }
  367. func CloseResponse(resp *http.Response) {
  368. if resp == nil || resp.Body == nil {
  369. return
  370. }
  371. reader := &CountingReader{reader: resp.Body}
  372. io.Copy(io.Discard, reader)
  373. resp.Body.Close()
  374. if reader.BytesRead > 0 {
  375. if resp.Request != nil && resp.Request.URL != nil {
  376. glog.V(1).Infof("response leftover %d bytes, url: %s", reader.BytesRead, resp.Request.URL.RequestURI())
  377. } else {
  378. glog.V(1).Infof("response leftover %d bytes", reader.BytesRead)
  379. }
  380. }
  381. }
  382. func CloseRequest(req *http.Request) {
  383. reader := &CountingReader{reader: req.Body}
  384. io.Copy(io.Discard, reader)
  385. req.Body.Close()
  386. if reader.BytesRead > 0 {
  387. if req.URL != nil {
  388. glog.V(1).Infof("request leftover %d bytes, url: %s", reader.BytesRead, req.URL.RequestURI())
  389. } else {
  390. glog.V(1).Infof("request leftover %d bytes", reader.BytesRead)
  391. }
  392. }
  393. }
  394. type CountingReader struct {
  395. reader io.Reader
  396. BytesRead int
  397. }
  398. func (r *CountingReader) Read(p []byte) (n int, err error) {
  399. n, err = r.reader.Read(p)
  400. r.BytesRead += n
  401. return n, err
  402. }