/*
   Unix SMB/CIFS implementation.
   SMB backend for the Common UNIX Printing System ("CUPS")

   Copyright (C) Michael R Sweet            1999
   Copyright (C) Andrew Tridgell	    1994-1998
   Copyright (C) Andrew Bartlett	    2002
   Copyright (C) Rodrigo Fernandez-Vizarra  2005
   Copyright (C) James Peach		    2008

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "includes.h"
#include "system/filesys.h"
#include "system/passwd.h"
#include "system/kerberos.h"
#include "libsmb/libsmb.h"
#include "lib/param/param.h"
#include "lib/krb5_wrap/krb5_samba.h"

/*
 * Starting with CUPS 1.3, Kerberos support is provided by cupsd including
 * the forwarding of user credentials via the authenticated session between
 * user and server and the KRB5CCNAME environment variable which will point
 * to a temporary file or an in-memory representation depending on the version
 * of Kerberos you use.  As a result, all of the ticket code that used to
 * live here has been removed, and we depend on the user session (if you
 * run smbspool by hand) or cupsd to provide the necessary Kerberos info.
 *
 * Also, the AUTH_USERNAME and AUTH_PASSWORD environment variables provide
 * for per-job authentication for non-Kerberized printing.  We use those
 * if there is no username and password specified in the device URI.
 *
 * Finally, if we have an authentication failure we return exit code 2
 * which tells CUPS to hold the job for authentication and bug the user
 * to get the necessary credentials.
 */

#define MAX_RETRY_CONNECT        3


/*
 * Globals...
 */



/*
 * Local functions...
 */

static int      get_exit_code(struct cli_state * cli, NTSTATUS nt_status, bool use_kerberos);
static void     list_devices(void);
static struct cli_state *smb_complete_connection(const char *, const char *,
	int, const char *, const char *, const char *, const char *, int, bool *need_auth);
static struct cli_state *smb_connect(const char *, const char *, int, const
	char *, const char *, const char *, const char *, bool *need_auth);
static int      smb_print(struct cli_state *, char *, FILE *);
static char    *uri_unescape_alloc(const char *);
#if 0
static bool     smb_encrypt;
#endif

/*
 * 'main()' - Main entry for SMB backend.
 */

