...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This example shows how to classify URLs according to a set of rules. It is inspired by Finicky application.
The URLs are classified and redirected to a browser according to their category.
See the example config.json
file.
/* This example shows how to classify URLs according to a set of rules. This example is inspired by Finicky. The URLs are classified and redirected to a browser according to their category. See the example config.json file. https://github.com/johnste/finicky */ #include <boost/url/url.hpp> #include <boost/url/parse.hpp> #include <boost/json/stream_parser.hpp> #include <boost/regex.hpp> #include <iostream> #include <fstream> namespace urls = boost::urls; namespace json = boost::json; json::value read_json( std::istream& is, json::error_code& ec ) { json::parse_options opt; opt.allow_comments = true; json::stream_parser p(json::storage_ptr(), opt); std::string line; while( std::getline( is, line ) ) { p.write( line, ec ); if( ec ) return nullptr; } p.finish( ec ); if( ec ) return nullptr; return p.release(); } bool glob_match( urls::string_view pattern, urls::string_view str) { // regex if (str.starts_with("/") && str.ends_with("/")) { const boost::regex pr(pattern.begin() + 1, pattern.end() - 1); return boost::regex_match(std::string(str), pr); } // literal if (!pattern.contains('*')) { return pattern == str; } // glob std::string p = pattern; std::size_t i = p.find('*'); while (i != std::string::npos) { auto e = std::min(p.find_first_not_of('*', i), p.size()); std::size_t n = e - i; if (n == 1) { p.replace(i, e, "[^/]*"); i += 5; } else { p.replace(i, e, ".*"); i += 2; } i = p.find('*', i); } const boost::regex pr(p); return boost::regex_match(std::string(str), pr); } bool url_match( json::value& mv, urls::url const& u) { if (mv.is_string()) { json::string& p = mv.as_string(); return glob_match(u.buffer(), p); } else if (mv.is_array()) { json::array& m = mv.as_array(); for (auto& mi: m) { if (!mi.is_string()) throw std::invalid_argument( "handle match is not a string"); if (glob_match(mi.as_string(), u.buffer())) return true; } } else if (mv.is_object()) { json::object& m = mv.as_object(); std::pair<urls::string_view, urls::string_view> field_values[] = { {"protocol", u.scheme()}, {"authority", u.encoded_authority()}, {"username", u.encoded_user()}, {"user", u.encoded_user()}, {"password", u.encoded_password()}, {"userinfo", u.encoded_userinfo()}, {"host", u.encoded_host()}, {"port", u.port()}, {"path", u.encoded_path()}, {"pathname", u.encoded_path()}, {"query", u.encoded_query()}, {"search", u.encoded_query()}, {"fragment", u.encoded_fragment()}, {"hash", u.encoded_fragment()}, }; for (auto& p: field_values) { auto it = m.find(p.first); if (it != m.end()) { if (!it->value().is_string()) throw std::invalid_argument( "match fields should be a strings"); if (glob_match(p.second, p.first)) return true; } } } return false; } #define CHECK(c, msg) \ if (!(c)) \ { \ std::cerr << msg << "\n"; \ return EXIT_FAILURE; \ } int main(int argc, char** argv) { if (argc < 3) { std::cout << argv[0] << "\n"; std::cout << "Usage: finicky <config> <url>\n" "options:\n" " <config>: Configuration file\n" " <url>: The url to open\n" "examples:\n" " finicky config.json \"http://www.example.com\"\n"; return EXIT_FAILURE; } // Parse url urls::result<urls::url> ru = urls::parse_uri(argv[2]); CHECK(ru, "Invalid URL"); urls::url u = *ru; // Open config file std::fstream fin(argv[1]); CHECK(fin.good(), "Cannot open configuration file"); json::error_code ec; json::value c = read_json(fin, ec); CHECK(!ec.failed(), "Cannot parse configuration file"); CHECK(c.is_object(), "Configuration file is not an object"); json::object& o = c.as_object(); // Set initial browser auto bit = o.find("defaultBrowser"); CHECK( bit != o.end(), "Configuration file has no defaultBrowser"); CHECK( bit->value().is_string(), "defaultBrowser should be a string"); json::string& browser = bit->value().as_string(); // Apply rewrites to the input string auto rsit = o.find("rewrite"); if (rsit != o.end()) { CHECK( rsit->value().is_array(), "rewrite rules should be an array"); auto& rs = rsit->value().as_array(); for (auto& rv: rs) { CHECK( rv.is_object(), "individual rewrite rule should be an object"); json::object& r = rv.as_object(); // Look for match auto mit = r.find("match"); CHECK( mit != r.end(), "rewrite rule should have a match field"); CHECK( mit->value().is_object() || mit->value().is_string(), "rewrite match field is not an object"); if (!url_match(mit->value(), u)) continue; // Apply replacement rule auto uit = r.find("url"); CHECK( uit != r.end(), "rewrite rule should have a url field"); CHECK( uit->value().is_object() || uit->value().is_string(), "url field must be an object or string"); if (uit->value().is_string()) { json::string& uo = uit->value().as_string(); auto ru1 = urls::parse_uri(uo); CHECK(ru1, "url " << uo.c_str() << " is invalid"); u = *ru; } else { json::object& uo = uit->value().as_object(); auto it = uo.find("protocol"); if (it != uo.end()) { CHECK( it->value().is_string(), "protocol field should be a string"); u.set_scheme(it->value().as_string()); } it = uo.find("authority"); if (it != uo.end()) { CHECK( it->value().is_string(), "authority field should be a string"); u.set_encoded_authority( it->value().as_string().subview()); } it = uo.find("username"); if (it == uo.end()) it = uo.find("user"); if (it != uo.end()) { CHECK( it->value().is_string(), "username field should be a string"); u.set_encoded_user( it->value().as_string().subview()); } it = uo.find("password"); if (it != uo.end()) { CHECK( it->value().is_string(), "password field should be a string"); u.set_encoded_password( it->value().as_string().subview()); } it = uo.find("userinfo"); if (it != uo.end()) { CHECK( it->value().is_string(), "userinfo field should be a string"); u.set_encoded_userinfo( it->value().as_string().subview()); } it = uo.find("host"); if (it != uo.end()) { CHECK( it->value().is_string(), "host field should be a string"); u.set_encoded_host( it->value().as_string().subview()); } it = uo.find("port"); if (it != uo.end()) { CHECK( it->value().is_string(), "port field should be a string"); u.set_port( it->value().as_string().subview()); } it = uo.find("path"); if (it == uo.end()) it = uo.find("pathname"); if (it != uo.end()) { CHECK( it->value().is_string(), "path field should be a string"); u.set_encoded_path( it->value().as_string().subview()); } it = uo.find("query"); if (it == uo.end()) it = uo.find("search"); if (it != uo.end()) { CHECK( it->value().is_string(), "query field should be a string"); u.set_encoded_query( it->value().as_string().subview()); } it = uo.find("fragment"); if (it == uo.end()) it = uo.find("hash"); if (it != uo.end()) { CHECK( it->value().is_string(), "fragment field should be a string"); u.set_encoded_fragment( it->value().as_string().subview()); } } } } // Determine which browser should handle the url auto hsit = o.find("handlers"); if (hsit != o.end()) { CHECK( hsit->value().is_array(), "handler rules should be an array"); auto& hs = hsit->value().as_array(); for (auto& hv: hs) { CHECK( hv.is_object(), "individual handlers should be an object"); json::object& h = hv.as_object(); auto mit = h.find("match"); CHECK( mit != h.end(), "handle rule should have a match field"); CHECK( mit->value().is_string() || mit->value().is_array(), "handle match field must be an array or a string"); auto hbit = h.find("browser"); CHECK( hbit != h.end(), "handle rule should have a browser field"); CHECK( hbit->value().is_string(), "browser field is not a string"); // Look for match and change browser if (url_match(mit->value(), u)) { browser = hbit->value().as_string().subview(); break; } } } // Print command finicky would run std::cout << "\"" << browser.c_str() << "\" " << u << '\n'; return EXIT_SUCCESS; }