//
// OAuth utility for CUPS.
//
// Copyright © 2024-2025 by OpenPrinting.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//
// Usage: cups-oauth [OPTIONS] [SUB-COMMAND [ARGUMENT(S)]]
//
// Commands:
//
//   authorize [RESOURCE]
//   clear [RESOURCE]
//   get-access-token [RESOURCE]
//   get-client-id
//   get-device-grant [RESOURCE]
//   get-device-token [RESOURCE] DEVICE-CODE
//   get-metadata [NAME]
//   get-refresh-token [RESOURCE]
//   get-user-id [RESOURCE] [NAME]
//   set-access-token [RESOURCE] TOKEN
//   set-client-data CLIENT-ID CLIENT-SECRET
//
// Options:
//
//   --help
//   --version
//   -a OAUTH-URI
//   -s SCOPE(S)
//

#include <config.h>
#include <cups/cups.h>
#include <cups/oauth.h>


//
// Macro for localized text...
//

#  define _(x) x


//
// Local functions...
//

static int	do_authorize(const char *oauth_uri, const char *scopes, const char *resource_uri);
static int	do_clear(const char *oauth_uri, const char *resource_uri);
static int	do_get_access_token(const char *oauth_uri, const char *resource_uri);
static int	do_get_client_id(const char *oauth_uri);
static int	do_get_device_grant(const char *oauth_uri, const char *scopes, const char *resource_uri);
static int	do_get_device_token(const char *oauth_uri, const char *resource_uri, const char *device_code);
static int	do_get_metadata(const char *oauth_uri, const char *name);
static int	do_get_user_id(const char *oauth_uri, const char *resource_uri, const char *name);
static int	do_set_access_token(const char *oauth_uri, const char *resource_uri, const char *token);
static int	do_set_client_data(const char *oauth_uri, const char *client_id, const char *client_secret);
static int	usage(FILE *out);


//
// '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
  const char	*opt,			// Current option
		*oauth_uri = getenv("CUPS_OAUTH_URI"),
					// OAuth authorization server URI
		*scopes = getenv("CUPS_OAUTH_SCOPES");
					// Scopes


  // Parse the command-line...
  for (i = 1; i < argc; i ++)
  {
    if (!strcmp(argv[i], "--help"))
    {
      return (usage(stdout));
    }
    else if (!strcmp(argv[i], "--version"))
    {
      puts(LIBCUPS_VERSION);
      exit(0);
    }
    else if (!strncmp(argv[i], "--", 2))
    {
      cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "cups-oauth", argv[i]);
      return (usage(stderr));
    }
    else if (argv[i][0] == '-' && argv[i][1] != '-')
    {
      for (opt = argv[i] + 1; *opt; opt ++)
      {
        switch (*opt)
        {
          case 'a' : // -a AUTH-URI
              i ++;
              if (i >= argc)
              {
                cupsLangPuts(stderr, _("cups-oauth: Missing Authorization Server URI after '-a'."));
                return (usage(stderr));
              }

              oauth_uri = argv[i];
              break;

          case 's' : // -s SCOPE(S)
              i ++;
              if (i >= argc)
              {
                cupsLangPuts(stderr, _("cups-oauth: Missing scope(s) after '-s'."));
                return (usage(stderr));
              }

              scopes = argv[i];
              break;

          default :
              cupsLangPrintf(stderr, _("%s: Unknown option '-%c'."), "cups-oauth", *opt);
              return (usage(stderr));
        }
      }
    }
    else if (!oauth_uri)
    {
      cupsLangPuts(stderr, _("cups-oauth: No authorization server specified."));
      return (usage(stderr));
    }
    else if (!strcmp(argv[i], "authorize"))
    {
      // authorize [RESOURCE]
      i ++;
      return (do_authorize(oauth_uri, scopes, argv[i]));
    }
    else if (!strcmp(argv[i], "clear"))
    {
      // clear [RESOURCE]
      i ++;
      return (do_clear(oauth_uri, argv[i]));
    }
    else if (!strcmp(argv[i], "get-access-token"))
    {
      // get-access-token [RESOURCE]
      i ++;
      return (do_get_access_token(oauth_uri, argv[i]));
    }
    else if (!strcmp(argv[i], "get-client-id"))
    {
      // get-client-id
      i ++;
      return (do_get_client_id(oauth_uri));
    }
    else if (!strcmp(argv[i], "get-device-grant"))
    {
      // get-device-grant [RESOURCE]
      i ++;
      return (do_get_device_grant(oauth_uri, scopes, argv[i]));
    }
    else if (!strcmp(argv[i], "get-device-token"))
    {
      // get-access-token [RESOURCE] DEVICE-CODE
      i ++;
      if (i >= argc)
      {
	cupsLangPuts(stderr, _("cups-oauth: Missing resource URI and/or device token."));
	return (usage(stderr));
      }

      if (argv[i + 1])
        return (do_get_device_token(oauth_uri, argv[i], argv[i + 1]));
      else
        return (do_get_device_token(oauth_uri, /*resource_uri*/NULL, argv[i]));
    }
    else if (!strcmp(argv[i], "get-metadata"))
    {
      // get-metadata [NAME]
      i ++;
      return (do_get_metadata(oauth_uri, argv[i]));
    }
    else if (!strcmp(argv[i], "get-user-id"))
    {
      // get-user-id [RESOURCE] [NAME]
      i ++;
      if (i < argc)
      {
        if (!strncmp(argv[i], "ipp://", 6) || !strncmp(argv[i], "ipps://", 7) || !strncmp(argv[i], "http://", 7) || !strncmp(argv[i], "https://", 8))
	  return (do_get_user_id(oauth_uri, argv[i], argv[i + 1]));
        else
	  return (do_get_user_id(oauth_uri, /*resource_uri*/NULL, argv[i]));
      }
      else
      {
        return (do_get_user_id(oauth_uri, /*resource_uri*/NULL, /*name*/NULL));
      }
    }
    else if (!strcmp(argv[i], "set-access-token"))
    {
      // set-access-token [RESOURCE] TOKEN
      i ++;
      if (i >= argc)
      {
	cupsLangPuts(stderr, _("cups-oauth: Missing resource URI and/or access token."));
	return (usage(stderr));
      }

      if (argv[i + 1])
        return (do_set_access_token(oauth_uri, argv[i], argv[i + 1]));
      else
        return (do_set_access_token(oauth_uri, /*resource_uri*/NULL, argv[i]));
    }
    else if (!strcmp(argv[i], "set-client-data"))
    {
      // set-client-data CLIENT-ID CLIENT-DATA
      i ++;
      if ((i + 1) >= argc)
      {
	cupsLangPuts(stderr, _("cups-oauth: Missing client_id and/or client_secret."));
	return (usage(stderr));
      }

      return (do_set_client_data(oauth_uri, argv[i], argv[i + 1]));
    }
    else
    {
      cupsLangPrintf(stderr, _("cups-oauth: Unknown command '%s'."), argv[i]);
      return (usage(stderr));
    }
  }

  // If we get this far, show usage...
  return (usage(argc == 1 ? stdout : stderr));
}