int				/* O - Exit status */
main(int argc,			/* I - Number of command-line arguments */
     char *argv[])
{				/* I - Command-line arguments */
	int             i;	/* Looping var */
	int             copies;	/* Number of copies */
	int             port;	/* Port number */
	char            uri[1024],	/* URI */
	               *sep,	/* Pointer to separator */
	               *tmp, *tmp2,	/* Temp pointers to do escaping */
	               *password;	/* Password */
	char           *username,	/* Username */
	               *server,	/* Server name */
	               *printer;/* Printer name */
	const char     *workgroup;	/* Workgroup */
	FILE           *fp;	/* File to print */
	int             status = 1;	/* Status of LPD job */
	struct cli_state *cli;	/* SMB interface */
	char            empty_str[] = "";
	int             tries = 0;
	bool		need_auth = true;
	const char     *dev_uri;
	const char     *config_file = NULL;
	TALLOC_CTX     *frame = talloc_stackframe();
	bool device_uri_cmdline = false;
	const char *print_file = NULL;
	const char *print_copies = NULL;
	int cmp;
	int len;

	if (argc == 1) {
		/*
	         * NEW!  In CUPS 1.1 the backends are run with no arguments
		 * to list the available devices.  These can be devices
		 * served by this backend or any other backends (i.e. you
		 * can have an SNMP backend that is only used to enumerate
		 * the available network printers... :)
	         */

		list_devices();
		status = 0;
		goto done;
	}

	/*
	 * We need at least 5 options if the DEVICE_URI is passed via an env
	 * variable and printing data comes via stdin.
	 * We don't accept more than 7 options in total, including optional.
	 */
	if (argc < 5 || argc > 8) {
		fprintf(stderr,
"Usage: %s [DEVICE_URI] job-id user title copies options [file]\n"
"       The DEVICE_URI environment variable can also contain the\n"
"       destination printer:\n"
"\n"
"           smb://[username:password@][workgroup/]server[:port]/printer\n",
			argv[0]);
		goto done;
	}

	/*
	 * If we have 6 arguments find out if we have the device_uri from the
	 * command line or the print data
	 */
	if (argc == 7) {
		cmp = strncmp(argv[1], "smb://", 6);
		if (cmp == 0) {
			device_uri_cmdline = true;
		} else {
			print_copies = argv[4];
			print_file = argv[6];
		}
	} else if (argc == 8) {
		device_uri_cmdline = true;
		print_copies = argv[5];
		print_file = argv[7];
	}

	if (print_file != NULL) {
		char *endp;

		fp = fopen(print_file, "rb");
		if (fp == NULL) {
			perror("ERROR: Unable to open print file");
			goto done;
		}

		copies = strtol(print_copies, &endp, 10);
		if (print_copies == endp) {
			perror("ERROR: Unable to determine number of copies");
			goto done;
		}
	} else {
		fp = stdin;
		copies = 1;
	}

	/*
	 * Find the URI ...
	 */
	if (device_uri_cmdline) {
		dev_uri = argv[1];
	} else {
		dev_uri = getenv("DEVICE_URI");
		if (dev_uri == NULL || strlen(dev_uri) == 0) {
			dev_uri = "";
		}
	}

	cmp = strncmp(dev_uri, "smb://", 6);
	if (cmp != 0) {
		fprintf(stderr,
			"ERROR: No valid device URI has been specified\n");
		goto done;
	}
	len = snprintf(uri, sizeof(uri), "%s", dev_uri);
	if (len >= sizeof(uri)) {
		fprintf(stderr,
			"ERROR: The URI is too long.\n");
		goto done;
	}

	/*
         * Extract the destination from the URI...
         */

	if ((sep = strrchr_m(uri, '@')) != NULL) {
		tmp = uri + 6;
		*sep++ = '\0';

		/* username is in tmp */

		server = sep;

		/*
	         * Extract password as needed...
	         */

		if ((tmp2 = strchr_m(tmp, ':')) != NULL) {
			*tmp2++ = '\0';
			password = uri_unescape_alloc(tmp2);
		} else {
			password = empty_str;
		}
		username = uri_unescape_alloc(tmp);
	} else {
		if ((username = getenv("AUTH_USERNAME")) == NULL) {
			username = empty_str;
		}

		if ((password = getenv("AUTH_PASSWORD")) == NULL) {
			password = empty_str;
		}

		server = uri + 6;
	}

	tmp = server;

	if ((sep = strchr_m(tmp, '/')) == NULL) {
		fputs("ERROR: Bad URI - need printer name!\n", stderr);
		goto done;
	}

	*sep++ = '\0';
	tmp2 = sep;

	if ((sep = strchr_m(tmp2, '/')) != NULL) {
		/*
	         * Convert to smb://[username:password@]workgroup/server/printer...
	         */

		*sep++ = '\0';

		workgroup = uri_unescape_alloc(tmp);
		server = uri_unescape_alloc(tmp2);
		printer = uri_unescape_alloc(sep);
	} else {
		workgroup = NULL;
		server = uri_unescape_alloc(tmp);
		printer = uri_unescape_alloc(tmp2);
	}

	if ((sep = strrchr_m(server, ':')) != NULL) {
		*sep++ = '\0';

		port = atoi(sep);
	} else {
		port = 0;
	}

	/*
         * Setup the SAMBA server state...
         */

	setup_logging("smbspool", DEBUG_STDERR);

	smb_init_locale();

	config_file = lp_default_path();
	if (!lp_load_client(config_file)) {
		fprintf(stderr,
			"ERROR: Can't load %s - run testparm to debug it\n",
			config_file);
		goto done;
	}

	if (workgroup == NULL) {
		workgroup = lp_workgroup();
	}

	load_interfaces();

	do {
		cli = smb_connect(workgroup, server, port, printer,
			username, password, argv[3], &need_auth);
		if (cli == NULL) {
			if (need_auth) {
				exit(2);
			} else if (getenv("CLASS") == NULL) {
				fprintf(stderr, "ERROR: Unable to connect to CIFS host, will retry in 60 seconds...\n");
				sleep(60);
				tries++;
			} else {
				fprintf(stderr, "ERROR: Unable to connect to CIFS host, trying next printer...\n");
				goto done;
			}
		}
	} while ((cli == NULL) && (tries < MAX_RETRY_CONNECT));

	if (cli == NULL) {
		fprintf(stderr, "ERROR: Unable to connect to CIFS host after (tried %d times)\n", tries);
		goto done;
	}

	/*
         * Now that we are connected to the server, ignore SIGTERM so that we
         * can finish out any page data the driver sends (e.g. to eject the
         * current page...  Only ignore SIGTERM if we are printing data from
         * stdin (otherwise you can't cancel raw jobs...)
         */

	if (argc < 7) {
		CatchSignal(SIGTERM, SIG_IGN);
	}

	/*
         * Queue the job...
         */

	for (i = 0; i < copies; i++) {
		status = smb_print(cli, argv[4] /* title */ , fp);
		if (status != 0) {
			break;
		}
	}

	cli_shutdown(cli);

	/*
         * Return the queue status...
         */

done:

	TALLOC_FREE(frame);
	return (status);
}


