Developer SDK

C examples

Install the SDK · View source on GitHub ↗

integration/server/allocator.c

/* 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 under an existing service-owned directory), and optional
 * URNETWORK_API_URL. POSIX CLI, libcurl + json-c. Confirm a crash-left .lock is
 * stale before removal. */
#define _DARWIN_C_SOURCE 1
#define _POSIX_C_SOURCE 200809L
#include <curl/curl.h>
#include <json-c/json.h>
#define CHECK(condition)                                                       \
  do {                                                                         \
    if (!(condition)) {                                                        \
      fputs("allocator self-test failed\n", stderr);                           \
      exit(1);                                                                 \
    }                                                                          \
  } while (0)
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define LIMIT (1u << 20)
typedef struct json_object Json;
typedef char *(*Provision)(Json *, void *);
static int user_valid(const char *s) {
  if (!s || strncmp(s, "user:", 5) || strlen(s) < 6 || strlen(s) > 128 ||
      !isalnum((unsigned char)s[5]))
    return 0;
  for (size_t i = 5; s[i]; i++)
    if (!isalnum((unsigned char)s[i]) && !strchr("_.:@-", s[i]))
      return 0;
  return 1;
}
static const char *service_user(int argc, char **argv) {
  return argc == 1 && user_valid(argv[0]) ? argv[0] : NULL;
}
static int id_valid(const char *s) {
  if (!s || strlen(s) != 36)
    return 0;
  for (size_t i = 0; i < 36; i++) {
    if (i == 8 || i == 13 || i == 18 || i == 23) {
      if (s[i] != '-')
        return 0;
    } else if (!strchr("0123456789abcdef", s[i]))
      return 0;
  }
  return 1;
}
static const char *string_field(Json *obj, const char *name) {
  Json *v = NULL;
  if (!obj || !json_object_object_get_ex(obj, name, &v) ||
      !json_object_is_type(v, json_type_string))
    return NULL;
  const char *s = json_object_get_string(v);
  return s && strlen(s) == (size_t)json_object_get_string_len(v) ? s : NULL;
}
static Json *parse_json(const char *raw) {
  if (!raw || strlen(raw) > LIMIT)
    return NULL;
  struct json_tokener *tok = json_tokener_new();
  if (!tok)
    return NULL;
  json_tokener_set_flags(tok, JSON_TOKENER_STRICT | JSON_TOKENER_VALIDATE_UTF8);
  Json *obj = json_tokener_parse_ex(tok, raw, (int)strlen(raw));
  size_t end = json_tokener_get_parse_end(tok);
  int ok = json_tokener_get_error(tok) == json_tokener_success;
  while (raw[end] && isspace((unsigned char)raw[end]))
    end++;
  if (!ok || raw[end]) {
    json_object_put(obj);
    obj = NULL;
  }
  json_tokener_free(tok);
  return obj;
}
static char *part(CURLU *url, CURLUPart field) {
  char *s = NULL;
  if (curl_url_get(url, field, &s, 0) != CURLUE_OK)
    return NULL;
  return s;
}
static char *endpoint(const char *base) {
  CURLU *u = curl_url();
  if (!u)
    return NULL;
  char *result = NULL, *scheme = NULL, *host = NULL, *path = NULL, *user = NULL,
       *pass = NULL, *query = NULL, *fragment = NULL;
  if (curl_url_set(u, CURLUPART_URL, base, 0) != CURLUE_OK)
    goto done;
  scheme = part(u, CURLUPART_SCHEME);
  host = part(u, CURLUPART_HOST);
  path = part(u, CURLUPART_PATH);
  user = part(u, CURLUPART_USER);
  pass = part(u, CURLUPART_PASSWORD);
  query = part(u, CURLUPART_QUERY);
  fragment = part(u, CURLUPART_FRAGMENT);
  int local = host && (!strcmp(host, "localhost") ||
                       !strcmp(host, "127.0.0.1") || !strcmp(host, "[::1]"));
  if (!scheme || !host || user || pass || query || fragment ||
      (path && strcmp(path, "/")) ||
      (strcmp(scheme, "https") && !(local && !strcmp(scheme, "http"))))
    goto done;
  if (curl_url_set(u, CURLUPART_PATH, "/network/auth-client", 0) == CURLUE_OK) {
    char *raw = part(u, CURLUPART_URL);
    if (raw) {
      result = strdup(raw);
      curl_free(raw);
    }
  }
done:
  curl_free(scheme);
  curl_free(host);
  curl_free(path);
  curl_free(user);
  curl_free(pass);
  curl_free(query);
  curl_free(fragment);
  curl_url_cleanup(u);
  return result;
}
static Json *request_for(const char *user, const char *client) {
  if (!user_valid(user) || (client && !id_valid(client)))
    return NULL;
  char description[160];
  snprintf(description, sizeof(description), "service %s", user);
  Json *body = json_object_new_object();
  json_object_object_add(body, "description",
                         json_object_new_string(description));
  json_object_object_add(body, "device_spec",
                         json_object_new_string("urnetwork-examples/c-server"));
  if (client)
    json_object_object_add(body, "client_id", json_object_new_string(client));
  return body;
}
static char *decode64(const char *raw, size_t size) {
  if (!size || size % 4 == 1)
    return NULL;
  char *out = malloc(size + 1);
  if (!out)
    return NULL;
  const char *alphabet =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  unsigned int bits = 0;
  int count = 0;
  size_t length = 0;
  for (size_t i = 0; i < size; i++) {
    const char *p = strchr(alphabet, raw[i]);
    if (!p || !raw[i]) {
      free(out);
      return NULL;
    }
    bits = (bits << 6) | (unsigned)(p - alphabet);
    count += 6;
    if (count >= 8) {
      count -= 8;
      out[length++] = (char)((bits >> count) & 255);
    }
  }
  out[length] = 0;
  if (strlen(out) != length) {
    free(out);
    return NULL;
  }
  return out;
}
static Json *parse_response(const char *raw, const char *expected) {
  Json *obj = parse_json(raw), *claims = NULL, *result = NULL, *error = NULL;
  char *decoded = NULL;
  if (!obj || !json_object_is_type(obj, json_type_object))
    goto done;
  if (json_object_object_get_ex(obj, "error", &error) && error &&
      !json_object_is_type(error, json_type_null))
    goto done;
  const char *id = string_field(obj, "client_id"),
             *jwt = string_field(obj, "by_client_jwt");
  if (!id_valid(id) || !jwt)
    goto done;
  const char *a = strchr(jwt, '.'), *b = a ? strchr(a + 1, '.') : NULL;
  if (!a || a == jwt || !b || b == a + 1 || !b[1] || strchr(b + 1, '.'))
    goto done;
  decoded = decode64(a + 1, (size_t)(b - a - 1));
  claims = parse_json(decoded);
  const char *claim = string_field(claims, "client_id");
  if (!claim || strcmp(claim, id) || (expected && strcmp(expected, id)))
    goto done;
  /* Claim comparison checks consistency; it is not local signature
   * verification. */
  result = json_object_new_object();
  json_object_object_add(result, "client_id", json_object_new_string(id));
  json_object_object_add(result, "by_client_jwt", json_object_new_string(jwt));
done:
  free(decoded);
  json_object_put(claims);
  json_object_put(obj);
  return result;
}
static Json *load_map(const char *path) {
  struct stat st;
  if (lstat(path, &st)) {
    if (errno != ENOENT)
      return NULL;
    Json *empty = json_object_new_object();
    json_object_object_add(empty, "version", json_object_new_int(1));
    json_object_object_add(empty, "clients", json_object_new_object());
    return empty;
  }
  if (!S_ISREG(st.st_mode) || (st.st_mode & 0077) || st.st_size > (off_t)LIMIT)
    return NULL;
  FILE *f = fopen(path, "rb");
  if (!f)
    return NULL;
  char *raw = malloc((size_t)st.st_size + 1);
  if (!raw) {
    fclose(f);
    return NULL;
  }
  size_t n = fread(raw, 1, (size_t)st.st_size, f);
  int read_ok = !ferror(f) && n == (size_t)st.st_size;
  fclose(f);
  raw[n] = 0;
  Json *map = read_ok && strlen(raw) == n ? parse_json(raw) : NULL;
  free(raw);
  Json *version = NULL, *clients = NULL, *seen = json_object_new_object();
  int valid = 0;
  if (!map || !json_object_object_get_ex(map, "version", &version) ||
      !json_object_is_type(version, json_type_int) ||
      json_object_get_int(version) != 1 ||
      !json_object_object_get_ex(map, "clients", &clients) ||
      !json_object_is_type(clients, json_type_object))
    goto done;
  valid = 1;
  json_object_object_foreach(clients, key, value) {
    const char *id = json_object_is_type(value, json_type_string)
                         ? json_object_get_string(value)
                         : NULL;
    Json *duplicate = NULL;
    if (!user_valid(key) || !id_valid(id) ||
        strlen(id) != (size_t)json_object_get_string_len(value) ||
        json_object_object_get_ex(seen, id, &duplicate)) {
      valid = 0;
      break;
    }
    json_object_object_add(seen, id, json_object_new_boolean(1));
  }
done:
  json_object_put(seen);
  if (!valid) {
    json_object_put(map);
    map = NULL;
  }
  return map;
}
static int save_map(const char *path, Json *map) {
  char *tmp = malloc(strlen(path) + 8);
  if (!tmp)
    return 0;
  sprintf(tmp, "%s.XXXXXX", path);
  int fd = mkstemp(tmp);
  if (fd < 0) {
    free(tmp);
    return 0;
  }
  int ok = fchmod(fd, 0600) == 0;
  const char *raw = json_object_to_json_string_ext(map, JSON_C_TO_STRING_PLAIN);
  size_t offset = 0, length = strlen(raw);
  while (ok && offset < length) {
    ssize_t n = write(fd, raw + offset, length - offset);
    if (n < 0 && errno == EINTR)
      continue;
    if (n <= 0) {
      ok = 0;
      break;
    }
    offset += (size_t)n;
  }
  if (ok)
    ok = fsync(fd) == 0;
  if (close(fd))
    ok = 0;
  if (ok)
    ok = rename(tmp, path) == 0;
  if (!ok)
    unlink(tmp);
  free(tmp);
  return ok;
}
static Json *allocate(const char *user, const char *path, Provision call,
                      void *context) {
  if (!user_valid(user) || !path || path[0] != '/')
    return NULL;
  char *lock = malloc(strlen(path) + 6);
  if (!lock)
    return NULL;
  sprintf(lock, "%s.lock", path);
  if (mkdir(lock, 0700)) {
    free(lock);
    return NULL;
  }
  Json *map = load_map(path), *clients = NULL, *body = NULL, *result = NULL;
  char *raw = NULL;
  if (!map || !json_object_object_get_ex(map, "clients", &clients))
    goto done;
  const char *old = string_field(clients, user);
  body = request_for(user, old);
  if (!body)
    goto done;
  raw = call(body, context);
  result = parse_response(raw, old);
  if (!result)
    goto done;
  if (!old) {
    const char *id = string_field(result, "client_id");
    json_object_object_foreach(clients, key, value) {
      (void)key;
      if (!strcmp(json_object_get_string(value), id)) {
        json_object_put(result);
        result = NULL;
        goto done;
      }
    }
    json_object_object_add(clients, user, json_object_new_string(id));
    if (!save_map(path, map)) {
      json_object_put(result);
      result = NULL;
    }
  }
done:
  free(raw);
  json_object_put(body);
  json_object_put(map);
  rmdir(lock);
  free(lock);
  return result;
}
struct Buffer {
  char *bytes;
  size_t length;
};
struct Backend {
  const char *url, *root;
};
static size_t receive(char *bytes, size_t size, size_t count, void *context) {
  struct Buffer *b = context;
  size_t n = size * count;
  if (n > LIMIT - b->length)
    return 0;
  char *next = realloc(b->bytes, b->length + n + 1);
  if (!next)
    return 0;
  b->bytes = next;
  memcpy(next + b->length, bytes, n);
  b->length += n;
  next[b->length] = 0;
  return n;
}
static char *post(Json *body, void *context) {
  struct Backend *backend = context;
  if (!backend->root || !*backend->root)
    return NULL;
  for (const char *p = backend->root; *p; p++)
    if (isspace((unsigned char)*p))
      return NULL;
  CURL *curl = curl_easy_init();
  if (!curl)
    return NULL;
  struct Buffer response = {0};
  char *authorization = malloc(strlen(backend->root) + 23);
  if (!authorization) {
    curl_easy_cleanup(curl);
    return NULL;
  }
  sprintf(authorization, "Authorization: Bearer %s", backend->root);
  struct curl_slist *headers = NULL;
  headers = curl_slist_append(headers, authorization);
  headers = curl_slist_append(headers, "Content-Type: application/json");
  free(authorization);
  const char *raw =
      json_object_to_json_string_ext(body, JSON_C_TO_STRING_PLAIN);
  curl_easy_setopt(curl, CURLOPT_URL, backend->url);
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, raw);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(raw));
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
  CURLcode error = curl_easy_perform(curl);
  long code = 0;
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
  curl_slist_free_all(headers);
  curl_easy_cleanup(curl);
  if (error != CURLE_OK || code < 200 || code >= 300 || !response.bytes ||
      strlen(response.bytes) != response.length) {
    free(response.bytes);
    return NULL;
  }
  return response.bytes;
}
struct Mock {
  const char *response;
  Json *calls[2];
  size_t count;
};
static char *mock(Json *body, void *context) {
  struct Mock *m = context;
  if (m->count >= 2)
    return NULL;
  m->calls[m->count++] = json_object_get(body);
  return strdup(m->response);
}
static void self_test(void) {
  const char *id = "11111111-1111-1111-1111-111111111111";
  const char *jwt = "e30."
                    "eyJjbGllbnRfaWQiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMT"
                    "ExMTExMTEifQ.test";
  Json *obj = json_object_new_object();
  json_object_object_add(obj, "client_id", json_object_new_string(id));
  json_object_object_add(obj, "by_client_jwt", json_object_new_string(jwt));
  struct Mock test = {json_object_to_json_string(obj), {0}, 0};
  char directory[] = "/tmp/ur-allocator-XXXXXX";
  CHECK(mkdtemp(directory));
  char path[128];
  snprintf(path, sizeof(path), "%s/clients.json", directory);
  Json *first = allocate("user:alice", path, mock, &test),
       *second = allocate("user:alice", path, mock, &test);
  CHECK(first && second);
  CHECK(!strcmp(string_field(first, "client_id"), id));
  CHECK(!strcmp(string_field(second, "by_client_jwt"), jwt));
  Json *unused = NULL;
  CHECK(!json_object_object_get_ex(test.calls[0], "client_id", &unused));
  CHECK(!json_object_object_get_ex(test.calls[0], "source_client_id", &unused));
  CHECK(!json_object_object_get_ex(test.calls[1], "source_client_id", &unused));
  CHECK(!strcmp(string_field(test.calls[1], "client_id"), id));
  Json *map = load_map(path), *clients = NULL;
  CHECK(map && json_object_object_get_ex(map, "clients", &clients) &&
        !strcmp(string_field(clients, "user:alice"), id));
  char *url = endpoint("http://127.0.0.1:1234");
  CHECK(url && !strcmp(url, "http://127.0.0.1:1234/network/auth-client"));
  free(url);
  char *invalid[] = {(char *)id},
       *extra[] = {"user:a", "--client-id", (char *)id};
  CHECK(!service_user(1, invalid) && !service_user(3, extra) &&
        !user_valid("user:../a"));
  CHECK(!endpoint("http://example.com") &&
        !endpoint("https://example.com/path") &&
        !parse_response("{\"error\":{}}", NULL) &&
        !parse_response(test.response, "22222222-2222-2222-2222-222222222222"));
  json_object_put(map);
  json_object_put(first);
  json_object_put(second);
  json_object_put(test.calls[0]);
  json_object_put(test.calls[1]);
  json_object_put(obj);
  unlink(path);
  rmdir(directory);
  puts("allocator self-test passed");
}
int main(int argc, char **argv) {
  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
    return 1;
  if (argc == 2 && !strcmp(argv[1], "--self-test")) {
    self_test();
    curl_global_cleanup();
    return 0;
  }
  const char *user = service_user(argc - 1, argv + 1),
             *base = getenv("URNETWORK_API_URL"),
             *file = getenv("URNETWORK_CLIENT_MAP"),
             *root = getenv("URNETWORK_ROOT_JWT");
  char *url = endpoint(base ? base : "https://api.bringyour.com");
  Json *result = NULL;
  if (user && url && file && root) {
    struct Backend backend = {url, root};
    result = allocate(user, file, post, &backend);
  }
  free(url);
  curl_global_cleanup();
  if (!result) {
    fputs("allocator failed: check service key, private mapping and backend "
          "API configuration\n",
          stderr);
    return 1;
  }
  puts(json_object_to_json_string_ext(result, JSON_C_TO_STRING_PLAIN));
  json_object_put(result);
  return 0;
}