Developer SDK

C++ examples

Install the SDK · View source on GitHub ↗

integration/server/allocator.cpp

// SERVER ONLY: ./allocator user:<authenticated-service-user-id> | --self-test
// The authenticated backend supplies the service key internally, never a raw
// request field or UR client ID. Set URNETWORK_ROOT_JWT, URNETWORK_CLIENT_MAP
// (absolute path in an existing service-owned directory), and optional
// URNETWORK_API_URL. This POSIX CLI uses libcurl + nlohmann/json. Confirm
// crash-left .lock is stale before removal.
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <cstdlib>
#include <curl/curl.h>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <nlohmann/json.hpp>
#include <regex>
#include <set>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
using Json = nlohmann::json;
namespace fs = std::filesystem;
constexpr size_t limit = 1 << 20;
const std::regex users("user:[A-Za-z0-9][A-Za-z0-9_.:@-]{0,122}");
const std::regex ids("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}");
void require(bool ok) {
  if (!ok)
    throw std::runtime_error("invalid allocator input or operation");
}
std::string service_user(const std::vector<std::string> &args) {
  require(args.size() == 1 && std::regex_match(args[0], users));
  return args[0];
}
std::string url_part(CURLU *u, CURLUPart part) {
  char *raw = nullptr;
  if (curl_url_get(u, part, &raw, 0) != CURLUE_OK)
    return {};
  std::unique_ptr<char, decltype(&curl_free)> owned(raw, curl_free);
  return raw;
}
std::string endpoint(const std::string &base) {
  std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> url(curl_url(),
                                                          curl_url_cleanup);
  require(url &&
          curl_url_set(url.get(), CURLUPART_URL, base.c_str(), 0) == CURLUE_OK);
  auto scheme = url_part(url.get(), CURLUPART_SCHEME),
       host = url_part(url.get(), CURLUPART_HOST),
       path = url_part(url.get(), CURLUPART_PATH);
  bool local = host == "localhost" || host == "127.0.0.1" || host == "[::1]";
  require(!host.empty() && (scheme == "https" || (scheme == "http" && local)) &&
          (path.empty() || path == "/") &&
          url_part(url.get(), CURLUPART_USER).empty() &&
          url_part(url.get(), CURLUPART_PASSWORD).empty() &&
          url_part(url.get(), CURLUPART_QUERY).empty() &&
          url_part(url.get(), CURLUPART_FRAGMENT).empty());
  require(curl_url_set(url.get(), CURLUPART_PATH, "/network/auth-client", 0) ==
          CURLUE_OK);
  return url_part(url.get(), CURLUPART_URL);
}
Json request_for(const std::string &user, const std::string &client = {}) {
  service_user({user});
  Json body = {{"description", "service " + user},
               {"device_spec", "urnetwork-examples/cpp-server"}};
  if (!client.empty()) {
    require(std::regex_match(client, ids));
    body["client_id"] = client;
  }
  return body;
}
std::string base64url_decode(const std::string &value) {
  const std::string alphabet =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  std::string output;
  unsigned int bits = 0;
  int count = 0;
  require(!value.empty() && value.size() % 4 != 1);
  for (unsigned char c : value) {
    auto index = alphabet.find(c);
    require(index != std::string::npos);
    bits = (bits << 6) | unsigned(index);
    count += 6;
    if (count >= 8) {
      count -= 8;
      output.push_back(char((bits >> count) & 255));
    }
  }
  return output;
}
Json parse_response(const std::string &raw, const std::string &expected = {}) {
  auto obj = Json::parse(raw);
  require(obj.is_object() &&
          (!obj.contains("error") || obj["error"].is_null()) &&
          obj["client_id"].is_string() && obj["by_client_jwt"].is_string());
  auto id = obj["client_id"].get<std::string>(),
       jwt = obj["by_client_jwt"].get<std::string>();
  auto a = jwt.find('.'), b = jwt.find('.', a == std::string::npos ? 0 : a + 1);
  require(std::regex_match(id, ids) && a != std::string::npos && a > 0 &&
          b != std::string::npos && b > a + 1 && b + 1 < jwt.size() &&
          jwt.find('.', b + 1) == std::string::npos);
  auto claims = Json::parse(base64url_decode(jwt.substr(a + 1, b - a - 1)));
  require(claims["client_id"] == id && (expected.empty() || expected == id));
  // Claim comparison checks consistency, not signatures locally.
  return {{"client_id", id}, {"by_client_jwt", jwt}};
}
Json load_map(const fs::path &file) {
  struct stat st{};
  if (lstat(file.c_str(), &st) != 0) {
    require(errno == ENOENT);
    return {{"version", 1}, {"clients", Json::object()}};
  }
  require(S_ISREG(st.st_mode) && !(st.st_mode & 0077) &&
          st.st_size <= long(limit));
  std::ifstream input(file);
  Json map = Json::parse(input);
  require(map["version"].is_number_integer() && map["version"] == 1 &&
          map["clients"].is_object());
  std::set<std::string> seen;
  for (auto &[user, value] : map["clients"].items()) {
    require(std::regex_match(user, users) && value.is_string());
    auto id = value.get<std::string>();
    require(std::regex_match(id, ids) && seen.insert(id).second);
  }
  return map;
}
void save_map(const fs::path &file, const Json &map) {
  std::string pattern = file.string() + ".XXXXXX";
  std::vector<char> temp(pattern.begin(), pattern.end());
  temp.push_back(0);
  int fd = mkstemp(temp.data());
  require(fd >= 0);
  try {
    require(fchmod(fd, 0600) == 0);
    auto raw = map.dump();
    size_t offset = 0;
    while (offset < raw.size()) {
      ssize_t n = write(fd, raw.data() + offset, raw.size() - offset);
      if (n < 0 && errno == EINTR)
        continue;
      require(n > 0);
      offset += size_t(n);
    }
    require(fsync(fd) == 0);
    int closed = close(fd);
    fd = -1;
    require(closed == 0);
    require(rename(temp.data(), file.c_str()) == 0);
  } catch (...) {
    if (fd >= 0)
      close(fd);
    unlink(temp.data());
    throw;
  }
}
struct Lock {
  fs::path path;
  explicit Lock(const fs::path &file) : path(file.string() + ".lock") {
    require(file.is_absolute() && mkdir(path.c_str(), 0700) == 0);
  }
  ~Lock() { rmdir(path.c_str()); }
};
Json allocate(const std::string &user, const fs::path &file,
              const std::function<std::string(const Json &)> &call) {
  Lock lock(file);
  auto map = load_map(file);
  std::string old = map["clients"].value(user, std::string{});
  auto result = parse_response(call(request_for(user, old)), old);
  if (old.empty()) {
    for (const auto &id : map["clients"])
      require(id != result["client_id"]);
    map["clients"][user] = result["client_id"];
    save_map(file, map);
  }
  return result;
}
size_t receive(char *bytes, size_t size, size_t count, void *context) {
  auto &output = *static_cast<std::string *>(context);
  size_t n = size * count;
  if (n > limit - output.size())
    return 0;
  try {
    output.append(bytes, n);
  } catch (...) {
    return 0;
  }
  return n;
}
std::string post(const std::string &url, const std::string &root,
                 const Json &body) {
  require(!root.empty() &&
          std::none_of(root.begin(), root.end(),
                       [](unsigned char c) { return std::isspace(c); }));
  std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl(curl_easy_init(),
                                                           curl_easy_cleanup);
  require(bool(curl));
  curl_slist *list = nullptr;
  list = curl_slist_append(list, ("Authorization: Bearer " + root).c_str());
  list = curl_slist_append(list, "Content-Type: application/json");
  std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)> headers(
      list, curl_slist_free_all);
  auto raw = body.dump();
  std::string response;
  curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
  curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, list);
  curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, raw.c_str());
  curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, long(raw.size()));
  curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 15L);
  curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 0L);
  curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, receive);
  curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response);
  require(curl_easy_perform(curl.get()) == CURLE_OK);
  long code = 0;
  curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &code);
  require(code >= 200 && code < 300);
  return response;
}
void reject(const std::function<void()> &action) {
  try {
    action();
  } catch (...) {
    return;
  }
  throw std::runtime_error("invalid input accepted");
}
void self_test() {
  std::string id = "11111111-1111-1111-1111-111111111111";
  // Fixed offline token payload contains only
  // {"client_id":"11111111-1111-1111-1111-111111111111"}.
  std::string jwt = "e30."
                    "eyJjbGllbnRfaWQiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMT"
                    "ExMTExMTEifQ.test";
  auto raw = Json{{"client_id", id}, {"by_client_jwt", jwt}}.dump();
  std::vector<Json> calls;
  auto mock = [&](const Json &body) {
    calls.push_back(body);
    return raw;
  };
  std::string pattern =
      (fs::temp_directory_path() / "ur-allocator-XXXXXX").string();
  std::vector<char> temp(pattern.begin(), pattern.end());
  temp.push_back(0);
  require(mkdtemp(temp.data()) != nullptr);
  fs::path dir(temp.data()), file = dir / "clients.json";
  try {
    require(allocate("user:alice", file, mock)["client_id"] == id);
    require(allocate("user:alice", file, mock)["by_client_jwt"] == jwt);
    require(!calls[0].contains("client_id") &&
            !calls[0].contains("source_client_id") &&
            !calls[1].contains("source_client_id") &&
            calls[1]["client_id"] == id);
    require(load_map(file)["clients"]["user:alice"] == id);
    require(endpoint("http://127.0.0.1:1234") ==
            "http://127.0.0.1:1234/network/auth-client");
    reject([&] { service_user({id}); });
    reject([&] { service_user({"user:a", "--client-id", id}); });
    reject([] { service_user({"user:../a"}); });
    reject([] { endpoint("http://example.com"); });
    reject([] { endpoint("https://example.com/path"); });
    reject([] { parse_response("{\"error\":{}}"); });
    reject(
        [&] { parse_response(raw, "22222222-2222-2222-2222-222222222222"); });
  } catch (...) {
    fs::remove_all(dir);
    throw;
  }
  fs::remove_all(dir);
  std::cout << "allocator self-test passed\n";
}
std::string env_required(const char *key) {
  const char *value = getenv(key);
  require(value && *value);
  return value;
}
int main(int argc, char **argv) {
  try {
    require(curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK);
    if (argc == 2 && std::string(argv[1]) == "--self-test") {
      self_test();
      curl_global_cleanup();
      return 0;
    }
    std::string user =
        service_user(std::vector<std::string>(argv + 1, argv + argc));
    const char *base = getenv("URNETWORK_API_URL");
    auto url = endpoint(base ? base : "https://api.bringyour.com");
    auto root = env_required("URNETWORK_ROOT_JWT"),
         file = env_required("URNETWORK_CLIENT_MAP");
    std::cout << allocate(
                     user, file,
                     [&](const Json &body) { return post(url, root, body); })
                     .dump()
              << '\n';
    curl_global_cleanup();
    return 0;
  } catch (...) {
    curl_global_cleanup();
    std::cerr << "allocator failed: check service key, private mapping and "
                 "backend API configuration\n";
    return 1;
  }
}