/*
 * 'get_exit_code()' - Get the backend exit code based on the current error.
 */

static int
get_exit_code(struct cli_state * cli,
	      NTSTATUS nt_status,
	      bool use_kerberos)
{
	int i;

	/* List of NTSTATUS errors that are considered
	 * authentication errors
	 */
	static const NTSTATUS auth_errors[] =
	{
		NT_STATUS_ACCESS_DENIED, NT_STATUS_ACCESS_VIOLATION,
		NT_STATUS_SHARING_VIOLATION, NT_STATUS_PRIVILEGE_NOT_HELD,
		NT_STATUS_INVALID_ACCOUNT_NAME, NT_STATUS_NO_SUCH_USER,
		NT_STATUS_WRONG_PASSWORD, NT_STATUS_LOGON_FAILURE,
		NT_STATUS_ACCOUNT_RESTRICTION, NT_STATUS_INVALID_LOGON_HOURS,
		NT_STATUS_PASSWORD_EXPIRED, NT_STATUS_ACCOUNT_DISABLED
	};


	fprintf(stderr, "DEBUG: get_exit_code(cli=%p, nt_status=%s [%x])\n",
		cli, nt_errstr(nt_status), NT_STATUS_V(nt_status));

	for (i = 0; i < ARRAY_SIZE(auth_errors); i++) {
		if (!NT_STATUS_EQUAL(nt_status, auth_errors[i])) {
			continue;
		}

		if (cli) {
			if (use_kerberos)
				fputs("ATTR: auth-info-required=negotiate\n", stderr);
			else
				fputs("ATTR: auth-info-required=username,password\n", stderr);
		}

		/*
		 * 2 = authentication required...
		 */

		return (2);

	}

	/*
         * 1 = fail
         */

	return (1);
}


/*
 * 'list_devices()' - List the available printers seen on the network...
 */

static void
list_devices(void)
{
	/*
         * Eventually, search the local workgroup for available hosts and printers.
         */

	puts("network smb \"Unknown\" \"Windows Printer via SAMBA\"");
}


