This is an automated email from the ASF dual-hosted git repository. rob pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git
commit a3a7be3f3003a504c2eed53238cd9e5e37e84435 Author: Jan van Doorn <jan_vando...@comcast.com> AuthorDate: Sat May 12 13:38:43 2018 -0600 Start of Range Request handler: getfull mode. No error/boundary checking yet --- grove/plugin/range_req_handler.go | 224 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/grove/plugin/range_req_handler.go b/grove/plugin/range_req_handler.go new file mode 100644 index 0000000..bc21544 --- /dev/null +++ b/grove/plugin/range_req_handler.go @@ -0,0 +1,224 @@ +package plugin + +import ( + "encoding/json" + + "crypto/rand" + "encoding/hex" + "fmt" + "github.com/apache/incubator-trafficcontrol/grove/web" + "github.com/apache/incubator-trafficcontrol/lib/go-log" + "net/http" + "strconv" + "strings" +) + +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +type byteRange struct { + Start int + End int +} + +type rangeRequestConfig struct { + Mode string `json::"mode"` +} + +func init() { + AddPlugin(10000, Funcs{load: rangeReqHandleLoad, onRequest: rangeReqHandlerOnRequest, beforeParentRequest: rangeReqHandleBeforeParent, beforeRespond: rangeReqHandleBeforeRespond}) +} + +// rangeReqHandleLoad loads the configuration +func rangeReqHandleLoad(b json.RawMessage) interface{} { + cfg := rangeRequestConfig{} + log.Errorf("rangeReqHandleLoad loading: %s", b) + + err := json.Unmarshal(b, &cfg) + if err != nil { + log.Errorln("range_rew_handler loading config, unmarshalling JSON: " + err.Error()) + return nil + } + log.Debugf("range_rew_handler: load success: %+v\n", cfg) + return &cfg +} + +// rangeReqHandlerOnRequest determines if there is a Range header, and puts the ranges in *d.Context as a []byteRanges +func rangeReqHandlerOnRequest(icfg interface{}, d OnRequestData) bool { + rHeader := d.R.Header.Get("Range") + if rHeader == "" { + log.Debugf("No Range header found") + return false + } + log.Debugf("Range string is: %s", rHeader) + byteRanges := parseRangeHeader(rHeader) + *d.Context = byteRanges + return false +} + +// rangeReqHandleBeforeParent changes the parent request if needed (mode == getfull) +func rangeReqHandleBeforeParent(icfg interface{}, d BeforeParentRequestData) { + log.Debugf("rangeReqHandleBeforeParent calling.") + rHeader := d.Req.Header.Get("Range") + if rHeader == "" { + log.Debugf("No Range header found") + return + } + log.Debugf("Range string is: %s", rHeader) + cfg, ok := icfg.(*rangeRequestConfig) + if !ok { + log.Errorf("range_req_handler config '%v' type '%T' expected *rangeRequestConfig\n", icfg, icfg) + return + } + if cfg.Mode == "getfull" { + // getfull means get the whole thing from parent/org, but serve the requested range. Just remove the Range header from the upstream request, and put the range header in the context so we can use it in the response. + d.Req.Header.Del("Range") + } + return +} + +//-- +//> GET /10Mb.txt HTTP/1.1 +//> Host: localhost +//> Range: bytes=0-1,500000-500009 +//> User-Agent: curl/7.54.0 +//> Accept: */* +//> +//< HTTP/1.1 206 Partial Content +//< Date: Sat, 12 May 2018 14:21:56 GMT +//< Server: Apache/2.4.29 (Unix) +//< Last-Modified: Sat, 12 May 2018 14:19:11 GMT +//< ETag: "a00000-56c02efb675c0" +//< Accept-Ranges: bytes +//< Content-Length: 216 +//< Cache-Control: max-age=60, public +//< Expires: Sat, 12 May 2018 14:22:56 GMT +//< Content-Type: multipart/byteranges; boundary=4d583e1cee600e82 +//< +// +//--4d583e1cee600e82 +//Content-type: text/plain +//Content-range: bytes 0-1/10485760 +// +//00 +//--4d583e1cee600e82 +//Content-type: text/plain +//Content-range: bytes 500000-500009/10485760 +// +//000500000 +// +//--4d583e1cee600e82-- +//-- + +// https://httpwg.org/specs/rfc7230.html#message.body.length + +// rangeReqHandleBeforeRespond builds the 206 response +// assume all the needed ranges have been put in cache before, which is the truth for "getfull" mode which gets the whole object into cache. +func rangeReqHandleBeforeRespond(icfg interface{}, d BeforeRespondData) { + log.Debugf("rangeReqHandleBeforeRespond calling\n") + ictx := d.Context + ctx, ok := (*ictx).([]byteRange) + if !ok { + log.Errorf("Invalid context: %v", ictx) + } + if len(ctx) == 0 { + return // there was no (valid) range header + } + + multipartBoundaryString := "" + originalContentType := d.Hdr.Get("Content-type") + *d.Hdr = web.CopyHeader(*d.Hdr) // copy the headers, we don't want to mod the cacheObj + if len(ctx) > 1 { + //multipart = true + multipartBoundaryBytes := make([]byte, 8) + if _, err := rand.Read(multipartBoundaryBytes); err != nil { + log.Errorf("Error with rand.Read: %v", err) + } + multipartBoundaryString = hex.EncodeToString(multipartBoundaryBytes) + d.Hdr.Set("Content-Type", fmt.Sprintf("multipart/byteranges; boundary=%s", multipartBoundaryString)) + } + totalContentLength, err := strconv.Atoi(d.Hdr.Get("Content-Length")) + if err != nil { + log.Errorf("Invalid Content-Length header: %v", d.Hdr.Get("Content-Length")) + } + body := make([]byte, 0) + for _, thisRange := range ctx { + if thisRange.End == -1 { + thisRange.End = totalContentLength + } + log.Debugf("range:%d-%d", thisRange.Start, thisRange.End) + if multipartBoundaryString != "" { + body = append(body, []byte(fmt.Sprintf("\r\n--%s\r\n", multipartBoundaryString))...) + body = append(body, []byte(fmt.Sprintf("Content-type: %s\r\n", originalContentType))...) + body = append(body, []byte(fmt.Sprintf("Content-range: bytes %d-%d/%d\r\n\r\n", thisRange.Start, thisRange.End, totalContentLength))...) + } else { + byteRangeString := fmt.Sprintf("bytes %d-%d/%d", thisRange.Start, thisRange.End, totalContentLength) + d.Hdr.Add("Content-Range", byteRangeString) + } + bSlice := (*d.Body)[thisRange.Start : thisRange.End+1] + body = append(body, bSlice...) + } + if multipartBoundaryString != "" { + body = append(body, []byte(fmt.Sprintf("\r\n--%s--\r\n", multipartBoundaryString))...) + } + d.Hdr.Set("Content-Length", fmt.Sprintf("%d", len(body))) + *d.Body = body + *d.Code = http.StatusPartialContent + return +} + +func parseRange(rangeString string) (byteRange, error) { + parts := strings.Split(rangeString, "-") + + var bRange byteRange + if parts[0] == "" { + bRange.Start = 0 + } else { + start, err := strconv.Atoi(parts[0]) + if err != nil { + log.Errorf("Error converting rangeString \"%\" to numbers", rangeString) + return byteRange{}, err + } + bRange.Start = start + } + if parts[1] == "" { + bRange.End = -1 // -1 means till the end + } else { + end, err := strconv.Atoi(parts[1]) + if err != nil { + log.Errorf("Error converting rangeString \"%\" to numbers", rangeString) + return byteRange{}, err + } + bRange.End = end + } + return bRange, nil +} + +func parseRangeHeader(rHdrVal string) []byteRange { + byteRanges := make([]byteRange, 0) + rangeStringParts := strings.Split(rHdrVal, "=") + if rangeStringParts[0] != "bytes" { + log.Errorf("Not a valid Range type: \"%s\"", rangeStringParts[0]) + } + + for _, thisRangeString := range strings.Split(rangeStringParts[1], ",") { + log.Debugf("bRangeStr: %s", thisRangeString) + thisRange, err := parseRange(thisRangeString) + if err != nil { + return nil + } + byteRanges = append(byteRanges, thisRange) + } + return byteRanges +} -- To stop receiving notification emails like this one, please contact r...@apache.org.