// extractBsoId tries to extract and validate a BSO id in the path func extractBsoId(r *http.Request) (bId string, ok bool) { bId, ok = mux.Vars(r)["bsoId"] if !ok { return } ok = syncstorage.BSOIdOk(bId) return }
// hCollectionPOSTBatch handles batch=? requests. It is called internally by hCollectionPOST // to handle batch request logic func (s *SyncUserHandler) hCollectionPOSTBatch(collectionId int, w http.ResponseWriter, r *http.Request) { // CHECK client provided headers to quickly determine if batch exceeds limits // this is meant to be a cheap(er) check without actually having to parse the // data provided by the user for _, headerName := range []string{"X-Weave-Total-Records", "X-Weave-Total-Bytes", "X-Weave-Records", "X-Weave-Bytes"} { if strVal := r.Header.Get(headerName); strVal != "" { if intVal, err := strconv.Atoi(strVal); err == nil { max := 0 switch headerName { case "X-Weave-Total-Records": max = s.config.MaxTotalRecords case "X-Weave-Total-Bytes": max = s.config.MaxTotalBytes case "X-Weave-Bytes": max = s.config.MaxPOSTBytes case "X-Weave-Records": max = s.config.MaxPOSTRecords } if intVal > max { WeaveSizeLimitExceeded(w, r, errors.Errorf("Limit %s exceed. %d/%d", headerName, intVal, max)) return } } else { // header value is invalid (not an int) sendRequestProblem(w, r, http.StatusBadRequest, errors.Errorf("Invalid integer value for %s", headerName)) return } } } // CHECK the POST size, if possible from client supplied data // hopefully shortcut a fail if this exceeds limits if r.ContentLength > 0 && r.ContentLength > int64(s.config.MaxPOSTBytes) { WeaveSizeLimitExceeded(w, r, errors.Errorf("MaxPOSTBytes exceeded in request.ContentLength(%d) > %d", r.ContentLength, s.config.MaxPOSTBytes)) return } // EXTRACT actual data to check bsoToBeProcessed, results, err := RequestToPostBSOInput(r, s.config.MaxRecordPayloadBytes) if err != nil { WeaveInvalidWBOError(w, r, errors.Wrap(err, "Failed turning POST body into BSO work list")) return } // CHECK actual BSOs sent to see if they exceed limits if len(bsoToBeProcessed) > s.config.MaxPOSTRecords { sendRequestProblem(w, r, http.StatusRequestEntityTooLarge, errors.Errorf("Exceeded %d BSO per request", s.config.MaxPOSTRecords)) return } // CHECK BSO decoding validation errors. Don't even start a Batch if there are. if len(results.Failed) > 0 { modified := syncstorage.Now() w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified)) JsonNewline(w, r, &PostResults{ Modified: modified, Success: nil, Failed: results.Failed, }) return } // Get batch id, commit command and internal collection Id _, batchId, batchCommit := GetBatchIdAndCommit(r) // CHECK batch id is valid for appends. Do this before loading and decoding // the body to be more efficient. if batchId != "true" { id, err := batchIdInt(batchId) if err != nil { sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid Batch ID Format")) return } if found, err := s.db.BatchExists(id, collectionId); err != nil { InternalError(w, r, err) } else if !found { sendRequestProblem(w, r, http.StatusBadRequest, errors.Errorf("Batch id: %s does not exist", batchId)) } } filteredBSOs := make([]*syncstorage.PutBSOInput, 0, len(bsoToBeProcessed)) failures := make(map[string][]string) for _, putInput := range bsoToBeProcessed { var failId string var failReason string if !syncstorage.BSOIdOk(putInput.Id) { failId = "na" failReason = fmt.Sprintf("Invalid BSO id %s", putInput.Id) } if putInput.SortIndex != nil && !syncstorage.SortIndexOk(*putInput.SortIndex) { failId = putInput.Id failReason = fmt.Sprintf("Invalid sort index for: %s", putInput.Id) } if putInput.TTL != nil && !syncstorage.TTLOk(*putInput.TTL) { failId = putInput.Id failReason = fmt.Sprintf("Invalid TTL for: %s", putInput.Id) } if failReason != "" { if failures[failId] == nil { failures[failId] = []string{failReason} } else { failures[failId] = append(failures[failId], failReason) } continue } filteredBSOs = append(filteredBSOs, putInput) } // JSON Serialize the data for storage in the DB buf := new(bytes.Buffer) if len(filteredBSOs) > 0 { encoder := json.NewEncoder(buf) for _, bso := range filteredBSOs { if err := encoder.Encode(bso); err != nil { // Note: this writes a newline after each record // whoa... presumably should never happen InternalError(w, r, errors.Wrap(err, "Failed encoding BSO for payload")) return } } } // Save either as a new batch or append to an existing batch var dbBatchId int // - batchIdInt used to track the internal batchId number in the database after // - the create || append appendedOkIds := make([]string, 0, len(filteredBSOs)) if batchId == "true" { newBatchId, err := s.db.BatchCreate(collectionId, buf.String()) if err != nil { InternalError(w, r, errors.Wrap(err, "Failed creating batch")) return } dbBatchId = newBatchId } else { id, err := batchIdInt(batchId) if err != nil { sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid Batch ID Format")) return } if len(filteredBSOs) > 0 { // append only if something to do if err := s.db.BatchAppend(id, collectionId, buf.String()); err != nil { InternalError(w, r, errors.Wrap(err, fmt.Sprintf("Failed append to batch id:%d", dbBatchId))) return } } dbBatchId = id } // The success list only includes the BSO Ids in the // POST request. These need to kept to be sent back in // the response. Matches the python server's behaviour / passes // the tests for _, bso := range filteredBSOs { appendedOkIds = append(appendedOkIds, bso.Id) } if batchCommit { batchRecord, err := s.db.BatchLoad(dbBatchId, collectionId) if err != nil { InternalError(w, r, errors.Wrap(err, "Failed Loading Batch to commit")) return } rawJSON := ReadNewlineJSON(bytes.NewBufferString(batchRecord.BSOS)) // CHECK final data before committing it to the database numInBatch := len(rawJSON) if numInBatch > s.config.MaxTotalRecords { s.db.BatchRemove(dbBatchId) WeaveSizeLimitExceeded(w, r, errors.Errorf("Too many BSOs (%d) in Batch(%d)", numInBatch, dbBatchId)) return } postData := make(syncstorage.PostBSOInput, len(rawJSON), len(rawJSON)) for i, bsoJSON := range rawJSON { var bso syncstorage.PutBSOInput if parseErr := parseIntoBSO(bsoJSON, &bso); parseErr != nil { // well there is definitely a bug somewhere if this happens InternalError(w, r, errors.Wrap(parseErr, "Could not decode batch data")) return } else { postData[i] = &bso } } // CHECK that actual Batch data size sum := 0 for _, bso := range postData { if bso.Payload == nil { continue } sum := sum + len(*bso.Payload) if sum > s.config.MaxTotalBytes { s.db.BatchRemove(dbBatchId) WeaveSizeLimitExceeded(w, r, errors.Errorf("Batch size(%d) exceeded MaxTotalBytes limit(%d)", sum, s.config.MaxTotalBytes)) return } } postResults, err := s.db.PostBSOs(collectionId, postData) if err != nil { InternalError(w, r, err) return } // merge failures for key, reasons := range postResults.Failed { if failures[key] == nil { failures[key] = reasons } else { failures[key] = append(failures[key], reasons...) } } // DELETE the batch from the DB s.db.BatchRemove(dbBatchId) w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(postResults.Modified)) JsonNewline(w, r, &PostResults{ Modified: postResults.Modified, Success: appendedOkIds, Failed: failures, }) } else { modified := syncstorage.Now() w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified)) JsonNewlineStatus(w, r, http.StatusAccepted, &PostResults{ Batch: batchIdString(dbBatchId), Modified: modified, Success: appendedOkIds, Failed: failures, }) } }
func (s *SyncUserHandler) hCollectionGET(w http.ResponseWriter, r *http.Request) { if !AcceptHeaderOk(w, r) { return } // query params that control searching var ( err error ids []string newer int older int full bool limit int offset int sort = syncstorage.SORT_NEWEST ) cId, err := s.getcid(r, false) if err != nil { if err == syncstorage.ErrNotFound { w.Header().Set("Content-Type", "application/json") w.Write([]byte("[]")) return } else { InternalError(w, r, err) return } } if err = r.ParseForm(); err != nil { sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Bad query parameters")) return } if v := r.Form.Get("ids"); v != "" { ids = strings.Split(v, ",") if len(ids) > s.config.MaxPOSTRecords { sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Exceeded max batch size")) return } for i, id := range ids { id = strings.TrimSpace(id) if syncstorage.BSOIdOk(id) { ids[i] = id } else { sendRequestProblem(w, r, http.StatusBadRequest, errors.Errorf("Invalid bso id %s", id)) return } } if len(ids) > 100 { sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Too many ids provided")) return } } // we expect to get sync's two decimal timestamps, these need // to be converted to milliseconds if v := r.Form.Get("older"); v != "" { floatNew, err := strconv.ParseFloat(v, 64) if err != nil { sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid older param format")) return } older = int(floatNew * 1000) if !syncstorage.NewerOk(newer) { sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid older value")) return } } else { older = syncstorage.MaxTimestamp } if v := r.Form.Get("newer"); v != "" { floatNew, err := strconv.ParseFloat(v, 64) if err != nil { sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid newer param format")) return } newer = int(floatNew * 1000) if !syncstorage.NewerOk(newer) { sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid newer value")) return } } if v := r.Form.Get("full"); v != "" { full = true } if v := r.Form.Get("limit"); v != "" { limit, err = strconv.Atoi(v) if err != nil || !syncstorage.LimitOk(limit) { errMessage := "Invalid limit value" if err != nil { err = errors.Wrap(err, errMessage) } else { err = errors.New(errMessage) } sendRequestProblem(w, r, http.StatusBadRequest, err) return } } // assign a default value for limit if nothing is supplied if limit <= 0 || limit > s.config.MaxBSOGetLimit { limit = s.config.MaxBSOGetLimit } if v := r.Form.Get("offset"); v != "" { offset, err = strconv.Atoi(v) if err != nil || !syncstorage.OffsetOk(offset) { errMessage := "Invalid offset value" if err != nil { err = errors.Wrap(err, errMessage) } else { err = errors.New(errMessage) } sendRequestProblem(w, r, http.StatusBadRequest, err) return } } if v := r.Form.Get("sort"); v != "" { switch v { case "newest": sort = syncstorage.SORT_NEWEST case "oldest": sort = syncstorage.SORT_OLDEST case "index": sort = syncstorage.SORT_INDEX default: sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid sort value")) return } } // this is way down here since IO is more expensive // than parsing if the GET params are valid cmodified, err := s.db.GetCollectionModified(cId) if err != nil { InternalError(w, r, err) return } else if sentNotModified(w, r, cmodified) { return } results, err := s.db.GetBSOs(cId, ids, older, newer, sort, limit, offset) if err != nil { InternalError(w, r, err) return } m := syncstorage.ModifiedToString(cmodified) w.Header().Set("X-Last-Modified", m) w.Header().Set("X-Weave-Records", strconv.Itoa(results.Total)) if results.More { w.Header().Set("X-Weave-Next-Offset", strconv.Itoa(results.Offset)) } if full { JsonNewline(w, r, results.BSOs) } else { bsoIds := make([]string, len(results.BSOs)) for i, b := range results.BSOs { bsoIds[i] = b.Id } JsonNewline(w, r, bsoIds) } }