static struct cli_state *
smb_complete_connection(const char *myname,
			const char *server,
			int port,
			const char *username,
			const char *password,
			const char *workgroup,
			const char *share,
			int flags,
			bool *need_auth)
{
	struct cli_state *cli;	/* New connection */
	NTSTATUS        nt_status;
	struct cli_credentials *creds = NULL;
	bool use_kerberos = false;

	/* Start the SMB connection */
	*need_auth = false;
	nt_status = cli_start_connection(&cli, myname, server, NULL, port,
					 SMB_SIGNING_DEFAULT, flags);
	if (!NT_STATUS_IS_OK(nt_status)) {
		fprintf(stderr, "ERROR: Connection failed: %s\n", nt_errstr(nt_status));
		return NULL;
	}

	/*
	 * We pretty much guarantee password must be valid or a pointer to a
	 * 0 char.
	 */
	if (!password) {
		*need_auth = true;
		return NULL;
	}

	if (flags & CLI_FULL_CONNECTION_USE_KERBEROS) {
		use_kerberos = true;
	}

	creds = cli_session_creds_init(cli,
				       username,
				       workgroup,
				       NULL, /* realm */
				       password,
				       use_kerberos,
				       false, /* fallback_after_kerberos */
				       false, /* use_ccache */
				       false); /* password_is_nt_hash */
	if (creds == NULL) {
		fprintf(stderr, "ERROR: cli_session_creds_init failed\n");
		cli_shutdown(cli);
		return NULL;
	}

	nt_status = cli_session_setup_creds(cli, creds);
	if (!NT_STATUS_IS_OK(nt_status)) {
		fprintf(stderr, "ERROR: Session setup failed: %s\n", nt_errstr(nt_status));

		if (get_exit_code(cli, nt_status, use_kerberos) == 2) {
			*need_auth = true;
		}

		cli_shutdown(cli);

		return NULL;
	}

	nt_status = cli_tree_connect_creds(cli, share, "?????", creds);
	if (!NT_STATUS_IS_OK(nt_status)) {
		fprintf(stderr, "ERROR: Tree connect failed (%s)\n",
			nt_errstr(nt_status));

		if (get_exit_code(cli, nt_status, use_kerberos) == 2) {
			*need_auth = true;
		}

		cli_shutdown(cli);

		return NULL;
	}
#if 0
	/* Need to work out how to specify this on the URL. */
	if (smb_encrypt) {
		if (!cli_cm_force_encryption_creds(cli, creds, share)) {
			fprintf(stderr, "ERROR: encryption setup failed\n");
			cli_shutdown(cli);
			return NULL;
		}
	}
#endif

	return cli;
}

static bool kerberos_ccache_is_valid(void) {
	krb5_context ctx;
	const char *ccache_name = NULL;
	krb5_ccache ccache = NULL;
	krb5_error_code code;

	code = smb_krb5_init_context_common(&ctx);
	if (code != 0) {
		DBG_ERR("kerberos init context failed (%s)\n",
			error_message(code));
		return false;
	}

	ccache_name = krb5_cc_default_name(ctx);
	if (ccache_name == NULL) {
		krb5_free_context(ctx);
		return false;
	}

	code = krb5_cc_resolve(ctx, ccache_name, &ccache);
	if (code != 0) {
		krb5_free_context(ctx);
		return false;
	} else {
		krb5_principal default_princ = NULL;

		code = krb5_cc_get_principal(ctx,
					     ccache,
					     &default_princ);
		if (code != 0) {
			krb5_cc_close(ctx, ccache);
			krb5_free_context(ctx);
			return false;
		}
		krb5_free_principal(ctx, default_princ);
	}
	krb5_cc_close(ctx, ccache);
	krb5_free_context(ctx);

	return true;
}

/*
 * 'smb_connect()' - Return a connection to a server.
 */

