]>
Commit | Line | Data |
---|---|---|
92f5a8d4 TL |
1 | // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- |
2 | // vim: ts=8 sw=2 smarttab | |
3 | /* | |
4 | * Ceph - scalable distributed file system | |
5 | * | |
6 | * Copyright (C) 2013 Inktank | |
7 | * | |
8 | * This is free software; you can redistribute it and/or | |
9 | * modify it under the terms of the GNU Lesser General Public | |
10 | * License version 2.1, as published by the Free Software | |
11 | * Foundation. See file COPYING. | |
12 | * | |
13 | */ | |
14 | ||
15 | #include <boost/config/warning_disable.hpp> | |
16 | #include <boost/spirit/include/qi_uint.hpp> | |
17 | #include <boost/spirit/include/qi.hpp> | |
18 | #include <boost/fusion/include/std_pair.hpp> | |
19 | #include <boost/spirit/include/phoenix.hpp> | |
20 | #include <boost/fusion/adapted/struct/adapt_struct.hpp> | |
21 | #include <boost/fusion/include/adapt_struct.hpp> | |
22 | #include <boost/algorithm/string/predicate.hpp> | |
23 | ||
24 | #include "MgrCap.h" | |
25 | #include "include/stringify.h" | |
26 | #include "include/ipaddr.h" | |
27 | #include "common/debug.h" | |
28 | #include "common/Formatter.h" | |
29 | ||
30 | #include <algorithm> | |
31 | #include <regex> | |
32 | ||
33 | #include "include/ceph_assert.h" | |
34 | ||
35 | static inline bool is_not_alnum_space(char c) { | |
36 | return !(isalpha(c) || isdigit(c) || (c == '-') || (c == '_')); | |
37 | } | |
38 | ||
39 | static std::string maybe_quote_string(const std::string& str) { | |
40 | if (find_if(str.begin(), str.end(), is_not_alnum_space) == str.end()) | |
41 | return str; | |
42 | return std::string("\"") + str + std::string("\""); | |
43 | } | |
44 | ||
45 | #define dout_subsys ceph_subsys_mgr | |
46 | ||
f67539c2 | 47 | std::ostream& operator<<(std::ostream& out, const mgr_rwxa_t& p) { |
92f5a8d4 TL |
48 | if (p == MGR_CAP_ANY) |
49 | return out << "*"; | |
50 | ||
51 | if (p & MGR_CAP_R) | |
52 | out << "r"; | |
53 | if (p & MGR_CAP_W) | |
54 | out << "w"; | |
55 | if (p & MGR_CAP_X) | |
56 | out << "x"; | |
57 | return out; | |
58 | } | |
59 | ||
f67539c2 | 60 | std::ostream& operator<<(std::ostream& out, const MgrCapGrantConstraint& c) { |
92f5a8d4 TL |
61 | switch (c.match_type) { |
62 | case MgrCapGrantConstraint::MATCH_TYPE_EQUAL: | |
63 | out << "="; | |
64 | break; | |
65 | case MgrCapGrantConstraint::MATCH_TYPE_PREFIX: | |
66 | out << " prefix "; | |
67 | break; | |
68 | case MgrCapGrantConstraint::MATCH_TYPE_REGEX: | |
69 | out << " regex "; | |
70 | break; | |
71 | default: | |
72 | break; | |
73 | } | |
74 | out << maybe_quote_string(c.value); | |
75 | return out; | |
76 | } | |
77 | ||
f67539c2 | 78 | std::ostream& operator<<(std::ostream& out, const MgrCapGrant& m) { |
92f5a8d4 TL |
79 | if (!m.profile.empty()) { |
80 | out << "profile " << maybe_quote_string(m.profile); | |
81 | } else { | |
82 | out << "allow"; | |
83 | if (!m.service.empty()) { | |
84 | out << " service " << maybe_quote_string(m.service); | |
85 | } else if (!m.module.empty()) { | |
86 | out << " module " << maybe_quote_string(m.module); | |
87 | } else if (!m.command.empty()) { | |
88 | out << " command " << maybe_quote_string(m.command); | |
89 | } | |
90 | } | |
91 | ||
92 | if (!m.arguments.empty()) { | |
93 | out << (!m.profile.empty() ? "" : " with"); | |
94 | for (auto& [key, constraint] : m.arguments) { | |
95 | out << " " << maybe_quote_string(key) << constraint; | |
96 | } | |
97 | } | |
98 | ||
99 | if (m.allow != 0) { | |
100 | out << " " << m.allow; | |
101 | } | |
102 | ||
103 | if (m.network.size()) { | |
104 | out << " network " << m.network; | |
105 | } | |
106 | return out; | |
107 | } | |
108 | ||
109 | // <magic> | |
110 | // fusion lets us easily populate structs via the qi parser. | |
111 | ||
112 | typedef std::map<std::string, MgrCapGrantConstraint> kvmap; | |
113 | ||
114 | BOOST_FUSION_ADAPT_STRUCT(MgrCapGrant, | |
115 | (std::string, service) | |
116 | (std::string, module) | |
117 | (std::string, profile) | |
118 | (std::string, command) | |
119 | (kvmap, arguments) | |
120 | (mgr_rwxa_t, allow) | |
121 | (std::string, network)) | |
122 | ||
123 | BOOST_FUSION_ADAPT_STRUCT(MgrCapGrantConstraint, | |
124 | (MgrCapGrantConstraint::MatchType, match_type) | |
125 | (std::string, value)) | |
126 | ||
127 | // </magic> | |
128 | ||
129 | void MgrCapGrant::parse_network() { | |
130 | network_valid = ::parse_network(network.c_str(), &network_parsed, | |
131 | &network_prefix); | |
132 | } | |
133 | ||
134 | void MgrCapGrant::expand_profile(std::ostream *err) const { | |
135 | // only generate this list once | |
136 | if (!profile_grants.empty()) { | |
137 | return; | |
138 | } | |
139 | ||
140 | if (profile == "read-only") { | |
141 | // grants READ-ONLY caps MGR-wide | |
142 | profile_grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_R}}); | |
143 | return; | |
144 | } | |
145 | ||
146 | if (profile == "read-write") { | |
147 | // grants READ-WRITE caps MGR-wide | |
148 | profile_grants.push_back({{}, {}, {}, {}, {}, | |
149 | mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W}}); | |
150 | return; | |
151 | } | |
152 | ||
153 | if (profile == "crash") { | |
154 | profile_grants.push_back({{}, {}, {}, "crash post", {}, {}}); | |
155 | return; | |
156 | } | |
157 | ||
158 | if (profile == "osd") { | |
159 | // this is a documented profile (so we need to accept it as valid), but it | |
160 | // currently doesn't do anything | |
161 | return; | |
162 | } | |
163 | ||
164 | if (profile == "mds") { | |
165 | // this is a documented profile (so we need to accept it as valid), but it | |
166 | // currently doesn't do anything | |
167 | return; | |
168 | } | |
169 | ||
170 | if (profile == "rbd" || profile == "rbd-read-only") { | |
171 | Arguments filtered_arguments; | |
172 | for (auto& [key, constraint] : arguments) { | |
173 | if (key == "pool" || key == "namespace") { | |
174 | filtered_arguments[key] = std::move(constraint); | |
175 | } else { | |
176 | if (err != nullptr) { | |
177 | *err << "profile '" << profile << "' does not recognize key '" << key | |
178 | << "'"; | |
179 | } | |
180 | return; | |
181 | } | |
182 | } | |
183 | ||
184 | mgr_rwxa_t perms = mgr_rwxa_t{MGR_CAP_R}; | |
185 | if (profile == "rbd") { | |
186 | perms = mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W}; | |
187 | } | |
188 | ||
f67539c2 | 189 | // allow all 'rbd_support' commands (restricted by optional |
92f5a8d4 TL |
190 | // pool/namespace constraints) |
191 | profile_grants.push_back({{}, "rbd_support", {}, {}, | |
192 | std::move(filtered_arguments), perms}); | |
193 | return; | |
194 | } | |
195 | ||
196 | if (err != nullptr) { | |
197 | *err << "unrecognized profile '" << profile << "'"; | |
198 | } | |
199 | } | |
200 | ||
201 | bool MgrCapGrant::validate_arguments( | |
202 | const std::map<std::string, std::string>& args) const { | |
203 | for (auto& [key, constraint] : arguments) { | |
204 | auto q = args.find(key); | |
205 | ||
206 | // argument must be present if a constraint exists | |
207 | if (q == args.end()) { | |
208 | return false; | |
209 | } | |
210 | ||
211 | switch (constraint.match_type) { | |
212 | case MgrCapGrantConstraint::MATCH_TYPE_EQUAL: | |
213 | if (constraint.value != q->second) | |
214 | return false; | |
215 | break; | |
216 | case MgrCapGrantConstraint::MATCH_TYPE_PREFIX: | |
217 | if (q->second.find(constraint.value) != 0) | |
218 | return false; | |
219 | break; | |
220 | case MgrCapGrantConstraint::MATCH_TYPE_REGEX: | |
221 | try { | |
222 | std::regex pattern(constraint.value, std::regex::extended); | |
223 | if (!std::regex_match(q->second, pattern)) { | |
224 | return false; | |
225 | } | |
226 | } catch(const std::regex_error&) { | |
227 | return false; | |
228 | } | |
229 | break; | |
230 | default: | |
231 | return false; | |
232 | } | |
233 | } | |
234 | ||
235 | return true; | |
236 | } | |
237 | ||
238 | mgr_rwxa_t MgrCapGrant::get_allowed( | |
239 | CephContext *cct, EntityName name, const std::string& s, | |
240 | const std::string& m, const std::string& c, | |
241 | const std::map<std::string, std::string>& args) const { | |
242 | if (!profile.empty()) { | |
243 | expand_profile(nullptr); | |
244 | mgr_rwxa_t a; | |
245 | for (auto& grant : profile_grants) { | |
246 | a = a | grant.get_allowed(cct, name, s, m, c, args); | |
247 | } | |
248 | return a; | |
249 | } | |
250 | ||
251 | if (!service.empty()) { | |
252 | if (service != s) { | |
253 | return mgr_rwxa_t{}; | |
254 | } | |
255 | return allow; | |
256 | } | |
257 | ||
258 | if (!module.empty()) { | |
259 | if (module != m) { | |
260 | return mgr_rwxa_t{}; | |
261 | } | |
262 | ||
263 | // don't test module arguments when validating a specific command | |
264 | if (c.empty() && !validate_arguments(args)) { | |
265 | return mgr_rwxa_t{}; | |
266 | } | |
267 | return allow; | |
268 | } | |
269 | ||
270 | if (!command.empty()) { | |
271 | if (command != c) { | |
272 | return mgr_rwxa_t{}; | |
273 | } | |
274 | if (!validate_arguments(args)) { | |
275 | return mgr_rwxa_t{}; | |
276 | } | |
277 | return mgr_rwxa_t{MGR_CAP_ANY}; | |
278 | } | |
279 | ||
280 | return allow; | |
281 | } | |
282 | ||
f67539c2 | 283 | std::ostream& operator<<(std::ostream&out, const MgrCap& m) { |
92f5a8d4 TL |
284 | bool first = true; |
285 | for (auto& grant : m.grants) { | |
286 | if (!first) { | |
287 | out << ", "; | |
288 | } | |
289 | first = false; | |
290 | ||
291 | out << grant; | |
292 | } | |
293 | return out; | |
294 | } | |
295 | ||
296 | bool MgrCap::is_allow_all() const { | |
297 | for (auto& grant : grants) { | |
298 | if (grant.is_allow_all()) { | |
299 | return true; | |
300 | } | |
301 | } | |
302 | return false; | |
303 | } | |
304 | ||
305 | void MgrCap::set_allow_all() { | |
306 | grants.clear(); | |
307 | grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_ANY}}); | |
308 | text = "allow *"; | |
309 | } | |
310 | ||
311 | bool MgrCap::is_capable( | |
312 | CephContext *cct, | |
313 | EntityName name, | |
314 | const std::string& service, | |
315 | const std::string& module, | |
316 | const std::string& command, | |
317 | const std::map<std::string, std::string>& command_args, | |
318 | bool op_may_read, bool op_may_write, bool op_may_exec, | |
319 | const entity_addr_t& addr) const { | |
320 | if (cct) { | |
321 | ldout(cct, 20) << "is_capable service=" << service << " " | |
322 | << "module=" << module << " " | |
323 | << "command=" << command | |
324 | << (op_may_read ? " read":"") | |
325 | << (op_may_write ? " write":"") | |
326 | << (op_may_exec ? " exec":"") | |
327 | << " addr " << addr | |
328 | << " on cap " << *this | |
329 | << dendl; | |
330 | } | |
331 | ||
332 | mgr_rwxa_t allow; | |
333 | for (auto& grant : grants) { | |
334 | if (cct) | |
335 | ldout(cct, 20) << " allow so far " << allow << ", doing grant " << grant | |
336 | << dendl; | |
337 | ||
338 | if (grant.network.size() && | |
339 | (!grant.network_valid || | |
340 | !network_contains(grant.network_parsed, | |
341 | grant.network_prefix, | |
342 | addr))) { | |
343 | continue; | |
344 | } | |
345 | ||
346 | if (grant.is_allow_all()) { | |
347 | if (cct) { | |
348 | ldout(cct, 20) << " allow all" << dendl; | |
349 | } | |
350 | return true; | |
351 | } | |
352 | ||
353 | // check enumerated caps | |
354 | allow = allow | grant.get_allowed(cct, name, service, module, command, | |
355 | command_args); | |
356 | if ((!op_may_read || (allow & MGR_CAP_R)) && | |
357 | (!op_may_write || (allow & MGR_CAP_W)) && | |
358 | (!op_may_exec || (allow & MGR_CAP_X))) { | |
359 | if (cct) { | |
360 | ldout(cct, 20) << " match" << dendl; | |
361 | } | |
362 | return true; | |
363 | } | |
364 | } | |
365 | return false; | |
366 | } | |
367 | ||
f67539c2 | 368 | void MgrCap::encode(ceph::buffer::list& bl) const { |
92f5a8d4 TL |
369 | // remain backwards compatible w/ MgrCap |
370 | ENCODE_START(4, 4, bl); | |
371 | encode(text, bl); | |
372 | ENCODE_FINISH(bl); | |
373 | } | |
374 | ||
f67539c2 | 375 | void MgrCap::decode(ceph::buffer::list::const_iterator& bl) { |
92f5a8d4 TL |
376 | // remain backwards compatible w/ MgrCap |
377 | std::string s; | |
378 | DECODE_START(4, bl); | |
379 | decode(s, bl); | |
380 | DECODE_FINISH(bl); | |
381 | parse(s, NULL); | |
382 | } | |
383 | ||
f67539c2 | 384 | void MgrCap::dump(ceph::Formatter *f) const { |
92f5a8d4 TL |
385 | f->dump_string("text", text); |
386 | } | |
387 | ||
f67539c2 | 388 | void MgrCap::generate_test_instances(std::list<MgrCap*>& ls) { |
92f5a8d4 TL |
389 | ls.push_back(new MgrCap); |
390 | ls.push_back(new MgrCap); | |
391 | ls.back()->parse("allow *"); | |
392 | ls.push_back(new MgrCap); | |
393 | ls.back()->parse("allow rwx"); | |
394 | ls.push_back(new MgrCap); | |
395 | ls.back()->parse("allow service foo x"); | |
396 | ls.push_back(new MgrCap); | |
397 | ls.back()->parse("allow command bar x"); | |
398 | ls.push_back(new MgrCap); | |
399 | ls.back()->parse("allow service foo r, allow command bar x"); | |
400 | ls.push_back(new MgrCap); | |
401 | ls.back()->parse("allow command bar with k1=v1 x"); | |
402 | ls.push_back(new MgrCap); | |
403 | ls.back()->parse("allow command bar with k1=v1 k2=v2 x"); | |
404 | ls.push_back(new MgrCap); | |
405 | ls.back()->parse("allow module bar with k1=v1 k2=v2 x"); | |
406 | ls.push_back(new MgrCap); | |
407 | ls.back()->parse("profile rbd pool=rbd"); | |
408 | } | |
409 | ||
410 | // grammar | |
411 | namespace qi = boost::spirit::qi; | |
412 | namespace ascii = boost::spirit::ascii; | |
413 | namespace phoenix = boost::phoenix; | |
414 | ||
415 | template <typename Iterator> | |
416 | struct MgrCapParser : qi::grammar<Iterator, MgrCap()> { | |
417 | MgrCapParser() : MgrCapParser::base_type(mgrcap) { | |
418 | using qi::char_; | |
419 | using qi::int_; | |
420 | using qi::ulong_long; | |
421 | using qi::lexeme; | |
422 | using qi::alnum; | |
423 | using qi::_val; | |
424 | using qi::_1; | |
425 | using qi::_2; | |
426 | using qi::_3; | |
427 | using qi::eps; | |
428 | using qi::lit; | |
429 | ||
430 | quoted_string %= | |
431 | lexeme['"' >> +(char_ - '"') >> '"'] | | |
432 | lexeme['\'' >> +(char_ - '\'') >> '\'']; | |
433 | unquoted_word %= +char_("a-zA-Z0-9_./-"); | |
434 | str %= quoted_string | unquoted_word; | |
435 | network_str %= +char_("/.:a-fA-F0-9]["); | |
436 | ||
437 | spaces = +(lit(' ') | lit('\n') | lit('\t')); | |
438 | ||
439 | // key <=|prefix|regex> value[ ...] | |
440 | str_match = -spaces >> lit('=') >> -spaces >> | |
441 | qi::attr(MgrCapGrantConstraint::MATCH_TYPE_EQUAL) >> str; | |
442 | str_prefix = spaces >> lit("prefix") >> spaces >> | |
443 | qi::attr(MgrCapGrantConstraint::MATCH_TYPE_PREFIX) >> str; | |
444 | str_regex = spaces >> lit("regex") >> spaces >> | |
445 | qi::attr(MgrCapGrantConstraint::MATCH_TYPE_REGEX) >> str; | |
446 | kv_pair = str >> (str_match | str_prefix | str_regex); | |
447 | kv_map %= kv_pair >> *(spaces >> kv_pair); | |
448 | ||
449 | // command := command[=]cmd [k1=v1 k2=v2 ...] | |
450 | command_match = -spaces >> lit("allow") >> spaces >> lit("command") >> (lit('=') | spaces) | |
451 | >> qi::attr(std::string()) | |
452 | >> qi::attr(std::string()) | |
453 | >> qi::attr(std::string()) | |
454 | >> str | |
455 | >> -(spaces >> lit("with") >> spaces >> kv_map) | |
456 | >> qi::attr(0) | |
457 | >> -(spaces >> lit("network") >> spaces >> network_str); | |
458 | ||
459 | // service foo rwxa | |
460 | service_match %= -spaces >> lit("allow") >> spaces >> lit("service") >> (lit('=') | spaces) | |
461 | >> str | |
462 | >> qi::attr(std::string()) | |
463 | >> qi::attr(std::string()) | |
464 | >> qi::attr(std::string()) | |
f67539c2 | 465 | >> qi::attr(std::map<std::string, MgrCapGrantConstraint>()) |
92f5a8d4 TL |
466 | >> spaces >> rwxa |
467 | >> -(spaces >> lit("network") >> spaces >> network_str); | |
468 | ||
469 | // module foo rwxa | |
470 | module_match %= -spaces >> lit("allow") >> spaces >> lit("module") >> (lit('=') | spaces) | |
471 | >> qi::attr(std::string()) | |
472 | >> str | |
473 | >> qi::attr(std::string()) | |
474 | >> qi::attr(std::string()) | |
475 | >> -(spaces >> lit("with") >> spaces >> kv_map) | |
476 | >> spaces >> rwxa | |
477 | >> -(spaces >> lit("network") >> spaces >> network_str); | |
478 | ||
479 | // profile foo | |
480 | profile_match %= -spaces >> -(lit("allow") >> spaces) | |
481 | >> lit("profile") >> (lit('=') | spaces) | |
482 | >> qi::attr(std::string()) | |
483 | >> qi::attr(std::string()) | |
484 | >> str | |
485 | >> qi::attr(std::string()) | |
486 | >> -(spaces >> kv_map) | |
487 | >> qi::attr(0) | |
488 | >> -(spaces >> lit("network") >> spaces >> network_str); | |
489 | ||
490 | // rwxa | |
491 | rwxa_match %= -spaces >> lit("allow") >> spaces | |
492 | >> qi::attr(std::string()) | |
493 | >> qi::attr(std::string()) | |
494 | >> qi::attr(std::string()) | |
495 | >> qi::attr(std::string()) | |
496 | >> qi::attr(std::map<std::string,MgrCapGrantConstraint>()) | |
497 | >> rwxa | |
498 | >> -(spaces >> lit("network") >> spaces >> network_str); | |
499 | ||
500 | // rwxa := * | [r][w][x] | |
501 | rwxa = | |
502 | (lit("*")[_val = MGR_CAP_ANY]) | | |
503 | (lit("all")[_val = MGR_CAP_ANY]) | | |
504 | ( eps[_val = 0] >> | |
505 | ( lit('r')[_val |= MGR_CAP_R] || | |
506 | lit('w')[_val |= MGR_CAP_W] || | |
507 | lit('x')[_val |= MGR_CAP_X] | |
508 | ) | |
509 | ); | |
510 | ||
511 | // grant := allow ... | |
512 | grant = -spaces >> (rwxa_match | profile_match | service_match | | |
513 | module_match | command_match) >> -spaces; | |
514 | ||
515 | // mgrcap := grant [grant ...] | |
516 | grants %= (grant % (*lit(' ') >> (lit(';') | lit(',')) >> *lit(' '))); | |
517 | mgrcap = grants [_val = phoenix::construct<MgrCap>(_1)]; | |
518 | } | |
519 | ||
520 | qi::rule<Iterator> spaces; | |
521 | qi::rule<Iterator, unsigned()> rwxa; | |
522 | qi::rule<Iterator, std::string()> quoted_string; | |
523 | qi::rule<Iterator, std::string()> unquoted_word; | |
524 | qi::rule<Iterator, std::string()> str, network_str; | |
525 | ||
526 | qi::rule<Iterator, MgrCapGrantConstraint()> str_match, str_prefix, str_regex; | |
527 | qi::rule<Iterator, std::pair<std::string, MgrCapGrantConstraint>()> kv_pair; | |
528 | qi::rule<Iterator, std::map<std::string, MgrCapGrantConstraint>()> kv_map; | |
529 | ||
530 | qi::rule<Iterator, MgrCapGrant()> rwxa_match; | |
531 | qi::rule<Iterator, MgrCapGrant()> command_match; | |
532 | qi::rule<Iterator, MgrCapGrant()> service_match; | |
533 | qi::rule<Iterator, MgrCapGrant()> module_match; | |
534 | qi::rule<Iterator, MgrCapGrant()> profile_match; | |
535 | qi::rule<Iterator, MgrCapGrant()> grant; | |
536 | qi::rule<Iterator, std::vector<MgrCapGrant>()> grants; | |
537 | qi::rule<Iterator, MgrCap()> mgrcap; | |
538 | }; | |
539 | ||
f67539c2 | 540 | bool MgrCap::parse(const std::string& str, std::ostream *err) { |
92f5a8d4 TL |
541 | auto iter = str.begin(); |
542 | auto end = str.end(); | |
543 | ||
544 | MgrCapParser<std::string::const_iterator> exp; | |
545 | bool r = qi::parse(iter, end, exp, *this); | |
546 | if (r && iter == end) { | |
547 | text = str; | |
548 | ||
549 | std::stringstream profile_err; | |
550 | for (auto& g : grants) { | |
551 | g.parse_network(); | |
552 | ||
553 | if (!g.profile.empty()) { | |
554 | g.expand_profile(&profile_err); | |
555 | } | |
556 | } | |
557 | ||
558 | if (!profile_err.str().empty()) { | |
559 | if (err != nullptr) { | |
560 | *err << "mgr capability parse failed during profile evaluation: " | |
561 | << profile_err.str(); | |
562 | } | |
563 | return false; | |
564 | } | |
565 | return true; | |
566 | } | |
567 | ||
568 | // Make sure no grants are kept after parsing failed! | |
569 | grants.clear(); | |
570 | ||
571 | if (err) { | |
572 | if (iter != end) | |
573 | *err << "mgr capability parse failed, stopped at '" | |
574 | << std::string(iter, end) << "' of '" << str << "'"; | |
575 | else | |
576 | *err << "mgr capability parse failed, stopped at end of '" << str << "'"; | |
577 | } | |
578 | ||
579 | return false; | |
580 | } |