1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "extensions/browser/quota_service.h" 6 7 #include "base/message_loop/message_loop.h" 8 #include "base/stl_util.h" 9 #include "extensions/browser/extension_function.h" 10 #include "extensions/common/error_utils.h" 11 12 namespace { 13 14 // If the browser stays open long enough, we reset state once a day. 15 // Whatever this value is, it should be an order of magnitude longer than 16 // the longest interval in any of the QuotaLimitHeuristics in use. 17 const int kPurgeIntervalInDays = 1; 18 19 const char kOverQuotaError[] = "This request exceeds the * quota."; 20 21 } // namespace 22 23 namespace extensions { 24 25 QuotaService::QuotaService() { 26 if (base::MessageLoop::current() != NULL) { // Null in unit tests. 27 purge_timer_.Start(FROM_HERE, 28 base::TimeDelta::FromDays(kPurgeIntervalInDays), 29 this, 30 &QuotaService::Purge); 31 } 32 } 33 34 QuotaService::~QuotaService() { 35 DCHECK(CalledOnValidThread()); 36 purge_timer_.Stop(); 37 Purge(); 38 } 39 40 std::string QuotaService::Assess(const std::string& extension_id, 41 ExtensionFunction* function, 42 const base::ListValue* args, 43 const base::TimeTicks& event_time) { 44 DCHECK(CalledOnValidThread()); 45 46 if (function->ShouldSkipQuotaLimiting()) 47 return std::string(); 48 49 // Lookup function list for extension. 50 FunctionHeuristicsMap& functions = function_heuristics_[extension_id]; 51 52 // Lookup heuristics for function, create if necessary. 53 QuotaLimitHeuristics& heuristics = functions[function->name()]; 54 if (heuristics.empty()) 55 function->GetQuotaLimitHeuristics(&heuristics); 56 57 if (heuristics.empty()) 58 return std::string(); // No heuristic implies no limit. 59 60 ViolationErrorMap::iterator violation_error = 61 violation_errors_.find(extension_id); 62 if (violation_error != violation_errors_.end()) 63 return violation_error->second; // Repeat offender. 64 65 QuotaLimitHeuristic* failed_heuristic = NULL; 66 for (QuotaLimitHeuristics::iterator heuristic = heuristics.begin(); 67 heuristic != heuristics.end(); 68 ++heuristic) { 69 // Apply heuristic to each item (bucket). 70 if (!(*heuristic)->ApplyToArgs(args, event_time)) { 71 failed_heuristic = *heuristic; 72 break; 73 } 74 } 75 76 if (!failed_heuristic) 77 return std::string(); 78 79 std::string error = failed_heuristic->GetError(); 80 DCHECK_GT(error.length(), 0u); 81 82 PurgeFunctionHeuristicsMap(&functions); 83 function_heuristics_.erase(extension_id); 84 violation_errors_[extension_id] = error; 85 return error; 86 } 87 88 void QuotaService::PurgeFunctionHeuristicsMap(FunctionHeuristicsMap* map) { 89 FunctionHeuristicsMap::iterator heuristics = map->begin(); 90 while (heuristics != map->end()) { 91 STLDeleteElements(&heuristics->second); 92 map->erase(heuristics++); 93 } 94 } 95 96 void QuotaService::Purge() { 97 DCHECK(CalledOnValidThread()); 98 std::map<std::string, FunctionHeuristicsMap>::iterator it = 99 function_heuristics_.begin(); 100 for (; it != function_heuristics_.end(); function_heuristics_.erase(it++)) 101 PurgeFunctionHeuristicsMap(&it->second); 102 } 103 104 void QuotaLimitHeuristic::Bucket::Reset(const Config& config, 105 const base::TimeTicks& start) { 106 num_tokens_ = config.refill_token_count; 107 expiration_ = start + config.refill_interval; 108 } 109 110 void QuotaLimitHeuristic::SingletonBucketMapper::GetBucketsForArgs( 111 const base::ListValue* args, 112 BucketList* buckets) { 113 buckets->push_back(&bucket_); 114 } 115 116 QuotaLimitHeuristic::QuotaLimitHeuristic(const Config& config, 117 BucketMapper* map, 118 const std::string& name) 119 : config_(config), bucket_mapper_(map), name_(name) {} 120 121 QuotaLimitHeuristic::~QuotaLimitHeuristic() {} 122 123 bool QuotaLimitHeuristic::ApplyToArgs(const base::ListValue* args, 124 const base::TimeTicks& event_time) { 125 BucketList buckets; 126 bucket_mapper_->GetBucketsForArgs(args, &buckets); 127 for (BucketList::iterator i = buckets.begin(); i != buckets.end(); ++i) { 128 if ((*i)->expiration().is_null()) // A brand new bucket. 129 (*i)->Reset(config_, event_time); 130 if (!Apply(*i, event_time)) 131 return false; // It only takes one to spoil it for everyone. 132 } 133 return true; 134 } 135 136 std::string QuotaLimitHeuristic::GetError() const { 137 return extensions::ErrorUtils::FormatErrorMessage(kOverQuotaError, name_); 138 } 139 140 QuotaService::SustainedLimit::SustainedLimit(const base::TimeDelta& sustain, 141 const Config& config, 142 BucketMapper* map, 143 const std::string& name) 144 : QuotaLimitHeuristic(config, map, name), 145 repeat_exhaustion_allowance_(sustain.InSeconds() / 146 config.refill_interval.InSeconds()), 147 num_available_repeat_exhaustions_(repeat_exhaustion_allowance_) {} 148 149 bool QuotaService::TimedLimit::Apply(Bucket* bucket, 150 const base::TimeTicks& event_time) { 151 if (event_time > bucket->expiration()) 152 bucket->Reset(config(), event_time); 153 154 return bucket->DeductToken(); 155 } 156 157 bool QuotaService::SustainedLimit::Apply(Bucket* bucket, 158 const base::TimeTicks& event_time) { 159 if (event_time > bucket->expiration()) { 160 // We reset state for this item and start over again if this request breaks 161 // the bad cycle that was previously being tracked. This occurs if the 162 // state in the bucket expired recently (it has been long enough since the 163 // event that we don't care about the last event), but the bucket still has 164 // tokens (so pressure was not sustained over that time), OR we are more 165 // than 1 full refill interval away from the last event (so even if we used 166 // up all the tokens in the last bucket, nothing happened in the entire 167 // next refill interval, so it doesn't matter). 168 if (bucket->has_tokens() || 169 event_time > bucket->expiration() + config().refill_interval) { 170 bucket->Reset(config(), event_time); 171 num_available_repeat_exhaustions_ = repeat_exhaustion_allowance_; 172 } else if (--num_available_repeat_exhaustions_ > 0) { 173 // The last interval was saturated with requests, and this is the first 174 // event in the next interval. If this happens 175 // repeat_exhaustion_allowance_ times, it's a violation. Reset the bucket 176 // state to start timing from the end of the last interval (and we'll 177 // deduct the token below) so we can detect this each time it happens. 178 bucket->Reset(config(), bucket->expiration()); 179 } else { 180 // No allowances left; this request is a violation. 181 return false; 182 } 183 } 184 185 // We can go negative since we check has_tokens when we get to *next* bucket, 186 // and for the small interval all that matters is whether we used up all the 187 // tokens (which is true if num_tokens_ <= 0). 188 bucket->DeductToken(); 189 return true; 190 } 191 192 } // namespace extensions 193