static struct cli_state *	/* O - SMB connection */
smb_connect(const char *workgroup,	/* I - Workgroup */
	    const char *server,	/* I - Server */
	    const int port,	/* I - Port */
	    const char *share,	/* I - Printer */
	    const char *username,	/* I - Username */
	    const char *password,	/* I - Password */
	    const char *jobusername,	/* I - User who issued the print job */
	    bool *need_auth)
{				/* O - Need authentication? */
	struct cli_state *cli;	/* New connection */
	char           *myname = NULL;	/* Client name */
	struct passwd  *pwd;

	/*
         * Get the names and addresses of the client and server...
         */
	myname = get_myname(talloc_tos());
	if (!myname) {
		return NULL;
	}

	/*
	 * See if we have a username first.  This is for backwards compatible
	 * behavior with 3.0.14a
	 */

	if (username != NULL && username[0] != '\0') {
		if (kerberos_ccache_is_valid()) {
			goto kerberos_auth;
		}
	}

	cli = smb_complete_connection(myname,
				      server,
				      port,
				      username,
				      password,
				      workgroup,
				      share,
				      0,
				      need_auth);
	if (cli != NULL) {
		fputs("DEBUG: Connected with username/password...\n", stderr);
		return (cli);
	}

kerberos_auth:
	/*
	 * Try to use the user kerberos credentials (if any) to authenticate
	 */
	cli = smb_complete_connection(myname, server, port, jobusername, "",
				      workgroup, share,
				 CLI_FULL_CONNECTION_USE_KERBEROS, need_auth);

	if (cli) {
		fputs("DEBUG: Connected using Kerberos...\n", stderr);
		return (cli);
	}

	/* give a chance for a passwordless NTLMSSP session setup */
	pwd = getpwuid(geteuid());
	if (pwd == NULL) {
		return NULL;
	}

	cli = smb_complete_connection(myname, server, port, pwd->pw_name, "",
				      workgroup, share, 0, need_auth);

	if (cli) {
		fputs("DEBUG: Connected with NTLMSSP...\n", stderr);
		return (cli);
	}

	/*
         * last try. Use anonymous authentication
         */

	cli = smb_complete_connection(myname, server, port, "", "",
				      workgroup, share, 0, need_auth);
	/*
         * Return the new connection...
         */

	return (cli);
}


/*
 * 'smb_print()' - Queue a job for printing using the SMB protocol.
 */

static int			/* O - 0 = success, non-0 = failure */
smb_print(struct cli_state * cli,	/* I - SMB connection */
	  char *title,		/* I - Title/job name */
	  FILE * fp)
{				/* I - File to print */
	uint16_t             fnum;	/* File number */
	int             nbytes,	/* Number of bytes read */
	                tbytes;	/* Total bytes read */
	char            buffer[8192],	/* Buffer for copy */
	               *ptr;	/* Pointer into title */
	NTSTATUS nt_status;


	/*
         * Sanitize the title...
         */

	for (ptr = title; *ptr; ptr++) {
		if (!isalnum((int) *ptr) && !isspace((int) *ptr)) {
			*ptr = '_';
		}
	}

	/*
         * Open the printer device...
         */

	nt_status = cli_open(cli, title, O_RDWR | O_CREAT | O_TRUNC, DENY_NONE,
			  &fnum);
	if (!NT_STATUS_IS_OK(nt_status)) {
		fprintf(stderr, "ERROR: %s opening remote spool %s\n",
			nt_errstr(nt_status), title);
		return get_exit_code(cli, nt_status, false);
	}

	/*
         * Copy the file to the printer...
         */

	if (fp != stdin)
		rewind(fp);

	tbytes = 0;

	while ((nbytes = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
		NTSTATUS status;

		status = cli_writeall(cli, fnum, 0, (uint8_t *)buffer,
				      tbytes, nbytes, NULL);
		if (!NT_STATUS_IS_OK(status)) {
			int ret = get_exit_code(cli, status, false);
			fprintf(stderr, "ERROR: Error writing spool: %s\n",
				nt_errstr(status));
			fprintf(stderr, "DEBUG: Returning status %d...\n",
				ret);
			cli_close(cli, fnum);

			return (ret);
		}
		tbytes += nbytes;
	}

	nt_status = cli_close(cli, fnum);
	if (!NT_STATUS_IS_OK(nt_status)) {
		fprintf(stderr, "ERROR: %s closing remote spool %s\n",
			nt_errstr(nt_status), title);
		return get_exit_code(cli, nt_status, false);
	} else {
		return (0);
	}
}

static char *
uri_unescape_alloc(const char *uritok)
{
	char *ret;
	char *end;
	ret = (char *) SMB_STRDUP(uritok);
	if (!ret) {
		return NULL;
	}

	end = rfc1738_unescape(ret);
	if (end == NULL) {
		free(ret);
		return NULL;
	}
	return ret;
}