//
// 'do_authorize()' - Authorize access.
//

static int				// O - Exit status
do_authorize(const char *oauth_uri,	// I - Authorization Server URI
             const char *scopes,	// I - Scope(s)
             const char *resource_uri)	// I - Resource URI
{
  int		status = 1;		// Exit status
  cups_json_t	*metadata;		// Server metadata
  char		*auth_code = NULL,	// Authorization code
		*access_token = NULL;	// Access token
  time_t	access_expires;		// Expiration date


  // Get the server metadata...
  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
    return (1);
  }

  // Authorize...
  if ((auth_code = cupsOAuthGetAuthorizationCode(oauth_uri, metadata, resource_uri, scopes, /*redirect_uri*/NULL)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get authorization from '%s': %s"), oauth_uri, cupsGetErrorString());
    goto done;
  }

  // Get the access token...
  if ((access_token = cupsOAuthGetTokens(oauth_uri, metadata, resource_uri, auth_code, CUPS_OGRANT_AUTHORIZATION_CODE, CUPS_OAUTH_REDIRECT_URI, &access_expires)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get access token from '%s': %s"), oauth_uri, cupsGetErrorString());
    goto done;
  }

  // Show access token
  puts(access_token);

  status = 0;

  // Clean up and return...
  done:

  cupsJSONDelete(metadata);
  free(auth_code);
  free(access_token);

  return (status);
}


//
// 'do_clear()' - Clear authorization information.
//

static int				// O - Exit status
do_clear(const char *oauth_uri,		// I - Authorization Server URI
         const char *resource_uri)	// I - Resource URI
{
  cupsOAuthClearTokens(oauth_uri, resource_uri);

  return (0);
}


//
// 'do_get_access_token()' - Get an access token.
//

static int				// O - Exit status
do_get_access_token(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *resource_uri)		// I - Resource URI
{
  char		*access_token;		// Access token
  time_t	access_expires;		// Expiration date


  if ((access_token = cupsOAuthCopyAccessToken(oauth_uri, resource_uri, &access_expires)) != NULL)
  {
    puts(access_token);
    free(access_token);
    return (0);
  }

  return (1);
}


//
// 'do_get_client_id()' - Get the client ID value.
//

static int				// O - Exit status
do_get_client_id(
    const char *oauth_uri)		// I - Authorization Server URI
{
  char	*client_id;			// Client ID


  if ((client_id = cupsOAuthCopyClientId(oauth_uri, CUPS_OAUTH_REDIRECT_URI)) != NULL)
  {
    puts(client_id);
    free(client_id);
    return (0);
  }

  return (1);
}


//
// 'do_get_device_grant()' - Start a device authorization workflow.
//

static int
do_get_device_grant(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *scopes,			// I - Scope(s)
    const char *resource_uri)		// I - Resource URI
{
  int		status = 1;		// Exit status
  cups_json_t	*metadata,		// Server metadata
		*grant = NULL;		// Device authorization grant
  const char	*device_code,		// Device code
		*user_code,		// User code
		*verification_url;	// Verification URL


  // Get the server metadata...
  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
    return (1);
  }

  // Authorize...
  if ((grant = cupsOAuthGetDeviceGrant(oauth_uri, metadata, resource_uri, scopes)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get device authorization from '%s': %s"), oauth_uri, cupsGetErrorString());
    goto done;
  }

  // Show grant...
  if ((device_code = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_DEVICE_CODE))) != NULL)
    cupsLangPrintf(stdout, _("Device Code: %s"), device_code);

  if ((user_code = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_USER_CODE))) != NULL)
    cupsLangPrintf(stdout, _("User Code: %s"), user_code);

  if ((verification_url = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URI))) != NULL)
    cupsLangPrintf(stdout, _("Web Page: %s"), verification_url);

  status = 0;

  // Clean up and return...
  done:

  cupsJSONDelete(metadata);
  cupsJSONDelete(grant);

  return (status);
}


