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.

479 lines
11 KiB

9 years ago
11 months 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
4 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
11 months ago
11 months ago
  1. package http
  2. import (
  3. "compress/gzip"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "github.com/seaweedfs/seaweedfs/weed/util/mem"
  8. "github.com/seaweedfs/seaweedfs/weed/util"
  9. "io"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "time"
  14. "github.com/seaweedfs/seaweedfs/weed/glog"
  15. )
  16. func Post(url string, values url.Values) ([]byte, error) {
  17. r, err := GetGlobalHttpClient().PostForm(url, values)
  18. if err != nil {
  19. return nil, err
  20. }
  21. defer r.Body.Close()
  22. b, err := io.ReadAll(r.Body)
  23. if r.StatusCode >= 400 {
  24. if err != nil {
  25. return nil, fmt.Errorf("%s: %d - %s", url, r.StatusCode, string(b))
  26. } else {
  27. return nil, fmt.Errorf("%s: %s", url, r.Status)
  28. }
  29. }
  30. if err != nil {
  31. return nil, err
  32. }
  33. return b, nil
  34. }
  35. // github.com/seaweedfs/seaweedfs/unmaintained/repeated_vacuum/repeated_vacuum.go
  36. // may need increasing http.Client.Timeout
  37. func Get(url string) ([]byte, bool, error) {
  38. return GetAuthenticated(url, "")
  39. }
  40. func GetAuthenticated(url, jwt string) ([]byte, bool, error) {
  41. request, err := http.NewRequest(http.MethodGet, url, nil)
  42. if err != nil {
  43. return nil, true, err
  44. }
  45. maybeAddAuth(request, jwt)
  46. request.Header.Add("Accept-Encoding", "gzip")
  47. response, err := GetGlobalHttpClient().Do(request)
  48. if err != nil {
  49. return nil, true, err
  50. }
  51. defer CloseResponse(response)
  52. var reader io.ReadCloser
  53. switch response.Header.Get("Content-Encoding") {
  54. case "gzip":
  55. reader, err = gzip.NewReader(response.Body)
  56. if err != nil {
  57. return nil, true, err
  58. }
  59. defer reader.Close()
  60. default:
  61. reader = response.Body
  62. }
  63. b, err := io.ReadAll(reader)
  64. if response.StatusCode >= 400 {
  65. retryable := response.StatusCode >= 500
  66. return nil, retryable, fmt.Errorf("%s: %s", url, response.Status)
  67. }
  68. if err != nil {
  69. return nil, false, err
  70. }
  71. return b, false, nil
  72. }
  73. func Head(url string) (http.Header, error) {
  74. r, err := GetGlobalHttpClient().Head(url)
  75. if err != nil {
  76. return nil, err
  77. }
  78. defer CloseResponse(r)
  79. if r.StatusCode >= 400 {
  80. return nil, fmt.Errorf("%s: %s", url, r.Status)
  81. }
  82. return r.Header, nil
  83. }
  84. func maybeAddAuth(req *http.Request, jwt string) {
  85. if jwt != "" {
  86. req.Header.Set("Authorization", "BEARER "+string(jwt))
  87. }
  88. }
  89. func Delete(url string, jwt string) error {
  90. req, err := http.NewRequest(http.MethodDelete, url, nil)
  91. maybeAddAuth(req, jwt)
  92. if err != nil {
  93. return err
  94. }
  95. resp, e := GetGlobalHttpClient().Do(req)
  96. if e != nil {
  97. return e
  98. }
  99. defer resp.Body.Close()
  100. body, err := io.ReadAll(resp.Body)
  101. if err != nil {
  102. return err
  103. }
  104. switch resp.StatusCode {
  105. case http.StatusNotFound, http.StatusAccepted, http.StatusOK:
  106. return nil
  107. }
  108. m := make(map[string]interface{})
  109. if e := json.Unmarshal(body, &m); e == nil {
  110. if s, ok := m["error"].(string); ok {
  111. return errors.New(s)
  112. }
  113. }
  114. return errors.New(string(body))
  115. }
  116. func DeleteProxied(url string, jwt string) (body []byte, httpStatus int, err error) {
  117. req, err := http.NewRequest(http.MethodDelete, url, nil)
  118. maybeAddAuth(req, jwt)
  119. if err != nil {
  120. return
  121. }
  122. resp, err := GetGlobalHttpClient().Do(req)
  123. if err != nil {
  124. return
  125. }
  126. defer resp.Body.Close()
  127. body, err = io.ReadAll(resp.Body)
  128. if err != nil {
  129. return
  130. }
  131. httpStatus = resp.StatusCode
  132. return
  133. }
  134. func GetBufferStream(url string, values url.Values, allocatedBytes []byte, eachBuffer func([]byte)) error {
  135. r, err := GetGlobalHttpClient().PostForm(url, values)
  136. if err != nil {
  137. return err
  138. }
  139. defer CloseResponse(r)
  140. if r.StatusCode != 200 {
  141. return fmt.Errorf("%s: %s", url, r.Status)
  142. }
  143. for {
  144. n, err := r.Body.Read(allocatedBytes)
  145. if n > 0 {
  146. eachBuffer(allocatedBytes[:n])
  147. }
  148. if err != nil {
  149. if err == io.EOF {
  150. return nil
  151. }
  152. return err
  153. }
  154. }
  155. }
  156. func GetUrlStream(url string, values url.Values, readFn func(io.Reader) error) error {
  157. r, err := GetGlobalHttpClient().PostForm(url, values)
  158. if err != nil {
  159. return err
  160. }
  161. defer CloseResponse(r)
  162. if r.StatusCode != 200 {
  163. return fmt.Errorf("%s: %s", url, r.Status)
  164. }
  165. return readFn(r.Body)
  166. }
  167. func DownloadFile(fileUrl string, jwt string) (filename string, header http.Header, resp *http.Response, e error) {
  168. req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
  169. if err != nil {
  170. return "", nil, nil, err
  171. }
  172. maybeAddAuth(req, jwt)
  173. response, err := GetGlobalHttpClient().Do(req)
  174. if err != nil {
  175. return "", nil, nil, err
  176. }
  177. header = response.Header
  178. contentDisposition := response.Header["Content-Disposition"]
  179. if len(contentDisposition) > 0 {
  180. idx := strings.Index(contentDisposition[0], "filename=")
  181. if idx != -1 {
  182. filename = contentDisposition[0][idx+len("filename="):]
  183. filename = strings.Trim(filename, "\"")
  184. }
  185. }
  186. resp = response
  187. return
  188. }
  189. func Do(req *http.Request) (resp *http.Response, err error) {
  190. return GetGlobalHttpClient().Do(req)
  191. }
  192. func NormalizeUrl(url string) (string, error) {
  193. return GetGlobalHttpClient().NormalizeHttpScheme(url)
  194. }
  195. func ReadUrl(fileUrl string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, buf []byte) (int64, error) {
  196. if cipherKey != nil {
  197. var n int
  198. _, err := readEncryptedUrl(fileUrl, "", cipherKey, isContentCompressed, isFullChunk, offset, size, func(data []byte) {
  199. n = copy(buf, data)
  200. })
  201. return int64(n), err
  202. }
  203. req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
  204. if err != nil {
  205. return 0, err
  206. }
  207. if !isFullChunk {
  208. req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
  209. } else {
  210. req.Header.Set("Accept-Encoding", "gzip")
  211. }
  212. r, err := GetGlobalHttpClient().Do(req)
  213. if err != nil {
  214. return 0, err
  215. }
  216. defer CloseResponse(r)
  217. if r.StatusCode >= 400 {
  218. return 0, fmt.Errorf("%s: %s", fileUrl, r.Status)
  219. }
  220. var reader io.ReadCloser
  221. contentEncoding := r.Header.Get("Content-Encoding")
  222. switch contentEncoding {
  223. case "gzip":
  224. reader, err = gzip.NewReader(r.Body)
  225. if err != nil {
  226. return 0, err
  227. }
  228. defer reader.Close()
  229. default:
  230. reader = r.Body
  231. }
  232. var (
  233. i, m int
  234. n int64
  235. )
  236. // refers to https://github.com/golang/go/blob/master/src/bytes/buffer.go#L199
  237. // commit id c170b14c2c1cfb2fd853a37add92a82fd6eb4318
  238. for {
  239. m, err = reader.Read(buf[i:])
  240. i += m
  241. n += int64(m)
  242. if err == io.EOF {
  243. return n, nil
  244. }
  245. if err != nil {
  246. return n, err
  247. }
  248. if n == int64(len(buf)) {
  249. break
  250. }
  251. }
  252. // drains the response body to avoid memory leak
  253. data, _ := io.ReadAll(reader)
  254. if len(data) != 0 {
  255. glog.V(1).Infof("%s reader has remaining %d bytes", contentEncoding, len(data))
  256. }
  257. return n, err
  258. }
  259. func ReadUrlAsStream(fileUrl string, cipherKey []byte, isContentGzipped bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (retryable bool, err error) {
  260. return ReadUrlAsStreamAuthenticated(fileUrl, "", cipherKey, isContentGzipped, isFullChunk, offset, size, fn)
  261. }
  262. func ReadUrlAsStreamAuthenticated(fileUrl, jwt 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, jwt, cipherKey, isContentGzipped, isFullChunk, offset, size, fn)
  265. }
  266. req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
  267. maybeAddAuth(req, jwt)
  268. if err != nil {
  269. return false, err
  270. }
  271. if isFullChunk {
  272. req.Header.Add("Accept-Encoding", "gzip")
  273. } else {
  274. req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
  275. }
  276. r, err := GetGlobalHttpClient().Do(req)
  277. if err != nil {
  278. return true, err
  279. }
  280. defer CloseResponse(r)
  281. if r.StatusCode >= 400 {
  282. retryable = r.StatusCode == http.StatusNotFound || r.StatusCode >= 499
  283. return retryable, fmt.Errorf("%s: %s", fileUrl, r.Status)
  284. }
  285. var reader io.ReadCloser
  286. contentEncoding := r.Header.Get("Content-Encoding")
  287. switch contentEncoding {
  288. case "gzip":
  289. reader, err = gzip.NewReader(r.Body)
  290. defer reader.Close()
  291. default:
  292. reader = r.Body
  293. }
  294. var (
  295. m int
  296. )
  297. buf := mem.Allocate(64 * 1024)
  298. defer mem.Free(buf)
  299. for {
  300. m, err = reader.Read(buf)
  301. if m > 0 {
  302. fn(buf[:m])
  303. }
  304. if err == io.EOF {
  305. return false, nil
  306. }
  307. if err != nil {
  308. return true, err
  309. }
  310. }
  311. }
  312. func readEncryptedUrl(fileUrl, jwt string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (bool, error) {
  313. encryptedData, retryable, err := GetAuthenticated(fileUrl, jwt)
  314. if err != nil {
  315. return retryable, fmt.Errorf("fetch %s: %v", fileUrl, err)
  316. }
  317. decryptedData, err := util.Decrypt(encryptedData, util.CipherKey(cipherKey))
  318. if err != nil {
  319. return false, fmt.Errorf("decrypt %s: %v", fileUrl, err)
  320. }
  321. if isContentCompressed {
  322. decryptedData, err = util.DecompressData(decryptedData)
  323. if err != nil {
  324. glog.V(0).Infof("unzip decrypt %s: %v", fileUrl, err)
  325. }
  326. }
  327. if len(decryptedData) < int(offset)+size {
  328. return false, fmt.Errorf("read decrypted %s size %d [%d, %d)", fileUrl, len(decryptedData), offset, int(offset)+size)
  329. }
  330. if isFullChunk {
  331. fn(decryptedData)
  332. } else {
  333. fn(decryptedData[int(offset) : int(offset)+size])
  334. }
  335. return false, nil
  336. }
  337. func ReadUrlAsReaderCloser(fileUrl string, jwt string, rangeHeader string) (*http.Response, io.ReadCloser, error) {
  338. req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
  339. if err != nil {
  340. return nil, nil, err
  341. }
  342. if rangeHeader != "" {
  343. req.Header.Add("Range", rangeHeader)
  344. } else {
  345. req.Header.Add("Accept-Encoding", "gzip")
  346. }
  347. maybeAddAuth(req, jwt)
  348. r, err := GetGlobalHttpClient().Do(req)
  349. if err != nil {
  350. return nil, nil, err
  351. }
  352. if r.StatusCode >= 400 {
  353. CloseResponse(r)
  354. return nil, nil, fmt.Errorf("%s: %s", fileUrl, r.Status)
  355. }
  356. var reader io.ReadCloser
  357. contentEncoding := r.Header.Get("Content-Encoding")
  358. switch contentEncoding {
  359. case "gzip":
  360. reader, err = gzip.NewReader(r.Body)
  361. if err != nil {
  362. return nil, nil, err
  363. }
  364. default:
  365. reader = r.Body
  366. }
  367. return r, reader, nil
  368. }
  369. func CloseResponse(resp *http.Response) {
  370. if resp == nil || resp.Body == nil {
  371. return
  372. }
  373. reader := &CountingReader{reader: resp.Body}
  374. io.Copy(io.Discard, reader)
  375. resp.Body.Close()
  376. if reader.BytesRead > 0 {
  377. glog.V(1).Infof("response leftover %d bytes", reader.BytesRead)
  378. }
  379. }
  380. func CloseRequest(req *http.Request) {
  381. reader := &CountingReader{reader: req.Body}
  382. io.Copy(io.Discard, reader)
  383. req.Body.Close()
  384. if reader.BytesRead > 0 {
  385. glog.V(1).Infof("request leftover %d bytes", reader.BytesRead)
  386. }
  387. }
  388. type CountingReader struct {
  389. reader io.Reader
  390. BytesRead int
  391. }
  392. func (r *CountingReader) Read(p []byte) (n int, err error) {
  393. n, err = r.reader.Read(p)
  394. r.BytesRead += n
  395. return n, err
  396. }
  397. func RetriedFetchChunkData(buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64) (n int, err error) {
  398. var shouldRetry bool
  399. for waitTime := time.Second; waitTime < util.RetryWaitTime; waitTime += waitTime / 2 {
  400. for _, urlString := range urlStrings {
  401. n = 0
  402. if strings.Contains(urlString, "%") {
  403. urlString = url.PathEscape(urlString)
  404. }
  405. shouldRetry, err = ReadUrlAsStream(urlString+"?readDeleted=true", cipherKey, isGzipped, isFullChunk, offset, len(buffer), func(data []byte) {
  406. if n < len(buffer) {
  407. x := copy(buffer[n:], data)
  408. n += x
  409. }
  410. })
  411. if !shouldRetry {
  412. break
  413. }
  414. if err != nil {
  415. glog.V(0).Infof("read %s failed, err: %v", urlString, err)
  416. } else {
  417. break
  418. }
  419. }
  420. if err != nil && shouldRetry {
  421. glog.V(0).Infof("retry reading in %v", waitTime)
  422. time.Sleep(waitTime)
  423. } else {
  424. break
  425. }
  426. }
  427. return n, err
  428. }