From 868ace55d2ad2552828c76b9a4c2ee6b155f17c2 Mon Sep 17 00:00:00 2001 From: Michael R Sweet Date: Tue, 4 Apr 2023 15:49:39 -0400 Subject: [PATCH] Save work on JWT additions (Issue #50) --- .gitignore | 1 + cups/Dependencies | 29 +- cups/Makefile | 16 + cups/cups.h | 1 + cups/hash.c | 227 +++++------ cups/http-support.c | 10 +- cups/json-private.h | 29 ++ cups/json.c | 951 ++++++++++++++++++++++++-------------------- cups/json.h | 12 +- cups/jwt.c | 688 ++++++++++++++++++++++++++++++++ cups/jwt.h | 78 ++++ cups/testjson.c | 30 +- cups/testjwt.c | 374 +++++++++++++++++ 13 files changed, 1866 insertions(+), 580 deletions(-) create mode 100644 cups/json-private.h create mode 100644 cups/jwt.c create mode 100644 cups/jwt.h create mode 100644 cups/testjwt.c diff --git a/.gitignore b/.gitignore index ea4db27e3..8f1a08312 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ /cups/testi18n /cups/testipp /cups/testjson +/cups/testjwt /cups/testlang /cups/testoptions /cups/testpwg diff --git a/cups/Dependencies b/cups/Dependencies index de64f033c..ebac4908f 100644 --- a/cups/Dependencies +++ b/cups/Dependencies @@ -528,7 +528,32 @@ json.o: json.c cups-private.h string-private.h ../config.h base.h \ \ \ \ - pwg-private.h thread.h json.h + pwg-private.h thread.h json-private.h json.h +jwt.o: jwt.c cups-private.h string-private.h ../config.h base.h \ + debug-internal.h debug-private.h array.h ipp-private.h cups.h file.h \ + ipp.h http.h language.h transcode.h pwg.h http-private.h \ + ../cups/language.h \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + pwg-private.h thread.h jwt.h json.h json-private.h langprintf.o: langprintf.c cups-private.h string-private.h ../config.h \ base.h debug-internal.h debug-private.h array.h ipp-private.h cups.h \ file.h ipp.h http.h language.h transcode.h pwg.h http-private.h \ @@ -972,6 +997,8 @@ testipp.o: testipp.c file.h base.h string-private.h ../config.h \ test-internal.h testjson.o: testjson.c cups.h file.h base.h ipp.h http.h array.h \ language.h transcode.h pwg.h json.h test-internal.h +testjwt.o: testjwt.c cups.h file.h base.h ipp.h http.h array.h language.h \ + transcode.h pwg.h jwt.h json.h test-internal.h testoptions.o: testoptions.c cups-private.h string-private.h ../config.h \ base.h debug-internal.h debug-private.h array.h ipp-private.h cups.h \ file.h ipp.h http.h language.h transcode.h pwg.h http-private.h \ diff --git a/cups/Makefile b/cups/Makefile index 26b795f3a..847e12a97 100644 --- a/cups/Makefile +++ b/cups/Makefile @@ -38,6 +38,7 @@ LIBOBJS = \ ipp-file.o \ ipp-support.o \ json.o \ + jwt.o \ langprintf.o \ language.o \ md5.o \ @@ -71,6 +72,7 @@ TESTOBJS = \ testi18n.o \ testipp.o \ testjson.o \ + testjwt.o \ testoptions.o \ testraster.o \ testtestpage.o \ @@ -99,6 +101,7 @@ HEADERS = \ http.h \ ipp.h \ json.h \ + jwt.h \ language.h \ ppd.h \ pwg.h \ @@ -150,6 +153,7 @@ UNITTARGETS = \ testi18n \ testipp \ testjson \ + testjwt \ testoptions \ testraster \ testtestpage \ @@ -196,6 +200,8 @@ test: $(UNITTARGETS) ./testi18n 2>>test.log echo Running JSON API tests... ./testjson 2>>test.log +# echo Running JWT API tests... +# ./testjwt 2>>test.log echo Running option API tests... ./testoptions 2>>test.log echo Running raster API tests... @@ -504,6 +510,16 @@ testjson: testjson.o $(LIBCUPS_STATIC) $(CODE_SIGN) $(CSFLAGS) $@ +# +# testjwt (dependency on static CUPS library is intentional) +# + +testjwt: testjwt.o $(LIBCUPS_STATIC) + echo Linking $@... + $(CC) $(LDFLAGS) $(OPTIM) -o $@ testjwt.o $(LIBCUPS_STATIC) $(LIBS) + $(CODE_SIGN) $(CSFLAGS) $@ + + # # testlang (dependency on static CUPS library is intentional) # diff --git a/cups/cups.h b/cups/cups.h index bbeeb54f7..6450230ea 100644 --- a/cups/cups.h +++ b/cups/cups.h @@ -328,6 +328,7 @@ extern const char *cupsGetUserAgent(void) _CUPS_PUBLIC; extern ssize_t cupsHashData(const char *algorithm, const void *data, size_t datalen, unsigned char *hash, size_t hashsize) _CUPS_PUBLIC; extern const char *cupsHashString(const unsigned char *hash, size_t hashsize, char *buffer, size_t bufsize) _CUPS_PUBLIC; +extern ssize_t cupsHMACData(const char *algorithm, const unsigned char *key, size_t keylen, const void *data, size_t datalen, unsigned char *hash, size_t hashsize) _CUPS_PUBLIC; extern ipp_status_t cupsLastError(void) _CUPS_PUBLIC; extern const char *cupsLastErrorString(void) _CUPS_PUBLIC; diff --git a/cups/hash.c b/cups/hash.c index 514c352a2..2131c188c 100644 --- a/cups/hash.c +++ b/cups/hash.c @@ -1,16 +1,16 @@ -/* - * Hashing function for CUPS. - * - * Copyright © 2022 by OpenPrinting. - * Copyright © 2015-2019 by Apple Inc. - * - * Licensed under Apache License v2.0. See the file "LICENSE" for more - * information. - */ - -/* - * Include necessary headers... - */ +// +// Hashing function for CUPS. +// +// Copyright © 2022-2023 by OpenPrinting. +// Copyright © 2015-2019 by Apple Inc. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +// +// Include necessary headers... +// #include "cups-private.h" #include "debug-internal.h" @@ -24,28 +24,29 @@ # include #else # include "md5-internal.h" -#endif /* __APPLE__ */ - - -/* - * 'cupsHashData()' - Perform a hash function on the given data. - * - * The "algorithm" argument can be any of the registered, non-deprecated IPP - * hash algorithms for the "job-password-encryption" attribute, including - * "sha" for SHA-1, "sha-256" for SHA2-256, etc. - * - * The "hash" argument points to a buffer of "hashsize" bytes and should be at - * least 64 bytes in length for all of the supported algorithms. - * - * The returned hash is binary data. - */ - -ssize_t /* O - Size of hash or -1 on error */ -cupsHashData(const char *algorithm, /* I - Algorithm name */ - const void *data, /* I - Data to hash */ - size_t datalen, /* I - Length of data to hash */ - unsigned char *hash, /* I - Hash buffer */ - size_t hashsize) /* I - Size of hash buffer */ +#endif // __APPLE__ + + +// +// 'cupsHashData()' - Perform a hash function on the given data. +// +// This function performs a hash function on the given data. The "algorithm" +// argument can be any of the registered, non-deprecated IPP hash algorithms for +// the "job-password-encryption" attribute, including "sha" for SHA-1, +// "sha2-256" for SHA2-256, etc. +// +// The "hash" argument points to a buffer of "hashsize" bytes and should be at +// least 64 bytes in length for all of the supported algorithms. +// +// The returned hash is binary data. +// + +ssize_t // O - Size of hash or -1 on error +cupsHashData(const char *algorithm, // I - Algorithm name + const void *data, // I - Data to hash + size_t datalen, // I - Length of data to hash + unsigned char *hash, // I - Hash buffer + size_t hashsize) // I - Size of hash buffer { if (!algorithm || !data || datalen == 0 || !hash || hashsize == 0) { @@ -56,11 +57,8 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ #ifdef __APPLE__ if (!strcmp(algorithm, "md5")) { - /* - * MD5 (deprecated but widely used...) - */ - - CC_MD5_CTX ctx; /* MD5 context */ + // MD5 (deprecated but widely used...) + CC_MD5_CTX ctx; // MD5 context if (hashsize < CC_MD5_DIGEST_LENGTH) goto too_small; @@ -73,11 +71,8 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } else if (!strcmp(algorithm, "sha")) { - /* - * SHA-1... - */ - - CC_SHA1_CTX ctx; /* SHA-1 context */ + // SHA-1... + CC_SHA1_CTX ctx; // SHA-1 context if (hashsize < CC_SHA1_DIGEST_LENGTH) goto too_small; @@ -91,7 +86,7 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ # ifdef CC_SHA224_DIGEST_LENGTH else if (!strcmp(algorithm, "sha2-224")) { - CC_SHA256_CTX ctx; /* SHA-224 context */ + CC_SHA256_CTX ctx; // SHA-224 context if (hashsize < CC_SHA224_DIGEST_LENGTH) goto too_small; @@ -102,10 +97,10 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ return (CC_SHA224_DIGEST_LENGTH); } -# endif /* CC_SHA224_DIGEST_LENGTH */ +# endif // CC_SHA224_DIGEST_LENGTH else if (!strcmp(algorithm, "sha2-256")) { - CC_SHA256_CTX ctx; /* SHA-256 context */ + CC_SHA256_CTX ctx; // SHA-256 context if (hashsize < CC_SHA256_DIGEST_LENGTH) goto too_small; @@ -118,7 +113,7 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } else if (!strcmp(algorithm, "sha2-384")) { - CC_SHA512_CTX ctx; /* SHA-384 context */ + CC_SHA512_CTX ctx; // SHA-384 context if (hashsize < CC_SHA384_DIGEST_LENGTH) goto too_small; @@ -131,7 +126,7 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } else if (!strcmp(algorithm, "sha2-512")) { - CC_SHA512_CTX ctx; /* SHA-512 context */ + CC_SHA512_CTX ctx; // SHA-512 context if (hashsize < CC_SHA512_DIGEST_LENGTH) goto too_small; @@ -145,14 +140,11 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ # ifdef CC_SHA224_DIGEST_LENGTH else if (!strcmp(algorithm, "sha2-512_224")) { - CC_SHA512_CTX ctx; /* SHA-512 context */ + CC_SHA512_CTX ctx; // SHA-512 context unsigned char temp[CC_SHA512_DIGEST_LENGTH]; - /* SHA-512 hash */ - - /* - * SHA2-512 truncated to 224 bits (28 bytes)... - */ + // SHA-512 hash + // SHA2-512 truncated to 224 bits (28 bytes)... if (hashsize < CC_SHA224_DIGEST_LENGTH) goto too_small; @@ -164,17 +156,14 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ return (CC_SHA224_DIGEST_LENGTH); } -# endif /* CC_SHA224_DIGEST_LENGTH */ +# endif // CC_SHA224_DIGEST_LENGTH else if (!strcmp(algorithm, "sha2-512_256")) { - CC_SHA512_CTX ctx; /* SHA-512 context */ + CC_SHA512_CTX ctx; // SHA-512 context unsigned char temp[CC_SHA512_DIGEST_LENGTH]; - /* SHA-512 hash */ - - /* - * SHA2-512 truncated to 256 bits (32 bytes)... - */ + // SHA-512 hash + // SHA2-512 truncated to 256 bits (32 bytes)... if (hashsize < CC_SHA256_DIGEST_LENGTH) goto too_small; @@ -189,18 +178,15 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ #elif defined(HAVE_GNUTLS) gnutls_digest_algorithm_t alg = GNUTLS_DIG_UNKNOWN; - /* Algorithm */ - unsigned char temp[64]; /* Temporary hash buffer */ - size_t tempsize = 0; /* Truncate to this size? */ + // Algorithm + unsigned char temp[64]; // Temporary hash buffer + size_t tempsize = 0; // Truncate to this size? if (!strcmp(algorithm, "md5")) { - /* - * Some versions of GNU TLS disable MD5 without warning... - */ - - _cups_md5_state_t state; /* MD5 state info */ + // Some versions of GNU TLS disable MD5 without warning... + _cups_md5_state_t state; // MD5 state info if (hashsize < 16) goto too_small; @@ -212,15 +198,26 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ return (16); } else if (!strcmp(algorithm, "sha")) + { + // SHA-1 alg = GNUTLS_DIG_SHA1; + } else if (!strcmp(algorithm, "sha2-224")) + { alg = GNUTLS_DIG_SHA224; + } else if (!strcmp(algorithm, "sha2-256")) + { alg = GNUTLS_DIG_SHA256; + } else if (!strcmp(algorithm, "sha2-384")) + { alg = GNUTLS_DIG_SHA384; + } else if (!strcmp(algorithm, "sha2-512")) + { alg = GNUTLS_DIG_SHA512; + } else if (!strcmp(algorithm, "sha2-512_224")) { alg = GNUTLS_DIG_SHA512; @@ -236,10 +233,7 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ { if (tempsize > 0) { - /* - * Truncate result to tempsize bytes... - */ - + // Truncate result to tempsize bytes... if (hashsize < tempsize) goto too_small; @@ -274,6 +268,7 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } else if (!strcmp(algorithm, "sha")) { + // SHA-1 algid = BCRYPT_SHA1_ALGORITHM; hashlen = 20; } @@ -350,13 +345,10 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } #else - /* - * No hash support beyond MD5 without CommonCrypto, GNU TLS, or CNG... - */ - + // No hash support beyond MD5 without CommonCrypto, GNU TLS, or CNG... if (!strcmp(algorithm, "md5")) { - _cups_md5_state_t state; /* MD5 state info */ + _cups_md5_state_t state; // MD5 state info if (hashsize < 16) goto too_small; @@ -369,20 +361,14 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } else if (hashsize < 64) goto too_small; -#endif /* __APPLE__ */ - - /* - * Unknown hash algorithm... - */ +#endif // __APPLE__ + // Unknown hash algorithm... _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Unknown hash algorithm."), 1); return (-1); - /* - * We get here if the buffer is too small. - */ - + // We get here if the buffer is too small. too_small: _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Hash buffer too small."), 1); @@ -390,30 +376,25 @@ cupsHashData(const char *algorithm, /* I - Algorithm name */ } -/* - * 'cupsHashString()' - Format a hash value as a hexadecimal string. - * - * The passed buffer must be at least 2 * hashsize + 1 characters in length. - * - * - */ +// +// 'cupsHashString()' - Format a hash value as a hexadecimal string. +// +// The passed buffer must be at least 2 * hashsize + 1 characters in length. +// -const char * /* O - Formatted string */ +const char * // O - Formatted string cupsHashString( - const unsigned char *hash, /* I - Hash */ - size_t hashsize, /* I - Size of hash */ - char *buffer, /* I - String buffer */ - size_t bufsize) /* I - Size of string buffer */ + const unsigned char *hash, // I - Hash + size_t hashsize, // I - Size of hash + char *buffer, // I - String buffer + size_t bufsize) // I - Size of string buffer { - char *bufptr = buffer; /* Pointer into buffer */ + char *bufptr = buffer; // Pointer into buffer static const char *hex = "0123456789abcdef"; - /* Hex characters (lowercase!) */ + // Hex characters (lowercase!) - /* - * Range check input... - */ - + // Range check input... if (!hash || hashsize < 1 || !buffer || bufsize < (2 * hashsize + 1)) { if (buffer) @@ -421,10 +402,7 @@ cupsHashString( return (NULL); } - /* - * Loop until we've converted the whole hash... - */ - + // Loop until we've converted the whole hash... while (hashsize > 0) { *bufptr++ = hex[*hash >> 4]; @@ -438,3 +416,30 @@ cupsHashString( return (buffer); } + + +// +// 'cupsHMACData()' - Perform a HMAC function on the given data. +// +// This function performs a HMAC function on the given data with the given key. +// The "algorithm" argument can be any of the registered, non-deprecated IPP +// hash algorithms for the "job-password-encryption" attribute, including +// "sha" for SHA-1, "sha2-256" for SHA2-256, etc. +// +// The "hash" argument points to a buffer of "hashsize" bytes and should be at +// least 64 bytes in length for all of the supported algorithms. +// +// The returned hash is binary data. +// + +ssize_t // O - The length of the hash or `-1` on error +cupsHMACData( + const char *algorithm, // I - Hash algorithm + const unsigned char *key, // I - Key + size_t keylen, // I - Length of key + const void *data, // I - Data to hash + size_t datalen, // I - Length of data to hash + unsigned char *hash, // I - Hash buffer + size_t hashsize) // I - Size of hash buffer +{ +} diff --git a/cups/http-support.c b/cups/http-support.c index 111050023..2102c4a5f 100644 --- a/cups/http-support.c +++ b/cups/http-support.c @@ -582,10 +582,10 @@ httpEncode64(char *out, // I - String to write to inlen --; if (inlen <= 0) { - if (outptr < outend) - *outptr ++ = '='; - if (outptr < outend) - *outptr ++ = '='; + if (!url && outptr < outend) + *outptr ++ = '='; + if (!url && outptr < outend) + *outptr ++ = '='; break; } @@ -601,7 +601,7 @@ httpEncode64(char *out, // I - String to write to inlen --; if (inlen <= 0) { - if (outptr < outend) + if (!url && outptr < outend) *outptr ++ = '='; break; } diff --git a/cups/json-private.h b/cups/json-private.h new file mode 100644 index 000000000..4301ff07e --- /dev/null +++ b/cups/json-private.h @@ -0,0 +1,29 @@ +// +// Private JSON API definitions for CUPS. +// +// Copyright © 2023 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#ifndef _CUPS_JSON_PRIVATE_H_ +# define _CUPS_JSON_PRIVATE_H_ +# include "json.h" +# ifdef __cplusplus +extern "C" { +# endif /* __cplusplus */ + + +// +// Functions... +// + +extern void _cupsJSONAdd(cups_json_t *parent, cups_json_t *after, cups_json_t *node) _CUPS_PRIVATE; +extern void _cupsJSONDelete(cups_json_t *json, const char *key) _CUPS_PRIVATE; + + +# ifdef __cplusplus +} +# endif /* __cplusplus */ +#endif // !_CUPS_JSON_PRIVATE_H_ diff --git a/cups/json.c b/cups/json.c index 46501a7c5..3ecf0f4f7 100644 --- a/cups/json.c +++ b/cups/json.c @@ -8,7 +8,7 @@ // #include "cups-private.h" -#include "json.h" +#include "json-private.h" #include @@ -34,9 +34,85 @@ struct _cups_json_s // JSON node // Local functions... // +static void delete_json(cups_json_t *json); static void free_json(cups_json_t *json); +// +// '_cupsJSONAdd()' - Add a node to a JSON node. +// + +void +_cupsJSONAdd(cups_json_t *parent, // I - Parent JSON node + cups_json_t *after, // I - Previous sibling node or `NULL` to append to the end + cups_json_t *node) // I - JSON node to add +{ + cups_json_t *current; // Current node + + + node->parent = parent; + + if (after) + { + // Append after the specified sibling... + node->sibling = after->sibling; + after->sibling = node; + } + else if ((current = parent->value.child) != NULL) + { + // Find the last child... + while (current && current->sibling) + current = current->sibling; + + current->sibling = node; + } + else + { + // This is the first child... + parent->value.child = node; + } +} + + +// +// '_cupsJSONDelete()' - Delete a key + value pair. +// + +void +_cupsJSONDelete(cups_json_t *json, // I - JSON node + const char *key) // I - Key +{ + cups_json_t *prev, + *current, // Current child node + *sibling; // Sibling (value) node + + + // Range check input... + if (!json || json->type != CUPS_JTYPE_OBJECT) + return; + + // Search for the named key... + for (prev = NULL, current = json->value.child; current; prev = current, current = current->sibling) + { + if (current->type == CUPS_JTYPE_KEY && !strcmp(key, current->value.string)) + { + // Remove the current and next siblings from the parent... + sibling = current->sibling; + + if (prev) + prev->sibling = sibling->sibling; + else + json->value.child = sibling->sibling; + + // Delete the key and value... + delete_json(current); + delete_json(sibling); + return; + } + } +} + + // // 'cupsJSONDelete()' - Delete a JSON node and all of its children. // @@ -44,8 +120,7 @@ static void free_json(cups_json_t *json); void cupsJSONDelete(cups_json_t *json) // I - JSON node { - cups_json_t *child, // Child node - *sibling; // Sibling node + cups_json_t *child; // Child node // Range check input... @@ -77,139 +152,394 @@ cupsJSONDelete(cups_json_t *json) // I - JSON node } // Free the value(s) - if (json->type == CUPS_JTYPE_ARRAY || json->type == CUPS_JTYPE_OBJECT) - { - for (child = json->value.child; child && child != json; child = sibling) - { - if ((child->type == CUPS_JTYPE_ARRAY || child->type == CUPS_JTYPE_OBJECT) && child->value.child) - { - // Descend into child nodes... - sibling = child->value.child; - } - else if ((sibling = child->sibling) == NULL) - { - // No more silbings, ascend unless the parent is the original node... - sibling = child->parent; - free_json(child); - - while (sibling && sibling != json) - { - cups_json_t *temp = sibling; // Save the current pointer - - if (sibling->sibling) - { - // More siblings at this level, free the parent... - sibling = sibling->sibling; - free_json(temp); - break; - } - else - { - // No more siblings, continue upwards... - sibling = sibling->parent; - free_json(temp); - } - } - } - else - { - // Free the memory for this node - free_json(child); - } - } - } - - free_json(json); + delete_json(json); } // -// 'cupsJSONFind()' - Find the value(s) associated with a given key. +// 'cupsJSONExportFile()' - Save a JSON node tree to a file. // -cups_json_t * // O - JSON value or `NULL` -cupsJSONFind(cups_json_t *json, // I - JSON object node - const char *key) // I - Object key +bool // O - `true` on success, `false` on failure +cupsJSONExportFile( + cups_json_t *json, // I - JSON root node + const char *filename) // I - JSON filename { - cups_json_t *current; // Current child node + char *s; // JSON string + int fd; // JSON file - // Range check input... - if (!json || json->type != CUPS_JTYPE_OBJECT) - return (NULL); + DEBUG_printf(("cupsJSONExportFile(json=%p, filename=\"%s\")", (void *)json, filename)); - // Search for the named key... - for (current = json->value.child; current; current = current->sibling) + // Get the JSON as a string... + if ((s = cupsJSONExportString(json)) == NULL) + return (false); + + // Create the file... + if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0) { - if (current->type == CUPS_JTYPE_KEY && !strcmp(key, current->value.string)) - return (current->sibling); + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); + free(s); + return (false); } - // If we get here there was no match... - return (NULL); + if (write(fd, s, strlen(s)) < 0) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); + close(fd); + unlink(filename); + free(s); + return (false); + } + + close(fd); + free(s); + + return (true); } // -// 'cupsJSONGetChild()' - Get the first child node of an array or object node. +// 'cupsJSONExportString()' - Save a JSON node tree to a string. +// +// This function saves a JSON node tree to an allocated string. The resulting +// string must be freed using the `free` function. // -cups_json_t * // O - First child node or `NULL` -cupsJSONGetChild(cups_json_t *json, // I - JSON array or object node - size_t n) // I - Child node number (starting at `0`) +char * // O - JSON string or `NULL` on error +cupsJSONExportString(cups_json_t *json) // I - JSON root node { - cups_json_t *current; // Current child node + cups_json_t *current; // Current node + size_t length; // Length of JSON data as a string + char *s, // JSON string + *ptr; // Pointer into string + const char *value; // Pointer into string value + struct lconv *loc; // Locale data + DEBUG_printf(("cupsJSONExportString(json=%p)", (void *)json)); + // Range check input... - if (!json || (json->type != CUPS_JTYPE_ARRAY && json->type != CUPS_JTYPE_OBJECT)) + if (!json) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), 0); + DEBUG_puts("3cupsJSONExportString: Returning NULL."); return (NULL); + } - // Search for the Nth child... - for (current = json->value.child; n > 0 && current; current = current->sibling) - n --; - - // Return the child node pointer... - return (current); -} - + // Figure out the necessary space needed in the string + current = json; + length = 1; // nul -// -// 'cupsJSONGetCount()' - Get the number of child nodes. -// + while (current) + { + if (current->parent && current->parent->value.child != current) + length ++; // Comma or colon separator -size_t // O - Number of child nodes -cupsJSONGetCount(cups_json_t *json) // I - JSON array or object node -{ - cups_json_t *current; // Current child node - size_t n; // Number of child nodes + switch (current->type) + { + case CUPS_JTYPE_NULL : + case CUPS_JTYPE_TRUE : + length += 4; + break; - // Range check input... - if (!json || (json->type != CUPS_JTYPE_ARRAY && json->type != CUPS_JTYPE_OBJECT)) - return (0); + case CUPS_JTYPE_FALSE : + length += 5; + break; - // Count the child nodes... - for (current = json->value.child, n = 0; current; current = current->sibling) - n ++; + case CUPS_JTYPE_ARRAY : + case CUPS_JTYPE_OBJECT : + length += 2; // Brackets/braces + break; - // Return the count... - return (n); -} + case CUPS_JTYPE_NUMBER : + length += 32; + break; + case CUPS_JTYPE_KEY : + case CUPS_JTYPE_STRING : + length += 2; // Quotes + for (value = current->value.string; *value; value ++) + { + if (strchr("\\\"\b\f\n\r\t", *value)) + length += 2; // Simple escaped char + else if ((*value & 255) < ' ') + length += 6; // Worst case for control char + else + length ++; // Literal char + } + break; + } -// -// 'cupsJSONGetKey()' - Get the key string, if any. -// -// This function returns the key string for a JSON key node or `NULL` if -// the node is not a key. -// + // Get next node... + if ((current->type == CUPS_JTYPE_ARRAY || current->type == CUPS_JTYPE_OBJECT) && current->value.child) + { + // Descend + current = current->value.child; + } + else if (current->sibling) + { + // Visit silbling + current = current->sibling; + } + else + { + // Ascend and continue... + current = current->parent; + while (current) + { + if (current->sibling) + { + current = current->sibling; + break; + } + else + { + current = current->parent; + } + } + } + } -const char * // O - String value -cupsJSONGetKey(cups_json_t *json) // I - JSON string node -{ - return (json && json->type == CUPS_JTYPE_KEY ? json->value.string : NULL); -} + DEBUG_printf(("2cupsJSONExportString: length=%u", (unsigned)length)); + + // Allocate memory and fill it up... + if ((s = malloc(length)) == NULL) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); + DEBUG_puts("3cupsJSONExportString: Returning NULL."); + return (NULL); + } + + current = json; + ptr = s; + loc = localeconv(); + + while (current) + { + if (current->parent && current->parent->value.child != current) + { + // Add separator + if (current->type == CUPS_JTYPE_KEY || current->parent->type == CUPS_JTYPE_ARRAY) + *ptr++ = ','; + else + *ptr++ = ':'; + } + + switch (current->type) + { + case CUPS_JTYPE_NULL : + memcpy(ptr, "null", 4); + ptr += 4; + break; + + case CUPS_JTYPE_TRUE : + memcpy(ptr, "true", 4); + ptr += 4; + break; + + case CUPS_JTYPE_FALSE : + memcpy(ptr, "false", 5); + ptr += 5; + break; + + case CUPS_JTYPE_ARRAY : + *ptr++ = '['; + break; + + case CUPS_JTYPE_OBJECT : + *ptr++ = '{'; + break; + + case CUPS_JTYPE_NUMBER : + _cupsStrFormatd(ptr, s + length, current->value.number, loc); + ptr += strlen(ptr); + break; + + case CUPS_JTYPE_KEY : + case CUPS_JTYPE_STRING : + *ptr++ = '\"'; + + for (value = current->value.string; *value; value ++) + { + // Quote/escape as needed... + if (*value == '\\' || *value == '\"') + { + *ptr++ = '\\'; + *ptr++ = *value; + } + else if (*value == '\b') + { + *ptr++ = '\\'; + *ptr++ = 'b'; + } + else if (*value == '\f') + { + *ptr++ = '\\'; + *ptr++ = 'f'; + } + else if (*value == '\n') + { + *ptr++ = '\\'; + *ptr++ = 'n'; + } + else if (*value == '\r') + { + *ptr++ = '\\'; + *ptr++ = 'r'; + } + else if (*value == '\t') + { + *ptr++ = '\\'; + *ptr++ = 't'; + } + if ((*value & 255) < ' ') + { + snprintf(ptr, length - (size_t)(ptr - s), "\\u%04x", *value); + ptr += 6; + } + else + { + *ptr++ = *value; + } + } + + *ptr++ = '\"'; + break; + } + + // Get next node... + if ((current->type == CUPS_JTYPE_ARRAY || current->type == CUPS_JTYPE_OBJECT) && current->value.child) + { + // Descend + current = current->value.child; + } + else if (current->sibling) + { + // Visit silbling + current = current->sibling; + } + else if ((current = current->parent) != NULL) + { + // Ascend and continue... + if (current->type == CUPS_JTYPE_ARRAY) + *ptr++ = ']'; + else + *ptr++ = '}'; + + while (current) + { + if (current->sibling) + { + current = current->sibling; + break; + } + else if ((current = current->parent) != NULL) + { + if (current->type == CUPS_JTYPE_ARRAY) + *ptr++ = ']'; + else + *ptr++ = '}'; + } + } + } + } + + *ptr = '\0'; + + DEBUG_printf(("3cupsJSONExportString: Returning \"%s\".", s)); + + return (s); +} + + +// +// 'cupsJSONFind()' - Find the value(s) associated with a given key. +// + +cups_json_t * // O - JSON value or `NULL` +cupsJSONFind(cups_json_t *json, // I - JSON object node + const char *key) // I - Object key +{ + cups_json_t *current; // Current child node + + + // Range check input... + if (!json || json->type != CUPS_JTYPE_OBJECT) + return (NULL); + + // Search for the named key... + for (current = json->value.child; current; current = current->sibling) + { + if (current->type == CUPS_JTYPE_KEY && !strcmp(key, current->value.string)) + return (current->sibling); + } + + // If we get here there was no match... + return (NULL); +} + + +// +// 'cupsJSONGetChild()' - Get the first child node of an array or object node. +// + +cups_json_t * // O - First child node or `NULL` +cupsJSONGetChild(cups_json_t *json, // I - JSON array or object node + size_t n) // I - Child node number (starting at `0`) +{ + cups_json_t *current; // Current child node + + + // Range check input... + if (!json || (json->type != CUPS_JTYPE_ARRAY && json->type != CUPS_JTYPE_OBJECT)) + return (NULL); + + // Search for the Nth child... + for (current = json->value.child; n > 0 && current; current = current->sibling) + n --; + + // Return the child node pointer... + return (current); +} + + +// +// 'cupsJSONGetCount()' - Get the number of child nodes. +// + +size_t // O - Number of child nodes +cupsJSONGetCount(cups_json_t *json) // I - JSON array or object node +{ + cups_json_t *current; // Current child node + size_t n; // Number of child nodes + + // Range check input... + if (!json || (json->type != CUPS_JTYPE_ARRAY && json->type != CUPS_JTYPE_OBJECT)) + return (0); + + // Count the child nodes... + for (current = json->value.child, n = 0; current; current = current->sibling) + n ++; + + // Return the count... + return (n); +} + + +// +// 'cupsJSONGetKey()' - Get the key string, if any. +// +// This function returns the key string for a JSON key node or `NULL` if +// the node is not a key. +// + +const char * // O - String value +cupsJSONGetKey(cups_json_t *json) // I - JSON string node +{ + return (json && json->type == CUPS_JTYPE_KEY ? json->value.string : NULL); +} // @@ -274,11 +604,11 @@ cupsJSONGetType(cups_json_t *json) // I - JSON node // -// 'cupsJSONLoadFile()' - Load a JSON object file. +// 'cupsJSONImportFile()' - Load a JSON object file. // cups_json_t * // O - Root JSON object node -cupsJSONLoadFile(const char *filename) // I - JSON filename +cupsJSONImportFile(const char *filename)// I - JSON filename { cups_json_t *json; // Root JSON object node int fd; // JSON file @@ -332,7 +662,7 @@ cupsJSONLoadFile(const char *filename) // I - JSON filename close(fd); // Load the resulting string - json = cupsJSONLoadString(s); + json = cupsJSONImportString(s); // Free the string and return... free(s); @@ -342,11 +672,11 @@ cupsJSONLoadFile(const char *filename) // I - JSON filename // -// 'cupsJSONLoadString()' - Load a JSON object from a string. +// 'cupsJSONImportString()' - Load a JSON object from a string. // cups_json_t * // O - Root JSON object node -cupsJSONLoadString(const char *s) // I - JSON string +cupsJSONImportString(const char *s) // I - JSON string { cups_json_t *json, // Root JSON object node *parent, // Current parent node @@ -357,12 +687,12 @@ cupsJSONLoadString(const char *s) // I - JSON string static const char *sep = ",]} \n\r\t";// Separator chars - DEBUG_printf(("cupsJSONLoadString(s=\"%s\")", s)); + DEBUG_printf(("cupsJSONImportString(s=\"%s\")", s)); // Range check input... if (!s || *s != '{') { - DEBUG_puts("2cupsJSONLoadString: Doesn't start with '{'."); + DEBUG_puts("2cupsJSONImportString: Doesn't start with '{'."); _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Invalid JSON data."), 1); return (NULL); } @@ -370,7 +700,7 @@ cupsJSONLoadString(const char *s) // I - JSON string // Create the root node... if ((json = cupsJSONNew(NULL, NULL, CUPS_JTYPE_OBJECT)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to create root object."); + DEBUG_puts("2cupsJSONImportString: Unable to create root object."); return (NULL); } @@ -395,7 +725,7 @@ cupsJSONLoadString(const char *s) // I - JSON string if (!parent->value.child) { // Cannot have a comma here - DEBUG_puts("2cupsJSONLoadString: Unexpected comma."); + DEBUG_puts("2cupsJSONImportString: Unexpected comma."); goto invalid; } @@ -414,7 +744,7 @@ cupsJSONLoadString(const char *s) // I - JSON string if (!parent->value.child || (count & 1)) { // Cannot have a comma here - DEBUG_puts("2cupsJSONLoadString: Unexpected comma."); + DEBUG_puts("2cupsJSONImportString: Unexpected comma."); goto invalid; } @@ -427,7 +757,7 @@ cupsJSONLoadString(const char *s) // I - JSON string if (!parent->value.child || !(count & 1)) { // Cannot have a colon here - DEBUG_puts("2cupsJSONLoadString: Unexpected colon."); + DEBUG_puts("2cupsJSONImportString: Unexpected colon."); goto invalid; } @@ -438,14 +768,14 @@ cupsJSONLoadString(const char *s) // I - JSON string else if (count & 1) { // Missing colon... - DEBUG_puts("2cupsJSONLoadString: Missing colon."); + DEBUG_puts("2cupsJSONImportString: Missing colon."); goto invalid; } if (!(count & 1) && *s != '\"' && *s != '}') { // Need a key string here... - DEBUG_puts("2cupsJSONLoadString: Missing key string."); + DEBUG_puts("2cupsJSONImportString: Missing key string."); goto invalid; } } @@ -467,19 +797,19 @@ cupsJSONLoadString(const char *s) // I - JSON string s ++; if (!*s || !strchr("\"\\/bfnrtu", *s)) { - DEBUG_printf(("2cupsJSONLoadString: Bad escape '\\%c'.", *s)); + DEBUG_printf(("2cupsJSONImportString: Bad escape '\\%c'.", *s)); goto invalid; } else if (*s == 'u' && (!isxdigit(s[1] & 255) || !isxdigit(s[2] & 255) || !isxdigit(s[3] & 255) || !isxdigit(s[4] & 255))) { - DEBUG_printf(("2cupsJSONLoadString: Bad escape '\\%s'.", s)); + DEBUG_printf(("2cupsJSONImportString: Bad escape '\\%s'.", s)); goto invalid; } } else if ((*s & 255) < ' ') { // Control characters are not allowed in a string... - DEBUG_printf(("2cupsJSONLoadString: Bad control character 0x%02x in string.", *s)); + DEBUG_printf(("2cupsJSONImportString: Bad control character 0x%02x in string.", *s)); goto invalid; } } @@ -487,7 +817,7 @@ cupsJSONLoadString(const char *s) // I - JSON string if (!*s) { // Missing close quote... - DEBUG_puts("2cupsJSONLoadString: Missing close quote."); + DEBUG_puts("2cupsJSONImportString: Missing close quote."); goto invalid; } @@ -499,7 +829,7 @@ cupsJSONLoadString(const char *s) // I - JSON string if (!current) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate key/string node."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate key/string node."); goto error; } else if ((current->value.string = malloc(len)) == NULL) @@ -597,7 +927,7 @@ cupsJSONLoadString(const char *s) // I - JSON string count ++; prev = current; - DEBUG_printf(("3cupsJSONLoadString: Added %s '%s'.", current->type == CUPS_JTYPE_KEY ? "key" : "string", current->value.string)); + DEBUG_printf(("3cupsJSONImportString: Added %s '%s'.", current->type == CUPS_JTYPE_KEY ? "key" : "string", current->value.string)); } else if (strchr("0123456789-", *s)) { @@ -609,14 +939,14 @@ cupsJSONLoadString(const char *s) // I - JSON string count ++; prev = current; - DEBUG_printf(("3cupsJSONLoadString: Added number %g.", current->value.number)); + DEBUG_printf(("3cupsJSONImportString: Added number %g.", current->value.number)); } else if (*s == '{') { // Start object if ((parent = cupsJSONNew(parent, prev, CUPS_JTYPE_OBJECT)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate object."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate object."); goto error; } @@ -624,7 +954,7 @@ cupsJSONLoadString(const char *s) // I - JSON string prev = NULL; s ++; - DEBUG_puts("3cupsJSONLoadString: Opened object."); + DEBUG_puts("3cupsJSONImportString: Opened object."); } else if (*s == '}') { @@ -632,11 +962,11 @@ cupsJSONLoadString(const char *s) // I - JSON string if (parent->type != CUPS_JTYPE_OBJECT) { // Not in an object, so this is unexpected... - DEBUG_puts("2cupsJSONLoadString: Got '}' in an array."); + DEBUG_puts("2cupsJSONImportString: Got '}' in an array."); goto invalid; } - DEBUG_puts("3cupsJSONLoadString: Closed object."); + DEBUG_puts("3cupsJSONImportString: Closed object."); if ((parent = parent->parent) == NULL) break; @@ -650,7 +980,7 @@ cupsJSONLoadString(const char *s) // I - JSON string // Start array if ((parent = cupsJSONNew(parent, prev, CUPS_JTYPE_ARRAY)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate array."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate array."); goto error; } @@ -658,7 +988,7 @@ cupsJSONLoadString(const char *s) // I - JSON string prev = NULL; s ++; - DEBUG_puts("3cupsJSONLoadString: Opened array."); + DEBUG_puts("3cupsJSONImportString: Opened array."); } else if (*s == ']') { @@ -666,11 +996,11 @@ cupsJSONLoadString(const char *s) // I - JSON string if (parent->type != CUPS_JTYPE_ARRAY) { // Not in an array, so this is unexpected... - DEBUG_puts("2cupsJSONLoadString: Got ']' in an object."); + DEBUG_puts("2cupsJSONImportString: Got ']' in an object."); goto invalid; } - DEBUG_puts("3cupsJSONLoadString: Closed array."); + DEBUG_puts("3cupsJSONImportString: Closed array."); parent = parent->parent; count = cupsJSONGetCount(parent); @@ -682,58 +1012,58 @@ cupsJSONLoadString(const char *s) // I - JSON string // null value if ((prev = cupsJSONNew(parent, prev, CUPS_JTYPE_NULL)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate null value."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate null value."); goto error; } count ++; s += 4; - DEBUG_puts("3cupsJSONLoadString: Added null value."); + DEBUG_puts("3cupsJSONImportString: Added null value."); } else if (!strncmp(s, "false", 5) && strchr(sep, s[5])) { // false value if ((prev = cupsJSONNew(parent, prev, CUPS_JTYPE_FALSE)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate false value."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate false value."); goto error; } count ++; s += 5; - DEBUG_puts("3cupsJSONLoadString: Added false value."); + DEBUG_puts("3cupsJSONImportString: Added false value."); } else if (!strncmp(s, "true", 4) && strchr(sep, s[4])) { // true value if ((prev = cupsJSONNew(parent, prev, CUPS_JTYPE_TRUE)) == NULL) { - DEBUG_puts("2cupsJSONLoadString: Unable to allocate true value."); + DEBUG_puts("2cupsJSONImportString: Unable to allocate true value."); goto error; } count ++; s += 4; - DEBUG_puts("3cupsJSONLoadString: Added true value."); + DEBUG_puts("3cupsJSONImportString: Added true value."); } else { // Something else we don't understand... - DEBUG_printf(("2cupsJSONLoadString: Unexpected '%s'.", s)); + DEBUG_printf(("2cupsJSONImportString: Unexpected '%s'.", s)); goto invalid; } } if (*s != '}') { - DEBUG_puts("2cupsJSONLoadString: Missing '}' at end."); + DEBUG_puts("2cupsJSONImportString: Missing '}' at end."); goto invalid; } - DEBUG_printf(("3cupsJSONLoadString: Returning %p (%u children)", (void *)json, (unsigned)cupsJSONGetCount(json))); + DEBUG_printf(("3cupsJSONImportString: Returning %p (%u children)", (void *)json, (unsigned)cupsJSONGetCount(json))); return (json); @@ -747,14 +1077,14 @@ cupsJSONLoadString(const char *s) // I - JSON string cupsJSONDelete(json); - DEBUG_puts("3cupsJSONLoadString: Returning NULL."); + DEBUG_puts("3cupsJSONImportString: Returning NULL."); return (NULL); } // -// 'cupsJSONLoadURL()' - Load a JSON object from a URL. +// 'cupsJSONImportURL()' - Load a JSON object from a URL. // // This function loads a JSON object from a URL. The "url" can be a "http:" or // "https:" URL. The "last_modified" argument provides a pointer to a `time_t` @@ -771,7 +1101,7 @@ cupsJSONLoadString(const char *s) // I - JSON string // cups_json_t * // O - Root JSON object node -cupsJSONLoadURL( +cupsJSONImportURL( const char *url, // I - URL time_t *last_modified) // IO - Last modified date/time or `NULL` { @@ -957,7 +1287,7 @@ cupsJSONLoadURL( // Load the JSON data, free the string, and return... if (data) { - json = cupsJSONLoadString(data); + json = cupsJSONImportString(data); free(data); } @@ -984,34 +1314,10 @@ cupsJSONNew(cups_json_t *parent, // I - Parent JSON node or `NULL` for a root n // Allocate the node... if ((node = calloc(1, sizeof(cups_json_t))) != NULL) { - node->parent = parent; - node->type = type; + node->type = type; if (parent) - { - // Add node to parent... - cups_json_t *current; // Current child node - - if (after) - { - // Append after the specified sibling... - node->sibling = after->sibling; - after->sibling = node; - } - else if ((current = parent->value.child) != NULL) - { - // Find the last child... - while (current && current->sibling) - current = current->sibling; - - current->sibling = node; - } - else - { - // This is the first child... - parent->value.child = node; - } - } + _cupsJSONAdd(parent, after, node); } return (node); @@ -1088,298 +1394,59 @@ cupsJSONNewString(cups_json_t *parent, // I - Parent JSON node or `NULL` for a r // -// 'cupsJSONSaveFile()' - Save a JSON node tree to a file. +// 'delete_json()' - Free the JSON node and its children. // -bool // O - `true` on success, `false` on failure -cupsJSONSaveFile(cups_json_t *json, // I - JSON root node - const char *filename) // I - JSON filename +static void +delete_json(cups_json_t *json) // I - JSON node { - char *s; // JSON string - int fd; // JSON file - - - DEBUG_printf(("cupsJSONSaveFile(json=%p, filename=\"%s\")", (void *)json, filename)); + cups_json_t *child, // Child node + *sibling; // Sibling node - // Get the JSON as a string... - if ((s = cupsJSONSaveString(json)) == NULL) - return (false); - // Create the file... - if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0) + if (json->type == CUPS_JTYPE_ARRAY || json->type == CUPS_JTYPE_OBJECT) { - _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); - free(s); - return (false); - } - - if (write(fd, s, strlen(s)) < 0) - { - _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); - close(fd); - unlink(filename); - free(s); - return (false); - } - - close(fd); - free(s); - - return (true); -} - - -// -// 'cupsJSONSaveString()' - Save a JSON node tree to a string. -// -// This function saves a JSON node tree to an allocated string. The resulting -// string must be freed using the `free` function. -// - -char * // O - JSON string or `NULL` on error -cupsJSONSaveString(cups_json_t *json) // I - JSON root node -{ - cups_json_t *current; // Current node - size_t length; // Length of JSON data as a string - char *s, // JSON string - *ptr; // Pointer into string - const char *value; // Pointer into string value - struct lconv *loc; // Locale data - - - DEBUG_printf(("cupsJSONSaveString(json=%p)", (void *)json)); - - // Range check input... - if (!json) - { - _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), 0); - DEBUG_puts("3cupsJSONSaveString: Returning NULL."); - return (NULL); - } - - // Figure out the necessary space needed in the string - current = json; - length = 1; // nul - - while (current) - { - if (current->parent && current->parent->value.child != current) - length ++; // Comma or colon separator - - switch (current->type) - { - case CUPS_JTYPE_NULL : - case CUPS_JTYPE_TRUE : - length += 4; - break; - - case CUPS_JTYPE_FALSE : - length += 5; - break; - - case CUPS_JTYPE_ARRAY : - case CUPS_JTYPE_OBJECT : - length += 2; // Brackets/braces - break; - - case CUPS_JTYPE_NUMBER : - length += 32; - break; - - case CUPS_JTYPE_KEY : - case CUPS_JTYPE_STRING : - length += 2; // Quotes - for (value = current->value.string; *value; value ++) - { - if (strchr("\\\"\b\f\n\r\t", *value)) - length += 2; // Simple escaped char - else if ((*value & 255) < ' ') - length += 6; // Worst case for control char - else - length ++; // Literal char - } - break; - } - - // Get next node... - if ((current->type == CUPS_JTYPE_ARRAY || current->type == CUPS_JTYPE_OBJECT) && current->value.child) - { - // Descend - current = current->value.child; - } - else if (current->sibling) - { - // Visit silbling - current = current->sibling; - } - else + for (child = json->value.child; child && child != json; child = sibling) { - // Ascend and continue... - current = current->parent; - while (current) + if ((child->type == CUPS_JTYPE_ARRAY || child->type == CUPS_JTYPE_OBJECT) && child->value.child) { - if (current->sibling) - { - current = current->sibling; - break; - } - else - { - current = current->parent; - } + // Descend into child nodes... + sibling = child->value.child; } - } - } - - DEBUG_printf(("2cupsJSONSaveString: length=%u", (unsigned)length)); - - // Allocate memory and fill it up... - if ((s = malloc(length)) == NULL) - { - _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), 0); - DEBUG_puts("3cupsJSONSaveString: Returning NULL."); - return (NULL); - } - - current = json; - ptr = s; - loc = localeconv(); - - while (current) - { - if (current->parent && current->parent->value.child != current) - { - // Add separator - if (current->type == CUPS_JTYPE_KEY || current->parent->type == CUPS_JTYPE_ARRAY) - *ptr++ = ','; - else - *ptr++ = ':'; - } - - switch (current->type) - { - case CUPS_JTYPE_NULL : - memcpy(ptr, "null", 4); - ptr += 4; - break; - - case CUPS_JTYPE_TRUE : - memcpy(ptr, "true", 4); - ptr += 4; - break; - - case CUPS_JTYPE_FALSE : - memcpy(ptr, "false", 5); - ptr += 5; - break; - - case CUPS_JTYPE_ARRAY : - *ptr++ = '['; - break; - - case CUPS_JTYPE_OBJECT : - *ptr++ = '{'; - break; - - case CUPS_JTYPE_NUMBER : - _cupsStrFormatd(ptr, s + length, current->value.number, loc); - ptr += strlen(ptr); - break; + else if ((sibling = child->sibling) == NULL) + { + // No more silbings, ascend unless the parent is the original node... + sibling = child->parent; + free_json(child); - case CUPS_JTYPE_KEY : - case CUPS_JTYPE_STRING : - *ptr++ = '\"'; + while (sibling && sibling != json) + { + cups_json_t *temp = sibling; // Save the current pointer - for (value = current->value.string; *value; value ++) + if (sibling->sibling) { - // Quote/escape as needed... - if (*value == '\\' || *value == '\"') - { - *ptr++ = '\\'; - *ptr++ = *value; - } - else if (*value == '\b') - { - *ptr++ = '\\'; - *ptr++ = 'b'; - } - else if (*value == '\f') - { - *ptr++ = '\\'; - *ptr++ = 'f'; - } - else if (*value == '\n') - { - *ptr++ = '\\'; - *ptr++ = 'n'; - } - else if (*value == '\r') - { - *ptr++ = '\\'; - *ptr++ = 'r'; - } - else if (*value == '\t') - { - *ptr++ = '\\'; - *ptr++ = 't'; - } - if ((*value & 255) < ' ') - { - snprintf(ptr, length - (size_t)(ptr - s), "\\u%04x", *value); - ptr += 6; - } - else - { - *ptr++ = *value; - } + // More siblings at this level, free the parent... + sibling = sibling->sibling; + free_json(temp); + break; } - - *ptr++ = '\"'; - break; - } - - // Get next node... - if ((current->type == CUPS_JTYPE_ARRAY || current->type == CUPS_JTYPE_OBJECT) && current->value.child) - { - // Descend - current = current->value.child; - } - else if (current->sibling) - { - // Visit silbling - current = current->sibling; - } - else if ((current = current->parent) != NULL) - { - // Ascend and continue... - if (current->type == CUPS_JTYPE_ARRAY) - *ptr++ = ']'; + else + { + // No more siblings, continue upwards... + sibling = sibling->parent; + free_json(temp); + } + } + } else - *ptr++ = '}'; - - while (current) { - if (current->sibling) - { - current = current->sibling; - break; - } - else if ((current = current->parent) != NULL) - { - if (current->type == CUPS_JTYPE_ARRAY) - *ptr++ = ']'; - else - *ptr++ = '}'; - } + // Free the memory for this node + free_json(child); } } } - *ptr = '\0'; - - DEBUG_printf(("3cupsJSONSaveString: Returning \"%s\".", s)); - - return (s); + free_json(json); } diff --git a/cups/json.h b/cups/json.h index 547c944f6..4d946f1ed 100644 --- a/cups/json.h +++ b/cups/json.h @@ -40,6 +40,9 @@ typedef struct _cups_json_s cups_json_t;// JSON node extern void cupsJSONDelete(cups_json_t *json) _CUPS_PUBLIC; +extern bool cupsJSONExportFile(cups_json_t *json, const char *filename) _CUPS_PUBLIC; +extern char *cupsJSONExportString(cups_json_t *json) _CUPS_PUBLIC; + extern cups_json_t *cupsJSONFind(cups_json_t *json, const char *key) _CUPS_PUBLIC; extern cups_json_t *cupsJSONGetChild(cups_json_t *json, size_t n) _CUPS_PUBLIC; @@ -51,18 +54,15 @@ extern double cupsJSONGetNumber(cups_json_t *json) _CUPS_PUBLIC; extern const char *cupsJSONGetString(cups_json_t *json) _CUPS_PUBLIC; extern cups_jtype_t cupsJSONGetType(cups_json_t *json) _CUPS_PUBLIC; -extern cups_json_t *cupsJSONLoadFile(const char *filename) _CUPS_PUBLIC; -extern cups_json_t *cupsJSONLoadString(const char *s) _CUPS_PUBLIC; -extern cups_json_t *cupsJSONLoadURL(const char *url, time_t *last_modified) _CUPS_PUBLIC; +extern cups_json_t *cupsJSONImportFile(const char *filename) _CUPS_PUBLIC; +extern cups_json_t *cupsJSONImportString(const char *s) _CUPS_PUBLIC; +extern cups_json_t *cupsJSONImportURL(const char *url, time_t *last_modified) _CUPS_PUBLIC; extern cups_json_t *cupsJSONNew(cups_json_t *parent, cups_json_t *after, cups_jtype_t type) _CUPS_PUBLIC; extern cups_json_t *cupsJSONNewKey(cups_json_t *parent, cups_json_t *after, const char *value) _CUPS_PUBLIC; extern cups_json_t *cupsJSONNewNumber(cups_json_t *parent, cups_json_t *after, double number) _CUPS_PUBLIC; extern cups_json_t *cupsJSONNewString(cups_json_t *parent, cups_json_t *after, const char *value) _CUPS_PUBLIC; -extern bool cupsJSONSaveFile(cups_json_t *json, const char *filename) _CUPS_PUBLIC; -extern char *cupsJSONSaveString(cups_json_t *json) _CUPS_PUBLIC; - # ifdef __cplusplus } diff --git a/cups/jwt.c b/cups/jwt.c new file mode 100644 index 000000000..7380293df --- /dev/null +++ b/cups/jwt.c @@ -0,0 +1,688 @@ +// +// JSON Web Token API implementation for CUPS. +// +// Copyright © 2023 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#include "cups-private.h" +#include "jwt.h" +#include "json-private.h" +#ifdef HAVE_OPENSSL +# include +#else +# include +#endif // HAVE_OPENSSL + + +// +// Constants... +// + +#define _CUPS_JWT_HMAC_B 64 // Block size for HMAC +#define _CUPS_JWT_MAX_SIGNATURE 32 // 256-bit hash + + +// +// Private types... +// + +struct _cups_jwt_s // JWT object +{ + cups_json_t *jose; // JOSE header + cups_json_t *claims; // JWT claims + cups_jwa_t sigalg; // Signature algorithm + size_t sigsize; // Size of signature + unsigned char *signature; // Signature +}; + + +// +// Local globals... +// + +static const char * const cups_jwa_strings[CUPS_JWA_MAX] = +{ + "none", // No algorithm + "HS256", // HMAC using SHA-256 + "HS384", // HMAC using SHA-384 + "HS512", // HMAC using SHA-512 + "RS256", // RSASSA-PKCS1-v1_5 using SHA-256 + "RS384", // RSASSA-PKCS1-v1_5 using SHA-384 + "RS512", // RSASSA-PKCS1-v1_5 using SHA-512 + "ES256", // ECDSA using P-256 and SHA-256 + "ES384", // ECDSA using P-384 and SHA-384 + "ES512" // ECDSA using P-521 and SHA-512 +}; + + +// +// Local functions... +// + +static bool make_signature(cups_jwt_t *jwt, cups_jwa_t alg, cups_json_t *jwk, unsigned char *signature, size_t *sigsize); +static char *make_string(cups_jwt_t *jwt, bool with_signature); + + +// +// 'cupsJWTDelete()' - Free the memory used for a JSON Web Token. +// + +void +cupsJWTDelete(cups_jwt_t *jwt) // I - JWT object +{ + if (jwt) + { + cupsJSONDelete(jwt->jose); + cupsJSONDelete(jwt->claims); + free(jwt->signature); + free(jwt); + } +} + + +// +// 'cupsJWTExportString()' - Export a JWT with the JWS Compact Serialization format. +// + +char * // O - JWT/JWS Compact Serialization string +cupsJWTExportString(cups_jwt_t *jwt) // I - JWT object +{ + if (jwt) + return (make_string(jwt, true)); + else + return (NULL); +} + + +// +// 'cupsJWTGetAlgorithm()' - Get the signature algorithm used by a JSON Web Token. +// + +cups_jwa_t // O - Signature algorithm +cupsJWTGetAlgorithm(cups_jwt_t *jwt) // I - JWT object +{ + return (jwt ? jwt->sigalg : CUPS_JWA_NONE); +} + + +// +// 'cupsJWTGetClaimNumber()' - Get the number value of a claim. +// + +double // O - Number value +cupsJWTGetClaimNumber(cups_jwt_t *jwt, // I - JWT object + const char *claim)// I - Claim name +{ + cups_json_t *node; // Value node + + + if (jwt && (node = cupsJSONFind(jwt->claims, claim)) != NULL) + return (cupsJSONGetNumber(node)); + else + return (0.0); +} + + +// +// 'cupsJWTGetClaimString()' - Get the string value of a claim. +// + +const char * // O - String value +cupsJWTGetClaimString(cups_jwt_t *jwt, // I - JWT object + const char *claim)// I - Claim name +{ + cups_json_t *node; // Value node + + + if (jwt && (node = cupsJSONFind(jwt->claims, claim)) != NULL) + return (cupsJSONGetString(node)); + else + return (NULL); +} + + +// +// 'cupsJWTGetClaimType()' - Get the value type of a claim. +// + +cups_jtype_t // O - JSON value type +cupsJWTGetClaimType(cups_jwt_t *jwt, // I - JWT object + const char *claim) // I - Claim name +{ + cups_json_t *node; // Value node + + + if (jwt && (node = cupsJSONFind(jwt->claims, claim)) != NULL) + return (cupsJSONGetType(node)); + else + return (CUPS_JTYPE_NULL); +} + + +// +// 'cupsJWTGetClaimValue()' - Get the value node of a claim. +// + +cups_json_t * // O - JSON value node +cupsJWTGetClaimValue(cups_jwt_t *jwt, // I - JWT object + const char *claim) // I - Claim name +{ + if (jwt) + return (cupsJSONFind(jwt->claims, claim)); + else + return (NULL); +} + + +// +// 'cupsJWTGetClaims()' - Get the JWT claims as a JSON object. +// + +cups_json_t * // O - JSON object +cupsJWTGetClaims(cups_jwt_t *jwt) // I - JWT object +{ + return (jwt ? jwt->claims : NULL); +} + + +// +// 'cupsJWTHasValidSignature()' - Determine whether the JWT has a valid signature. +// + +bool // O - `true` if value, `false` otherwise +cupsJWTHasValidSignature( + cups_jwt_t *jwt, // I - JWT object + cups_json_t *jwk) // I - JWK key set +{ + unsigned char signature[_CUPS_JWT_MAX_SIGNATURE]; + // Signature + size_t sigsize = _CUPS_JWT_MAX_SIGNATURE; + // Size of signature + + + // Range check input... + if (!jwt || !jwt->signature || !jwk) + return (false); + + // Calculate signature with keys... + if (!make_signature(jwt, jwt->sigalg, jwk, signature, &sigsize)) + return (false); + + fprintf(stderr, "orig sig(%u) = %02X%02X%02X%02X...%02X%02X%02X%02X\n", (unsigned)jwt->sigsize, jwt->signature[0], jwt->signature[1], jwt->signature[2], jwt->signature[3], jwt->signature[jwt->sigsize - 4], jwt->signature[jwt->sigsize - 3], jwt->signature[jwt->sigsize - 2], jwt->signature[jwt->sigsize - 1]); + fprintf(stderr, "calc sig(%u) = %02X%02X%02X%02X...%02X%02X%02X%02X\n", (unsigned)sigsize, signature[0], signature[1], signature[2], signature[3], signature[sigsize - 4], signature[sigsize - 3], signature[sigsize - 2], signature[sigsize - 1]); + + // Compae and return the result... + return (jwt->sigsize == sigsize && !memcmp(jwt->signature, signature, sigsize)); +} + + +// +// 'cupsJWTImportString()' - Import a JSON Web Token or JSON Web Signature. +// + +cups_jwt_t * // O - JWT object +cupsJWTImportString(const char *token) // I - JWS Compact Serialization string +{ + cups_jwt_t *jwt; // JWT object + const char *tokptr; // Pointer into the token + size_t datalen; // Size of data + char data[65536]; // Data + const char *alg; // Signature algorithm, if any + + + // Allocate a JWT... + if ((jwt = calloc(1, sizeof(cups_jwt_t))) == NULL) + return (NULL); + + // Extract the JOSE header... + datalen = sizeof(data) - 1; + if (!httpDecode64(data, &datalen, token, &tokptr)) + goto import_error; + + if (!tokptr || *tokptr != '.') + goto import_error; + + tokptr ++; + data[datalen] = '\0'; + if ((jwt->jose = cupsJSONImportString(data)) == NULL) + goto import_error; + + // Extract the JWT claims... + datalen = sizeof(data) - 1; + if (!httpDecode64(data, &datalen, tokptr, &tokptr)) + goto import_error; + + if (!tokptr || *tokptr != '.') + goto import_error; + + tokptr ++; + data[datalen] = '\0'; + if ((jwt->claims = cupsJSONImportString(data)) == NULL) + goto import_error; + + // Extract the signature, if any... + datalen = sizeof(data); + if (!httpDecode64(data, &datalen, tokptr, &tokptr)) + goto import_error; + + if (!tokptr || *tokptr) + goto import_error; + + if (datalen > 0) + { + if ((jwt->signature = malloc(datalen)) == NULL) + goto import_error; + + memcpy(jwt->signature, data, datalen); + jwt->sigsize = datalen; + } + + if ((alg = cupsJSONGetString(cupsJSONFind(jwt->jose, "alg"))) != NULL) + { + cups_jwa_t sigalg; // Signing algorithm + + for (sigalg = CUPS_JWA_NONE; sigalg < CUPS_JWA_MAX; sigalg ++) + { + if (!strcmp(alg, cups_jwa_strings[sigalg])) + { + jwt->sigalg = sigalg; + break; + } + } + } + + // Can't have signature with none or no signature for !none... + if ((jwt->sigalg == CUPS_JWA_NONE) != (jwt->sigsize == 0)) + goto import_error; + + // Return the new JWT... + return (jwt); + + import_error: + + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Invalid JSON web token."), 1); + cupsJWTDelete(jwt); + + return (NULL); +} + + +// +// 'cupsJWTNew()' - Create a new, empty JSON Web Token. +// + +cups_jwt_t * // O - JWT object +cupsJWTNew(const char *type) // I - JWT type or `NULL` for default ("JWT") +{ + cups_jwt_t *jwt; // JWT object + + + if ((jwt = calloc(1, sizeof(cups_jwt_t))) != NULL) + { + if ((jwt->jose = cupsJSONNew(NULL, NULL, CUPS_JTYPE_OBJECT)) != NULL) + { + cupsJSONNewString(jwt->jose, cupsJSONNewKey(jwt->jose, NULL, "typ"), type ? type : "JWT"); + + if ((jwt->claims = cupsJSONNew(NULL, NULL, CUPS_JTYPE_OBJECT)) != NULL) + return (jwt); + } + } + + cupsJWTDelete(jwt); + return (NULL); +} + + +// +// 'cupsJWTSetClaimNumber()' - Set a claim number. +// + +void +cupsJWTSetClaimNumber(cups_jwt_t *jwt, // I - JWT object + const char *claim,// I - Claim name + double value) // I - Number value +{ + // Range check input + if (!jwt || !claim) + return; + + // Remove existing claim, if any... + _cupsJSONDelete(jwt->claims, claim); + + // Add claim... + cupsJSONNewNumber(jwt->claims, cupsJSONNewKey(jwt->claims, NULL, claim), value); +} + + +// +// 'cupsJWTSetClaimString()' - Set a claim string. +// + +void +cupsJWTSetClaimString(cups_jwt_t *jwt, // I - JWT object + const char *claim,// I - Claim name + const char *value)// I - String value +{ + // Range check input + if (!jwt || !claim || !value) + return; + + // Remove existing claim, if any... + _cupsJSONDelete(jwt->claims, claim); + + // Add claim... + cupsJSONNewString(jwt->claims, cupsJSONNewKey(jwt->claims, NULL, claim), value); +} + + +// +// 'cupsJWTSetClaimValue()' - Set a claim value. +// + +void +cupsJWTSetClaimValue( + cups_jwt_t *jwt, // I - JWT object + const char *claim, // I - Claim name + cups_json_t *value) // I - JSON value node +{ + // Range check input + if (!jwt || !claim) + return; + + // Remove existing claim, if any... + _cupsJSONDelete(jwt->claims, claim); + + // Add claim... + _cupsJSONAdd(jwt->claims, cupsJSONNewKey(jwt->claims, NULL, claim), value); +} + + +// +// 'cupsJWTSign()' - Sign a JSON Web Token, creating a JSON Web Signature. +// + +bool // O - `true` on success, `false` on error +cupsJWTSign(cups_jwt_t *jwt, // I - JWT object + cups_jwa_t alg, // I - Signing algorithm + cups_json_t *jwk) // I - JWK key set +{ + unsigned char signature[_CUPS_JWT_MAX_SIGNATURE]; + // Signature + size_t sigsize = _CUPS_JWT_MAX_SIGNATURE; + // Size of signature + + + // Range check input... + if (!jwt || alg <= CUPS_JWA_NONE || alg >= CUPS_JWA_MAX || !jwk) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), 0); + return (false); + } + + // Clear existing signature... + free(jwt->signature); + jwt->signature = NULL; + jwt->sigsize = 0; + jwt->sigalg = CUPS_JWA_NONE; + + _cupsJSONDelete(jwt->jose, "alg"); + + // Create new signature... + if (!make_signature(jwt, alg, jwk, signature, &sigsize)) + return (false); + + if ((jwt->signature = malloc(sigsize)) == NULL) + return (false); + + memcpy(jwt->signature, signature, sigsize); + jwt->sigalg = alg; + jwt->sigsize = sigsize; + + cupsJSONNewString(jwt->jose, cupsJSONNewKey(jwt->jose, NULL, "alg"), cups_jwa_strings[alg]); + + return (true); +} + + +// +// 'make_signature()' - Make a signature. +// + +static bool // O - `true` on success, `false` on failure +make_signature(cups_jwt_t *jwt, // I - JWT + cups_jwa_t alg, // I - Algorithm + cups_json_t *jwk, // I - JSON Web Key Set + unsigned char *signature,// I - Signature buffer + size_t *sigsize) // IO - Signature size +{ + const char *k; // "k" value + char key[256]; // Key value + size_t key_idx, // Index into key + key_len; // Length of key + char *text; // JWS Signing Input + size_t text_len; // Lengtb of signing input + unsigned char buffer[65536], // Buffer to hash + hash[32]; // SHA2-256 hash + size_t buffer_len; // Length of buffer + + + *sigsize = 0; + + memset(key, 0, sizeof(key)); + k = cupsJSONGetString(cupsJSONFind(jwk, "k")); + key_len = sizeof(key); + httpDecode64(key, &key_len, k, NULL); + + if (key_len > 64) + { + cupsHashData("sha2-256", key, key_len, hash, sizeof(hash)); + memcpy(key, hash, 32); + memset(key + 32, 0, 32); + key_len = 64; + } + + fprintf(stderr, "alg=%u(%s)\n", alg, cups_jwa_strings[alg]); + fprintf(stderr, "key(%u): [%d", (unsigned)key_len, key[0] & 255); + for (size_t i = 1; i < key_len; i ++) + fprintf(stderr, ", %d", key[i] & 255); + fputs("]\n", stderr); + + text = make_string(jwt, false); + text_len = strlen(text); + + fprintf(stderr, "text (%u): %s\n", (unsigned)text_len, text); + fprintf(stderr, "text (%u): [%d", (unsigned)text_len, *text); + for (char *tptr = text + 1; *tptr; tptr ++) + fprintf(stderr, ", %d", *tptr); + fputs("]\n", stderr); + + // H1 = H(K XOR ipad,text) + for (key_idx = 0; key_idx < _CUPS_JWT_HMAC_B; key_idx ++) + buffer[key_idx] = key[key_idx] ^ 0x36; + memcpy(buffer + _CUPS_JWT_HMAC_B, text, text_len); + buffer_len = _CUPS_JWT_HMAC_B + text_len; + + cupsHashData("sha2-256", buffer, buffer_len, hash, sizeof(hash)); + + // H(K XOR opad,H1) + for (key_idx = 0; key_idx < _CUPS_JWT_HMAC_B; key_idx ++) + buffer[key_idx] = key[key_idx] ^ 0x5c; + memcpy(buffer + _CUPS_JWT_HMAC_B, hash, sizeof(hash)); + buffer_len = _CUPS_JWT_HMAC_B + sizeof(hash); + cupsHashData("sha2-256", buffer, buffer_len, signature, sizeof(hash)); + + free(text); + + *sigsize = sizeof(hash); + + fprintf(stderr, "HMAC: %s\n", cupsHashString(signature, 32, key, sizeof(key))); + +#if 0 +#ifdef HAVE_OPENSSL + const EVP_MD *md; // Message digest + unsigned mdlen; // Length of digest + const char *k = NULL; // "k" value + char key[512]; // Key value + size_t key_len; // Length of key + char *text; // JWS Signing Input + + + *sigsize = 0; + + switch (alg) + { + case CUPS_JWA_HS256 : // HMAC using SHA-256 + k = cupsJSONGetString(cupsJSONFind(jwk, "k")); + md = EVP_sha256(); + break; + + case CUPS_JWA_HS384 : // HMAC using SHA-384 + k = cupsJSONGetString(cupsJSONFind(jwk, "k")); + md = EVP_sha384(); + break; + + case CUPS_JWA_HS512 : // HMAC using SHA-512 + k = cupsJSONGetString(cupsJSONFind(jwk, "k")); + md = EVP_sha512(); + break; + + default : + return (false); + +# if 0 + case CUPS_JWA_RS256 : // RSASSA-PKCS1-v1_5 using SHA-256 + break; + + case CUPS_JWA_RS384 : // RSASSA-PKCS1-v1_5 using SHA-384 + break; + + case CUPS_JWA_RS512 : // RSASSA-PKCS1-v1_5 using SHA-512 + break; + + case CUPS_JWA_ES256 : // ECDSA using P-256 and SHA-256 + break; + + case CUPS_JWA_ES384 : // ECDSA using P-384 and SHA-384 + break; + + case CUPS_JWA_ES512 : // ECDSA using P-521 and SHA-512 + break; +# endif // 0 + } + + if (!k) + return (false); + + key_len = sizeof(key); + httpDecode64(key, &key_len, k, NULL); + + fprintf(stderr, "alg=%u(%s)\n", alg, cups_jwa_strings[alg]); + fprintf(stderr, "key(%u): [%d", (unsigned)key_len, key[0] & 255); + for (size_t i = 1; i < key_len; i ++) + fprintf(stderr, ", %d", key[i] & 255); + fputs("]\n", stderr); + + text = make_string(jwt, false); + mdlen = (unsigned)*sigsize; + + fprintf(stderr, "text (%u): %s\n", (unsigned)strlen(text), text); + fprintf(stderr, "text (%u): [%d", (unsigned)strlen(text), *text); + for (char *tptr = text + 1; *tptr; tptr ++) + fprintf(stderr, ", %d", *tptr); + fputs("]\n", stderr); + + HMAC(md, key, (int)key_len, (unsigned char *)text, strlen(text), signature, &mdlen); + + free(text); + *sigsize = mdlen; + +#else + *sigsize = 0; + + switch (alg) + { + case CUPS_JWA_HS256 : // HMAC using SHA-256 + case CUPS_JWA_HS384 : // HMAC using SHA-384 + case CUPS_JWA_HS512 : // HMAC using SHA-512 + break; + + case CUPS_JWA_RS256 : // RSASSA-PKCS1-v1_5 using SHA-256 + case CUPS_JWA_RS384 : // RSASSA-PKCS1-v1_5 using SHA-384 + case CUPS_JWA_RS512 : // RSASSA-PKCS1-v1_5 using SHA-512 + break; + + case CUPS_JWA_ES256 : // ECDSA using P-256 and SHA-256 + case CUPS_JWA_ES384 : // ECDSA using P-384 and SHA-384 + case CUPS_JWA_ES512 : // ECDSA using P-521 and SHA-512 + break; + } +#endif // HAVE_OPENSSL +#endif // 0 + + return (true); +} + + +// +// 'make_string()' - Make a JWT/JWS Compact Serialization string. +// + +static char * // O - JWT/JWS string +make_string(cups_jwt_t *jwt, // I - JWT object + bool with_signature) // I - Include signature field? +{ + char *jose, // JOSE header string + *claims, // Claims string + *s = NULL, // JWT/JWS string + *ptr, // Pointer into string + *end; // End of string + size_t jose_len, // Length of JOSE header + claims_len, // Length of claims string + len; // Allocation length + + + // Get the JOSE header and claims object strings... + jose = cupsJSONExportString(jwt->jose); + claims = cupsJSONExportString(jwt->claims); + + if (!jose || !claims) + goto done; + + jose_len = strlen(jose); + claims_len = strlen(claims); + + // Calculate the maximum Base64URL-encoded string length... + len = ((jose_len + 2) * 4 / 3) + 1 + ((claims_len + 2) * 4 / 3) + 1 + ((_CUPS_JWT_MAX_SIGNATURE + 2) * 4 / 3) + 1; + + if ((s = malloc(len)) == NULL) + goto done; + + ptr = s; + end = s + len; + + httpEncode64(ptr, (size_t)(end - ptr), jose, jose_len, true); + ptr += strlen(ptr); + *ptr++ = '.'; + + httpEncode64(ptr, (size_t)(end - ptr), claims, claims_len, true); + ptr += strlen(ptr); + + if (with_signature) + { + *ptr++ = '.'; + + if (jwt->sigsize) + httpEncode64(ptr, (size_t)(end - ptr), (char *)jwt->signature, jwt->sigsize, true); + } + + // Free temporary strings... + done: + + free(jose); + free(claims); + + return (s); +} diff --git a/cups/jwt.h b/cups/jwt.h new file mode 100644 index 000000000..581e325f2 --- /dev/null +++ b/cups/jwt.h @@ -0,0 +1,78 @@ +// +// JSON Web Token API definitions for CUPS. +// +// Copyright © 2023 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#ifndef _CUPS_JWT_H_ +# define _CUPS_JWT_H_ +# include "json.h" +# ifdef __cplusplus +extern "C" { +# endif // __cplusplus + + +// +// Constants... +// + +#define CUPS_JWT_AUD "aud" // JWT audience claim +#define CUPS_JWT_EXP "exp" // JWT expiration date/time claim +#define CUPS_JWT_IAT "iat" // JWT issued at date/time claim +#define CUPS_JWT_ISS "iss" // JWT issuer claim (authorization server) +#define CUPS_JWT_JTI "jti" // JWT unique identifier claim +#define CUPS_JWT_NAME "name" // OpenID display name +#define CUPS_JWT_NBF "nbf" // JWT not before date/time claim +#define CUPS_JWT_SUB "sub" // JWT subject claim (username/ID) + + +// +// Types... +// + +typedef enum cups_jwa_e // JSON Web Algorithms +{ + CUPS_JWA_NONE, // No algorithm + CUPS_JWA_HS256, // HMAC using SHA-256 + CUPS_JWA_HS384, // HMAC using SHA-384 + CUPS_JWA_HS512, // HMAC using SHA-512 + CUPS_JWA_RS256, // RSASSA-PKCS1-v1_5 using SHA-256 + CUPS_JWA_RS384, // RSASSA-PKCS1-v1_5 using SHA-384 + CUPS_JWA_RS512, // RSASSA-PKCS1-v1_5 using SHA-512 + CUPS_JWA_ES256, // ECDSA using P-256 and SHA-256 + CUPS_JWA_ES384, // ECDSA using P-384 and SHA-384 + CUPS_JWA_ES512, // ECDSA using P-521 and SHA-512 + CUPS_JWA_MAX // @private@ Maximum JWA value +} cups_jwa_t; + +typedef struct _cups_jwt_s cups_jwt_t; // JSON Web Token + + +// +// Functions... +// + +extern void cupsJWTDelete(cups_jwt_t *jwt) _CUPS_PUBLIC; +extern char *cupsJWTExportString(cups_jwt_t *jwt) _CUPS_PUBLIC; +extern cups_jwa_t cupsJWTGetAlgorithm(cups_jwt_t *jwt) _CUPS_PUBLIC; +extern double cupsJWTGetClaimNumber(cups_jwt_t *jwt, const char *claim) _CUPS_PUBLIC; +extern const char *cupsJWTGetClaimString(cups_jwt_t *jwt, const char *claim) _CUPS_PUBLIC; +extern cups_jtype_t cupsJWTGetClaimType(cups_jwt_t *jwt, const char *claim) _CUPS_PUBLIC; +extern cups_json_t *cupsJWTGetClaimValue(cups_jwt_t *jwt, const char *claim) _CUPS_PUBLIC; +extern cups_json_t *cupsJWTGetClaims(cups_jwt_t *jwt) _CUPS_PUBLIC; +extern bool cupsJWTHasValidSignature(cups_jwt_t *jwt, cups_json_t *keys) _CUPS_PUBLIC; +extern cups_jwt_t *cupsJWTImportString(const char *token) _CUPS_PUBLIC; +extern cups_jwt_t *cupsJWTNew(const char *type) _CUPS_PUBLIC; +extern void cupsJWTSetClaimNumber(cups_jwt_t *jwt, const char *claim, double value) _CUPS_PUBLIC; +extern void cupsJWTSetClaimString(cups_jwt_t *jwt, const char *claim, const char *value) _CUPS_PUBLIC; +extern void cupsJWTSetClaimValue(cups_jwt_t *jwt, const char *claim, cups_json_t *value) _CUPS_PUBLIC; +extern bool cupsJWTSign(cups_jwt_t *jwt, cups_jwa_t alg, cups_json_t *keys) _CUPS_PUBLIC; + + +# ifdef __cplusplus +} +# endif // __cplusplus +#endif // !_CUPS_JWT_H_ diff --git a/cups/testjson.c b/cups/testjson.c index 22a0e1702..025138368 100644 --- a/cups/testjson.c +++ b/cups/testjson.c @@ -33,7 +33,7 @@ main(int argc, // I - Number of command-line arguments size_t count; // Number of children char *s; // JSON string time_t last_modified = 0; // Last-Modified value - static const char * const types[] = // Note types + static const char * const types[] = // Node types { "CUPS_JTYPE_NULL", // Null value "CUPS_JTYPE_FALSE", // Boolean false value @@ -181,13 +181,13 @@ main(int argc, // I - Number of command-line arguments count = cupsJSONGetCount(json); testEndMessage(count == 14, "%u", (unsigned)count); - testBegin("cupsJSONSaveFile(root, 'test.json')"); - if (cupsJSONSaveFile(json, "test.json")) + testBegin("cupsJSONExportFile(root, 'test.json')"); + if (cupsJSONExportFile(json, "test.json")) { testEnd(true); - testBegin("cupsJSONLoadFile('test.json')"); - parent = cupsJSONLoadFile("test.json"); + testBegin("cupsJSONImportFile('test.json')"); + parent = cupsJSONImportFile("test.json"); testEnd(parent != NULL); cupsJSONDelete(parent); @@ -197,13 +197,13 @@ main(int argc, // I - Number of command-line arguments testEndMessage(false, "%s", cupsLastErrorString()); } - testBegin("cupsJSONSaveString(root)"); - if ((s = cupsJSONSaveString(json)) != NULL) + testBegin("cupsJSONExportString(root)"); + if ((s = cupsJSONExportString(json)) != NULL) { testEnd(true); - testBegin("cupsJSONLoadString('%s')", s); - parent = cupsJSONLoadString(s); + testBegin("cupsJSONImportString('%s')", s); + parent = cupsJSONImportString(s); testEnd(parent != NULL); cupsJSONDelete(parent); @@ -218,8 +218,8 @@ main(int argc, // I - Number of command-line arguments cupsJSONDelete(json); testEnd(true); - testBegin("cupsJSONLoadURL('https://accounts.google.com/.well-known/openid-configuration', no last modified)"); - json = cupsJSONLoadURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); + testBegin("cupsJSONImportURL('https://accounts.google.com/.well-known/openid-configuration', no last modified)"); + json = cupsJSONImportURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); if (json) { @@ -228,8 +228,8 @@ main(int argc, // I - Number of command-line arguments testEnd(true); cupsJSONDelete(json); - testBegin("cupsJSONLoadURL('https://accounts.google.com/.well-known/openid-configuration', since %s)", httpGetDateString(last_modified, last_modified_date, sizeof(last_modified_date))); - json = cupsJSONLoadURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); + testBegin("cupsJSONImportURL('https://accounts.google.com/.well-known/openid-configuration', since %s)", httpGetDateString(last_modified, last_modified_date, sizeof(last_modified_date))); + json = cupsJSONImportURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); if (json) testEnd(true); @@ -260,12 +260,12 @@ main(int argc, // I - Number of command-line arguments if (argv[i][0] == '{') { // Load JSON string... - if ((json = cupsJSONLoadString(argv[i])) != NULL) + if ((json = cupsJSONImportString(argv[i])) != NULL) printf("string%d: OK, %u key/value pairs in root object.\n", i, (unsigned)(cupsJSONGetCount(json) / 2)); else fprintf(stderr, "string%d: %s\n", i, cupsLastErrorString()); } - else if ((json = cupsJSONLoadFile(argv[i])) != NULL) + else if ((json = cupsJSONImportFile(argv[i])) != NULL) printf("%s: OK, %u key/value pairs in root object.\n", argv[i], (unsigned)(cupsJSONGetCount(json) / 2)); else fprintf(stderr, "%s: %s\n", argv[i], cupsLastErrorString()); diff --git a/cups/testjwt.c b/cups/testjwt.c new file mode 100644 index 000000000..573b4e847 --- /dev/null +++ b/cups/testjwt.c @@ -0,0 +1,374 @@ +// +// JWT API unit tests for CUPS. +// +// Copyright © 2023 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#include "cups.h" +#include "jwt.h" +#include "test-internal.h" + + +// +// 'main()' - Main entry. +// + +int // O - Exit status +main(int argc, // I - Number of command-line arguments + char *argv[]) // I - Command-line arguments +{ + int i; // Looping var + cups_jwt_t *jwt; // JSON Web Token object + + + if (argc == 1) + { + // Do unit tests... + static const char * const examples[][2] = + { // JWT examples + { + "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9." + "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQo" + "gImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ." + "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "{\"kty\":\"oct\"," + "\"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75" + "aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}" + }, + { + "eyJhbGciOiJSUzI1NiJ9." + "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt" + "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ." + "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7" + "AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4" + "BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K" + "0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqv" + "hJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrB" + "p0igcN_IoypGlUPQGe77Rw", + "{\"kty\":\"RSA\"," + "\"n\":\"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx" + "HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs" + "D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH" + "SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV" + "MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8" + "NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ\"," + "\"e\":\"AQAB\"," + "\"d\":\"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I" + "jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0" + "BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn" + "439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT" + "CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh" + "BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ\"," + "\"p\":\"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi" + "YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG" + "BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc\"," + "\"q\":\"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa" + "ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA" + "-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc\"," + "\"dp\":\"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q" + "CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb" + "34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0\"," + "\"dq\":\"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa" + "7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky" + "NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU\"," + "\"qi\":\"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o" + "y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU" + "W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U\"" + "}" + }, + { + "eyJhbGciOiJFUzI1NiJ9." + "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt" + "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ." + "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSA" + "pmWQxfKTUJqPP3-Kg6NU1Q", + "{\"kty\":\"EC\"," + "\"crv\":\"P-256\"," + "\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\"," + "\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\"," + "\"d\":\"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI\"" + "}" + } + }; +#if 0 + static const char * const types[] = // Node types + { + "CUPS_JTYPE_NULL", // Null value + "CUPS_JTYPE_FALSE", // Boolean false value + "CUPS_JTYPE_TRUE", // Boolean true value + "CUPS_JTYPE_NUMBER", // Number value + "CUPS_JTYPE_STRING", // String value + "CUPS_JTYPE_ARRAY", // Array value + "CUPS_JTYPE_OBJECT", // Object value + "CUPS_JTYPE_KEY" // Object key (string) + }; +#endif // 0 + + testBegin("cupsJWTNew(NULL)"); + jwt = cupsJWTNew(NULL); + testEnd(jwt != NULL); + + for (i = 0; i < (int)(sizeof(examples) / sizeof(examples[0])); i ++) + { + cups_json_t *jwk = NULL; // JSON Web Key Set + char *claims; // Claims + + testBegin("cupsJWTImportString(\"%s\")", examples[i][0]); + if ((jwt = cupsJWTImportString(examples[i][0])) != NULL) + { + testEnd(true); + + claims = cupsJSONExportString(cupsJWTGetClaims(jwt)); + testMessage("Claims: %s", claims); + free(claims); + + testBegin("cupsJSONImportString(\"%s\")", examples[i][1]); + if ((jwk = cupsJSONImportString(examples[i][1])) != NULL) + { + testEnd(true); + testBegin("cupsJWTHasValidSignature()"); + testEnd(cupsJWTHasValidSignature(jwt, jwk)); + } + else + { + testEndMessage(false, "%s", cupsLastErrorString()); + } + } + else + { + testEndMessage(false, "%s", cupsLastErrorString()); + } + + cupsJSONDelete(jwk); + cupsJWTDelete(jwt); + } + +#if 0 + testBegin("cupsJSONGetCount(root)"); + count = cupsJSONGetCount(json); + testEndMessage(count == 0, "%u", (unsigned)count); + + testBegin("cupsJSONGetType(root)"); + type = cupsJSONGetType(json); + testEndMessage(type == CUPS_JTYPE_OBJECT, "%s", types[type]); + + testBegin("cupsJSONNewKey('string')"); + current = cupsJSONNewKey(json, NULL, "string"); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(key)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_KEY, "%s", types[type]); + + testBegin("cupsJSONNewString('value')"); + current = cupsJSONNewString(json, current, "value"); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(string)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_STRING, "%s", types[type]); + + testBegin("cupsJSONNewKey('number')"); + current = cupsJSONNewKey(json, NULL, "number"); + testEnd(current != NULL); + + testBegin("cupsJSONNewNumber(42)"); + current = cupsJSONNewNumber(json, current, 42); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(number)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_NUMBER, "%s", types[type]); + + testBegin("cupsJSONNewKey('null')"); + current = cupsJSONNewKey(json, NULL, "null"); + testEnd(current != NULL); + + testBegin("cupsJSONNew(null)"); + current = cupsJSONNew(json, current, CUPS_JTYPE_NULL); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(null)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_NULL, "%s", types[type]); + + testBegin("cupsJSONNewKey('false')"); + current = cupsJSONNewKey(json, NULL, "false"); + testEnd(current != NULL); + + testBegin("cupsJSONNew(false)"); + current = cupsJSONNew(json, current, CUPS_JTYPE_FALSE); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(false)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_FALSE, "%s", types[type]); + + testBegin("cupsJSONNewKey('true')"); + current = cupsJSONNewKey(json, NULL, "true"); + testEnd(current != NULL); + + testBegin("cupsJSONNew(true)"); + current = cupsJSONNew(json, current, CUPS_JTYPE_TRUE); + testEnd(current != NULL); + + testBegin("cupsJSONGetType(true)"); + type = cupsJSONGetType(current); + testEndMessage(type == CUPS_JTYPE_TRUE, "%s", types[type]); + + testBegin("cupsJSONNewKey('array')"); + current = cupsJSONNewKey(json, NULL, "array"); + testEnd(current != NULL); + + testBegin("cupsJSONNew(array)"); + parent = cupsJSONNew(json, current, CUPS_JTYPE_ARRAY); + testEnd(parent != NULL); + + testBegin("cupsJSONGetType(array)"); + type = cupsJSONGetType(parent); + testEndMessage(type == CUPS_JTYPE_ARRAY, "%s", types[type]); + + testBegin("cupsJSONNewString(array, 'foo')"); + current = cupsJSONNewString(parent, NULL, "foo"); + testEnd(current != NULL); + + testBegin("cupsJSONNewString(array, 'bar')"); + current = cupsJSONNewString(parent, current, "bar"); + testEnd(current != NULL); + + testBegin("cupsJSONNewNumber(array, 0.5)"); + current = cupsJSONNewNumber(parent, current, 0.5); + testEnd(current != NULL); + + testBegin("cupsJSONNewNumber(array, 123456789123456789.0)"); + current = cupsJSONNewNumber(parent, current, 123456789123456789.0); + testEnd(current != NULL); + + testBegin("cupsJSONNew(array, null)"); + current = cupsJSONNew(parent, current, CUPS_JTYPE_NULL); + testEnd(current != NULL); + + testBegin("cupsJSONNewKey('object')"); + current = cupsJSONNewKey(json, NULL, "object"); + testEnd(current != NULL); + + testBegin("cupsJSONNew(object)"); + parent = cupsJSONNew(json, current, CUPS_JTYPE_OBJECT); + testEnd(parent != NULL); + + testBegin("cupsJSONNewKey(object, 'a')"); + current = cupsJSONNewKey(parent, NULL, "a"); + testEnd(current != NULL); + + testBegin("cupsJSONNewString(object, 'one')"); + current = cupsJSONNewString(parent, current, "one"); + testEnd(current != NULL); + + testBegin("cupsJSONNewKey(object, 'b')"); + current = cupsJSONNewKey(parent, current, "b"); + testEnd(current != NULL); + + testBegin("cupsJSONNewNumber(object, 2)"); + current = cupsJSONNewNumber(parent, current, 2); + testEnd(current != NULL); + + testBegin("cupsJSONGetCount(root)"); + count = cupsJSONGetCount(json); + testEndMessage(count == 14, "%u", (unsigned)count); + + testBegin("cupsJSONSaveFile(root, 'test.json')"); + if (cupsJSONSaveFile(json, "test.json")) + { + testEnd(true); + + testBegin("cupsJSONLoadFile('test.json')"); + parent = cupsJSONLoadFile("test.json"); + testEnd(parent != NULL); + + cupsJSONDelete(parent); + } + else + { + testEndMessage(false, "%s", cupsLastErrorString()); + } + + testBegin("cupsJSONSaveString(root)"); + if ((s = cupsJSONSaveString(json)) != NULL) + { + testEnd(true); + + testBegin("cupsJSONLoadString('%s')", s); + parent = cupsJSONLoadString(s); + testEnd(parent != NULL); + + cupsJSONDelete(parent); + free(s); + } + else + { + testEndMessage(false, "%s", cupsLastErrorString()); + } + + testBegin("cupsJSONDelete(root)"); + cupsJSONDelete(json); + testEnd(true); + + testBegin("cupsJSONLoadURL('https://accounts.google.com/.well-known/openid-configuration', no last modified)"); + json = cupsJSONLoadURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); + + if (json) + { + char last_modified_date[256];// Last-Modified string value + + testEnd(true); + cupsJSONDelete(json); + + testBegin("cupsJSONLoadURL('https://accounts.google.com/.well-known/openid-configuration', since %s)", httpGetDateString(last_modified, last_modified_date, sizeof(last_modified_date))); + json = cupsJSONLoadURL("https://accounts.google.com/.well-known/openid-configuration", &last_modified); + + if (json) + testEnd(true); + else if (cupsLastError() == IPP_STATUS_OK_EVENTS_COMPLETE) + testEndMessage(true, "no change from last request"); + else + testEndMessage(false, cupsLastErrorString()); + + cupsJSONDelete(json); + } + else if (cupsLastError() == IPP_STATUS_ERROR_SERVICE_UNAVAILABLE) + { + testEndMessage(true, "%s", cupsLastErrorString()); + } + else + { + testEndMessage(false, "%s", cupsLastErrorString()); + } +#endif // 0 + + if (!testsPassed) + return (1); + } + else + { + // Try loading JWT string on the command-line... + for (i = 1; i < argc; i ++) + { + if ((jwt = cupsJWTImportString(argv[i])) != NULL) + { +// printf("%s: OK, %u key/value pairs in root object.\n", argv[i], (unsigned)(cupsJSONGetCount(json) / 2)); + + cupsJWTDelete(jwt); + } + else + { + fprintf(stderr, "%s: %s\n", argv[i], cupsLastErrorString()); + return (1); + } + } + } + + return (0); +}