| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you 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. |
| |
| |
| #include <google/protobuf/descriptor.h> |
| #include "brpc/log.h" |
| #include "brpc/restful.h" |
| #include "brpc/details/method_status.h" |
| |
| |
| namespace brpc { |
| |
| // Define in http_parser.cpp |
| extern bool is_url_char(char c); |
| |
| inline butil::StringPiece remove_last_char(butil::StringPiece s) { |
| if (!s.empty()) { |
| s.remove_suffix(1); |
| } |
| return s; |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const RestfulMethodPath& p) { |
| if (!p.service_name.empty()) { |
| os << '/' << p.service_name; |
| } |
| if (p.has_wildcard) { |
| os << p.prefix << '*' << remove_last_char(p.postfix); |
| } else { |
| os << remove_last_char(p.prefix); |
| } |
| return os; |
| } |
| |
| std::string RestfulMethodPath::to_string() const { |
| std::string s; |
| s.reserve(service_name.size() + prefix.size() + 2 + postfix.size()); |
| if (!service_name.empty()) { |
| s.push_back('/'); |
| s.append(service_name); |
| } |
| if (has_wildcard) { |
| s.append(prefix); |
| s.push_back('*'); |
| butil::StringPiece tmp = remove_last_char(postfix); |
| s.append(tmp.data(), tmp.size()); |
| } else { |
| butil::StringPiece tmp = remove_last_char(prefix); |
| s.append(tmp.data(), tmp.size()); |
| } |
| return s; |
| } |
| |
| struct DebugPrinter { |
| explicit DebugPrinter(const RestfulMethodPath& p) : path(&p) {} |
| const RestfulMethodPath* path; |
| }; |
| |
| std::ostream& operator<<(std::ostream& os, const DebugPrinter& p) { |
| os << "{service=" << p.path->service_name |
| << " prefix=" << p.path->prefix |
| << " postfix=" << p.path->postfix |
| << " wildcard=" << p.path->has_wildcard |
| << '}'; |
| return os; |
| } |
| |
| bool ParseRestfulPath(butil::StringPiece path, |
| RestfulMethodPath* path_out) { |
| path.trim_spaces(); |
| if (path.empty()) { |
| LOG(ERROR) << "Parameter[path] is empty"; |
| return false; |
| } |
| // Check validity of the path. |
| // TODO(gejun): Probably too strict. |
| int star_index = -1; |
| for (const char* p = path.data(); p != path.data() + path.size(); ++p) { |
| if (*p == '*') { |
| if (star_index < 0) { |
| star_index = (int)(p - path.data()); |
| } else { |
| LOG(ERROR) << "More than one wildcard in restful_path=`" |
| << path << '\''; |
| return false; |
| } |
| } else if (!is_url_char(*p)) { |
| LOG(ERROR) << "Invalid character=`" << *p << "' (index=" |
| << p - path.data() << ") in path=`" << path << '\''; |
| return false; |
| } |
| } |
| path_out->has_wildcard = (star_index >= 0); |
| |
| butil::StringPiece first_part; |
| butil::StringPiece second_part; |
| if (star_index < 0) { |
| first_part = path; |
| } else { |
| first_part = path.substr(0, star_index); |
| second_part = path.substr(star_index + 1); |
| } |
| |
| // Extract service_name and prefix from first_part |
| // The prefix is normalized as: |
| // / - "*B => M" |
| // /A - "/A*B => M" (disabled for performance) |
| // /A/ - "/A/*B => M" |
| path_out->service_name.clear(); |
| path_out->prefix.clear(); |
| { |
| // remove heading slashes. |
| size_t i = 0; |
| for (; i < first_part.size() && first_part[i] == '/'; ++i) {} |
| first_part.remove_prefix(i); |
| } |
| const size_t slash_pos = first_part.find('/'); |
| if (slash_pos != butil::StringPiece::npos) { |
| path_out->service_name.assign(first_part.data(), slash_pos); |
| butil::StringPiece prefix_raw = first_part.substr(slash_pos + 1); |
| butil::StringSplitter sp(prefix_raw.data(), |
| prefix_raw.data() + prefix_raw.size(), '/'); |
| for (; sp; ++sp) { |
| // Put first component into service_name and others into prefix. |
| if (path_out->prefix.empty()) { |
| path_out->prefix.reserve(prefix_raw.size() + 2); |
| } |
| path_out->prefix.push_back('/'); |
| path_out->prefix.append(sp.field(), sp.length()); |
| } |
| if (!path_out->has_wildcard || |
| prefix_raw.empty() || |
| prefix_raw.back() == '/') { |
| path_out->prefix.push_back('/'); |
| } else { |
| LOG(ERROR) << "Pattern A* (A is not ended with /) in path=`" |
| << path << "' is disallowed for performance concerns"; |
| return false; |
| } |
| } else if (!path_out->has_wildcard) { |
| // no slashes, no wildcard. Example: abc => Method |
| path_out->service_name.assign(first_part.data(), first_part.size()); |
| path_out->prefix.push_back('/'); |
| } else { // no slashes, has wildcard. Example: abc* => Method |
| if (!first_part.empty()) { |
| LOG(ERROR) << "Pattern A* (A is not ended with /) in path=`" |
| << path << "' is disallowed for performance concerns"; |
| return false; |
| } |
| path_out->prefix.push_back('/'); |
| path_out->prefix.append(first_part.data(), first_part.size()); |
| } |
| |
| // Normalize second_part as postfix: |
| // / - "A* => M" or "A => M" |
| // B/ - "A*B => M" |
| // /B/ - "A*/B => M" |
| path_out->postfix.clear(); |
| if (path_out->has_wildcard) { |
| if (second_part.empty() || second_part[0] == '/') { |
| path_out->postfix.push_back('/'); |
| } |
| butil::StringSplitter sp2(second_part.data(), |
| second_part.data() + second_part.size(), '/'); |
| for (; sp2; ++sp2) { |
| if (path_out->postfix.empty()) { |
| path_out->postfix.reserve(second_part.size() + 2); |
| } |
| path_out->postfix.append(sp2.field(), sp2.length()); |
| path_out->postfix.push_back('/'); |
| } |
| } else { |
| path_out->postfix.push_back('/'); |
| } |
| VLOG(RPC_VLOG_LEVEL + 1) << "orig_path=" << path |
| << " first_part=" << first_part |
| << " second_part=" << second_part |
| << " path=" << DebugPrinter(*path_out); |
| return true; |
| } |
| |
| bool ParseRestfulMappings(const butil::StringPiece& mappings, |
| std::vector<RestfulMapping>* list) { |
| if (list == NULL) { |
| LOG(ERROR) << "Param[list] is NULL"; |
| return false; |
| } |
| list->clear(); |
| list->reserve(8); |
| butil::StringSplitter sp( |
| mappings.data(), mappings.data() + mappings.size(), ','); |
| int nmappings = 0; |
| for (; sp; ++sp) { |
| ++nmappings; |
| size_t i = 0; |
| const char* p = sp.field(); |
| const size_t n = sp.length(); |
| bool added_sth = false; |
| for (; i < n; ++i) { |
| // find = |
| if (p[i] != '=') { |
| continue; |
| } |
| const size_t equal_sign_pos = i; |
| for (; i < n && p[i] == '='; ++i) {} // skip repeated = |
| // If the = ends with >, it's the arrow that we're finding. |
| // otherwise just skip and keep searching. |
| if (i < n && p[i] == '>') { |
| RestfulMapping m; |
| // Parse left part of the arrow as url path. |
| butil::StringPiece path(sp.field(), equal_sign_pos); |
| if (!ParseRestfulPath(path, &m.path)) { |
| LOG(ERROR) << "Fail to parse path=`" << path << '\''; |
| return false; |
| } |
| // Treat right part of the arrow as method_name. |
| butil::StringPiece method_name_piece(p + i + 1, n - (i + 1)); |
| method_name_piece.trim_spaces(); |
| if (method_name_piece.empty()) { |
| LOG(ERROR) << "No method name in " << nmappings |
| << "-th mapping"; |
| return false; |
| } |
| m.method_name.assign(method_name_piece.data(), |
| method_name_piece.size()); |
| list->push_back(m); |
| added_sth = true; |
| break; |
| } |
| } |
| // If we don't get a valid mapping from the string, issue error. |
| if (!added_sth) { |
| LOG(ERROR) << "Invalid mapping: " |
| << butil::StringPiece(sp.field(), sp.length()); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| RestfulMap::~RestfulMap() { |
| ClearMethods(); |
| } |
| |
| // This function inserts a mapping into _dedup_map. |
| bool RestfulMap::AddMethod(const RestfulMethodPath& path, |
| google::protobuf::Service* service, |
| const Server::MethodProperty::OpaqueParams& params, |
| const std::string& method_name, |
| MethodStatus* status) { |
| if (service == NULL) { |
| LOG(ERROR) << "Param[service] is NULL"; |
| return false; |
| } |
| const google::protobuf::MethodDescriptor* md = |
| service->GetDescriptor()->FindMethodByName(method_name); |
| if (md == NULL) { |
| LOG(ERROR) << service->GetDescriptor()->full_name() |
| << " has no method called `" << method_name << '\''; |
| return false; |
| } |
| if (path.service_name != _service_name) { |
| LOG(ERROR) << "Impossible: path.service_name does not match name" |
| " of this RestfulMap"; |
| return false; |
| } |
| // Use the string-form of path as key is a MUST to implement |
| // RemoveByPathString which is used in Server.RemoveMethodsOf |
| std::string dedup_key = path.to_string(); |
| DedupMap::const_iterator it = _dedup_map.find(dedup_key); |
| if (it != _dedup_map.end()) { |
| LOG(ERROR) << "Already mapped `" << it->second.path |
| << "' to `" << it->second.method->full_name() << '\''; |
| return false; |
| } |
| RestfulMethodProperty& info = _dedup_map[dedup_key]; |
| info.is_builtin_service = false; |
| info.own_method_status = false; |
| info.params = params; |
| info.service = service; |
| info.method = md; |
| info.status = status; |
| info.path = path; |
| info.ownership = SERVER_DOESNT_OWN_SERVICE; |
| RPC_VLOG << "Mapped `" << path << "' to `" << md->full_name() << '\''; |
| return true; |
| } |
| |
| void RestfulMap::ClearMethods() { |
| _sorted_paths.clear(); |
| for (DedupMap::iterator it = _dedup_map.begin(); |
| it != _dedup_map.end(); ++it) { |
| if (it->second.own_method_status) { |
| delete it->second.status; |
| } |
| } |
| _dedup_map.clear(); |
| } |
| |
| struct CompareItemInPathList { |
| bool operator()(const RestfulMethodProperty* e1, |
| const RestfulMethodProperty* e2) const { |
| const int rc1 = e1->path.prefix.compare(e2->path.prefix); |
| if (rc1 != 0) { |
| return rc1 < 0; |
| } |
| // /A/*/B is put before /A/B so that we try exact patterns first |
| // (the matching is in reversed order) |
| if (e1->path.has_wildcard != e2->path.has_wildcard) { |
| return e1->path.has_wildcard > e2->path.has_wildcard; |
| } |
| // Compare postfix from back to front. |
| // TODO: Optimize this. |
| std::string::const_reverse_iterator it1 = e1->path.postfix.rbegin(); |
| std::string::const_reverse_iterator it2 = e2->path.postfix.rbegin(); |
| while (it1 != e1->path.postfix.rend() && |
| it2 != e2->path.postfix.rend()) { |
| if (*it1 != *it2) { |
| return (*it1 < *it2); |
| } |
| ++it1; |
| ++it2; |
| } |
| return (it1 == e1->path.postfix.rend()) |
| > (it2 == e2->path.postfix.rend()); |
| } |
| }; |
| |
| void RestfulMap::PrepareForFinding() { |
| _sorted_paths.clear(); |
| _sorted_paths.reserve(_dedup_map.size()); |
| for (DedupMap::iterator it = _dedup_map.begin(); it != _dedup_map.end(); |
| ++it) { |
| _sorted_paths.push_back(&it->second); |
| } |
| std::sort(_sorted_paths.begin(), _sorted_paths.end(), |
| CompareItemInPathList()); |
| if (VLOG_IS_ON(RPC_VLOG_LEVEL + 1)) { |
| std::ostringstream os; |
| os << "_sorted_paths(" << _service_name << "):"; |
| for (PathList::const_iterator it = _sorted_paths.begin(); |
| it != _sorted_paths.end(); ++it) { |
| os << ' ' << (*it)->path; |
| } |
| VLOG(RPC_VLOG_LEVEL + 1) << os.str(); |
| } |
| } |
| |
| // Remove last component from the (normalized) path: |
| // Say /A/B/C/ -> /A/B/ |
| // Notice that /A/ is modified to / and returns true. |
| static bool RemoveLastComponent(butil::StringPiece* path) { |
| if (path->empty()) { |
| return false; |
| } |
| if (path->back() == '/') { |
| path->remove_suffix(1); |
| } |
| size_t slash_pos = path->rfind('/'); |
| if (slash_pos == std::string::npos) { |
| return false; |
| } |
| path->remove_suffix(path->size() - slash_pos - 1); // keep the slash |
| return true; |
| } |
| |
| // Normalized as /A/B/C/ |
| static std::string NormalizeSlashes(const butil::StringPiece& path) { |
| std::string out_path; |
| out_path.reserve(path.size() + 2); |
| butil::StringSplitter sp(path.data(), path.data() + path.size(), '/'); |
| for (; sp; ++sp) { |
| out_path.push_back('/'); |
| out_path.append(sp.field(), sp.length()); |
| } |
| out_path.push_back('/'); |
| return out_path; |
| } |
| |
| size_t RestfulMap::RemoveByPathString(const std::string& path) { |
| // removal only happens when server stops, clear _sorted_paths to make |
| // sure wild pointers do not exist. |
| if (!_sorted_paths.empty()) { |
| _sorted_paths.clear(); |
| } |
| return _dedup_map.erase(path); |
| } |
| |
| struct PrefixLess { |
| bool operator()(const butil::StringPiece& path, |
| const RestfulMethodProperty* p) const { |
| return path < p->path.prefix; |
| } |
| }; |
| |
| const Server::MethodProperty* |
| RestfulMap::FindMethodProperty(const butil::StringPiece& method_path, |
| std::string* unresolved_path) const { |
| if (_sorted_paths.empty()) { |
| LOG(ERROR) << "_sorted_paths is empty, method_path=" << method_path; |
| return NULL; |
| } |
| const std::string full_path = NormalizeSlashes(method_path); |
| butil::StringPiece sub_path = full_path; |
| PathList::const_iterator last_find_pos = _sorted_paths.end(); |
| do { |
| if (last_find_pos == _sorted_paths.begin()) { |
| return NULL; |
| } |
| // Note: stop trying places that we already visited or skipped. |
| PathList::const_iterator it = |
| std::upper_bound(_sorted_paths.begin(), last_find_pos/*note*/, |
| sub_path, PrefixLess()); |
| if (it != _sorted_paths.begin()) { |
| --it; |
| } |
| |
| bool matched = false; |
| bool remove_heading_slash_from_unresolved = false; |
| butil::StringPiece left; |
| do { |
| const RestfulMethodPath& rpath = (*it)->path; |
| if (!sub_path.starts_with(rpath.prefix)) { |
| VLOG(RPC_VLOG_LEVEL + 1) |
| << "sub_path=" << sub_path << " does not match prefix=" |
| << rpath.prefix << " full_path=" << full_path |
| << " candidate=" << DebugPrinter(rpath); |
| // NOTE: We can stop trying patterns before *it because pattern |
| // "/A*B => M" is disabled which makes prefixes of all restful |
| // paths end with /. If `full_path' matches with a prefix, the |
| // prefix must be a sub path of the full_path, which makes |
| // prefix matching runs at most #components-of-path times. |
| // Otherwise we have to match all "/A*B" patterns before *it, |
| // which is more complicated but rarely needed by users. |
| break; |
| } |
| left = full_path; |
| // Remove matched prefix from `left'. |
| if (!rpath.prefix.empty()) { |
| // make sure `left' is still starting with / |
| size_t removal = rpath.prefix.size(); |
| if (rpath.prefix[removal - 1] == '/') { |
| --removal; |
| remove_heading_slash_from_unresolved = true; |
| } |
| left.remove_prefix(removal); |
| } |
| // Match postfix. |
| if (left.ends_with(rpath.postfix)) { |
| left.remove_suffix(rpath.postfix.size()); |
| if (!left.empty() && !rpath.has_wildcard) { |
| VLOG(RPC_VLOG_LEVEL + 1) |
| << "Unmatched extra=" << left |
| << " sub_path=" << sub_path |
| << " full_path=" << full_path |
| << " candidate=" << DebugPrinter(rpath); |
| } else { |
| matched = true; |
| VLOG(RPC_VLOG_LEVEL + 1) |
| << "Matched sub_path=" << sub_path |
| << " full_path=" << full_path |
| << " with restful_path=" << DebugPrinter(rpath); |
| break; |
| } |
| } |
| if (it == _sorted_paths.begin()) { |
| VLOG(RPC_VLOG_LEVEL + 1) |
| << "Hit beginning, sub_path=" << sub_path |
| << " full_path=" << full_path |
| << " candidate=" << DebugPrinter(rpath); |
| return NULL; |
| } |
| // Matched with prefix but postfix or wildcard, moving forward |
| --it; |
| } while (true); |
| last_find_pos = it; |
| |
| if (!matched) { |
| continue; |
| } |
| if (unresolved_path) { |
| if (!left.empty()) { |
| if (remove_heading_slash_from_unresolved && left[0] == '/') { |
| unresolved_path->assign(left.data() + 1, left.size() - 1); |
| } else { |
| unresolved_path->assign(left.data(), left.size()); |
| } |
| } else { |
| unresolved_path->clear(); |
| } |
| } |
| return *it; |
| } while (RemoveLastComponent(&sub_path)); |
| // ^^^^^^^^ |
| // sub_path can be / to match patterns like "*.flv => M" whose prefix is / |
| return NULL; |
| } |
| |
| } // namespace brpc |