//
// 'do_get_device_token()' - Wait to get a device access token.
//

static int				// O - Exit status
do_get_device_token(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *resource_uri,		// I - Resource URI
    const char *device_code)		// I - Device code
{
  int		status = 1;		// Exit status
  cups_json_t	*metadata;		// Server metadata
  char		*access_token;		// Device access token
  time_t	access_expires;		// Expiration date
  int		interval;		// Seconds to sleep

  // Get the server metadata...
  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
    return (1);
  }

  // Loop until we have an access token or a hard error...
  while ((access_token = cupsOAuthGetTokens(oauth_uri, metadata, resource_uri, device_code, CUPS_OGRANT_DEVICE_CODE, CUPS_OAUTH_REDIRECT_URI, &access_expires)) == NULL)
  {
    if (!access_expires)
    {
      cupsLangPrintf(stderr, _("cups-oauth: Unable to get device token from '%s': %s"), oauth_uri, cupsGetErrorString());
      goto done;
    }

    if ((interval = access_expires - time(NULL)) < 5)
      interval = 5;

    sleep((unsigned)interval);
  }

  // Show access token
  puts(access_token);

  status = 0;

  // Clean up and return...
  done:

  cupsJSONDelete(metadata);
  free(access_token);

  return (status);
}


//
// 'do_get_metadata()' - Get authorization server metadata.
//

static int				// O - Exit status
do_get_metadata(const char *oauth_uri,	// I - Authorization Server URI
                const char *name)	// I - Field name
{
  int		ret = 0;		// Exit status
  cups_json_t	*metadata;		// Metadata
  char		*json;			// JSON string


  // Get the metadata...
  if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get metadata for '%s': %s"), oauth_uri, cupsGetErrorString());
    return (1);
  }

  // Show metadata...
  if (name)
  {
    cups_json_t	*value = cupsJSONFind(metadata, name);
					// Metadata value

    if (value)
    {
      switch (cupsJSONGetType(value))
      {
        case CUPS_JTYPE_NULL :
            puts("null");
            break;

        case CUPS_JTYPE_FALSE :
            puts("false");
            break;

        case CUPS_JTYPE_TRUE :
            puts("true");
            break;

        case CUPS_JTYPE_NUMBER :
            printf("%g\n", cupsJSONGetNumber(value));
            break;

        case CUPS_JTYPE_STRING :
            puts(cupsJSONGetString(value));
            break;

        default :
            if ((json = cupsJSONExportString(value)) != NULL)
            {
              puts(json);
              free(json);
            }
            break;
      }
    }
    else
    {
      ret = 1;
    }
  }
  else if ((json = cupsJSONExportString(metadata)) != NULL)
  {
    puts(json);
    free(json);
  }

  cupsJSONDelete(metadata);

  return (ret);
}


//
// 'do_get_user_id()' - Get user identification.
//

static int				// O - Exit status
do_get_user_id(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *resource_uri,		// I - Resource URI
    const char *name)			// I - Claim name
{
  cups_jwt_t	*user_id;		// User ID information
  cups_json_t	*claims;		// Claims
  char		*json,			// JSON string
		date[256];		// Date
  int		ret = 0;		// Exit status


  // Get the user_id...
  if ((user_id = cupsOAuthCopyUserId(oauth_uri, resource_uri)) == NULL)
  {
    cupsLangPrintf(stderr, _("cups-oauth: Unable to get user ID for '%s': %s"), oauth_uri, cupsGetErrorString());
    return (1);
  }

  claims = cupsJWTGetClaims(user_id);

  // Show user information...
  if (name)
  {
    cups_json_t	*value = cupsJSONFind(claims, name);
					// Claim value

    if (value)
    {
      switch (cupsJSONGetType(value))
      {
        case CUPS_JTYPE_NULL :
            puts("null");
            break;

        case CUPS_JTYPE_FALSE :
            puts("false");
            break;

        case CUPS_JTYPE_TRUE :
            puts("true");
            break;

        case CUPS_JTYPE_NUMBER :
            if (!strcmp(name, "exp") || !strcmp(name, "iat") || !strcmp(name, "nbf"))
              puts(httpGetDateString((time_t)cupsJSONGetNumber(value), date, sizeof(date)));
            else
	      printf("%g\n", cupsJSONGetNumber(value));
            break;

        case CUPS_JTYPE_STRING :
            puts(cupsJSONGetString(value));
            break;

        default :
            if ((json = cupsJSONExportString(value)) != NULL)
            {
              puts(json);
              free(json);
            }
            break;
      }
    }
    else
    {
      ret = 1;
    }
  }
  else if ((json = cupsJSONExportString(claims)) != NULL)
  {
    puts(json);
    free(json);
  }

  cupsJWTDelete(user_id);

  return (ret);
}


//
// 'do_set_access_token()' - Set the access token.
//

static int				// O - Exit status
do_set_access_token(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *resource_uri,		// I - Resource URI
    const char *token)			// I - Access token
{
  cupsOAuthSaveTokens(oauth_uri, resource_uri, token, /*access_expires*/time(NULL) + 365 * 86400, /*user_id*/NULL, /*refresh_token*/NULL);

  return (0);
}


//
// 'do_set_client_data()' - Save client_id and client_secret values.
//

static int				// O - Exit status
do_set_client_data(
    const char *oauth_uri,		// I - Authorization Server URI
    const char *client_id,		// I - Client ID
    const char *client_secret)		// I - Client secret
{
  cupsOAuthSaveClientData(oauth_uri, CUPS_OAUTH_REDIRECT_URI, client_id, client_secret);

  return (0);
}


//
// 'usage()' - Show usage.
//

static int				// O - Exit status
usage(FILE *out)			// I - Output file
{
  cupsLangPuts(out, _("Usage: cups-oauth [OPTIONS] [SUB-COMMAND [ARGUMENT(S)]]"));
  cupsLangPuts(out, "");
  cupsLangPuts(out, _("Sub-Commands:"));
  cupsLangPuts(out, "");
  cupsLangPuts(out, _("authorize [RESOURCE]           Authorize access to a resource"));
  cupsLangPuts(out, _("clear [RESOURCE]               Clear the authorization for a resource"));
  cupsLangPuts(out, _("get-access-token [RESOURCE]    Get the current access token"));
  cupsLangPuts(out, _("get-client-id                  Get the client ID for the authorization server"));
  cupsLangPuts(out, _("get-device-grant [RESOURCE]    Authorize access through a device"));
  cupsLangPuts(out, _("get-device-token [RESOURCE] DEVICE-CODE\n"
                      "                               Wait for the device access token"));
  cupsLangPuts(out, _("get-metadata [NAME]            Get metadata from the authorization server"));
  cupsLangPuts(out, _("get-user-id [RESOURCE] [NAME]  Get the authorized user ID"));
  cupsLangPuts(out, _("set-access-token [RESOURCE] TOKEN\n"
                      "                               Set the current access token"));
  cupsLangPuts(out, _("set-client-data CLIENT-ID CLIENT-SECRET\n"
                      "                               Set the client ID and secret for the authorization server."));
  cupsLangPuts(out, "");
  cupsLangPuts(out, _("Options:"));
  cupsLangPuts(out, "");
  cupsLangPuts(out, _("--help                         Show this help"));
  cupsLangPuts(out, _("--version                      Show the program version"));
  cupsLangPuts(out, _("-a OAUTH-URI                   Specify the OAuth authorization server URL"));
  cupsLangPuts(out, _("-s SCOPE(S)                    Specify the scope(s) to authorize"));

  return (out == stdout ? 0 : 1);
}
