]> git.ipfire.org Git - thirdparty/cups.git/commitdiff
Save work on IPP Infra server and proxy (WIP)
authormsweet <msweet@a1ca3aef-8c08-0410-bb20-df032aa958be>
Wed, 1 Oct 2014 19:00:21 +0000 (19:00 +0000)
committermsweet <msweet@a1ca3aef-8c08-0410-bb20-df032aa958be>
Wed, 1 Oct 2014 19:00:21 +0000 (19:00 +0000)
git-svn-id: svn+ssh://src.apple.com/svn/cups/cups.org/trunk@12191 a1ca3aef-8c08-0410-bb20-df032aa958be

test/Dependencies
test/Makefile
test/ippinfra.c [new file with mode: 0644]
test/ippinfra.man [new file with mode: 0644]
test/ippproxy.c [new file with mode: 0644]
test/ippproxy.man [new file with mode: 0644]

index 8405117d24bdb274b09a2224e640a2792f19df80..e2822d07306140023322640005af3e929445eb24 100644 (file)
@@ -5,9 +5,17 @@ ippfind.o: ippfind.c ../cups/cups-private.h ../cups/string-private.h \
   ../cups/md5-private.h ../cups/language-private.h ../cups/transcode.h \
   ../cups/pwg-private.h ../cups/cups.h ../cups/file.h ../cups/pwg.h \
   ../cups/ppd-private.h ../cups/ppd.h ../cups/thread-private.h
-ippserver.o: ippserver.c ../cups/cups.h ../cups/file.h \
+ippinfra.o: ippinfra.c ../config.h ../cups/cups.h ../cups/file.h \
   ../cups/versioning.h ../cups/ipp.h ../cups/http.h ../cups/array.h \
-  ../cups/language.h ../cups/pwg.h ../config.h ../cups/thread-private.h
+  ../cups/language.h ../cups/pwg.h ../cups/string-private.h \
+  ../cups/thread-private.h
+ippproxy.o: ippproxy.c ../cups/cups.h ../cups/file.h ../cups/versioning.h \
+  ../cups/ipp.h ../cups/http.h ../cups/array.h ../cups/language.h \
+  ../cups/pwg.h
+ippserver.o: ippserver.c ../config.h ../cups/cups.h ../cups/file.h \
+  ../cups/versioning.h ../cups/ipp.h ../cups/http.h ../cups/array.h \
+  ../cups/language.h ../cups/pwg.h ../cups/string-private.h \
+  ../cups/thread-private.h
 ipptool.o: ipptool.c ../cups/cups-private.h ../cups/string-private.h \
   ../config.h ../cups/debug-private.h ../cups/versioning.h \
   ../cups/array-private.h ../cups/array.h ../cups/ipp-private.h \
index 2f43f3e75771294e947fbcae660191de070954a8..2133789a1baced1db5fbc338896e35b4f6c20728 100644 (file)
@@ -3,7 +3,7 @@
 #
 #   IPP test makefile for CUPS.
 #
-#   Copyright 2007-2013 by Apple Inc.
+#   Copyright 2007-2014 by Apple Inc.
 #   Copyright 1997-2006 by Easy Software Products, all rights reserved.
 #
 #   These coded instructions, statements, and computer programs are the
@@ -54,6 +54,8 @@ TESTFILES     =       \
                        validate-job.test
 OBJS           =       \
                        ippfind.o \
+                       ippinfra.o \
+                       ippproxy.o \
                        ippserver.o \
                        ipptool.o \
                        xmltotest.o
@@ -90,7 +92,7 @@ unittests:
 #
 
 clean:
-       $(RM) $(TARGETS) $(OBJS)
+       $(RM) $(TARGETS) $(OBJS) ippinfra ippinfra-shared ippproxy ippproxy-shared ippserver-shared
 
 
 #
@@ -184,6 +186,44 @@ ippfind:   ippfind.o ../cups/$(LIBCUPS) ../cups/$(LIBCUPSSTATIC)
                $(LIBGSSAPI) $(SSLLIBS) $(DNSSDLIBS) $(COMMONLIBS) $(LIBZ)
 
 
+#
+# ippinfra
+#
+
+ippinfra:      ippinfra.o ../cups/$(LIBCUPSSTATIC)
+       echo Linking $@...
+       $(CC) $(LDFLAGS) -o $@ ippinfra.o  ../cups/$(LIBCUPSSTATIC) \
+               $(LIBGSSAPI) $(SSLLIBS) $(DNSSDLIBS) $(COMMONLIBS) $(LIBZ)
+
+
+#
+# ippinfra-shared
+#
+
+ippinfra-shared:       ippinfra.o ../cups/$(LIBCUPS)
+       echo Linking $@...
+       $(CC) $(LDFLAGS) -o $@ ippinfra.o $(LIBS)
+
+
+#
+# ippproxy
+#
+
+ippproxy:      ippproxy.o ../cups/$(LIBCUPSSTATIC)
+       echo Linking $@...
+       $(CC) $(LDFLAGS) -o $@ ippproxy.o  ../cups/$(LIBCUPSSTATIC) \
+               $(LIBGSSAPI) $(SSLLIBS) $(DNSSDLIBS) $(COMMONLIBS) $(LIBZ)
+
+
+#
+# ippproxy-shared
+#
+
+ippproxy-shared:       ippproxy.o ../cups/$(LIBCUPS)
+       echo Linking $@...
+       $(CC) $(LDFLAGS) -o $@ ippproxy.o $(LIBS)
+
+
 #
 # ippserver
 #
diff --git a/test/ippinfra.c b/test/ippinfra.c
new file mode 100644 (file)
index 0000000..2e82c02
--- /dev/null
@@ -0,0 +1,6118 @@
+/*
+ * "$Id$"
+ *
+ * Sample IPP INFRA server for CUPS.
+ *
+ * Copyright 2010-2014 by Apple Inc.
+ *
+ * These coded instructions, statements, and computer programs are the
+ * property of Apple Inc. and are protected by Federal copyright
+ * law.  Distribution and use rights are outlined in the file "LICENSE.txt"
+ * which should have been included with this file.  If this file is
+ * file is missing or damaged, see the license at "http://www.cups.org/".
+ *
+ * This file is subject to the Apple OS-Developed Software exception.
+ */
+
+/*
+ * Disable private and deprecated stuff so we can verify that the public API
+ * is sufficient to implement a server.
+ */
+
+#define _IPP_PRIVATE_STRUCTURES 0      /* Disable private IPP stuff */
+#define _CUPS_NO_DEPRECATED 1          /* Disable deprecated stuff */
+
+
+/*
+ * Include necessary headers...
+ */
+
+#include <config.h>                    /* CUPS configuration header */
+#include <cups/cups.h>                 /* Public API */
+#include <cups/string-private.h>       /* CUPS string functions */
+#include <cups/thread-private.h>       /* For multithreading functions */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/stat.h>
+
+#ifdef WIN32
+#  include <fcntl.h>
+#  include <io.h>
+#  include <process.h>
+#  define WEXITSTATUS(s) (s)
+#  include <winsock2.h>
+typedef ULONG nfds_t;
+#  define poll WSAPoll
+#else
+extern char **environ;
+
+#  include <sys/fcntl.h>
+#  include <sys/wait.h>
+#  include <poll.h>
+#endif /* WIN32 */
+
+#ifdef HAVE_SYS_MOUNT_H
+#  include <sys/mount.h>
+#endif /* HAVE_SYS_MOUNT_H */
+#ifdef HAVE_SYS_STATFS_H
+#  include <sys/statfs.h>
+#endif /* HAVE_SYS_STATFS_H */
+#ifdef HAVE_SYS_STATVFS_H
+#  include <sys/statvfs.h>
+#endif /* HAVE_SYS_STATVFS_H */
+#ifdef HAVE_SYS_VFS_H
+#  include <sys/vfs.h>
+#endif /* HAVE_SYS_VFS_H */
+
+
+/*
+ * Constants...
+ */
+
+/* New IPP operation codes from IPP INFRA */
+#  define _IPP_OP_ACKNOWLEDGE_DOCUMENT                 (ipp_op_t)0x003f
+#  define _IPP_OP_ACKNOWLEDGE_IDENTIFY_PRINTER         (ipp_op_t)0x0040
+#  define _IPP_OP_ACKNOWLEDGE_JOB                      (ipp_op_t)0x0041
+#  define _IPP_OP_FETCH_DOCUMENT                       (ipp_op_t)0x0042
+#  define _IPP_OP_FETCH_JOB                            (ipp_op_t)0x0043
+#  define _IPP_OP_GET_OUTPUT_DEVICE_ATTRIBUTES         (ipp_op_t)0x0044
+#  define _IPP_OP_UPDATE_ACTIVE_JOBS                   (ipp_op_t)0x0045
+#  define _IPP_OP_UPDATE_DOCUMENT_STATUS               (ipp_op_t)0x0047
+#  define _IPP_OP_UPDATE_JOB_STATUS                    (ipp_op_t)0x0048
+#  define _IPP_OP_UPDATE_OUTPUT_DEVICE_ATTRIBUTES      (ipp_op_t)0x0049
+
+/* New IPP status code from IPP INFRA */
+#  define _IPP_STATUS_ERROR_NOT_FETCHABLE              (ipp_status_t)0x0420
+
+/* Maximum lease duration value from RFC 3995 - 2^26-1 or ~2 years */
+#  define _IPP_NOTIFY_LEASE_DURATION_MAX               67108863
+/* But a value of 0 means "never expires"... */
+#  define _IPP_NOTIFY_LEASE_DURATION_FOREVER           0
+
+/*
+ * Event mask enumeration...
+ */
+
+enum _ipp_event_e                      /* notify-events bit values */
+{
+  _IPP_EVENT_DOCUMENT_COMPLETED = 0x00000001,
+  _IPP_EVENT_DOCUMENT_CONFIG_CHANGED = 0x00000002,
+  _IPP_EVENT_DOCUMENT_CREATED = 0x00000004,
+  _IPP_EVENT_DOCUMENT_FETCHABLE = 0x00000008,
+  _IPP_EVENT_DOCUMENT_STATE_CHANGED = 0x00000010,
+  _IPP_EVENT_DOCUMENT_STOPPED = 0x00000020,
+  _IPP_EVENT_JOB_COMPLETED = 0x00000040,
+  _IPP_EVENT_JOB_CONFIG_CHANGED = 0x00000080,
+  _IPP_EVENT_JOB_CREATED = 0x00000100,
+  _IPP_EVENT_JOB_FETCHABLE = 0x00000200,
+  _IPP_EVENT_JOB_PROGRESS = 0x00000400,
+  _IPP_EVENT_JOB_STATE_CHANGED = 0x00000800,
+  _IPP_EVENT_JOB_STOPPED = 0x00001000,
+  _IPP_EVENT_PRINTER_CONFIG_CHANGED = 0x00002000,
+  _IPP_EVENT_PRINTER_FINISHINGS_CHANGED = 0x00004000,
+  _IPP_EVENT_PRINTER_MEDIA_CHANGED = 0x00008000,
+  _IPP_EVENT_PRINTER_QUEUE_ORDER_CHANGED = 0x00010000,
+  _IPP_EVENT_PRINTER_RESTARTED = 0x00020000,
+  _IPP_EVENT_PRINTER_SHUTDOWN = 0x00040000,
+  _IPP_EVENT_PRINTER_STATE_CHANGED = 0x00080000,
+  _IPP_EVENT_PRINTER_STOPPED = 0x00100000,
+
+  /* "Wildcard" values... */
+  _IPP_EVENT_NONE = 0x00000000,                /* Nothing */
+  _IPP_EVENT_DOCUMENT_ALL = 0x0000003f,
+  _IPP_EVENT_DOCUMENT_STATE_ALL = 0x00000037,
+  _IPP_EVENT_JOB_ALL = 0x00001fc0,
+  _IPP_EVENT_JOB_STATE_ALL = 0x00001940,
+  _IPP_EVENT_PRINTER_ALL = 0x001fe000,
+  _IPP_EVENT_PRINTER_CONFIG_ALL = 0x0000e000,
+  _IPP_EVENT_PRINTER_STATE_ALL = 0x001e0000,
+  _IPP_EVENT_ALL = 0x001fffff          /* Everything */
+};
+typedef unsigned int _ipp_event_t;     /* Bitfield for notify-events */
+static const char * const _ipp_events[] =
+{                                      /* Strings for bits */
+  "document-completed",
+  "document-config-changed",
+  "document-created",
+  "document-fetchable",
+  "document-state-changed",
+  "document-stopped",
+  "job-completed",
+  "job-config-changed",
+  "job-created",
+  "job-fetchable",
+  "job-progress",
+  "job-state-changed",
+  "job-stopped",
+  "printer-config-changed",
+  "printer-finishings-changed",
+  "printer-media-changed",
+  "printer-queue-order-changed",
+  "printer-restarted",
+  "printer-shutdown",
+  "printer-state-changed",
+  "printer-stopped"
+};
+
+enum _ipp_jreason_e                    /* job-state-reasons bit values */
+{
+  _IPP_JREASON_NONE = 0x00000000,      /* none */
+  _IPP_JREASON_ABORTED_BY_SYSTEM = 0x00000001,
+  _IPP_JREASON_COMPRESSION_ERROR = 0x00000002,
+  _IPP_JREASON_DOCUMENT_ACCESS_ERROR = 0x00000004,
+  _IPP_JREASON_DOCUMENT_FORMAT_ERROR = 0x00000008,
+  _IPP_JREASON_DOCUMENT_PASSWORD_ERROR = 0x00000010,
+  _IPP_JREASON_DOCUMENT_PERMISSION_ERROR = 0x00000020,
+  _IPP_JREASON_DOCUMENT_SECURITY_ERROR = 0x00000040,
+  _IPP_JREASON_DOCUMENT_UNPRINTABLE_ERROR = 0x00000080,
+  _IPP_JREASON_ERRORS_DETECTED = 0x00000100,
+  _IPP_JREASON_JOB_CANCELED_AT_DEVICE = 0x00000200,
+  _IPP_JREASON_JOB_CANCELED_BY_USER = 0x00000400,
+  _IPP_JREASON_JOB_COMPLETED_SUCCESSFULLY = 0x00000800,
+  _IPP_JREASON_JOB_COMPLETED_WITH_ERRORS = 0x00001000,
+  _IPP_JREASON_JOB_COMPLETED_WITH_WARNINGS = 0x00002000,
+  _IPP_JREASON_JOB_DATA_INSUFFICIENT = 0x00004000,
+  _IPP_JREASON_JOB_FETCHABLE = 0x00008000,
+  _IPP_JREASON_JOB_INCOMING = 0x00010000,
+  _IPP_JREASON_JOB_PASSWORD_WAIT = 0x00020000,
+  _IPP_JREASON_JOB_PRINTING = 0x00040000,
+  _IPP_JREASON_JOB_QUEUED = 0x00080000,
+  _IPP_JREASON_JOB_SPOOLING = 0x00100000,
+  _IPP_JREASON_JOB_STOPPED = 0x00200000,
+  _IPP_JREASON_JOB_TRANSFORMING = 0x00400000,
+  _IPP_JREASON_PRINTER_STOPPED = 0x00800000,
+  _IPP_JREASON_PRINTER_STOPPED_PARTLY = 0x01000000,
+  _IPP_JREASON_PROCESSING_TO_STOP_POINT = 0x02000000,
+  _IPP_JREASON_QUEUED_IN_DEVICE = 0x04000000,
+  _IPP_JREASON_WARNINGS_DETECTED = 0x08000000
+};
+typedef unsigned int _ipp_jreason_t;   /* Bitfield for job-state-reasons */
+static const char * const _ipp_jreasons[] =
+{                                      /* Strings for bits */
+  "aborted-by-system",
+  "compression-error",
+  "document-access-error",
+  "document-format-error",
+  "document-password-error",
+  "document-permission-error",
+  "document-security-error",
+  "document-unprintable-error",
+  "errors-detected",
+  "job-canceled-at-device",
+  "job-canceled-by-user",
+  "job-completed-successfully",
+  "job-completed-with-errors",
+  "job-completed-with-warnings",
+  "job-data-insufficient",
+  "job-fetchable",
+  "job-incoming",
+  "job-password-wait",
+  "job-printing",
+  "job-queued",
+  "job-spooling",
+  "job-stopped",
+  "job-transforming",
+  "printer-stopped",
+  "printer-stopped-partly",
+  "processing-to-stop-point",
+  "queued-in-device",
+  "warnings-detected"
+};
+
+enum _ipp_preason_e                    /* printer-state-reasons bit values */
+{
+  _IPP_PREASON_NONE = 0x0000,          /* none */
+  _IPP_PREASON_OTHER = 0x0001,         /* other */
+  _IPP_PREASON_COVER_OPEN = 0x0002,    /* cover-open */
+  _IPP_PREASON_INPUT_TRAY_MISSING = 0x0004,
+                                       /* input-tray-missing */
+  _IPP_PREASON_MARKER_SUPPLY_EMPTY = 0x0008,
+                                       /* marker-supply-empty */
+  _IPP_PREASON_MARKER_SUPPLY_LOW = 0x0010,
+                                       /* marker-supply-low */
+  _IPP_PREASON_MARKER_WASTE_ALMOST_FULL = 0x0020,
+                                       /* marker-waste-almost-full */
+  _IPP_PREASON_MARKER_WASTE_FULL = 0x0040,
+                                       /* marker-waste-full */
+  _IPP_PREASON_MEDIA_EMPTY = 0x0080,   /* media-empty */
+  _IPP_PREASON_MEDIA_JAM = 0x0100,     /* media-jam */
+  _IPP_PREASON_MEDIA_LOW = 0x0200,     /* media-low */
+  _IPP_PREASON_MEDIA_NEEDED = 0x0400,  /* media-needed */
+  _IPP_PREASON_MOVING_TO_PAUSED = 0x0800,
+                                       /* moving-to-paused */
+  _IPP_PREASON_PAUSED = 0x1000,                /* paused */
+  _IPP_PREASON_SPOOL_AREA_FULL = 0x2000,/* spool-area-full */
+  _IPP_PREASON_TONER_EMPTY = 0x4000,   /* toner-empty */
+  _IPP_PREASON_TONER_LOW = 0x8000      /* toner-low */
+};
+typedef unsigned int _ipp_preason_t;   /* Bitfield for printer-state-reasons */
+static const char * const _ipp_preasons[] =
+{                                      /* Strings for bits */
+  "other",
+  "cover-open",
+  "input-tray-missing",
+  "marker-supply-empty",
+  "marker-supply-low",
+  "marker-waste-almost-full",
+  "marker-waste-full",
+  "media-empty",
+  "media-jam",
+  "media-low",
+  "media-needed",
+  "moving-to-paused",
+  "paused",
+  "spool-area-full",
+  "toner-empty",
+  "toner-low"
+};
+
+
+/*
+ * Structures...
+ */
+
+typedef struct _ipp_filter_s           /**** Attribute filter ****/
+{
+  cups_array_t         *ra;            /* Requested attributes */
+  ipp_tag_t            group_tag;      /* Group to copy */
+} _ipp_filter_t;
+
+typedef struct _ipp_job_s _ipp_job_t;
+
+typedef struct _ipp_device_s           /**** Output Device data ****/
+{
+  _cups_rwlock_t       rwlock;         /* Printer lock */
+  char                 *name,          /* printer-name (mapped to output-device) */
+                       *uuid;          /* output-device-uuid */
+  ipp_t                        *attrs;         /* All printer attributes */
+  ipp_pstate_t         state;          /* printer-state value */
+  _ipp_preason_t       reasons;        /* printer-state-reasons values */
+} _ipp_device_t;
+
+typedef struct _ipp_printer_s          /**** Printer data ****/
+{
+  _cups_rwlock_t       rwlock;         /* Printer lock */
+  int                  ipv4,           /* IPv4 listener */
+                       ipv6;           /* IPv6 listener */
+  char                 *name,          /* printer-name */
+                       *directory,     /* Spool directory */
+                       *hostname,      /* Hostname */
+                       *uri,           /* printer-uri-supported */
+                       *proxy_user,    /* Proxy username */
+                       *proxy_pass;    /* Proxy password */
+  int                  port;           /* Port */
+  size_t               urilen;         /* Length of printer URI */
+  cups_array_t         *devices;       /* Associated devices */
+  ipp_t                        *attrs;         /* Static attributes */
+  ipp_t                        *dev_attrs;     /* Current device attributes */
+  time_t               start_time;     /* Startup time */
+  time_t               config_time;    /* printer-config-change-time */
+  ipp_pstate_t         state,          /* printer-state value */
+                       dev_state;      /* Current device printer-state value */
+  _ipp_preason_t       state_reasons,  /* printer-state-reasons values */
+                       dev_reasons;    /* Current device printer-state-reasons values */
+  time_t               state_time;     /* printer-state-change-time */
+  cups_array_t         *jobs,          /* Jobs */
+                       *active_jobs,   /* Active jobs */
+                       *completed_jobs;/* Completed jobs */
+  _ipp_job_t           *processing_job;/* Current processing job */
+  int                  next_job_id;    /* Next job-id value */
+  cups_array_t         *subscriptions; /* Subscriptions */
+  int                  next_sub_id;    /* Next notify-subscription-id value */
+} _ipp_printer_t;
+
+struct _ipp_job_s                      /**** Job data ****/
+{
+  int                  id;             /* job-id */
+  _cups_rwlock_t       rwlock;         /* Job lock */
+  const char           *name,          /* job-name */
+                       *username,      /* job-originating-user-name */
+                       *format;        /* document-format */
+  int                  priority;       /* job-priority */
+  char                 *dev_uuid;      /* output-device-uuid-assigned */
+  ipp_jstate_t         state,          /* job-state value */
+                       dev_state;      /* output-device-job-state value */
+  _ipp_jreason_t       state_reasons,  /* job-state-reasons values */
+                       dev_state_reasons;
+                                       /* output-device-job-state-reasons values */
+  char                 *dev_state_message;
+                                       /* output-device-job-state-message value */
+  time_t               created,        /* time-at-creation value */
+                       processing,     /* time-at-processing value */
+                       completed;      /* time-at-completed value */
+  int                  impressions,    /* job-impressions value */
+                       impcompleted;   /* job-impressions-completed value */
+  ipp_t                        *attrs;         /* Attributes */
+  int                  cancel;         /* Non-zero when job canceled */
+  char                 *filename;      /* Print file name */
+  int                  fd;             /* Print file descriptor */
+  _ipp_printer_t       *printer;       /* Printer */
+};
+
+typedef struct _ipp_subscription_s     /**** Subscription data ****/
+{
+  int                  id;             /* notify-subscription-id */
+  const char           *uuid;          /* notify-subscription-uuid */
+  _cups_rwlock_t       rwlock;         /* Subscription lock */
+  _ipp_event_t         mask;           /* Event mask */
+  _ipp_printer_t       *printer;       /* Printer */
+  _ipp_job_t           *job;           /* Job, if any */
+  ipp_t                        *attrs;         /* Attributes */
+  const char           *username;      /* notify-subscriber-user-name */
+  int                  lease;          /* notify-lease-duration */
+  int                  interval;       /* notify-time-interval */
+  time_t               expire;         /* Lease expiration time */
+  int                  first_sequence, /* First notify-sequence-number in cache */
+                       last_sequence;  /* Last notify-sequence-number used */
+  cups_array_t         *events;        /* Events (ipp_t *'s) */
+  int                  pending_delete; /* Non-zero when the subscription is about to be deleted/canceled */
+} _ipp_subscription_t;
+
+typedef struct _ipp_client_s           /**** Client data ****/
+{
+  http_t               *http;          /* HTTP connection */
+  ipp_t                        *request,       /* IPP request */
+                       *response;      /* IPP response */
+  time_t               start;          /* Request start time */
+  http_state_t         operation;      /* Request operation */
+  ipp_op_t             operation_id;   /* IPP operation-id */
+  char                 uri[1024],      /* Request URI */
+                       *options;       /* URI options */
+  http_addr_t          addr;           /* Client address */
+  char                 hostname[256];  /* Client hostname */
+  _ipp_printer_t       *printer;       /* Printer */
+  _ipp_job_t           *job;           /* Current job, if any */
+} _ipp_client_t;
+
+
+/*
+ * Local functions...
+ */
+
+static void            add_event(_ipp_printer_t *printer, _ipp_job_t *job, _ipp_event_t event, const char *message, ...) __attribute__((__format__(__printf__, 4, 5)));
+static void            check_jobs(_ipp_printer_t *printer);
+static void            clean_jobs(_ipp_printer_t *printer);
+static int             compare_active_jobs(_ipp_job_t *a, _ipp_job_t *b);
+static int             compare_completed_jobs(_ipp_job_t *a, _ipp_job_t *b);
+static int             compare_devices(_ipp_device_t *a, _ipp_device_t *b);
+static int             compare_jobs(_ipp_job_t *a, _ipp_job_t *b);
+static void            copy_attributes(ipp_t *to, ipp_t *from, cups_array_t *ra,
+                                       ipp_tag_t group_tag, int quickcopy);
+static void            copy_job_attributes(_ipp_client_t *client,
+                                           _ipp_job_t *job, cups_array_t *ra);
+static void            copy_job_state_reasons(ipp_t *ipp, ipp_tag_t group_tag, _ipp_job_t *job);
+static void            copy_printer_state_reasons(ipp_t *ipp, ipp_tag_t group_tag, _ipp_printer_t *printer);
+static void            copy_subscription_attributes(_ipp_client_t *client, _ipp_subscription_t *sub, cups_array_t *ra);
+static _ipp_client_t   *create_client(_ipp_printer_t *printer, int sock);
+static _ipp_job_t      *create_job(_ipp_client_t *client);
+static void            create_job_filename(_ipp_printer_t *printer, _ipp_job_t *job, char *fname, size_t fnamesize);
+static int             create_listener(int family, int port);
+static _ipp_subscription_t *create_subscription(_ipp_printer_t *printer, _ipp_job_t *job, int lease, const char *username, ipp_attribute_t *events, ipp_attribute_t *notify_attrs, ipp_attribute_t *userdata);
+static _ipp_printer_t  *create_printer(const char *servername, int port, const char *name, const char *directory, const char *proxy_user, const char *proxy_pass);
+static void            debug_attributes(const char *title, ipp_t *ipp,
+                                        int response);
+static void            delete_client(_ipp_client_t *client);
+static void            delete_job(_ipp_job_t *job);
+static void            delete_printer(_ipp_printer_t *printer);
+static void            delete_subscription(_ipp_subscription_t *sub);
+static int             filter_cb(_ipp_filter_t *filter, ipp_t *dst, ipp_attribute_t *attr);
+static _ipp_job_t      *find_job(_ipp_client_t *client);
+static _ipp_jreason_t  get_job_state_reasons_bits(ipp_attribute_t *attr);
+static _ipp_event_t    get_notify_events_bits(ipp_attribute_t *attr);
+static const char      *get_notify_subscribed_event(_ipp_event_t event);
+static _ipp_preason_t  get_printer_state_reasons_bits(ipp_attribute_t *attr);
+static void            html_escape(_ipp_client_t *client, const char *s,
+                                   size_t slen);
+static void            html_footer(_ipp_client_t *client);
+static void            html_header(_ipp_client_t *client, const char *title);
+static void            html_printf(_ipp_client_t *client, const char *format, ...) __attribute__((__format__(__printf__, 2, 3)));
+static void            ipp_acknowledge_document(_ipp_client_t *client);
+static void            ipp_acknowledge_identify_printer(_ipp_client_t *client);
+static void            ipp_acknowledge_job(_ipp_client_t *client);
+static void            ipp_cancel_job(_ipp_client_t *client);
+static void            ipp_cancel_my_jobs(_ipp_client_t *client);
+static void            ipp_cancel_subscription(_ipp_client_t *client);
+static void            ipp_close_job(_ipp_client_t *client);
+static void            ipp_create_job(_ipp_client_t *client);
+static void            ipp_create_xxx_subscriptions(_ipp_client_t *client);
+static void            ipp_fetch_document(_ipp_client_t *client);
+static void            ipp_fetch_job(_ipp_client_t *client);
+static void            ipp_get_document_attributes(_ipp_client_t *client);
+static void            ipp_get_documents(_ipp_client_t *client);
+static void            ipp_get_job_attributes(_ipp_client_t *client);
+static void            ipp_get_jobs(_ipp_client_t *client);
+static void            ipp_get_notifications(_ipp_client_t *client);
+static void            ipp_get_output_device_attributes(_ipp_client_t *client);
+static void            ipp_get_printer_attributes(_ipp_client_t *client);
+static void            ipp_get_printer_supported_values(_ipp_client_t *client);
+static void            ipp_get_subscription_attributes(_ipp_client_t *client);
+static void            ipp_get_subscriptions(_ipp_client_t *client);
+static void            ipp_identify_printer(_ipp_client_t *client);
+static void            ipp_print_job(_ipp_client_t *client);
+static void            ipp_print_uri(_ipp_client_t *client);
+static void            ipp_renew_subscription(_ipp_client_t *client);
+static void            ipp_send_document(_ipp_client_t *client);
+static void            ipp_send_uri(_ipp_client_t *client);
+static void            ipp_update_active_jobs(_ipp_client_t *client);
+static void            ipp_update_document_status(_ipp_client_t *client);
+static void            ipp_update_job_status(_ipp_client_t *client);
+static void            ipp_update_output_device_attributes(_ipp_client_t *client);
+static void            ipp_validate_document(_ipp_client_t *client);
+static void            ipp_validate_job(_ipp_client_t *client);
+static int             parse_options(_ipp_client_t *client, cups_option_t **options);
+static void            *process_client(_ipp_client_t *client);
+static int             process_http(_ipp_client_t *client);
+static int             process_ipp(_ipp_client_t *client);
+static void            *process_job(_ipp_job_t *job);
+static int             respond_http(_ipp_client_t *client, http_status_t code,
+                                    const char *content_coding,
+                                    const char *type, size_t length);
+static void            respond_ipp(_ipp_client_t *client, ipp_status_t status,
+                                   const char *message, ...)
+                       __attribute__ ((__format__ (__printf__, 3, 4)));
+static void            respond_unsupported(_ipp_client_t *client,
+                                           ipp_attribute_t *attr);
+static void            run_printer(_ipp_printer_t *printer);
+static char            *time_string(time_t tv, char *buffer, size_t bufsize);
+static void            usage(int status) __attribute__((noreturn));
+static int             valid_doc_attributes(_ipp_client_t *client);
+static int             valid_job_attributes(_ipp_client_t *client);
+
+
+/*
+ * Globals...
+ */
+
+static int             KeepFiles = 0,
+                       Verbosity = 0;
+static pthread_mutex_t SubscriptionMutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t  SubscriptionCondition = PTHREAD_COND_INITIALIZER;
+
+
+/*
+ * 'main()' - Main entry to the sample infrastructure server.
+ */
+
+int                                    /* O - Exit status */
+main(int  argc,                                /* I - Number of command-line args */
+     char *argv[])                     /* I - Command-line arguments */
+{
+  int          i;                      /* Looping var */
+  const char   *opt,                   /* Current option character */
+               *servername = NULL,     /* Server host name */
+               *name = NULL;           /* Printer name */
+#ifdef HAVE_SSL
+  const char   *keypath = NULL;        /* Keychain path */
+#endif /* HAVE_SSL */
+  int          port = 0;               /* Port number (0 = auto) */
+  char         directory[1024] = "",   /* Spool directory */
+               hostname[1024],         /* Auto-detected hostname */
+               proxy_user[256] = "",   /* Proxy username */
+               *proxy_pass = NULL;     /* Proxy password */
+  _ipp_printer_t *printer;             /* Printer object */
+
+
+ /*
+  * Parse command-line arguments...
+  */
+
+  for (i = 1; i < argc; i ++)
+    if (argv[i][0] == '-')
+    {
+      for (opt = argv[i] + 1; *opt; opt ++)
+      {
+        switch (*opt)
+       {
+#ifdef HAVE_SSL
+         case 'K' : /* -K keypath */
+             i ++;
+             if (i >= argc)
+               usage(1);
+             keypath = argv[i];
+             break;
+#endif /* HAVE_SSL */
+
+         case 'd' : /* -d spool-directory */
+             i ++;
+             if (i >= argc)
+               usage(1);
+             strlcpy(directory, argv[i], sizeof(directory));
+             break;
+
+          case 'h' : /* -h (show help) */
+             usage(0);
+
+         case 'k' : /* -k (keep files) */
+             KeepFiles = 1;
+             break;
+
+         case 'n' : /* -n hostname */
+             i ++;
+             if (i >= argc)
+               usage(1);
+             servername = argv[i];
+             break;
+
+         case 'p' : /* -p port */
+             i ++;
+             if (i >= argc || !isdigit(argv[i][0] & 255))
+               usage(1);
+             port = atoi(argv[i]);
+             break;
+
+          case 'u' : /* -u user:pass */
+             i ++;
+             if (i >= argc)
+               usage(1);
+             strlcpy(proxy_user, argv[i], sizeof(proxy_user));
+             if ((proxy_pass = strchr(proxy_user, ':')) != NULL)
+               *proxy_pass++ = '\0';
+             break;
+
+         case 'v' : /* -v (be verbose) */
+             Verbosity ++;
+             break;
+
+          default : /* Unknown */
+             fprintf(stderr, "Unknown option \"-%c\".\n", *opt);
+             usage(1);
+       }
+      }
+    }
+    else if (!name)
+    {
+      name = argv[i];
+    }
+    else
+    {
+      fprintf(stderr, "Unexpected command-line argument \"%s\"\n", argv[i]);
+      usage(1);
+    }
+
+  if (!name)
+    usage(1);
+
+ /*
+  * Apply defaults as needed...
+  */
+
+  if (!servername)
+    servername = httpGetHostname(NULL, hostname, sizeof(hostname));
+
+  if (!port)
+  {
+#ifdef WIN32
+   /*
+    * Windows is almost always used as a single user system, so use a default port
+    * number of 8631.
+    */
+
+    port = 8631;
+
+#else
+   /*
+    * Use 8000 + UID mod 1000 for the default port number...
+    */
+
+    port = 8000 + ((int)getuid() % 1000);
+#endif /* WIN32 */
+
+    fprintf(stderr, "Listening on port %d.\n", port);
+  }
+
+  if (!directory[0])
+  {
+    snprintf(directory, sizeof(directory), "/tmp/ippserver.%d", (int)getpid());
+
+    if (mkdir(directory, 0777) && errno != EEXIST)
+    {
+      fprintf(stderr, "Unable to create spool directory \"%s\": %s\n",
+             directory, strerror(errno));
+      usage(1);
+    }
+
+    if (Verbosity)
+      fprintf(stderr, "Using spool directory \"%s\".\n", directory);
+  }
+
+  if (!proxy_user[0])
+  {
+    strlcpy(proxy_user, "test", sizeof(proxy_user));
+
+    if (Verbosity)
+      fputs("Using proxy username \"test\".\n", stderr);
+  }
+
+  if (!proxy_pass)
+  {
+    proxy_pass = "test123";
+
+    if (Verbosity)
+      fputs("Using proxy password \"test123\".\n", stderr);
+  }
+
+#ifdef HAVE_SSL
+  cupsSetServerCredentials(keypath, servername, 1);
+#endif /* HAVE_SSL */
+
+ /*
+  * Create the printer...
+  */
+
+  if ((printer = create_printer(servername, port, name, directory, proxy_user, proxy_pass)) == NULL)
+    return (1);
+
+ /*
+  * Run the print service...
+  */
+
+  run_printer(printer);
+
+ /*
+  * Destroy the printer and exit...
+  */
+
+  delete_printer(printer);
+
+  /* TODO: Silence compiler warnings until everything is implemented */
+  add_event(NULL, NULL, _IPP_EVENT_NONE, "");
+  check_jobs(printer);
+  copy_subscription_attributes(NULL, NULL, NULL);
+  create_subscription(NULL);
+  delete_subscription(NULL);
+  get_job_state_reasons_bits(NULL);
+  get_printer_state_reasons_bits(NULL);
+  parse_options(NULL, NULL);
+
+  return (0);
+}
+
+
+/*
+ * 'add_event()' - Add an event to a subscription.
+ */
+
+static void
+add_event(_ipp_printer_t *printer,     /* I - Printer */
+          _ipp_job_t     *job,         /* I - Job, if any */
+         _ipp_event_t   event,         /* I - Event */
+         const char     *message,      /* I - Printf-style notify-text message */
+         ...)                          /* I - Additional printf arguments */
+{
+  _ipp_subscription_t *sub;            /* Current subscription */
+  ipp_t                *n;                     /* Notify attributes */
+  char         text[1024];             /* notify-text value */
+  va_list      ap;                     /* Argument pointer */
+
+  va_start(ap, message);
+  vsnprintf(text, sizeof(text), message, ap);
+  va_end(ap);
+
+  for (sub = (_ipp_subscription_t *)cupsArrayFirst(printer->subscriptions);
+       sub;
+       sub = (_ipp_subscription_t *)cupsArrayNext(printer->subscriptions))
+  {
+    if (sub->mask & event && (!sub->job || job == sub->job))
+    {
+      _cupsRWLockWrite(&sub->rwlock);
+
+      n = ippNew();
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_CHARSET, "notify-charset", NULL, "utf-8");
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_LANGUAGE, "notify-natural-language", NULL, "en");
+      ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_INTEGER, "notify-printer-up-time", time(NULL) - printer->start_time);
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_URI, "notify-printer-uri", NULL, printer->uri);
+      if (job)
+       ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_INTEGER, "notify-job-id", job->id);
+      ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_INTEGER, "notify-subcription-id", sub->id);
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_URI, "notify-subscription-uuid", NULL, sub->uuid);
+      ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_INTEGER, "notify-sequence-number", ++ sub->last_sequence);
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_KEYWORD, "notify-subscribed-event", NULL, get_notify_subscribed_event(event));
+      ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_TEXT, "notify-text", NULL, text);
+      if (event & _IPP_EVENT_PRINTER_ALL)
+      {
+       ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_ENUM, "printer-state", printer->state);
+       copy_printer_state_reasons(n, IPP_TAG_EVENT_NOTIFICATION, printer);
+      }
+      if (event & _IPP_EVENT_JOB_ALL)
+      {
+       ippAddInteger(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_ENUM, "job-state", job->state);
+       copy_job_state_reasons(n, IPP_TAG_EVENT_NOTIFICATION, job);
+       if (event == _IPP_EVENT_JOB_CREATED)
+       {
+         ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_NAME, "job-name", NULL, job->name);
+         ippAddString(n, IPP_TAG_EVENT_NOTIFICATION, IPP_TAG_NAME, "job-originating-user-name", NULL, job->username);
+       }
+      }
+
+      cupsArrayAdd(sub->events, n);
+      if (cupsArrayCount(sub->events) > 100)
+      {
+        n = (ipp_t *)cupsArrayFirst(sub->events);
+       cupsArrayRemove(sub->events, n);
+       ippDelete(n);
+       sub->first_sequence ++;
+      }
+
+      _cupsRWUnlock(&sub->rwlock);
+      pthread_cond_broadcast(&SubscriptionCondition);
+    }
+  }
+}
+
+
+/*
+ * 'check_jobs()' - Check for new jobs to process.
+ */
+
+static void
+check_jobs(_ipp_printer_t *printer)    /* I - Printer */
+{
+  _ipp_job_t   *job;                   /* Current job */
+
+
+  if (printer->processing_job)
+    return;
+
+  _cupsRWLockWrite(&(printer->rwlock));
+  for (job = (_ipp_job_t *)cupsArrayFirst(printer->active_jobs);
+       job;
+       job = (_ipp_job_t *)cupsArrayNext(printer->active_jobs))
+  {
+    if (job->state == IPP_JSTATE_PENDING)
+    {
+      if (!_cupsThreadCreate((_cups_thread_func_t)process_job, job))
+      {
+        job->state     = IPP_JSTATE_ABORTED;
+       job->completed = time(NULL);
+
+        add_event(printer, job, _IPP_EVENT_JOB_COMPLETED, "Job aborted because creation of processing thread failed.");
+      }
+      break;
+    }
+  }
+  _cupsRWUnlock(&(printer->rwlock));
+}
+
+
+/*
+ * 'clean_jobs()' - Clean out old (completed) jobs.
+ */
+
+static void
+clean_jobs(_ipp_printer_t *printer)    /* I - Printer */
+{
+  _ipp_job_t   *job;                   /* Current job */
+  time_t       cleantime;              /* Clean time */
+
+
+  if (cupsArrayCount(printer->jobs) == 0)
+    return;
+
+  cleantime = time(NULL) - 60;
+
+  _cupsRWLockWrite(&(printer->rwlock));
+  for (job = (_ipp_job_t *)cupsArrayFirst(printer->jobs);
+       job;
+       job = (_ipp_job_t *)cupsArrayNext(printer->jobs))
+    if (job->completed && job->completed < cleantime)
+    {
+      cupsArrayRemove(printer->jobs, job);
+      delete_job(job);
+    }
+    else
+      break;
+  _cupsRWUnlock(&(printer->rwlock));
+}
+
+
+/*
+ * 'compare_active_jobs()' - Compare two active jobs.
+ */
+
+static int                             /* O - Result of comparison */
+compare_active_jobs(_ipp_job_t *a,     /* I - First job */
+                    _ipp_job_t *b)     /* I - Second job */
+{
+  int  diff;                           /* Difference */
+
+
+  if ((diff = b->priority - a->priority) == 0)
+    diff = b->id - a->id;
+
+  return (diff);
+}
+
+
+/*
+ * 'compare_completed_jobs()' - Compare two completed jobs.
+ */
+
+static int                             /* O - Result of comparison */
+compare_completed_jobs(_ipp_job_t *a,  /* I - First job */
+                       _ipp_job_t *b)  /* I - Second job */
+{
+  int  diff;                           /* Difference */
+
+
+  if ((diff = a->completed - b->completed) == 0)
+    diff = b->id - a->id;
+
+  return (diff);
+}
+
+
+/*
+ * 'compare_devices()' - Compare two devices...
+ */
+
+static int                             /* O - Result of comparison */
+compare_devices(_ipp_device_t *a,      /* I - First device */
+                _ipp_device_t *b)      /* I - Second device */
+{
+  return (strcmp(a->uuid, b->uuid));
+}
+
+
+/*
+ * 'compare_jobs()' - Compare two jobs.
+ */
+
+static int                             /* O - Result of comparison */
+compare_jobs(_ipp_job_t *a,            /* I - First job */
+             _ipp_job_t *b)            /* I - Second job */
+{
+  return (b->id - a->id);
+}
+
+
+/*
+ * 'copy_attributes()' - Copy attributes from one request to another.
+ */
+
+static void
+copy_attributes(ipp_t        *to,      /* I - Destination request */
+               ipp_t        *from,     /* I - Source request */
+               cups_array_t *ra,       /* I - Requested attributes */
+               ipp_tag_t    group_tag, /* I - Group to copy */
+               int          quickcopy) /* I - Do a quick copy? */
+{
+  _ipp_filter_t        filter;                 /* Filter data */
+
+
+  filter.ra        = ra;
+  filter.group_tag = group_tag;
+
+  ippCopyAttributes(to, from, quickcopy, (ipp_copycb_t)filter_cb, &filter);
+}
+
+
+/*
+ * 'copy_job_attrs()' - Copy job attributes to the response.
+ */
+
+static void
+copy_job_attributes(
+    _ipp_client_t *client,             /* I - Client */
+    _ipp_job_t    *job,                        /* I - Job */
+    cups_array_t  *ra)                 /* I - requested-attributes */
+{
+  copy_attributes(client->response, job->attrs, ra, IPP_TAG_JOB, 0);
+
+  if (!ra || cupsArrayFind(ra, "date-time-at-completed"))
+  {
+    if (job->completed)
+      ippAddDate(client->response, IPP_TAG_JOB, "date-time-at-completed", ippTimeToDate(job->completed));
+    else
+      ippAddOutOfBand(client->response, IPP_TAG_JOB, IPP_TAG_NOVALUE, "date-time-at-completed");
+  }
+
+  if (!ra || cupsArrayFind(ra, "date-time-at-processing"))
+  {
+    if (job->processing)
+      ippAddDate(client->response, IPP_TAG_JOB, "date-time-at-processing", ippTimeToDate(job->processing));
+    else
+      ippAddOutOfBand(client->response, IPP_TAG_JOB, IPP_TAG_NOVALUE, "date-time-at-processing");
+  }
+
+  if (!ra || cupsArrayFind(ra, "job-impressions"))
+    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-impressions", job->impressions);
+
+  if (!ra || cupsArrayFind(ra, "job-impressions-completed"))
+    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-impressions-completed", job->impcompleted);
+
+  if (!ra || cupsArrayFind(ra, "job-printer-up-time"))
+    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-printer-up-time", (int)(time(NULL) - client->printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "job-state"))
+    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_ENUM,
+                 "job-state", job->state);
+
+  if (!ra || cupsArrayFind(ra, "job-state-message"))
+  {
+    if (job->dev_state_message)
+    {
+      ippAddString(client->response, IPP_TAG_JOB, IPP_TAG_TEXT, "job-state-message", NULL, job->dev_state_message);
+    }
+    else
+    {
+      const char *message = "";                /* Message string */
+
+      switch (job->state)
+      {
+       case IPP_JSTATE_PENDING :
+           message = "Job pending.";
+           break;
+
+       case IPP_JSTATE_HELD :
+           if (job->state_reasons & _IPP_JREASON_JOB_INCOMING)
+             message = "Job incoming.";
+           else if (ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_ZERO))
+             message = "Job held.";
+           else
+             message = "Job created.";
+           break;
+
+       case IPP_JSTATE_PROCESSING :
+           if (job->state_reasons & _IPP_JREASON_PROCESSING_TO_STOP_POINT)
+           {
+             if (job->cancel)
+               message = "Cancel in progress.";
+             else
+               message = "Abort in progress.";
+           }
+           else
+             message = "Job printing.";
+           break;
+
+       case IPP_JSTATE_STOPPED :
+           message = "Job stopped.";
+           break;
+
+       case IPP_JSTATE_CANCELED :
+           message = "Job canceled.";
+           break;
+
+       case IPP_JSTATE_ABORTED :
+           message = "Job aborted.";
+           break;
+
+       case IPP_JSTATE_COMPLETED :
+           message = "Job completed.";
+           break;
+      }
+
+      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, message);
+    }
+  }
+
+  if (!ra || cupsArrayFind(ra, "job-state-reasons"))
+    copy_job_state_reasons(client->response, IPP_TAG_JOB, job);
+/*
+    switch (job->state)
+    {
+      case IPP_JSTATE_PENDING :
+         ippAddString(client->response, IPP_TAG_JOB,
+                      IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
+                      NULL, "none");
+         break;
+
+      case IPP_JSTATE_HELD :
+          if (job->fd >= 0)
+           ippAddString(client->response, IPP_TAG_JOB,
+                        IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                        "job-state-reasons", NULL, "job-incoming");
+         else if (ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_ZERO))
+           ippAddString(client->response, IPP_TAG_JOB,
+                        IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                        "job-state-reasons", NULL, "job-hold-until-specified");
+          else
+           ippAddString(client->response, IPP_TAG_JOB,
+                        IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                        "job-state-reasons", NULL, "job-data-insufficient");
+         break;
+
+      case IPP_JSTATE_PROCESSING :
+         if (job->cancel)
+           ippAddString(client->response, IPP_TAG_JOB,
+                        IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                        "job-state-reasons", NULL, "processing-to-stop-point");
+         else
+           ippAddString(client->response, IPP_TAG_JOB,
+                        IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                        "job-state-reasons", NULL, "job-printing");
+         break;
+
+      case IPP_JSTATE_STOPPED :
+         ippAddString(client->response, IPP_TAG_JOB,
+                      IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
+                      NULL, "job-stopped");
+         break;
+
+      case IPP_JSTATE_CANCELED :
+         ippAddString(client->response, IPP_TAG_JOB,
+                      IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
+                      NULL, "job-canceled-by-user");
+         break;
+
+      case IPP_JSTATE_ABORTED :
+         ippAddString(client->response, IPP_TAG_JOB,
+                      IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
+                      NULL, "aborted-by-system");
+         break;
+
+      case IPP_JSTATE_COMPLETED :
+         ippAddString(client->response, IPP_TAG_JOB,
+                      IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
+                      NULL, "job-completed-successfully");
+         break;
+    }
+*/
+
+  if (!ra || cupsArrayFind(ra, "time-at-completed"))
+    ippAddInteger(client->response, IPP_TAG_JOB,
+                  job->completed ? IPP_TAG_INTEGER : IPP_TAG_NOVALUE,
+                  "time-at-completed", (int)(job->completed - client->printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "time-at-processing"))
+    ippAddInteger(client->response, IPP_TAG_JOB,
+                  job->processing ? IPP_TAG_INTEGER : IPP_TAG_NOVALUE,
+                  "time-at-processing", (int)(job->processing - client->printer->start_time));
+}
+
+
+/*
+ * 'copy_job_state_reasons()' - Copy printer-state-reasons values.
+ */
+
+static void
+copy_job_state_reasons(
+    ipp_t      *ipp,                   /* I - Attributes */
+    ipp_tag_t  group_tag,              /* I - Group */
+    _ipp_job_t *job)                   /* I - Printer */
+{
+  _ipp_jreason_t       creasons;       /* Combined job-state-reasons */
+
+
+  creasons = job->state_reasons | job->dev_state_reasons;
+
+  if (!creasons)
+  {
+    ippAddString(ipp, group_tag, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons", NULL, "none");
+  }
+  else
+  {
+    int                        i,              /* Looping var */
+                       num_reasons;    /* Number of reasons */
+    _ipp_jreason_t     reason;         /* Current reason */
+    const char         *reasons[32];   /* Reason strings */
+
+    for (i = 0, reason = 1; i < (int)(sizeof(_ipp_jreasons) / sizeof(_ipp_jreasons[0])); i ++, reason <<= 1)
+    {
+      if (creasons & reason)
+        reasons[num_reasons ++] = _ipp_jreasons[i];
+    }
+
+    ippAddStrings(ipp, group_tag, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons", num_reasons, NULL, reasons);
+  }
+}
+
+
+/*
+ * 'copy_printer_state_reasons()' - Copy printer-state-reasons values.
+ */
+
+static void
+copy_printer_state_reasons(
+    ipp_t          *ipp,               /* I - Attributes */
+    ipp_tag_t      group_tag,          /* I - Group */
+    _ipp_printer_t *printer)           /* I - Printer */
+{
+  _ipp_preason_t       creasons = printer->state_reasons | printer->dev_reasons;
+                                       /* Combined reasons */
+
+
+  if (creasons == _IPP_PREASON_NONE)
+  {
+    ippAddString(ipp, group_tag, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-state-reasons", NULL, "none");
+  }
+  else
+  {
+    int                        i,              /* Looping var */                               num_reasons = 0;/* Number of reasons */
+    _ipp_preason_t     reason;         /* Current reason */
+    const char         *reasons[32];   /* Reason strings */
+
+    for (i = 0, reason = 1; i < (int)(sizeof(_ipp_preasons) / sizeof(_ipp_preasons[0])); i ++, reason <<= 1)
+    {
+      if (creasons & reason)
+       reasons[num_reasons ++] = _ipp_preasons[i];
+    }
+
+    ippAddStrings(ipp, group_tag, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-state-reasons", num_reasons, NULL, reasons);
+  }
+}
+
+
+/*
+ * 'copy_sub_attrs()' - Copy job attributes to the response.
+ */
+
+static void
+copy_subscription_attributes(
+    _ipp_client_t       *client,       /* I - Client */
+    _ipp_subscription_t *sub,          /* I - Subscription */
+    cups_array_t        *ra)           /* I - requested-attributes */
+{
+  copy_attributes(client->response, sub->attrs, ra, IPP_TAG_SUBSCRIPTION, 0);
+
+  if (!ra || cupsArrayFind(ra, "notify-lease-expiration-time"))
+    ippAddInteger(client->response, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-lease-expiration-time", (int)(sub->expire - client->printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "notify-printer-up-time"))
+    ippAddInteger(client->response, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-printer-up-time", (int)(time(NULL) - client->printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "notify-sequence-number"))
+    ippAddInteger(client->response, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-sequence-number", sub->last_sequence);
+}
+
+
+/*
+ * 'create_client()' - Accept a new network connection and create a client
+ *                     object.
+ */
+
+static _ipp_client_t *                 /* O - Client */
+create_client(_ipp_printer_t *printer, /* I - Printer */
+              int            sock)     /* I - Listen socket */
+{
+  _ipp_client_t        *client;                /* Client */
+
+
+  if ((client = calloc(1, sizeof(_ipp_client_t))) == NULL)
+  {
+    perror("Unable to allocate memory for client");
+    return (NULL);
+  }
+
+  client->printer = printer;
+
+ /*
+  * Accept the client and get the remote address...
+  */
+
+  if ((client->http = httpAcceptConnection(sock, 1)) == NULL)
+  {
+    perror("Unable to accept client connection");
+
+    free(client);
+
+    return (NULL);
+  }
+
+  httpGetHostname(client->http, client->hostname, sizeof(client->hostname));
+
+  if (Verbosity)
+    fprintf(stderr, "Accepted connection from %s\n", client->hostname);
+
+  return (client);
+}
+
+
+/*
+ * 'create_job()' - Create a new job object from a Print-Job or Create-Job
+ *                  request.
+ */
+
+static _ipp_job_t *                    /* O - Job */
+create_job(_ipp_client_t *client)      /* I - Client */
+{
+  _ipp_job_t           *job;           /* Job */
+  ipp_attribute_t      *attr;          /* Job attribute */
+  char                 uri[1024],      /* job-uri value */
+                       uuid[64];       /* job-uuid value */
+
+
+  _cupsRWLockWrite(&(client->printer->rwlock));
+
+ /*
+  * Allocate and initialize the job object...
+  */
+
+  if ((job = calloc(1, sizeof(_ipp_job_t))) == NULL)
+  {
+    perror("Unable to allocate memory for job");
+    return (NULL);
+  }
+
+  job->printer    = client->printer;
+  job->attrs      = ippNew();
+  job->state      = IPP_JSTATE_HELD;
+  job->fd         = -1;
+
+ /*
+  * Copy all of the job attributes...
+  */
+
+  copy_attributes(job->attrs, client->request, NULL, IPP_TAG_JOB, 0);
+
+ /*
+  * Get the requesting-user-name, document format, and priority...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "job-priority", IPP_TAG_INTEGER)) != NULL)
+    job->priority = ippGetInteger(attr, 0);
+  else
+    job->priority = 50;
+
+  if ((attr = ippFindAttribute(client->request, "requesting-user-name", IPP_TAG_NAME)) != NULL)
+    job->username = ippGetString(attr, 0, NULL);
+  else
+    job->username = "anonymous";
+
+  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_NAME, "job-originating-user-name", NULL, job->username);
+
+  if (ippGetOperation(client->request) != IPP_OP_CREATE_JOB)
+  {
+    if ((attr = ippFindAttribute(job->attrs, "document-format-detected", IPP_TAG_MIMETYPE)) != NULL)
+      job->format = ippGetString(attr, 0, NULL);
+    else if ((attr = ippFindAttribute(job->attrs, "document-format-supplied", IPP_TAG_MIMETYPE)) != NULL)
+      job->format = ippGetString(attr, 0, NULL);
+    else
+      job->format = "application/octet-stream";
+  }
+
+  if ((attr = ippFindAttribute(client->request, "job-impressions", IPP_TAG_INTEGER)) != NULL)
+    job->impressions = ippGetInteger(attr, 0);
+
+  if ((attr = ippFindAttribute(client->request, "job-name", IPP_TAG_NAME)) != NULL)
+    job->name = ippGetString(attr, 0, NULL);
+
+ /*
+  * Add job description attributes and add to the jobs array...
+  */
+
+  job->id = client->printer->next_job_id ++;
+
+  snprintf(uri, sizeof(uri), "%s/%d", client->printer->uri, job->id);
+  httpAssembleUUID(client->printer->hostname, client->printer->port, client->printer->name, job->id, uuid, sizeof(uuid));
+
+  ippAddDate(job->attrs, IPP_TAG_JOB, "date-time-at-creation", ippTimeToDate(time(&job->created)));
+  ippAddInteger(job->attrs, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-id", job->id);
+  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-uri", NULL, uri);
+  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-uuid", NULL, uuid);
+  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-printer-uri", NULL, client->printer->uri);
+  ippAddInteger(job->attrs, IPP_TAG_JOB, IPP_TAG_INTEGER, "time-at-creation", (int)(job->created - client->printer->start_time));
+
+  cupsArrayAdd(client->printer->jobs, job);
+  cupsArrayAdd(client->printer->active_jobs, job);
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+  return (job);
+}
+
+
+/*
+ * 'create_job_filename()' - Create the filename for a document in a job.
+ */
+
+static void create_job_filename(
+    _ipp_printer_t *printer,           /* I - Printer */
+    _ipp_job_t     *job,               /* I - Job */
+    char           *fname,             /* I - Filename buffer */
+    size_t         fnamesize)          /* I - Size of filename buffer */
+{
+  char                 name[256],      /* "Safe" filename */
+                       *nameptr;       /* Pointer into filename */
+  const char           *ext,           /* Filename extension */
+                       *job_name;      /* job-name value */
+  ipp_attribute_t      *job_name_attr; /* job-name attribute */
+
+
+ /*
+  * Make a name from the job-name attribute...
+  */
+
+  if ((job_name_attr = ippFindAttribute(job->attrs, "job-name", IPP_TAG_NAME)) != NULL)
+    job_name = ippGetString(job_name_attr, 0, NULL);
+  else
+    job_name = "untitled";
+
+  for (nameptr = name; *job_name && nameptr < (name + sizeof(name) - 1); job_name ++)
+    if (isalnum(*job_name & 255) || *job_name == '-')
+      *nameptr++ = (char)tolower(*job_name & 255);
+    else
+      *nameptr++ = '_';
+
+  *nameptr = '\0';
+
+ /*
+  * Figure out the extension...
+  */
+
+  if (!strcasecmp(job->format, "image/jpeg"))
+    ext = "jpg";
+  else if (!strcasecmp(job->format, "image/png"))
+    ext = "png";
+  else if (!strcasecmp(job->format, "image/pwg-raster"))
+    ext = "ras";
+  else if (!strcasecmp(job->format, "image/urf"))
+    ext = "urf";
+  else if (!strcasecmp(job->format, "application/pdf"))
+    ext = "pdf";
+  else if (!strcasecmp(job->format, "application/postscript"))
+    ext = "ps";
+  else
+    ext = "prn";
+
+ /*
+  * Create a filename with the job-id, job-name, and document-format (extension)...
+  */
+
+  snprintf(fname, fnamesize, "%s/%d-%s.%s", printer->directory, job->id, name, ext);
+}
+
+
+/*
+ * 'create_listener()' - Create a listener socket.
+ */
+
+static int                             /* O - Listener socket or -1 on error */
+create_listener(int family,            /* I - Address family */
+                int port)              /* I - Port number */
+{
+  int                  sock;           /* Listener socket */
+  http_addrlist_t      *addrlist;      /* Listen address */
+  char                 service[255];   /* Service port */
+
+
+  snprintf(service, sizeof(service), "%d", port);
+  if ((addrlist = httpAddrGetList(NULL, family, service)) == NULL)
+    return (-1);
+
+  sock = httpAddrListen(&(addrlist->addr), port);
+
+  httpAddrFreeList(addrlist);
+
+  return (sock);
+}
+
+
+/*
+ * 'create_printer()' - Create, register, and listen for connections to a
+ *                      printer object.
+ */
+
+static _ipp_printer_t *                        /* O - Printer */
+create_printer(const char *servername, /* I - Server hostname (NULL for default) */
+               int        port,                /* I - Port number */
+               const char *name,       /* I - printer-name */
+              const char *directory,   /* I - Spool directory */
+              const char *proxy_user,  /* I - Proxy account username */
+              const char *proxy_pass)  /* I - Proxy account password */
+{
+  _ipp_printer_t       *printer;       /* Printer */
+  char                 uri[1024],      /* Printer URI */
+                       adminurl[1024], /* printer-more-info URI */
+                       supplyurl[1024],/* printer-supply-info-uri URI */
+                       uuid[128];      /* printer-uuid */
+  int                  k_supported;    /* Maximum file size supported */
+#ifdef HAVE_STATVFS
+  struct statvfs       spoolinfo;      /* FS info for spool directory */
+  double               spoolsize;      /* FS size */
+#elif defined(HAVE_STATFS)
+  struct statfs                spoolinfo;      /* FS info for spool directory */
+  double               spoolsize;      /* FS size */
+#endif /* HAVE_STATVFS */
+  static const char * const versions[] =/* ipp-versions-supported values */
+  {
+    "1.0",
+    "1.1",
+    "2.0"
+  };
+  static const char * const features[] =/* ipp-features-supported values */
+  {
+    "document-object",
+    "ipp-everywhere",
+    "infrastructure-printer",
+    "page-overrides"
+  };
+  static const int     ops[] =         /* operations-supported values */
+  {
+    IPP_OP_PRINT_JOB,
+    IPP_OP_PRINT_URI,
+    IPP_OP_VALIDATE_JOB,
+    IPP_OP_CREATE_JOB,
+    IPP_OP_SEND_DOCUMENT,
+    IPP_OP_SEND_URI,
+    IPP_OP_CANCEL_JOB,
+    IPP_OP_GET_JOB_ATTRIBUTES,
+    IPP_OP_GET_JOBS,
+    IPP_OP_GET_PRINTER_ATTRIBUTES,
+    IPP_OP_GET_PRINTER_SUPPORTED_VALUES,
+    IPP_OP_CREATE_PRINTER_SUBSCRIPTIONS,
+    IPP_OP_CREATE_JOB_SUBSCRIPTIONS,
+    IPP_OP_GET_SUBSCRIPTION_ATTRIBUTES,
+    IPP_OP_GET_SUBSCRIPTIONS,
+    IPP_OP_RENEW_SUBSCRIPTION,
+    IPP_OP_CANCEL_SUBSCRIPTION,
+    IPP_OP_GET_NOTIFICATIONS,
+    IPP_OP_GET_DOCUMENT_ATTRIBUTES,
+    IPP_OP_GET_DOCUMENTS,
+    IPP_OP_CANCEL_MY_JOBS,
+    IPP_OP_CLOSE_JOB,
+    IPP_OP_IDENTIFY_PRINTER,
+    IPP_OP_VALIDATE_DOCUMENT,
+    _IPP_OP_ACKNOWLEDGE_DOCUMENT,
+    _IPP_OP_ACKNOWLEDGE_IDENTIFY_PRINTER,
+    _IPP_OP_ACKNOWLEDGE_JOB,
+    _IPP_OP_FETCH_DOCUMENT,
+    _IPP_OP_FETCH_JOB,
+    _IPP_OP_GET_OUTPUT_DEVICE_ATTRIBUTES,
+    _IPP_OP_UPDATE_ACTIVE_JOBS,
+    _IPP_OP_UPDATE_DOCUMENT_STATUS,
+    _IPP_OP_UPDATE_JOB_STATUS,
+    _IPP_OP_UPDATE_OUTPUT_DEVICE_ATTRIBUTES
+  };
+  static const char * const charsets[] =/* charset-supported values */
+  {
+    "us-ascii",
+    "utf-8"
+  };
+  static const char * const compressions[] =/* compression-supported values */
+  {
+#ifdef HAVE_LIBZ
+    "deflate",
+    "gzip",
+#endif /* HAVE_LIBZ */
+    "none"
+  };
+  static const char * const notify_attributes[] =
+  {                                    /* notify-attributes-supported */
+    "printer-state-change-time",
+    "notify-lease-expiration-time",
+    "notify-subscriber-user-name"
+  };
+  static const char * const reference_uri_schemes_supported[] =
+  {                                    /* reference-uri-schemes-supported */
+    "file",
+    "ftp",
+    "http"
+#ifdef HAVE_SSL
+    , "https"
+#endif /* HAVE_SSL */
+  };
+  static const char * const which_jobs[] =
+  {                                    /* which-jobs-supported values */
+    "completed",
+    "not-completed",
+    "aborted",
+    "all",
+    "canceled",
+    "pending",
+    "pending-held",
+    "processing",
+    "processing-stopped"
+  };
+
+
+ /*
+  * Allocate memory for the printer...
+  */
+
+  if ((printer = calloc(1, sizeof(_ipp_printer_t))) == NULL)
+  {
+    perror("ippserver: Unable to allocate memory for printer");
+    return (NULL);
+  }
+
+  printer->ipv4           = -1;
+  printer->ipv6           = -1;
+  printer->name           = strdup(name);
+  printer->directory      = strdup(directory);
+  printer->hostname       = strdup(servername);
+  printer->port           = port;
+  printer->start_time     = time(NULL);
+  printer->config_time    = printer->start_time;
+  printer->state          = IPP_PSTATE_IDLE;
+  printer->state_reasons  = _IPP_PREASON_NONE;
+  printer->state_time     = printer->start_time;
+  printer->jobs           = cupsArrayNew3((cups_array_func_t)compare_jobs, NULL, NULL, 0, NULL, (cups_afree_func_t)delete_job);
+  printer->active_jobs    = cupsArrayNew((cups_array_func_t)compare_active_jobs, NULL);
+  printer->completed_jobs = cupsArrayNew((cups_array_func_t)compare_active_jobs, NULL);
+  printer->next_job_id    = 1;
+
+  httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL,
+                 printer->hostname, printer->port, "/ipp/print");
+  printer->uri    = strdup(uri);
+  printer->urilen = strlen(uri);
+
+  if (proxy_user)
+    printer->proxy_user = strdup(proxy_user);
+  if (proxy_pass)
+    printer->proxy_pass = strdup(proxy_pass);
+
+  printer->devices = cupsArrayNew((cups_array_func_t)compare_devices, NULL);
+
+  _cupsRWInit(&(printer->rwlock));
+
+ /*
+  * Create the listener sockets...
+  */
+
+  if ((printer->ipv4 = create_listener(AF_INET, printer->port)) < 0)
+  {
+    perror("Unable to create IPv4 listener");
+    goto bad_printer;
+  }
+
+  if ((printer->ipv6 = create_listener(AF_INET6, printer->port)) < 0)
+  {
+    perror("Unable to create IPv6 listener");
+    goto bad_printer;
+  }
+
+ /*
+  * Prepare values for the printer attributes...
+  */
+
+  httpAssembleURI(HTTP_URI_CODING_ALL, adminurl, sizeof(adminurl), "http", NULL, printer->hostname, printer->port, "/");
+  httpAssembleURI(HTTP_URI_CODING_ALL, supplyurl, sizeof(supplyurl), "http", NULL, printer->hostname, printer->port, "/supplies");
+
+  if (Verbosity)
+  {
+    fprintf(stderr, "printer-more-info=\"%s\"\n", adminurl);
+    fprintf(stderr, "printer-supply-info-uri=\"%s\"\n", supplyurl);
+    fprintf(stderr, "printer-uri=\"%s\"\n", uri);
+  }
+
+ /*
+  * Get the maximum spool size based on the size of the filesystem used for
+  * the spool directory.  If the host OS doesn't support the statfs call
+  * or the filesystem is larger than 2TiB, always report INT_MAX.
+  */
+
+#ifdef HAVE_STATVFS
+  if (statvfs(printer->directory, &spoolinfo))
+    k_supported = INT_MAX;
+  else if ((spoolsize = (double)spoolinfo.f_frsize *
+                        spoolinfo.f_blocks / 1024) > INT_MAX)
+    k_supported = INT_MAX;
+  else
+    k_supported = (int)spoolsize;
+
+#elif defined(HAVE_STATFS)
+  if (statfs(printer->directory, &spoolinfo))
+    k_supported = INT_MAX;
+  else if ((spoolsize = (double)spoolinfo.f_bsize *
+                        spoolinfo.f_blocks / 1024) > INT_MAX)
+    k_supported = INT_MAX;
+  else
+    k_supported = (int)spoolsize;
+
+#else
+  k_supported = INT_MAX;
+#endif /* HAVE_STATVFS */
+
+ /*
+  * Create the printer attributes.  This list of attributes is sorted to improve
+  * performance when the client provides a requested-attributes attribute...
+  */
+
+  printer->attrs = ippNew();
+
+  /* charset-configured */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER,
+               IPP_CONST_TAG(IPP_TAG_CHARSET),
+               "charset-configured", NULL, "utf-8");
+
+  /* charset-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER,
+                IPP_CONST_TAG(IPP_TAG_CHARSET),
+                "charset-supported", sizeof(charsets) / sizeof(charsets[0]),
+               NULL, charsets);
+
+  /* compression-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER,
+                IPP_CONST_TAG(IPP_TAG_KEYWORD),
+               "compression-supported",
+               (int)(sizeof(compressions) / sizeof(compressions[0])), NULL,
+               compressions);
+
+  /* generated-natural-language-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER,
+               IPP_CONST_TAG(IPP_TAG_LANGUAGE),
+               "generated-natural-language-supported", NULL, "en");
+
+  /* ipp-features-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "ipp-features-supported", sizeof(features) / sizeof(features[0]), NULL, features);
+
+  /* ipp-versions-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "ipp-versions-supported", sizeof(versions) / sizeof(versions[0]), NULL, versions);
+
+  /* ippget-event-life */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "ippget-event-life", 300);
+
+  /* job-ids-supported */
+  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "job-ids-supported", 1);
+
+  /* job-k-octets-supported */
+  ippAddRange(printer->attrs, IPP_TAG_PRINTER, "job-k-octets-supported", 0,
+             k_supported);
+
+  /* job-priority-default */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER,
+                "job-priority-default", 50);
+
+  /* job-priority-supported */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER,
+                "job-priority-supported", 100);
+
+  /* multiple-document-jobs-supported */
+  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "multiple-document-jobs-supported", 0);
+
+  /* multiple-operation-time-out */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "multiple-operation-time-out", 60);
+
+  /* multiple-operation-time-out-action */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "multiple-operation-time-out-action", NULL, "abort-job");
+
+  /* natural-language-configured */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER,
+               IPP_CONST_TAG(IPP_TAG_LANGUAGE),
+               "natural-language-configured", NULL, "en");
+
+  /* notify-attributes-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "notify-attributes-supported", sizeof(notify_attributes) / sizeof(notify_attributes[0]), NULL, notify_attributes);
+
+  /* notify-events-default */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "notify-events-default", NULL, "job-completed");
+
+  /* notify-events-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "notify-events-supported", sizeof(_ipp_events) / sizeof(_ipp_events[0]), NULL, _ipp_events);
+
+  /* notify-lease-duration-default */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "notify-lease-duration-default", 86400);
+
+  /* notify-lease-duration-supported */
+  ippAddRange(printer->attrs, IPP_TAG_PRINTER, "notify-lease-duration-supported", 0, _IPP_NOTIFY_LEASE_DURATION_MAX);
+
+  /* notify-max-events-supported */
+  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "notify-lease-duration-default", (int)(sizeof(_ipp_events) / sizeof(_ipp_events[0])));
+
+  /* notify-pull-method-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "notify-pull-method-supported", NULL, "ippget");
+
+  /* operations-supported */
+  ippAddIntegers(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM,
+                "operations-supported", sizeof(ops) / sizeof(ops[0]), ops);
+
+  /* printer-get-attributes-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-get-attributes-supported", NULL, "document-format");
+
+  /* printer-is-accepting-jobs */
+  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "printer-is-accepting-jobs", 1);
+
+  /* printer-info */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_TEXT, "printer-info", NULL, name);
+
+  /* printer-more-info */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-more-info", NULL, adminurl);
+
+  /* printer-name */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_NAME, "printer-name", NULL, name);
+
+  /* printer-supply-info-uri */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-supply-info-uri", NULL, supplyurl);
+
+  /* printer-uri-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-uri-supported", NULL, uri);
+
+  /* printer-uuid */
+  httpAssembleUUID(printer->hostname, port, name, 0, uuid, sizeof(uuid));
+  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-uuid", NULL, uuid);
+
+  /* reference-uri-scheme-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER,
+                IPP_CONST_TAG(IPP_TAG_URISCHEME),
+                "reference-uri-schemes-supported",
+                (int)(sizeof(reference_uri_schemes_supported) /
+                      sizeof(reference_uri_schemes_supported[0])),
+                NULL, reference_uri_schemes_supported);
+
+  /* uri-authentication-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER,
+               IPP_CONST_TAG(IPP_TAG_KEYWORD),
+               "uri-authentication-supported", NULL, "basic");
+
+  /* uri-security-supported */
+  ippAddString(printer->attrs, IPP_TAG_PRINTER,
+               IPP_CONST_TAG(IPP_TAG_KEYWORD),
+               "uri-security-supported", NULL, "tls");
+
+  /* which-jobs-supported */
+  ippAddStrings(printer->attrs, IPP_TAG_PRINTER,
+                IPP_CONST_TAG(IPP_TAG_KEYWORD),
+                "which-jobs-supported",
+                sizeof(which_jobs) / sizeof(which_jobs[0]), NULL, which_jobs);
+
+  debug_attributes("Printer", printer->attrs, 0);
+
+ /*
+  * Return it!
+  */
+
+  return (printer);
+
+
+ /*
+  * If we get here we were unable to create the printer...
+  */
+
+  bad_printer:
+
+  delete_printer(printer);
+  return (NULL);
+}
+
+
+/*
+ * 'create_subscription()' - Create a new subscription object from a
+ *                           Print-Job, Create-Job, or Create-xxx-Subscription
+ *                           request.
+ */
+
+static _ipp_subscription_t *           /* O - Subscription object */
+create_subscription(
+    _ipp_printer_t  *printer,          /* I - Printer */
+    _ipp_job_t      *job,              /* I - Job, if any */
+    int             lease,             /* I - Lease duration */
+    const char      *username,         /* I - User creating the subscription */
+    ipp_attribute_t *events,           /* I - Events to monitor */
+    ipp_attribute_t *notify_attrs,     /* I - Attributes to report */
+    ipp_attribute_t *userdata)         /* I - User data, if any */
+{
+  _ipp_subscription_t  *sub;           /* Subscription */
+  ipp_attribute_t      *attr;          /* Subscription attribute */
+  char                 uuid[64];       /* notify-subscription-uuid value */
+
+
+ /*
+  * Allocate and initialize the subscription object...
+  */
+
+  if ((sub = calloc(1, sizeof(_ipp_subscription_t))) == NULL)
+  {
+    perror("Unable to allocate memory for subscription");
+    return (NULL);
+  }
+
+  _cupsRWLockWrite(&(printer->rwlock));
+
+  sub->id      = printer->next_sub_id ++;
+  sub->mask    = get_notify_events_bits(events);
+  sub->printer = printer;
+  sub->job     = job;
+  sub->lease   = lease;
+  sub->expire  = time(NULL) + sub->lease;
+  sub->attrs   = ippNew();
+
+  _cupsMutexInit(&(sub->mutex));
+  pthread_cond_init(&(sub->cond), NULL);
+
+ /*
+  * Add subscription description attributes and add to the subscriptions
+  * array...
+  */
+
+  ippAddInteger(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-subscription-id", sub->id);
+
+  httpAssembleUUID(printer->hostname, printer->port, printer->name, -sub->id, uuid, sizeof(uuid));
+  attr = ippAddString(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_URI, "notify-subscription-uuid", NULL, uuid);
+  sub->uuid = ippGetString(attr, 0, NULL);
+
+  ippAddString(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_URI, "notify-printer-uri", NULL, printer->uri);
+
+  if (job)
+    ippAddInteger(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-job-id", job->id);
+  else
+    ippAddInteger(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, "notify-lease-duration", sub->lease);
+
+  attr = ippAddString(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_TAG_NAME, "notify-subscriber-user-name", NULL, username);
+  sub->username = ippGetString(attr, 0, NULL);
+
+  ippCopyAttribute(sub->attrs, events, 0);
+
+  ippAddString(sub->attrs, IPP_TAG_SUBSCRIPTION, IPP_CONST_TAG(IPP_TAG_KEYWORD), "notify-pull-method", NULL, "ippget");
+
+  if (notify_attrs)
+    ippCopyAttribute(sub->attrs, notify_attrs, 0);
+
+  if (userdata)
+    ippCopyAttribute(sub->attrs, userdata, 0);
+
+  sub->events = cupsArrayNew3(NULL, NULL, NULL, 0, NULL, (cups_afree_func_t)ippDelete);
+
+  cupsArrayAdd(printer->subscriptions, sub);
+
+  _cupsRWUnlock(&(printer->rwlock));
+
+  return (sub);
+}
+
+
+/*
+ * 'debug_attributes()' - Print attributes in a request or response.
+ */
+
+static void
+debug_attributes(const char *title,    /* I - Title */
+                 ipp_t      *ipp,      /* I - Request/response */
+                 int        type)      /* I - 0 = object, 1 = request, 2 = response */
+{
+  ipp_tag_t            group_tag;      /* Current group */
+  ipp_attribute_t      *attr;          /* Current attribute */
+  char                 buffer[2048];   /* String buffer for value */
+  int                  major, minor;   /* Version */
+
+
+  if (Verbosity <= 1)
+    return;
+
+  fprintf(stderr, "%s:\n", title);
+  major = ippGetVersion(ipp, &minor);
+  fprintf(stderr, "  version=%d.%d\n", major, minor);
+  if (type == 1)
+    fprintf(stderr, "  operation-id=%s(%04x)\n",
+            ippOpString(ippGetOperation(ipp)), ippGetOperation(ipp));
+  else if (type == 2)
+    fprintf(stderr, "  status-code=%s(%04x)\n",
+            ippErrorString(ippGetStatusCode(ipp)), ippGetStatusCode(ipp));
+  fprintf(stderr, "  request-id=%d\n\n", ippGetRequestId(ipp));
+
+  for (attr = ippFirstAttribute(ipp), group_tag = IPP_TAG_ZERO;
+       attr;
+       attr = ippNextAttribute(ipp))
+  {
+    if (ippGetGroupTag(attr) != group_tag)
+    {
+      group_tag = ippGetGroupTag(attr);
+      fprintf(stderr, "  %s\n", ippTagString(group_tag));
+    }
+
+    if (ippGetName(attr))
+    {
+      ippAttributeString(attr, buffer, sizeof(buffer));
+      fprintf(stderr, "    %s (%s%s) %s\n", ippGetName(attr),
+             ippGetCount(attr) > 1 ? "1setOf " : "",
+             ippTagString(ippGetValueTag(attr)), buffer);
+    }
+  }
+}
+
+
+/*
+ * 'delete_client()' - Close the socket and free all memory used by a client
+ *                     object.
+ */
+
+static void
+delete_client(_ipp_client_t *client)   /* I - Client */
+{
+  if (Verbosity)
+    fprintf(stderr, "Closing connection from %s\n", client->hostname);
+
+ /*
+  * Flush pending writes before closing...
+  */
+
+  httpFlushWrite(client->http);
+
+ /*
+  * Free memory...
+  */
+
+  httpClose(client->http);
+
+  ippDelete(client->request);
+  ippDelete(client->response);
+
+  free(client);
+}
+
+
+/*
+ * 'delete_job()' - Remove from the printer and free all memory used by a job
+ *                  object.
+ */
+
+static void
+delete_job(_ipp_job_t *job)            /* I - Job */
+{
+  if (Verbosity)
+    fprintf(stderr, "Removing job #%d from history.\n", job->id);
+
+  _cupsRWLockWrite(&job->rwlock);
+
+  ippDelete(job->attrs);
+
+  if (job->filename)
+  {
+    if (!KeepFiles)
+      unlink(job->filename);
+
+    free(job->filename);
+  }
+
+  pthread_rwlock_destroy(&job->rwlock);
+
+  free(job);
+}
+
+
+/*
+ * 'delete_printer()' - Unregister, close listen sockets, and free all memory
+ *                      used by a printer object.
+ */
+
+static void
+delete_printer(_ipp_printer_t *printer)        /* I - Printer */
+{
+  _cupsRWLockWrite(&printer->rwlock);
+
+  if (printer->ipv4 >= 0)
+    close(printer->ipv4);
+
+  if (printer->ipv6 >= 0)
+    close(printer->ipv6);
+
+  if (printer->name)
+    free(printer->name);
+  if (printer->directory)
+    free(printer->directory);
+  if (printer->hostname)
+    free(printer->hostname);
+  if (printer->uri)
+    free(printer->uri);
+  if (printer->proxy_user)
+    free(printer->proxy_user);
+  if (printer->proxy_pass)
+    free(printer->proxy_pass);
+
+  ippDelete(printer->attrs);
+  ippDelete(printer->dev_attrs);
+
+  cupsArrayDelete(printer->active_jobs);
+  cupsArrayDelete(printer->completed_jobs);
+  cupsArrayDelete(printer->jobs);
+  cupsArrayDelete(printer->subscriptions);
+
+  pthread_rwlock_destroy(&printer->rwlock);
+
+  free(printer);
+}
+
+
+/*
+ * 'delete_subscription()' - Delete a subscription.
+ */
+
+static void
+delete_subscription(
+    _ipp_subscription_t *sub)          /* I - Subscription */
+{
+  sub->pending_delete = 1;
+
+  pthread_cond_broadcast(&SubscriptionCondition);
+
+  _cupsRWLockWrite(&sub->rwlock);
+
+  ippDelete(sub->attrs);
+  cupsArrayDelete(sub->events);
+
+  pthread_rwlock_destroy(&sub->rwlock);
+
+  free(sub);
+}
+
+
+/*
+ * 'filter_cb()' - Filter printer attributes based on the requested array.
+ */
+
+static int                             /* O - 1 to copy, 0 to ignore */
+filter_cb(_ipp_filter_t   *filter,     /* I - Filter parameters */
+          ipp_t           *dst,                /* I - Destination (unused) */
+         ipp_attribute_t *attr)        /* I - Source attribute */
+{
+ /*
+  * Filter attributes as needed...
+  */
+
+  (void)dst;
+
+  ipp_tag_t group = ippGetGroupTag(attr);
+  const char *name = ippGetName(attr);
+
+  if ((filter->group_tag != IPP_TAG_ZERO && group != filter->group_tag && group != IPP_TAG_ZERO) || !name || (!strcmp(name, "media-col-database") && !cupsArrayFind(filter->ra, (void *)name)))
+    return (0);
+
+  return (!filter->ra || cupsArrayFind(filter->ra, (void *)name) != NULL);
+}
+
+
+/*
+ * 'find_job()' - Find a job specified in a request.
+ */
+
+static _ipp_job_t *                    /* O - Job or NULL */
+find_job(_ipp_client_t *client)                /* I - Client */
+{
+  ipp_attribute_t      *attr;          /* job-id or job-uri attribute */
+  _ipp_job_t           key,            /* Job search key */
+                       *job;           /* Matching job, if any */
+
+
+  if ((attr = ippFindAttribute(client->request, "job-uri", IPP_TAG_URI)) != NULL)
+  {
+    const char *uri = ippGetString(attr, 0, NULL);
+
+    if (!strncmp(uri, client->printer->uri, client->printer->urilen) &&
+        uri[client->printer->urilen] == '/')
+      key.id = atoi(uri + client->printer->urilen + 1);
+    else
+      return (NULL);
+  }
+  else if ((attr = ippFindAttribute(client->request, "job-id", IPP_TAG_INTEGER)) != NULL)
+    key.id = ippGetInteger(attr, 0);
+
+  _cupsRWLockRead(&(client->printer->rwlock));
+  job = (_ipp_job_t *)cupsArrayFind(client->printer->jobs, &key);
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+  return (job);
+}
+
+
+/*
+ * 'get_job_state_reasons_bits()' - Get the bits associates with "job-state-reasons" values.
+ */
+
+static _ipp_jreason_t                  /* O - Bits */
+get_job_state_reasons_bits(
+    ipp_attribute_t *attr)             /* I - "job-state-reasons" attribute */
+{
+  int                  i, j,           /* Looping vars */
+                       count;          /* Number of "job-state-reasons" values */
+  const char           *keyword;       /* "job-state-reasons" value */
+  _ipp_jreason_t       jreasons = _IPP_JREASON_NONE;
+                                       /* Bits for "job-state-reasons" values */
+
+
+  count = ippGetCount(attr);
+  for (i = 0; i < count; i ++)
+  {
+    keyword = ippGetString(attr, i, NULL);
+
+    for (j = 0; j < (int)(sizeof(_ipp_jreasons) / sizeof(_ipp_jreasons[0])); j ++)
+    {
+      if (!strcmp(keyword, _ipp_jreasons[j]))
+      {
+        jreasons |= 1 << j;
+       break;
+      }
+    }
+  }
+
+  return (jreasons);
+}
+
+
+/*
+ * 'get_notify_event_bits()' - Get the bits associated with "notify-events" values.
+ */
+
+static _ipp_event_t                    /* O - Bits */
+get_notify_events_bits(
+    ipp_attribute_t *attr)             /* I - "notify-events" attribute */
+{
+  int          i, j,                   /* Looping vars */
+               count;                  /* Number of "notify-events" values */
+  const char   *keyword;               /* "notify-events" value */
+  _ipp_event_t events = _IPP_EVENT_NONE;
+                                       /* Bits for "notify-events" values */
+
+
+  count = ippGetCount(attr);
+  for (i = 0; i < count; i ++)
+  {
+    keyword = ippGetString(attr, i, NULL);
+
+    for (j = 0; j < (int)(sizeof(_ipp_events) / sizeof(_ipp_events[0])); j ++)
+    {
+      if (!strcmp(keyword, _ipp_jreasons[j]))
+      {
+        events |= 1 << j;
+       break;
+      }
+    }
+  }
+
+  return (events);
+}
+
+
+/*
+ * 'get_notify_subscribed_event()' - Get the event name.
+ */
+
+static const char *                    /* O - Event name */
+get_notify_subscribed_event(
+    _ipp_event_t event)                        /* I - Event bit */
+{
+  int          i;                      /* Looping var */
+  _ipp_event_t mask;                   /* Current mask */
+
+  for (i = 0, mask = 1; i < (int)(sizeof(_ipp_events) / sizeof(_ipp_events[0])); i ++, mask <<= 1)
+    if (event & mask)
+      return (_ipp_events[i]);
+
+  return ("none");
+}
+
+
+/*
+ * 'get_printer_state_reasons_bits()' - Get the bits associated with "printer-state-reasons" values.
+ */
+
+static _ipp_preason_t                  /* O - Bits */
+get_printer_state_reasons_bits(
+    ipp_attribute_t *attr)             /* I - "printer-state-reasons" bits */
+{
+  int                  i, j,           /* Looping vars */
+                       count;          /* Number of "printer-state-reasons" values */
+  const char           *keyword;       /* "printer-state-reasons" value */
+  _ipp_preason_t       preasons = _IPP_PREASON_NONE;
+                                       /* Bits for "printer-state-reasons" values */
+
+
+  count = ippGetCount(attr);
+  for (i = 0; i < count; i ++)
+  {
+    keyword = ippGetString(attr, i, NULL);
+
+    for (j = 0; j < (int)(sizeof(_ipp_preasons) / sizeof(_ipp_preasons[0])); j ++)
+    {
+      if (!strcmp(keyword, _ipp_preasons[j]))
+      {
+        preasons |= 1 << j;
+       break;
+      }
+    }
+  }
+
+  return (preasons);
+}
+
+
+/*
+ * 'html_escape()' - Write a HTML-safe string.
+ */
+
+static void
+html_escape(_ipp_client_t *client,     /* I - Client */
+           const char    *s,           /* I - String to write */
+           size_t        slen)         /* I - Number of characters to write */
+{
+  const char   *start,                 /* Start of segment */
+               *end;                   /* End of string */
+
+
+  start = s;
+  end   = s + (slen > 0 ? slen : strlen(s));
+
+  while (*s && s < end)
+  {
+    if (*s == '&' || *s == '<')
+    {
+      if (s > start)
+        httpWrite2(client->http, start, (size_t)(s - start));
+
+      if (*s == '&')
+        httpWrite2(client->http, "&amp;", 5);
+      else
+        httpWrite2(client->http, "&lt;", 4);
+
+      start = s + 1;
+    }
+
+    s ++;
+  }
+
+  if (s > start)
+    httpWrite2(client->http, start, (size_t)(s - start));
+}
+
+
+/*
+ * 'html_footer()' - Show the web interface footer.
+ *
+ * This function also writes the trailing 0-length chunk.
+ */
+
+static void
+html_footer(_ipp_client_t *client)     /* I - Client */
+{
+  html_printf(client,
+             "</div>\n"
+             "</body>\n"
+             "</html>\n");
+  httpWrite2(client->http, "", 0);
+}
+
+
+/*
+ * 'html_header()' - Show the web interface header and title.
+ */
+
+static void
+html_header(_ipp_client_t *client,     /* I - Client */
+            const char    *title)      /* I - Title */
+{
+  html_printf(client,
+             "<!doctype html>\n"
+             "<html>\n"
+             "<head>\n"
+             "<title>%s</title>\n"
+             "<link rel=\"shortcut icon\" href=\"/icon.png\" type=\"image/png\">\n"
+             "<link rel=\"apple-touch-icon\" href=\"/icon.png\" type=\"image/png\">\n"
+             "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=9\">\n"
+             "<meta name=\"viewport\" content=\"width=device-width\">\n"
+             "<style>\n"
+             "body { font-family: sans-serif; margin: 0; }\n"
+             "div.body { padding: 0px 10px 10px; }\n"
+             "blockquote { background: #dfd; border-radius: 5px; color: #006; padding: 10px; }\n"
+             "table.form { border-collapse: collapse; margin-top: 10px; width: 100%%; }\n"
+             "table.form td, table.form th { padding: 5px 2px; width: 50%%; }\n"
+             "table.form th { text-align: right; }\n"
+             "table.striped { border-bottom: solid thin black; border-collapse: collapse; width: 100%%; }\n"
+             "table.striped tr:nth-child(even) { background: #fcfcfc; }\n"
+             "table.striped tr:nth-child(odd) { background: #f0f0f0; }\n"
+             "table.striped th { background: white; border-bottom: solid thin black; text-align: left; vertical-align: bottom; }\n"
+             "table.striped td { margin: 0; padding: 5px; vertical-align: top; }\n"
+             "table.nav { border-collapse: collapse; width: 100%%; }\n"
+             "table.nav td { margin: 0; text-align: center; }\n"
+             "td.nav a, td.nav a:active, td.nav a:hover, td.nav a:hover:link, td.nav a:hover:link:visited, td.nav a:link, td.nav a:link:visited, td.nav a:visited { background: inherit; color: inherit; font-size: 80%%; text-decoration: none; }\n"
+             "td.nav { background: #333; color: #fff; padding: 4px 8px; width: 33%%; }\n"
+             "td.nav.sel { background: #fff; color: #000; font-weight: bold; }\n"
+             "td.nav:hover { background: #666; color: #fff; }\n"
+             "td.nav:active { background: #000; color: #ff0; }\n"
+             "</style>\n"
+             "</head>\n"
+             "<body>\n"
+             "<table class=\"nav\"><tr>"
+             "<td class=\"nav%s\"><a href=\"/\">Status</a></td>"
+             "<td class=\"nav%s\"><a href=\"/supplies\">Supplies</a></td>"
+             "<td class=\"nav%s\"><a href=\"/media\">Media</a></td>"
+             "</tr></table>\n"
+             "<div class=\"body\">\n", title, !strcmp(client->uri, "/") ? " sel" : "", !strcmp(client->uri, "/supplies") ? " sel" : "", !strcmp(client->uri, "/media") ? " sel" : "");
+}
+
+
+/*
+ * 'html_printf()' - Send formatted text to the client, quoting as needed.
+ */
+
+static void
+html_printf(_ipp_client_t *client,     /* I - Client */
+           const char    *format,      /* I - Printf-style format string */
+           ...)                        /* I - Additional arguments as needed */
+{
+  va_list      ap;                     /* Pointer to arguments */
+  const char   *start;                 /* Start of string */
+  char         size,                   /* Size character (h, l, L) */
+               type;                   /* Format type character */
+  int          width,                  /* Width of field */
+               prec;                   /* Number of characters of precision */
+  char         tformat[100],           /* Temporary format string for sprintf() */
+               *tptr,                  /* Pointer into temporary format */
+               temp[1024];             /* Buffer for formatted numbers */
+  char         *s;                     /* Pointer to string */
+
+
+ /*
+  * Loop through the format string, formatting as needed...
+  */
+
+  va_start(ap, format);
+  start = format;
+
+  while (*format)
+  {
+    if (*format == '%')
+    {
+      if (format > start)
+        httpWrite2(client->http, start, (size_t)(format - start));
+
+      tptr    = tformat;
+      *tptr++ = *format++;
+
+      if (*format == '%')
+      {
+        httpWrite2(client->http, "%", 1);
+        format ++;
+       start = format;
+       continue;
+      }
+      else if (strchr(" -+#\'", *format))
+        *tptr++ = *format++;
+
+      if (*format == '*')
+      {
+       /*
+        * Get width from argument...
+       */
+
+       format ++;
+       width = va_arg(ap, int);
+
+       snprintf(tptr, sizeof(tformat) - (size_t)(tptr - tformat), "%d", width);
+       tptr += strlen(tptr);
+      }
+      else
+      {
+       width = 0;
+
+       while (isdigit(*format & 255))
+       {
+         if (tptr < (tformat + sizeof(tformat) - 1))
+           *tptr++ = *format;
+
+         width = width * 10 + *format++ - '0';
+       }
+      }
+
+      if (*format == '.')
+      {
+       if (tptr < (tformat + sizeof(tformat) - 1))
+         *tptr++ = *format;
+
+        format ++;
+
+        if (*format == '*')
+       {
+         /*
+         * Get precision from argument...
+         */
+
+         format ++;
+         prec = va_arg(ap, int);
+
+         snprintf(tptr, sizeof(tformat) - (size_t)(tptr - tformat), "%d", prec);
+         tptr += strlen(tptr);
+       }
+       else
+       {
+         prec = 0;
+
+         while (isdigit(*format & 255))
+         {
+           if (tptr < (tformat + sizeof(tformat) - 1))
+             *tptr++ = *format;
+
+           prec = prec * 10 + *format++ - '0';
+         }
+       }
+      }
+
+      if (*format == 'l' && format[1] == 'l')
+      {
+        size = 'L';
+
+       if (tptr < (tformat + sizeof(tformat) - 2))
+       {
+         *tptr++ = 'l';
+         *tptr++ = 'l';
+       }
+
+       format += 2;
+      }
+      else if (*format == 'h' || *format == 'l' || *format == 'L')
+      {
+       if (tptr < (tformat + sizeof(tformat) - 1))
+         *tptr++ = *format;
+
+        size = *format++;
+      }
+      else
+        size = 0;
+
+
+      if (!*format)
+      {
+        start = format;
+        break;
+      }
+
+      if (tptr < (tformat + sizeof(tformat) - 1))
+        *tptr++ = *format;
+
+      type  = *format++;
+      *tptr = '\0';
+      start = format;
+
+      switch (type)
+      {
+       case 'E' : /* Floating point formats */
+       case 'G' :
+       case 'e' :
+       case 'f' :
+       case 'g' :
+           if ((size_t)(width + 2) > sizeof(temp))
+             break;
+
+           sprintf(temp, tformat, va_arg(ap, double));
+
+            httpWrite2(client->http, temp, strlen(temp));
+           break;
+
+        case 'B' : /* Integer formats */
+       case 'X' :
+       case 'b' :
+        case 'd' :
+       case 'i' :
+       case 'o' :
+       case 'u' :
+       case 'x' :
+           if ((size_t)(width + 2) > sizeof(temp))
+             break;
+
+#  ifdef HAVE_LONG_LONG
+            if (size == 'L')
+             sprintf(temp, tformat, va_arg(ap, long long));
+           else
+#  endif /* HAVE_LONG_LONG */
+            if (size == 'l')
+             sprintf(temp, tformat, va_arg(ap, long));
+           else
+             sprintf(temp, tformat, va_arg(ap, int));
+
+            httpWrite2(client->http, temp, strlen(temp));
+           break;
+
+       case 'p' : /* Pointer value */
+           if ((size_t)(width + 2) > sizeof(temp))
+             break;
+
+           sprintf(temp, tformat, va_arg(ap, void *));
+
+            httpWrite2(client->http, temp, strlen(temp));
+           break;
+
+        case 'c' : /* Character or character array */
+            if (width <= 1)
+            {
+              temp[0] = (char)va_arg(ap, int);
+              temp[1] = '\0';
+              html_escape(client, temp, 1);
+            }
+            else
+              html_escape(client, va_arg(ap, char *), (size_t)width);
+           break;
+
+       case 's' : /* String */
+           if ((s = va_arg(ap, char *)) == NULL)
+             s = "(null)";
+
+            html_escape(client, s, strlen(s));
+           break;
+      }
+    }
+    else
+      format ++;
+  }
+
+  if (format > start)
+    httpWrite2(client->http, start, (size_t)(format - start));
+
+  va_end(ap);
+}
+
+
+/*
+ * 'ipp_acknowledge_document()' - Acknowledge receipt of a document.
+ */
+
+static void
+ipp_acknowledge_document(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_acknowledge_identify_printer()' - Acknowledge an identify command.
+ */
+
+static void
+ipp_acknowledge_identify_printer(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_acknowledge_job()' - Acknowledge receipt of a job.
+ */
+
+static void
+ipp_acknowledge_job(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_cancel_job()' - Cancel a job.
+ */
+
+static void
+ipp_cancel_job(_ipp_client_t *client)  /* I - Client */
+{
+  _ipp_job_t           *job;           /* Job information */
+
+
+ /*
+  * Get the job...
+  */
+
+  if ((job = find_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
+    return;
+  }
+
+ /*
+  * See if the job is already completed, canceled, or aborted; if so,
+  * we can't cancel...
+  */
+
+  switch (job->state)
+  {
+    case IPP_JSTATE_CANCELED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is already canceled - can\'t cancel.", job->id);
+        break;
+
+    case IPP_JSTATE_ABORTED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is already aborted - can\'t cancel.", job->id);
+        break;
+
+    case IPP_JSTATE_COMPLETED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is already completed - can\'t cancel.", job->id);
+        break;
+
+    default :
+       /*
+        * Cancel the job...
+       */
+
+       _cupsRWLockWrite(&(client->printer->rwlock));
+
+       if (job->state == IPP_JSTATE_PROCESSING ||
+           (job->state == IPP_JSTATE_HELD && job->fd >= 0))
+          job->cancel = 1;
+       else
+       {
+         job->state     = IPP_JSTATE_CANCELED;
+         job->completed = time(NULL);
+       }
+
+       _cupsRWUnlock(&(client->printer->rwlock));
+
+       respond_ipp(client, IPP_STATUS_OK, NULL);
+        break;
+  }
+}
+
+
+/*
+ * 'ipp_cancel_my_jobs()' - Cancel a user's jobs.
+ */
+
+static void
+ipp_cancel_my_jobs(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_cancel_subscription()' - Cancel a subscription.
+ */
+
+static void
+ipp_cancel_subscription(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_close_job()' - Close an open job.
+ */
+
+static void
+ipp_close_job(_ipp_client_t *client)   /* I - Client */
+{
+  _ipp_job_t           *job;           /* Job information */
+
+
+ /*
+  * Get the job...
+  */
+
+  if ((job = find_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
+    return;
+  }
+
+ /*
+  * See if the job is already completed, canceled, or aborted; if so,
+  * we can't cancel...
+  */
+
+  switch (job->state)
+  {
+    case IPP_JSTATE_CANCELED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is canceled - can\'t close.", job->id);
+        break;
+
+    case IPP_JSTATE_ABORTED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is aborted - can\'t close.", job->id);
+        break;
+
+    case IPP_JSTATE_COMPLETED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is completed - can\'t close.", job->id);
+        break;
+
+    case IPP_JSTATE_PROCESSING :
+    case IPP_JSTATE_STOPPED :
+       respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                   "Job #%d is already closed.", job->id);
+        break;
+
+    default :
+       respond_ipp(client, IPP_STATUS_OK, NULL);
+        break;
+  }
+}
+
+
+/*
+ * 'ipp_create_job()' - Create a job object.
+ */
+
+static void
+ipp_create_job(_ipp_client_t *client)  /* I - Client */
+{
+  _ipp_job_t           *job;           /* New job */
+  cups_array_t         *ra;            /* Attributes to send in response */
+
+
+ /*
+  * Validate print job attributes...
+  */
+
+  if (!valid_job_attributes(client))
+  {
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Do we have a file to print?
+  */
+
+  if (httpGetState(client->http) == HTTP_STATE_POST_RECV)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Unexpected document data following request.");
+    return;
+  }
+
+ /*
+  * Create the job...
+  */
+
+  if ((job = create_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BUSY,
+                "Currently printing another job.");
+    return;
+  }
+
+ /*
+  * Return the job info...
+  */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = cupsArrayNew((cups_array_func_t)strcmp, NULL);
+  cupsArrayAdd(ra, "job-id");
+  cupsArrayAdd(ra, "job-state");
+  cupsArrayAdd(ra, "job-state-message");
+  cupsArrayAdd(ra, "job-state-reasons");
+  cupsArrayAdd(ra, "job-uri");
+
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_create_xxx_subscriptions()' - Create job and printer subscriptions.
+ */
+
+static void
+ipp_create_xxx_subscriptions(
+    _ipp_client_t *client)
+{
+  ipp_attribute_t      *attr;          /* Subscription attribute */
+  _ipp_job_t           *job = NULL;    /* Job */
+  const char           *user;          /* Username */
+  int                  lease = -1;     /* notify-lease-duration */
+  _ipp_event_t         events = _IPP_EVENT_NONE;
+                                       /* notify-events */
+
+
+  if (ippGetOperation(client->request) == IPP_OP_CREATE_JOB_SUBSCRIPTIONS)
+  {
+    if ((job = find_job(client)) == NULL)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job not found.");
+      return;
+    }
+  }
+
+ /*
+  * See if we have the required subscription attributes...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "notify-pull-method", IPP_TAG_ZERO)) == NULL ||
+      ippGetGroupTag(attr) != IPP_TAG_SUBSCRIPTION || ippGetValueTag(attr) != IPP_TAG_KEYWORD ||
+      strcmp(ippGetString(attr, 0, NULL), "ippget"))
+  {
+    // TODO: Return an error?
+    return (NULL);
+  }
+
+  if ((attr = ippFindAttribute(client->request, "notify-events", IPP_TAG_ZERO)) == NULL ||
+      ippGetGroupTag(attr) != IPP_TAG_SUBSCRIPTION || ippGetValueTag(attr) != IPP_TAG_KEYWORD)
+  {
+    // TODO: Return an error?
+    return (NULL);
+  }
+
+ /*
+  * Get the requesting-user-name and other attributes...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "requesting-user-name", IPP_TAG_NAME)) != NULL)
+    sub->username = ippGetString(attr, 0, NULL);
+  else
+    sub->username = "anonymous";
+
+  if ((attr = ippFindAttribute(client->request, "notify-lease-duration", IPP_TAG_INTEGER)) != NULL)
+    sub->lease = ippGetInteger(attr, 0);
+  else
+    sub->lease = 86400;
+
+get_notify_events_bits(attr)
+
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_fetch_document()' - Download a document.
+ */
+
+static void
+ipp_fetch_document(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_fetch_job()' - Download a job.
+ */
+
+static void
+ipp_fetch_job(_ipp_client_t *client)   /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_get_document_attributes()' - Get the attributes for a document object.
+ *
+ * Note: This implementation only supports single document jobs so we
+ *       synthesize the information for a single document from the job.
+ */
+
+static void
+ipp_get_document_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_get_documents()' - Get the list of documents in a job.
+ *
+ * Note: This implementation only supports single document jobs so we
+ *       synthesize the information for a single document from the job.
+ */
+
+static void
+ipp_get_documents(_ipp_client_t *client)/* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_get_job_attributes()' - Get the attributes for a job object.
+ */
+
+static void
+ipp_get_job_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  _ipp_job_t   *job;                   /* Job */
+  cups_array_t *ra;                    /* requested-attributes */
+
+
+  if ((job = find_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job not found.");
+    return;
+  }
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = ippCreateRequestedArray(client->request);
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_get_jobs()' - Get a list of job objects.
+ */
+
+static void
+ipp_get_jobs(_ipp_client_t *client)    /* I - Client */
+{
+  ipp_attribute_t      *attr;          /* Current attribute */
+  const char           *which_jobs = NULL;
+                                       /* which-jobs values */
+  int                  job_comparison; /* Job comparison */
+  ipp_jstate_t         job_state;      /* job-state value */
+  int                  first_job_id,   /* First job ID */
+                       limit,          /* Maximum number of jobs to return */
+                       count;          /* Number of jobs that match */
+  const char           *username;      /* Username */
+  _ipp_job_t           *job;           /* Current job pointer */
+  cups_array_t         *ra;            /* Requested attributes array */
+
+
+ /*
+  * See if the "which-jobs" attribute have been specified...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "which-jobs",
+                               IPP_TAG_KEYWORD)) != NULL)
+  {
+    which_jobs = ippGetString(attr, 0, NULL);
+    fprintf(stderr, "%s Get-Jobs which-jobs=%s", client->hostname, which_jobs);
+  }
+
+  if (!which_jobs || !strcmp(which_jobs, "not-completed"))
+  {
+    job_comparison = -1;
+    job_state      = IPP_JSTATE_STOPPED;
+  }
+  else if (!strcmp(which_jobs, "completed"))
+  {
+    job_comparison = 1;
+    job_state      = IPP_JSTATE_CANCELED;
+  }
+  else if (!strcmp(which_jobs, "aborted"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_ABORTED;
+  }
+  else if (!strcmp(which_jobs, "all"))
+  {
+    job_comparison = 1;
+    job_state      = IPP_JSTATE_PENDING;
+  }
+  else if (!strcmp(which_jobs, "canceled"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_CANCELED;
+  }
+  else if (!strcmp(which_jobs, "pending"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_PENDING;
+  }
+  else if (!strcmp(which_jobs, "pending-held"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_HELD;
+  }
+  else if (!strcmp(which_jobs, "processing"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_PROCESSING;
+  }
+  else if (!strcmp(which_jobs, "processing-stopped"))
+  {
+    job_comparison = 0;
+    job_state      = IPP_JSTATE_STOPPED;
+  }
+  else
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES,
+                "The which-jobs value \"%s\" is not supported.", which_jobs);
+    ippAddString(client->response, IPP_TAG_UNSUPPORTED_GROUP, IPP_TAG_KEYWORD,
+                 "which-jobs", NULL, which_jobs);
+    return;
+  }
+
+ /*
+  * See if they want to limit the number of jobs reported...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "limit",
+                               IPP_TAG_INTEGER)) != NULL)
+  {
+    limit = ippGetInteger(attr, 0);
+
+    fprintf(stderr, "%s Get-Jobs limit=%d", client->hostname, limit);
+  }
+  else
+    limit = 0;
+
+  if ((attr = ippFindAttribute(client->request, "first-job-id",
+                               IPP_TAG_INTEGER)) != NULL)
+  {
+    first_job_id = ippGetInteger(attr, 0);
+
+    fprintf(stderr, "%s Get-Jobs first-job-id=%d", client->hostname,
+            first_job_id);
+  }
+  else
+    first_job_id = 1;
+
+ /*
+  * See if we only want to see jobs for a specific user...
+  */
+
+  username = NULL;
+
+  if ((attr = ippFindAttribute(client->request, "my-jobs",
+                               IPP_TAG_BOOLEAN)) != NULL)
+  {
+    int my_jobs = ippGetBoolean(attr, 0);
+
+    fprintf(stderr, "%s Get-Jobs my-jobs=%s\n", client->hostname,
+            my_jobs ? "true" : "false");
+
+    if (my_jobs)
+    {
+      if ((attr = ippFindAttribute(client->request, "requesting-user-name",
+                                       IPP_TAG_NAME)) == NULL)
+      {
+       respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                   "Need requesting-user-name with my-jobs.");
+       return;
+      }
+
+      username = ippGetString(attr, 0, NULL);
+
+      fprintf(stderr, "%s Get-Jobs requesting-user-name=\"%s\"\n",
+              client->hostname, username);
+    }
+  }
+
+ /*
+  * OK, build a list of jobs for this printer...
+  */
+
+  ra = ippCreateRequestedArray(client->request);
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  _cupsRWLockRead(&(client->printer->rwlock));
+
+  for (count = 0, job = (_ipp_job_t *)cupsArrayFirst(client->printer->jobs);
+       (limit <= 0 || count < limit) && job;
+       job = (_ipp_job_t *)cupsArrayNext(client->printer->jobs))
+  {
+   /*
+    * Filter out jobs that don't match...
+    */
+
+    if ((job_comparison < 0 && job->state > job_state) ||
+       (job_comparison == 0 && job->state != job_state) ||
+       (job_comparison > 0 && job->state < job_state) ||
+       job->id < first_job_id ||
+       (username && job->username &&
+        strcasecmp(username, job->username)))
+      continue;
+
+    if (count > 0)
+      ippAddSeparator(client->response);
+
+    count ++;
+    copy_job_attributes(client, job, ra);
+  }
+
+  cupsArrayDelete(ra);
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+}
+
+
+/*
+ * 'ipp_get_notifications()' - Get notification events for one or more subscriptions.
+ */
+
+static void
+ipp_get_notifications(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_get_output_device_attributes()' - Get attributes for an output device.
+ */
+
+static void
+ipp_get_output_device_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_get_printer_attributes()' - Get the attributes for a printer object.
+ */
+
+static void
+ipp_get_printer_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  cups_array_t         *ra;            /* Requested attributes array */
+  _ipp_printer_t       *printer;       /* Printer */
+
+
+ /*
+  * Send the attributes...
+  */
+
+  ra      = ippCreateRequestedArray(client->request);
+  printer = client->printer;
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  _cupsRWLockRead(&(printer->rwlock));
+
+  copy_attributes(client->response, printer->attrs, ra, IPP_TAG_ZERO,
+                 IPP_TAG_CUPS_CONST);
+  copy_attributes(client->response, printer->dev_attrs, ra, IPP_TAG_ZERO, IPP_TAG_ZERO);
+
+  if (!ra || cupsArrayFind(ra, "printer-config-change-date-time"))
+    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-config-change-date-time", ippTimeToDate(printer->config_time));
+
+  if (!ra || cupsArrayFind(ra, "printer-config-change-time"))
+    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-config-change-time", (int)(printer->config_time - printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "printer-current-time"))
+    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-current-time", ippTimeToDate(time(NULL)));
+
+
+  if (!ra || cupsArrayFind(ra, "printer-state"))
+    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_ENUM,
+                  "printer-state", printer->state > printer->dev_state ? printer->state : printer->dev_state);
+
+  if (!ra || cupsArrayFind(ra, "printer-state-change-date-time"))
+    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-state-change-date-time", ippTimeToDate(printer->state_time));
+
+  if (!ra || cupsArrayFind(ra, "printer-state-change-time"))
+    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-state-change-time", (int)(printer->state_time - printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "printer-state-message"))
+  {
+    static const char * const messages[] = { "Idle.", "Printing.", "Stopped." };
+
+    if (printer->state > printer->dev_state)
+      ippAddString(client->response, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-state-message", NULL, messages[printer->state - IPP_PSTATE_IDLE]);
+    else
+      ippAddString(client->response, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-state-message", NULL, messages[printer->dev_state - IPP_PSTATE_IDLE]);
+  }
+
+  if (!ra || cupsArrayFind(ra, "printer-state-reasons"))
+    copy_printer_state_reasons(client->response, IPP_TAG_PRINTER, printer);
+
+  if (!ra || cupsArrayFind(ra, "printer-up-time"))
+    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-up-time", (int)(time(NULL) - printer->start_time));
+
+  if (!ra || cupsArrayFind(ra, "queued-job-count"))
+    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "queued-job-count", cupsArrayCount(printer->active_jobs));
+
+  _cupsRWUnlock(&(printer->rwlock));
+
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_get_printer_supported_values()' - Return the supported values for
+ *                                        the infrastructure printer.
+ */
+
+static void
+ipp_get_printer_supported_values(
+    _ipp_client_t *client)             /* I - Client */
+{
+  /* TODO: Do something */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_get_subscription_attributes()' - Get attributes for a subscription.
+ */
+
+static void
+ipp_get_subscription_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  /* TODO: Do something */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_get_subscriptions()' - Get attributes for all subscriptions.
+ */
+
+static void
+ipp_get_subscriptions(
+    _ipp_client_t *client)             /* I - Client */
+{
+  /* TODO: Do something */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_identify_printer()' - Beep or display a message.
+ */
+
+static void
+ipp_identify_printer(
+    _ipp_client_t *client)             /* I - Client */
+{
+  /* TODO: Do something */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_print_job()' - Create a job object with an attached document.
+ */
+
+static void
+ipp_print_job(_ipp_client_t *client)   /* I - Client */
+{
+  _ipp_job_t           *job;           /* New job */
+  char                 filename[1024], /* Filename buffer */
+                       buffer[4096];   /* Copy buffer */
+  ssize_t              bytes;          /* Bytes read */
+  cups_array_t         *ra;            /* Attributes to send in response */
+
+
+ /*
+  * Validate print job attributes...
+  */
+
+  if (!valid_job_attributes(client))
+  {
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Do we have a file to print?
+  */
+
+  if (httpGetState(client->http) == HTTP_STATE_POST_SEND)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "No file in request.");
+    return;
+  }
+
+ /*
+  * Print the job...
+  */
+
+  if ((job = create_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BUSY,
+                "Currently printing another job.");
+    return;
+  }
+
+ /*
+  * Create a file for the request data...
+  */
+
+  create_job_filename(client->printer, job, filename, sizeof(filename));
+
+  if (Verbosity)
+    fprintf(stderr, "Creating job file \"%s\", format \"%s\".\n", filename, job->format);
+
+  if ((job->fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600)) < 0)
+  {
+    job->state = IPP_JSTATE_ABORTED;
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to create print file: %s", strerror(errno));
+    return;
+  }
+
+  while ((bytes = httpRead2(client->http, buffer, sizeof(buffer))) > 0)
+  {
+    if (write(job->fd, buffer, (size_t)bytes) < bytes)
+    {
+      int error = errno;               /* Write error */
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+
+      respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                  "Unable to write print file: %s", strerror(error));
+      return;
+    }
+  }
+
+  if (bytes < 0)
+  {
+   /*
+    * Got an error while reading the print data, so abort this job.
+    */
+
+    job->state = IPP_JSTATE_ABORTED;
+
+    close(job->fd);
+    job->fd = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to read print file.");
+    return;
+  }
+
+  if (close(job->fd))
+  {
+    int error = errno;         /* Write error */
+
+    job->state = IPP_JSTATE_ABORTED;
+    job->fd    = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to write print file: %s", strerror(error));
+    return;
+  }
+
+  job->fd       = -1;
+  job->filename = strdup(filename);
+  job->state    = IPP_JSTATE_PENDING;
+
+ /*
+  * Process the job, if possible...
+  */
+
+  check_jobs(client->printer);
+  
+ /*
+  * Return the job info...
+  */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = cupsArrayNew((cups_array_func_t)strcmp, NULL);
+  cupsArrayAdd(ra, "job-id");
+  cupsArrayAdd(ra, "job-state");
+  cupsArrayAdd(ra, "job-state-message");
+  cupsArrayAdd(ra, "job-state-reasons");
+  cupsArrayAdd(ra, "job-uri");
+
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_print_uri()' - Create a job object with a referenced document.
+ */
+
+static void
+ipp_print_uri(_ipp_client_t *client)   /* I - Client */
+{
+  _ipp_job_t           *job;           /* New job */
+  ipp_attribute_t      *uri;           /* document-uri */
+  char                 scheme[256],    /* URI scheme */
+                       userpass[256],  /* Username and password info */
+                       hostname[256],  /* Hostname */
+                       resource[1024]; /* Resource path */
+  int                  port;           /* Port number */
+  http_uri_status_t    uri_status;     /* URI decode status */
+  http_encryption_t    encryption;     /* Encryption to use, if any */
+  http_t               *http;          /* Connection for http/https URIs */
+  http_status_t                status;         /* Access status for http/https URIs */
+  int                  infile;         /* Input file for local file URIs */
+  char                 filename[1024], /* Filename buffer */
+                       buffer[4096];   /* Copy buffer */
+  ssize_t              bytes;          /* Bytes read */
+  cups_array_t         *ra;            /* Attributes to send in response */
+  static const char * const uri_status_strings[] =
+  {                                    /* URI decode errors */
+    "URI too large.",
+    "Bad arguments to function.",
+    "Bad resource in URI.",
+    "Bad port number in URI.",
+    "Bad hostname in URI.",
+    "Bad username in URI.",
+    "Bad scheme in URI.",
+    "Bad/empty URI."
+  };
+
+
+ /*
+  * Validate print job attributes...
+  */
+
+  if (!valid_job_attributes(client))
+  {
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Do we have a file to print?
+  */
+
+  if (httpGetState(client->http) == HTTP_STATE_POST_RECV)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Unexpected document data following request.");
+    return;
+  }
+
+ /*
+  * Do we have a document URI?
+  */
+
+  if ((uri = ippFindAttribute(client->request, "document-uri",
+                              IPP_TAG_URI)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing document-uri.");
+    return;
+  }
+
+  if (ippGetCount(uri) != 1)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Too many document-uri values.");
+    return;
+  }
+
+  uri_status = httpSeparateURI(HTTP_URI_CODING_ALL, ippGetString(uri, 0, NULL),
+                               scheme, sizeof(scheme), userpass,
+                               sizeof(userpass), hostname, sizeof(hostname),
+                               &port, resource, sizeof(resource));
+  if (uri_status < HTTP_URI_STATUS_OK)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Bad document-uri: %s",
+                uri_status_strings[uri_status - HTTP_URI_STATUS_OVERFLOW]);
+    return;
+  }
+
+  if (strcmp(scheme, "file") &&
+#ifdef HAVE_SSL
+      strcmp(scheme, "https") &&
+#endif /* HAVE_SSL */
+      strcmp(scheme, "http"))
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_URI_SCHEME,
+                "URI scheme \"%s\" not supported.", scheme);
+    return;
+  }
+
+  if (!strcmp(scheme, "file") && access(resource, R_OK))
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                "Unable to access URI: %s", strerror(errno));
+    return;
+  }
+
+ /*
+  * Print the job...
+  */
+
+  if ((job = create_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BUSY,
+                "Currently printing another job.");
+    return;
+  }
+
+ /*
+  * Create a file for the request data...
+  */
+
+  if (!strcasecmp(job->format, "image/jpeg"))
+    snprintf(filename, sizeof(filename), "%s/%d.jpg",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "image/png"))
+    snprintf(filename, sizeof(filename), "%s/%d.png",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "application/pdf"))
+    snprintf(filename, sizeof(filename), "%s/%d.pdf",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "application/postscript"))
+    snprintf(filename, sizeof(filename), "%s/%d.ps",
+             client->printer->directory, job->id);
+  else
+    snprintf(filename, sizeof(filename), "%s/%d.prn",
+             client->printer->directory, job->id);
+
+  if ((job->fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600)) < 0)
+  {
+    job->state = IPP_JSTATE_ABORTED;
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to create print file: %s", strerror(errno));
+    return;
+  }
+
+  if (!strcmp(scheme, "file"))
+  {
+    if ((infile = open(resource, O_RDONLY)) < 0)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to access URI: %s", strerror(errno));
+      return;
+    }
+
+    do
+    {
+      if ((bytes = read(infile, buffer, sizeof(buffer))) < 0 &&
+          (errno == EAGAIN || errno == EINTR))
+        bytes = 1;
+      else if (bytes > 0 && write(job->fd, buffer, (size_t)bytes) < bytes)
+      {
+       int error = errno;              /* Write error */
+
+       job->state = IPP_JSTATE_ABORTED;
+
+       close(job->fd);
+       job->fd = -1;
+
+       unlink(filename);
+       close(infile);
+
+       respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                   "Unable to write print file: %s", strerror(error));
+       return;
+      }
+    }
+    while (bytes > 0);
+
+    close(infile);
+  }
+  else
+  {
+#ifdef HAVE_SSL
+    if (port == 443 || !strcmp(scheme, "https"))
+      encryption = HTTP_ENCRYPTION_ALWAYS;
+    else
+#endif /* HAVE_SSL */
+    encryption = HTTP_ENCRYPTION_IF_REQUESTED;
+
+    if ((http = httpConnect2(hostname, port, NULL, AF_UNSPEC, encryption,
+                             1, 30000, NULL)) == NULL)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to connect to %s: %s", hostname,
+                 cupsLastErrorString());
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      return;
+    }
+
+    httpClearFields(http);
+    httpSetField(http, HTTP_FIELD_ACCEPT_LANGUAGE, "en");
+    if (httpGet(http, resource))
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to GET URI: %s", strerror(errno));
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      httpClose(http);
+      return;
+    }
+
+    while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE);
+
+    if (status != HTTP_STATUS_OK)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to GET URI: %s", httpStatus(status));
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      httpClose(http);
+      return;
+    }
+
+    while ((bytes = httpRead2(http, buffer, sizeof(buffer))) > 0)
+    {
+      if (write(job->fd, buffer, (size_t)bytes) < bytes)
+      {
+       int error = errno;              /* Write error */
+
+       job->state = IPP_JSTATE_ABORTED;
+
+       close(job->fd);
+       job->fd = -1;
+
+       unlink(filename);
+       httpClose(http);
+
+       respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                   "Unable to write print file: %s", strerror(error));
+       return;
+      }
+    }
+
+    httpClose(http);
+  }
+
+  if (close(job->fd))
+  {
+    int error = errno;         /* Write error */
+
+    job->state = IPP_JSTATE_ABORTED;
+    job->fd    = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to write print file: %s", strerror(error));
+    return;
+  }
+
+  job->fd       = -1;
+  job->filename = strdup(filename);
+  job->state    = IPP_JSTATE_PENDING;
+
+  /* TODO: Do something different here - only process if the printer is idle */
+ /*
+  * Process the job...
+  */
+
+  check_jobs(client->printer);
+
+ /*
+  * Return the job info...
+  */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = cupsArrayNew((cups_array_func_t)strcmp, NULL);
+  cupsArrayAdd(ra, "job-id");
+  cupsArrayAdd(ra, "job-state");
+  cupsArrayAdd(ra, "job-state-reasons");
+  cupsArrayAdd(ra, "job-uri");
+
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_renew_subscription()' - Renew a subscription.
+ */
+
+static void
+ipp_renew_subscription(
+    _ipp_client_t *client)             /* I - Client */
+{
+  /* TODO: Do something */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_send_document()' - Add an attached document to a job object created with
+ *                         Create-Job.
+ */
+
+static void
+ipp_send_document(_ipp_client_t *client)/* I - Client */
+{
+  _ipp_job_t           *job;           /* Job information */
+  char                 filename[1024], /* Filename buffer */
+                       buffer[4096];   /* Copy buffer */
+  ssize_t              bytes;          /* Bytes read */
+  ipp_attribute_t      *attr;          /* Current attribute */
+  cups_array_t         *ra;            /* Attributes to send in response */
+
+
+ /*
+  * Get the job...
+  */
+
+  if ((job = find_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * See if we already have a document for this job or the job has already
+  * in a non-pending state...
+  */
+
+  if (job->state > IPP_JSTATE_HELD)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                "Job is not in a pending state.");
+    httpFlush(client->http);
+    return;
+  }
+  else if (job->filename || job->fd >= 0)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_MULTIPLE_JOBS_NOT_SUPPORTED,
+                "Multiple document jobs are not supported.");
+    httpFlush(client->http);
+    return;
+  }
+
+  if ((attr = ippFindAttribute(client->request, "last-document",
+                               IPP_TAG_ZERO)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Missing required last-document attribute.");
+    httpFlush(client->http);
+    return;
+  }
+  else if (ippGetValueTag(attr) != IPP_TAG_BOOLEAN || ippGetCount(attr) != 1 ||
+           !ippGetBoolean(attr, 0))
+  {
+    respond_unsupported(client, attr);
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Validate document attributes...
+  */
+
+  if (!valid_doc_attributes(client))
+  {
+    httpFlush(client->http);
+    return;
+  }
+
+  copy_attributes(job->attrs, client->request, NULL, IPP_TAG_JOB, 0);
+
+ /*
+  * Get the document format for the job...
+  */
+
+  _cupsRWLockWrite(&(client->printer->rwlock));
+
+  if ((attr = ippFindAttribute(job->attrs, "document-format-detected", IPP_TAG_MIMETYPE)) != NULL)
+    job->format = ippGetString(attr, 0, NULL);
+  else if ((attr = ippFindAttribute(job->attrs, "document-format-supplied", IPP_TAG_MIMETYPE)) != NULL)
+    job->format = ippGetString(attr, 0, NULL);
+  else
+    job->format = "application/octet-stream";
+
+ /*
+  * Create a file for the request data...
+  */
+
+  create_job_filename(client->printer, job, filename, sizeof(filename));
+
+  if (Verbosity)
+    fprintf(stderr, "Creating job file \"%s\", format \"%s\".\n", filename, job->format);
+
+  job->fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+  if (job->fd < 0)
+  {
+    job->state = IPP_JSTATE_ABORTED;
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to create print file: %s", strerror(errno));
+    return;
+  }
+
+  while ((bytes = httpRead2(client->http, buffer, sizeof(buffer))) > 0)
+  {
+    if (write(job->fd, buffer, (size_t)bytes) < bytes)
+    {
+      int error = errno;               /* Write error */
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+
+      respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                  "Unable to write print file: %s", strerror(error));
+      return;
+    }
+  }
+
+  if (bytes < 0)
+  {
+   /*
+    * Got an error while reading the print data, so abort this job.
+    */
+
+    job->state = IPP_JSTATE_ABORTED;
+
+    close(job->fd);
+    job->fd = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to read print file.");
+    return;
+  }
+
+  if (close(job->fd))
+  {
+    int error = errno;                 /* Write error */
+
+    job->state = IPP_JSTATE_ABORTED;
+    job->fd    = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to write print file: %s", strerror(error));
+    return;
+  }
+
+  _cupsRWLockWrite(&(client->printer->rwlock));
+
+  job->fd       = -1;
+  job->filename = strdup(filename);
+  job->state    = IPP_JSTATE_PENDING;
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+ /*
+  * Process the job...
+  */
+
+#if 0
+  if (!_cupsThreadCreate((_cups_thread_func_t)process_job, job))
+  {
+    job->state = IPP_JSTATE_ABORTED;
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to process job.");
+    return;
+  }
+
+#else
+  process_job(job);
+#endif /* 0 */
+
+ /*
+  * Return the job info...
+  */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = cupsArrayNew((cups_array_func_t)strcmp, NULL);
+  cupsArrayAdd(ra, "job-id");
+  cupsArrayAdd(ra, "job-state");
+  cupsArrayAdd(ra, "job-state-reasons");
+  cupsArrayAdd(ra, "job-uri");
+
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_send_uri()' - Add a referenced document to a job object created with
+ *                    Create-Job.
+ */
+
+static void
+ipp_send_uri(_ipp_client_t *client)    /* I - Client */
+{
+  _ipp_job_t           *job;           /* Job information */
+  ipp_attribute_t      *uri;           /* document-uri */
+  char                 scheme[256],    /* URI scheme */
+                       userpass[256],  /* Username and password info */
+                       hostname[256],  /* Hostname */
+                       resource[1024]; /* Resource path */
+  int                  port;           /* Port number */
+  http_uri_status_t    uri_status;     /* URI decode status */
+  http_encryption_t    encryption;     /* Encryption to use, if any */
+  http_t               *http;          /* Connection for http/https URIs */
+  http_status_t                status;         /* Access status for http/https URIs */
+  int                  infile;         /* Input file for local file URIs */
+  char                 filename[1024], /* Filename buffer */
+                       buffer[4096];   /* Copy buffer */
+  ssize_t              bytes;          /* Bytes read */
+  ipp_attribute_t      *attr;          /* Current attribute */
+  cups_array_t         *ra;            /* Attributes to send in response */
+  static const char * const uri_status_strings[] =
+  {                                    /* URI decode errors */
+    "URI too large.",
+    "Bad arguments to function.",
+    "Bad resource in URI.",
+    "Bad port number in URI.",
+    "Bad hostname in URI.",
+    "Bad username in URI.",
+    "Bad scheme in URI.",
+    "Bad/empty URI."
+  };
+
+
+ /*
+  * Get the job...
+  */
+
+  if ((job = find_job(client)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * See if we already have a document for this job or the job has already
+  * in a non-pending state...
+  */
+
+  if (job->state > IPP_JSTATE_HELD)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
+                "Job is not in a pending state.");
+    httpFlush(client->http);
+    return;
+  }
+  else if (job->filename || job->fd >= 0)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_MULTIPLE_JOBS_NOT_SUPPORTED,
+                "Multiple document jobs are not supported.");
+    httpFlush(client->http);
+    return;
+  }
+
+  if ((attr = ippFindAttribute(client->request, "last-document",
+                               IPP_TAG_ZERO)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Missing required last-document attribute.");
+    httpFlush(client->http);
+    return;
+  }
+  else if (ippGetValueTag(attr) != IPP_TAG_BOOLEAN || ippGetCount(attr) != 1 ||
+           !ippGetBoolean(attr, 0))
+  {
+    respond_unsupported(client, attr);
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Validate document attributes...
+  */
+
+  if (!valid_doc_attributes(client))
+  {
+    httpFlush(client->http);
+    return;
+  }
+
+ /*
+  * Do we have a file to print?
+  */
+
+  if (httpGetState(client->http) == HTTP_STATE_POST_RECV)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Unexpected document data following request.");
+    return;
+  }
+
+ /*
+  * Do we have a document URI?
+  */
+
+  if ((uri = ippFindAttribute(client->request, "document-uri",
+                              IPP_TAG_URI)) == NULL)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing document-uri.");
+    return;
+  }
+
+  if (ippGetCount(uri) != 1)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "Too many document-uri values.");
+    return;
+  }
+
+  uri_status = httpSeparateURI(HTTP_URI_CODING_ALL, ippGetString(uri, 0, NULL),
+                               scheme, sizeof(scheme), userpass,
+                               sizeof(userpass), hostname, sizeof(hostname),
+                               &port, resource, sizeof(resource));
+  if (uri_status < HTTP_URI_STATUS_OK)
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Bad document-uri: %s",
+                uri_status_strings[uri_status - HTTP_URI_STATUS_OVERFLOW]);
+    return;
+  }
+
+  if (strcmp(scheme, "file") &&
+#ifdef HAVE_SSL
+      strcmp(scheme, "https") &&
+#endif /* HAVE_SSL */
+      strcmp(scheme, "http"))
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_URI_SCHEME,
+                "URI scheme \"%s\" not supported.", scheme);
+    return;
+  }
+
+  if (!strcmp(scheme, "file") && access(resource, R_OK))
+  {
+    respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                "Unable to access URI: %s", strerror(errno));
+    return;
+  }
+
+ /*
+  * Get the document format for the job...
+  */
+
+  _cupsRWLockWrite(&(client->printer->rwlock));
+
+  if ((attr = ippFindAttribute(job->attrs, "document-format",
+                               IPP_TAG_MIMETYPE)) != NULL)
+    job->format = ippGetString(attr, 0, NULL);
+  else
+    job->format = "application/octet-stream";
+
+ /*
+  * Create a file for the request data...
+  */
+
+  if (!strcasecmp(job->format, "image/jpeg"))
+    snprintf(filename, sizeof(filename), "%s/%d.jpg",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "image/png"))
+    snprintf(filename, sizeof(filename), "%s/%d.png",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "application/pdf"))
+    snprintf(filename, sizeof(filename), "%s/%d.pdf",
+             client->printer->directory, job->id);
+  else if (!strcasecmp(job->format, "application/postscript"))
+    snprintf(filename, sizeof(filename), "%s/%d.ps",
+             client->printer->directory, job->id);
+  else
+    snprintf(filename, sizeof(filename), "%s/%d.prn",
+             client->printer->directory, job->id);
+
+  job->fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+  if (job->fd < 0)
+  {
+    job->state = IPP_JSTATE_ABORTED;
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to create print file: %s", strerror(errno));
+    return;
+  }
+
+  if (!strcmp(scheme, "file"))
+  {
+    if ((infile = open(resource, O_RDONLY)) < 0)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to access URI: %s", strerror(errno));
+      return;
+    }
+
+    do
+    {
+      if ((bytes = read(infile, buffer, sizeof(buffer))) < 0 &&
+          (errno == EAGAIN || errno == EINTR))
+        bytes = 1;
+      else if (bytes > 0 && write(job->fd, buffer, (size_t)bytes) < bytes)
+      {
+       int error = errno;              /* Write error */
+
+       job->state = IPP_JSTATE_ABORTED;
+
+       close(job->fd);
+       job->fd = -1;
+
+       unlink(filename);
+       close(infile);
+
+       respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                   "Unable to write print file: %s", strerror(error));
+       return;
+      }
+    }
+    while (bytes > 0);
+
+    close(infile);
+  }
+  else
+  {
+#ifdef HAVE_SSL
+    if (port == 443 || !strcmp(scheme, "https"))
+      encryption = HTTP_ENCRYPTION_ALWAYS;
+    else
+#endif /* HAVE_SSL */
+    encryption = HTTP_ENCRYPTION_IF_REQUESTED;
+
+    if ((http = httpConnect2(hostname, port, NULL, AF_UNSPEC, encryption,
+                             1, 30000, NULL)) == NULL)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to connect to %s: %s", hostname,
+                 cupsLastErrorString());
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      return;
+    }
+
+    httpClearFields(http);
+    httpSetField(http, HTTP_FIELD_ACCEPT_LANGUAGE, "en");
+    if (httpGet(http, resource))
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to GET URI: %s", strerror(errno));
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      httpClose(http);
+      return;
+    }
+
+    while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE);
+
+    if (status != HTTP_STATUS_OK)
+    {
+      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS,
+                  "Unable to GET URI: %s", httpStatus(status));
+
+      job->state = IPP_JSTATE_ABORTED;
+
+      close(job->fd);
+      job->fd = -1;
+
+      unlink(filename);
+      httpClose(http);
+      return;
+    }
+
+    while ((bytes = httpRead2(http, buffer, sizeof(buffer))) > 0)
+    {
+      if (write(job->fd, buffer, (size_t)bytes) < bytes)
+      {
+       int error = errno;              /* Write error */
+
+       job->state = IPP_JSTATE_ABORTED;
+
+       close(job->fd);
+       job->fd = -1;
+
+       unlink(filename);
+       httpClose(http);
+
+       respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                   "Unable to write print file: %s", strerror(error));
+       return;
+      }
+    }
+
+    httpClose(http);
+  }
+
+  if (close(job->fd))
+  {
+    int error = errno;         /* Write error */
+
+    job->state = IPP_JSTATE_ABORTED;
+    job->fd    = -1;
+
+    unlink(filename);
+
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL,
+                "Unable to write print file: %s", strerror(error));
+    return;
+  }
+
+  _cupsRWLockWrite(&(client->printer->rwlock));
+
+  job->fd       = -1;
+  job->filename = strdup(filename);
+  job->state    = IPP_JSTATE_PENDING;
+
+  _cupsRWUnlock(&(client->printer->rwlock));
+
+ /*
+  * Process the job...
+  */
+
+#if 0
+  if (!_cupsThreadCreate((_cups_thread_func_t)process_job, job))
+  {
+    job->state = IPP_JSTATE_ABORTED;
+    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to process job.");
+    return;
+  }
+
+#else
+  process_job(job);
+#endif /* 0 */
+
+ /*
+  * Return the job info...
+  */
+
+  respond_ipp(client, IPP_STATUS_OK, NULL);
+
+  ra = cupsArrayNew((cups_array_func_t)strcmp, NULL);
+  cupsArrayAdd(ra, "job-id");
+  cupsArrayAdd(ra, "job-state");
+  cupsArrayAdd(ra, "job-state-reasons");
+  cupsArrayAdd(ra, "job-uri");
+
+  copy_job_attributes(client, job, ra);
+  cupsArrayDelete(ra);
+}
+
+
+/*
+ * 'ipp_update_active_jobs()' - Update the list of active jobs.
+ */
+
+static void
+ipp_update_active_jobs(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_update_document_status()' - Update the state of a document.
+ */
+
+static void
+ipp_update_document_status(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_update_job_status()' - Update the state of a job.
+ */
+
+static void
+ipp_update_job_status(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_update_output_device_attributes()' - Update the values for an output device.
+ */
+
+static void
+ipp_update_output_device_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  // TODO: Implement this!
+  respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Need to implement this.");
+}
+
+
+/*
+ * 'ipp_validate_document()' - Validate document creation attributes.
+ */
+
+static void
+ipp_validate_document(
+    _ipp_client_t *client)             /* I - Client */
+{
+  if (valid_doc_attributes(client))
+    respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'ipp_validate_job()' - Validate job creation attributes.
+ */
+
+static void
+ipp_validate_job(_ipp_client_t *client)        /* I - Client */
+{
+  if (valid_job_attributes(client))
+    respond_ipp(client, IPP_STATUS_OK, NULL);
+}
+
+
+/*
+ * 'parse_options()' - Parse URL options into CUPS options.
+ *
+ * The client->options string is destroyed by this function.
+ */
+
+static int                             /* O - Number of options */
+parse_options(_ipp_client_t *client,   /* I - Client */
+              cups_option_t **options) /* O - Options */
+{
+  char *name,                          /* Name */
+       *value,                         /* Value */
+       *next;                          /* Next name=value pair */
+  int  num_options = 0;                /* Number of options */
+
+
+  *options = NULL;
+
+  for (name = client->options; name && *name; name = next)
+  {
+    if ((value = strchr(name, '=')) == NULL)
+      break;
+
+    *value++ = '\0';
+    if ((next = strchr(value, '&')) != NULL)
+      *next++ = '\0';
+
+    num_options = cupsAddOption(name, value, num_options, options);
+  }
+
+  return (num_options);
+}
+
+
+/*
+ * 'process_client()' - Process client requests on a thread.
+ */
+
+static void *                          /* O - Exit status */
+process_client(_ipp_client_t *client)  /* I - Client */
+{
+ /*
+  * Loop until we are out of requests or timeout (30 seconds)...
+  */
+
+#ifdef HAVE_SSL
+  int first_time = 1;                  /* First time request? */
+#endif /* HAVE_SSL */
+
+  while (httpWait(client->http, 30000))
+  {
+#ifdef HAVE_SSL
+    if (first_time)
+    {
+     /*
+      * See if we need to negotiate a TLS connection...
+      */
+
+      char buf[1];                     /* First byte from client */
+
+      if (recv(httpGetFd(client->http), buf, 1, MSG_PEEK) == 1 && (!buf[0] || !strchr("DGHOPT", buf[0])))
+      {
+        fprintf(stderr, "%s Starting HTTPS session.\n", client->hostname);
+
+       if (httpEncryption(client->http, HTTP_ENCRYPTION_ALWAYS))
+       {
+         fprintf(stderr, "%s Unable to encrypt connection: %s\n", client->hostname, cupsLastErrorString());
+         break;
+        }
+
+        fprintf(stderr, "%s Connection now encrypted.\n", client->hostname);
+      }
+
+      first_time = 0;
+    }
+#endif /* HAVE_SSL */
+
+    if (!process_http(client))
+      break;
+  }
+
+ /*
+  * Close the conection to the client and return...
+  */
+
+  delete_client(client);
+
+  return (NULL);
+}
+
+
+/*
+ * 'process_http()' - Process a HTTP request.
+ */
+
+int                                    /* O - 1 on success, 0 on failure */
+process_http(_ipp_client_t *client)    /* I - Client connection */
+{
+  char                 uri[1024];      /* URI */
+  http_state_t         http_state;     /* HTTP state */
+  http_status_t                http_status;    /* HTTP status */
+  ipp_state_t          ipp_state;      /* State of IPP transfer */
+  char                 scheme[32],     /* Method/scheme */
+                       userpass[128],  /* Username:password */
+                       hostname[HTTP_MAX_HOST];
+                                       /* Hostname */
+  int                  port;           /* Port number */
+  const char           *encoding;      /* Content-Encoding value */
+  static const char * const http_states[] =
+  {                                    /* Strings for logging HTTP method */
+    "WAITING",
+    "OPTIONS",
+    "GET",
+    "GET_SEND",
+    "HEAD",
+    "POST",
+    "POST_RECV",
+    "POST_SEND",
+    "PUT",
+    "PUT_RECV",
+    "DELETE",
+    "TRACE",
+    "CONNECT",
+    "STATUS",
+    "UNKNOWN_METHOD",
+    "UNKNOWN_VERSION"
+  };
+
+
+ /*
+  * Clear state variables...
+  */
+
+  ippDelete(client->request);
+  ippDelete(client->response);
+
+  client->request   = NULL;
+  client->response  = NULL;
+  client->operation = HTTP_STATE_WAITING;
+
+ /*
+  * Read a request from the connection...
+  */
+
+  while ((http_state = httpReadRequest(client->http, uri,
+                                       sizeof(uri))) == HTTP_STATE_WAITING)
+    usleep(1);
+
+ /*
+  * Parse the request line...
+  */
+
+  if (http_state == HTTP_STATE_ERROR)
+  {
+    if (httpError(client->http) == EPIPE)
+      fprintf(stderr, "%s Client closed connection.\n", client->hostname);
+    else
+      fprintf(stderr, "%s Bad request line (%s).\n", client->hostname,
+              strerror(httpError(client->http)));
+
+    return (0);
+  }
+  else if (http_state == HTTP_STATE_UNKNOWN_METHOD)
+  {
+    fprintf(stderr, "%s Bad/unknown operation.\n", client->hostname);
+    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+    return (0);
+  }
+  else if (http_state == HTTP_STATE_UNKNOWN_VERSION)
+  {
+    fprintf(stderr, "%s Bad HTTP version.\n", client->hostname);
+    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+    return (0);
+  }
+
+  fprintf(stderr, "%s %s %s\n", client->hostname, http_states[http_state],
+          uri);
+
+ /*
+  * Separate the URI into its components...
+  */
+
+  if (httpSeparateURI(HTTP_URI_CODING_MOST, uri, scheme, sizeof(scheme),
+                     userpass, sizeof(userpass),
+                     hostname, sizeof(hostname), &port,
+                     client->uri, sizeof(client->uri)) < HTTP_URI_STATUS_OK &&
+      (http_state != HTTP_STATE_OPTIONS || strcmp(uri, "*")))
+  {
+    fprintf(stderr, "%s Bad URI \"%s\".\n", client->hostname, uri);
+    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+    return (0);
+  }
+
+  if ((client->options = strchr(client->uri, '?')) != NULL)
+    *(client->options)++ = '\0';
+
+ /*
+  * Process the request...
+  */
+
+  client->start     = time(NULL);
+  client->operation = httpGetState(client->http);
+
+ /*
+  * Parse incoming parameters until the status changes...
+  */
+
+  while ((http_status = httpUpdate(client->http)) == HTTP_STATUS_CONTINUE);
+
+  if (http_status != HTTP_STATUS_OK)
+  {
+    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+    return (0);
+  }
+
+  if (!httpGetField(client->http, HTTP_FIELD_HOST)[0] &&
+      httpGetVersion(client->http) >= HTTP_VERSION_1_1)
+  {
+   /*
+    * HTTP/1.1 and higher require the "Host:" field...
+    */
+
+    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+    return (0);
+  }
+
+ /*
+  * Handle HTTP Upgrade...
+  */
+
+  if (!strcasecmp(httpGetField(client->http, HTTP_FIELD_CONNECTION),
+                        "Upgrade"))
+  {
+#ifdef HAVE_SSL
+    if (strstr(httpGetField(client->http, HTTP_FIELD_UPGRADE), "TLS/") != NULL && !httpIsEncrypted(client->http))
+    {
+      if (!respond_http(client, HTTP_STATUS_SWITCHING_PROTOCOLS, NULL, NULL, 0))
+        return (0);
+
+      fprintf(stderr, "%s Upgrading to encrypted connection.\n", client->hostname);
+
+      if (httpEncryption(client->http, HTTP_ENCRYPTION_REQUIRED))
+      {
+        fprintf(stderr, "%s Unable to encrypt connection: %s\n", client->hostname, cupsLastErrorString());
+       return (0);
+      }
+
+      fprintf(stderr, "%s Connection now encrypted.\n", client->hostname);
+    }
+    else
+#endif /* HAVE_SSL */
+
+    if (!respond_http(client, HTTP_STATUS_NOT_IMPLEMENTED, NULL, NULL, 0))
+      return (0);
+  }
+
+ /*
+  * Handle HTTP Expect...
+  */
+
+  if (httpGetExpect(client->http) &&
+      (client->operation == HTTP_STATE_POST ||
+       client->operation == HTTP_STATE_PUT))
+  {
+    if (httpGetExpect(client->http) == HTTP_STATUS_CONTINUE)
+    {
+     /*
+      * Send 100-continue header...
+      */
+
+      if (!respond_http(client, HTTP_STATUS_CONTINUE, NULL, NULL, 0))
+       return (0);
+    }
+    else
+    {
+     /*
+      * Send 417-expectation-failed header...
+      */
+
+      if (!respond_http(client, HTTP_STATUS_EXPECTATION_FAILED, NULL, NULL, 0))
+       return (0);
+    }
+  }
+
+ /*
+  * Handle new transfers...
+  */
+
+  encoding = httpGetContentEncoding(client->http);
+
+  switch (client->operation)
+  {
+    case HTTP_STATE_OPTIONS :
+       /*
+       * Do OPTIONS command...
+       */
+
+       return (respond_http(client, HTTP_STATUS_OK, NULL, NULL, 0));
+
+    case HTTP_STATE_HEAD :
+#if 0 /* TODO: Work out icon support */
+        if (!strcmp(client->uri, "/icon.png"))
+         return (respond_http(client, HTTP_STATUS_OK, NULL, "image/png", 0));
+       else
+#endif /* 0 */
+       if (!strcmp(client->uri, "/") || !strcmp(client->uri, "/media") || !strcmp(client->uri, "/supplies"))
+         return (respond_http(client, HTTP_STATUS_OK, NULL, "text/html", 0));
+       else
+         return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
+
+    case HTTP_STATE_GET :
+#if 0 /* TODO: Work out icon support */
+        if (!strcmp(client->uri, "/icon.png"))
+       {
+        /*
+         * Send PNG icon file.
+         */
+
+          int          fd;             /* Icon file */
+         struct stat   fileinfo;       /* Icon file information */
+         char          buffer[4096];   /* Copy buffer */
+         ssize_t       bytes;          /* Bytes */
+
+          fprintf(stderr, "Icon file is \"%s\".\n", client->printer->icon);
+
+          if (!stat(client->printer->icon, &fileinfo) &&
+             (fd = open(client->printer->icon, O_RDONLY)) >= 0)
+         {
+           if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png",
+                             (size_t)fileinfo.st_size))
+           {
+             close(fd);
+             return (0);
+           }
+
+           while ((bytes = read(fd, buffer, sizeof(buffer))) > 0)
+             httpWrite2(client->http, buffer, (size_t)bytes);
+
+           httpFlushWrite(client->http);
+
+           close(fd);
+         }
+         else
+           return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
+       }
+       else
+#endif /* 0 */
+       if (!strcmp(client->uri, "/"))
+       {
+        /*
+         * Show web status page...
+         */
+
+          _ipp_job_t   *job;           /* Current job */
+         int           i;              /* Looping var */
+         _ipp_preason_t reason;        /* Current reason */
+         static const char * const reasons[] =
+         {                             /* Reason strings */
+           "Other",
+           "Cover Open",
+           "Input Tray Missing",
+           "Marker Supply Empty",
+           "Marker Supply Low",
+           "Marker Waste Almost Full",
+           "Marker Waste Full",
+           "Media Empty",
+           "Media Jam",
+           "Media Low",
+           "Media Needed",
+           "Moving to Paused",
+           "Paused",
+           "Spool Area Full",
+           "Toner Empty",
+           "Toner Low"
+         };
+
+          if (!respond_http(client, HTTP_STATUS_OK, encoding, "text/html", 0))
+           return (0);
+
+          html_header(client, client->printer->name);
+          html_printf(client,
+                     "<p><img align=\"right\" src=\"/icon.png\" width=\"64\" height=\"64\"><b>ippserver (" CUPS_SVERSION ")</b></p>\n"
+                     "<p>%s, %d job(s).", client->printer->state == IPP_PSTATE_IDLE ? "Idle" : client->printer->state == IPP_PSTATE_PROCESSING ? "Printing" : "Stopped", cupsArrayCount(client->printer->jobs));
+         for (i = 0, reason = 1; i < (int)(sizeof(reasons) / sizeof(reasons[0])); i ++, reason <<= 1)
+           if (client->printer->state_reasons & reason)
+             html_printf(client, "\n<br>&nbsp;&nbsp;&nbsp;&nbsp;%s", reasons[i]);
+         html_printf(client, "</p>\n");
+         
+          if (cupsArrayCount(client->printer->jobs) > 0)
+         {
+            _cupsRWLockRead(&(client->printer->rwlock));
+
+           html_printf(client, "<table class=\"striped\" summary=\"Jobs\"><thead><tr><th>Job #</th><th>Name</th><th>Owner</th><th>When</th></tr></thead><tbody>\n");
+           for (job = (_ipp_job_t *)cupsArrayFirst(client->printer->jobs); job; job = (_ipp_job_t *)cupsArrayNext(client->printer->jobs))
+           {
+             char      when[256],      /* When job queued/started/finished */
+                       hhmmss[64];     /* Time HH:MM:SS */
+
+              switch (job->state)
+             {
+               case IPP_JSTATE_PENDING :
+               case IPP_JSTATE_HELD :
+                   snprintf(when, sizeof(when), "Queued at %s", time_string(job->created, hhmmss, sizeof(hhmmss)));
+                   break;
+               case IPP_JSTATE_PROCESSING :
+               case IPP_JSTATE_STOPPED :
+                   snprintf(when, sizeof(when), "Started at %s", time_string(job->processing, hhmmss, sizeof(hhmmss)));
+                   break;
+               case IPP_JSTATE_ABORTED :
+                   snprintf(when, sizeof(when), "Aborted at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
+                   break;
+               case IPP_JSTATE_CANCELED :
+                   snprintf(when, sizeof(when), "Canceled at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
+                   break;
+               case IPP_JSTATE_COMPLETED :
+                   snprintf(when, sizeof(when), "Completed at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
+                   break;
+             }
+
+             html_printf(client, "<tr><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>\n", job->id, job->name, job->username, when);
+           }
+           html_printf(client, "</tbody></table>\n");
+
+           _cupsRWUnlock(&(client->printer->rwlock));
+         }
+          html_footer(client);
+
+         return (1);
+       }
+#if 0 /* TODO: Pull media and supply info from device attrs */
+       else if (!strcmp(client->uri, "/media"))
+       {
+        /*
+         * Show web media page...
+         */
+
+         int           i,              /* Looping var */
+                       num_options;    /* Number of form options */
+         cups_option_t *options;       /* Form options */
+          static const char * const sizes[] =
+         {                             /* Size strings */
+           "ISO A4",
+           "ISO A5",
+           "ISO A6",
+           "DL Envelope",
+           "US Legal",
+           "US Letter",
+           "#10 Envelope",
+           "3x5 Photo",
+           "3.5x5 Photo",
+           "4x6 Photo",
+           "5x7 Photo"
+         };
+         static const char * const types[] =
+                                         /* Type strings */
+         {
+           "Auto",
+           "Cardstock",
+           "Envelope",
+           "Labels",
+           "Other",
+           "Glossy Photo",
+           "High-Gloss Photo",
+           "Matte Photo",
+           "Satin Photo",
+           "Semi-Gloss Photo",
+           "Plain",
+           "Letterhead",
+           "Transparency"
+         };
+         static const int sheets[] =   /* Number of sheets */
+         {
+           250,
+           100,
+           25,
+           5,
+           0
+         };
+
+          if (!respond_http(client, HTTP_STATUS_OK, encoding, "text/html", 0))
+           return (0);
+
+          html_header(client, client->printer->name);
+
+         if ((num_options = parse_options(client, &options)) > 0)
+         {
+          /*
+           * WARNING: A real printer/server implementation MUST NOT implement
+           * media updates via a GET request - GET requests are supposed to be
+           * idempotent (without side-effects) and we obviously are not
+           * authenticating access here.  This form is provided solely to
+           * enable testing and development!
+           */
+
+           const char  *val;           /* Form value */
+
+           if ((val = cupsGetOption("main_size", num_options, options)) != NULL)
+             client->printer->main_size = atoi(val);
+           if ((val = cupsGetOption("main_type", num_options, options)) != NULL)
+             client->printer->main_type = atoi(val);
+           if ((val = cupsGetOption("main_level", num_options, options)) != NULL)
+             client->printer->main_level = atoi(val);
+
+           if ((val = cupsGetOption("envelope_size", num_options, options)) != NULL)
+             client->printer->envelope_size = atoi(val);
+           if ((val = cupsGetOption("envelope_level", num_options, options)) != NULL)
+             client->printer->envelope_level = atoi(val);
+
+           if ((val = cupsGetOption("photo_size", num_options, options)) != NULL)
+             client->printer->photo_size = atoi(val);
+           if ((val = cupsGetOption("photo_type", num_options, options)) != NULL)
+             client->printer->photo_type = atoi(val);
+           if ((val = cupsGetOption("photo_level", num_options, options)) != NULL)
+             client->printer->photo_level = atoi(val);
+
+            if ((client->printer->main_level < 100 && client->printer->main_level > 0) || (client->printer->envelope_level < 25 && client->printer->envelope_level > 0) || (client->printer->photo_level < 25 && client->printer->photo_level > 0))
+             client->printer->state_reasons |= _IPP_PREASON_MEDIA_LOW;
+           else
+             client->printer->state_reasons &= (_ipp_preason_t)~_IPP_PREASON_MEDIA_LOW;
+
+            if ((client->printer->main_level == 0 && client->printer->main_size > _IPP_MEDIA_SIZE_NONE) || (client->printer->envelope_level == 0 && client->printer->envelope_size > _IPP_MEDIA_SIZE_NONE) || (client->printer->photo_level == 0 && client->printer->photo_size > _IPP_MEDIA_SIZE_NONE))
+           {
+             client->printer->state_reasons |= _IPP_PREASON_MEDIA_EMPTY;
+             if (client->printer->active_job)
+               client->printer->state_reasons |= _IPP_PREASON_MEDIA_NEEDED;
+           }
+           else
+             client->printer->state_reasons &= (_ipp_preason_t)~(_IPP_PREASON_MEDIA_EMPTY | _IPP_PREASON_MEDIA_NEEDED);
+
+           html_printf(client, "<blockquote>Media updated.</blockquote>\n");
+          }
+
+          html_printf(client, "<form method=\"GET\" action=\"/media\">\n");
+
+          html_printf(client, "<table class=\"form\" summary=\"Media\">\n");
+          html_printf(client, "<tr><th>Main Tray:</th><td><select name=\"main_size\"><option value=\"-1\">None</option>");
+          for (i = 0; i < (int)(sizeof(sizes) / sizeof(sizes[0])); i ++)
+           if (!strstr(sizes[i], "Envelope") && !strstr(sizes[i], "Photo"))
+             html_printf(client, "<option value=\"%d\"%s>%s</option>", i, i == client->printer->main_size ? " selected" : "", sizes[i]);
+         html_printf(client, "</select> <select name=\"main_type\"><option value=\"-1\">None</option>");
+          for (i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i ++)
+           if (!strstr(types[i], "Photo"))
+             html_printf(client, "<option value=\"%d\"%s>%s</option>", i, i == client->printer->main_type ? " selected" : "", types[i]);
+         html_printf(client, "</select> <select name=\"main_level\">");
+          for (i = 0; i < (int)(sizeof(sheets) / sizeof(sheets[0])); i ++)
+           html_printf(client, "<option value=\"%d\"%s>%d sheets</option>", sheets[i], sheets[i] == client->printer->main_level ? " selected" : "", sheets[i]);
+         html_printf(client, "</select></td></tr>\n");
+
+          html_printf(client,
+                     "<tr><th>Envelope Feeder:</th><td><select name=\"envelope_size\"><option value=\"-1\">None</option>");
+          for (i = 0; i < (int)(sizeof(sizes) / sizeof(sizes[0])); i ++)
+           if (strstr(sizes[i], "Envelope"))
+             html_printf(client, "<option value=\"%d\"%s>%s</option>", i, i == client->printer->envelope_size ? " selected" : "", sizes[i]);
+         html_printf(client, "</select> <select name=\"envelope_level\">");
+          for (i = 0; i < (int)(sizeof(sheets) / sizeof(sheets[0])); i ++)
+           html_printf(client, "<option value=\"%d\"%s>%d sheets</option>", sheets[i], sheets[i] == client->printer->envelope_level ? " selected" : "", sheets[i]);
+         html_printf(client, "</select></td></tr>\n");
+
+          html_printf(client,
+                     "<tr><th>Photo Tray:</th><td><select name=\"photo_size\"><option value=\"-1\">None</option>");
+          for (i = 0; i < (int)(sizeof(sizes) / sizeof(sizes[0])); i ++)
+           if (strstr(sizes[i], "Photo"))
+             html_printf(client, "<option value=\"%d\"%s>%s</option>", i, i == client->printer->photo_size ? " selected" : "", sizes[i]);
+         html_printf(client, "</select> <select name=\"photo_type\"><option value=\"-1\">None</option>");
+          for (i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i ++)
+           if (strstr(types[i], "Photo"))
+             html_printf(client, "<option value=\"%d\"%s>%s</option>", i, i == client->printer->photo_type ? " selected" : "", types[i]);
+         html_printf(client, "</select> <select name=\"photo_level\">");
+          for (i = 0; i < (int)(sizeof(sheets) / sizeof(sheets[0])); i ++)
+           html_printf(client, "<option value=\"%d\"%s>%d sheets</option>", sheets[i], sheets[i] == client->printer->photo_level ? " selected" : "", sheets[i]);
+         html_printf(client, "</select></td></tr>\n");
+
+         html_printf(client, "<tr><td></td><td><input type=\"submit\" value=\"Update Media\"></td></tr></table></form>\n");
+          html_footer(client);
+
+         return (1);
+       }
+       else if (!strcmp(client->uri, "/supplies"))
+       {
+        /*
+         * Show web supplies page...
+         */
+
+          int          i, j,           /* Looping vars */
+                       num_options;    /* Number of form options */
+         cups_option_t *options;       /* Form options */
+         static const int levels[] = { 0, 5, 10, 25, 50, 75, 90, 95, 100 };
+
+          if (!respond_http(client, HTTP_STATUS_OK, encoding, "text/html", 0))
+           return (0);
+
+          html_header(client, client->printer->name);
+
+         if ((num_options = parse_options(client, &options)) > 0)
+         {
+          /*
+           * WARNING: A real printer/server implementation MUST NOT implement
+           * supply updates via a GET request - GET requests are supposed to be
+           * idempotent (without side-effects) and we obviously are not
+           * authenticating access here.  This form is provided solely to
+           * enable testing and development!
+           */
+
+           char        name[64];       /* Form field */
+           const char  *val;           /* Form value */
+
+            client->printer->state_reasons &= (_ipp_preason_t)~(_IPP_PREASON_MARKER_SUPPLY_EMPTY | _IPP_PREASON_MARKER_SUPPLY_LOW | _IPP_PREASON_MARKER_WASTE_ALMOST_FULL | _IPP_PREASON_MARKER_WASTE_FULL | _IPP_PREASON_TONER_EMPTY | _IPP_PREASON_TONER_LOW);
+
+           for (i = 0; i < (int)(sizeof(printer_supplies) / sizeof(printer_supplies[0])); i ++)
+           {
+             snprintf(name, sizeof(name), "supply_%d", i);
+             if ((val = cupsGetOption(name, num_options, options)) != NULL)
+             {
+               int level = client->printer->supplies[i] = atoi(val);
+                                       /* New level */
+
+               if (i < 4)
+               {
+                 if (level == 0)
+                   client->printer->state_reasons |= _IPP_PREASON_TONER_EMPTY;
+                 else if (level < 10)
+                   client->printer->state_reasons |= _IPP_PREASON_TONER_LOW;
+               }
+               else
+               {
+                 if (level == 100)
+                   client->printer->state_reasons |= _IPP_PREASON_MARKER_WASTE_FULL;
+                 else if (level > 90)
+                   client->printer->state_reasons |= _IPP_PREASON_MARKER_WASTE_ALMOST_FULL;
+               }
+             }
+            }
+
+           html_printf(client, "<blockquote>Supplies updated.</blockquote>\n");
+          }
+
+          html_printf(client, "<form method=\"GET\" action=\"/supplies\">\n");
+
+         html_printf(client, "<table class=\"form\" summary=\"Supplies\">\n");
+         for (i = 0; i < (int)(sizeof(printer_supplies) / sizeof(printer_supplies[0])); i ++)
+         {
+           html_printf(client, "<tr><th>%s:</th><td><select name=\"supply_%d\">", printer_supplies[i], i);
+           for (j = 0; j < (int)(sizeof(levels) / sizeof(levels[0])); j ++)
+             html_printf(client, "<option value=\"%d\"%s>%d%%</option>", levels[j], levels[j] == client->printer->supplies[i] ? " selected" : "", levels[j]);
+           html_printf(client, "</select></td></tr>\n");
+         }
+         html_printf(client, "<tr><td></td><td><input type=\"submit\" value=\"Update Supplies\"></td></tr>\n</table>\n</form>\n");
+          html_footer(client);
+
+         return (1);
+       }
+#endif /* 0 */
+       else
+         return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
+       break;
+
+    case HTTP_STATE_POST :
+       if (strcmp(httpGetField(client->http, HTTP_FIELD_CONTENT_TYPE),
+                  "application/ipp"))
+        {
+        /*
+         * Not an IPP request...
+         */
+
+         return (respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0));
+       }
+
+       /*
+        * Read the IPP request...
+       */
+
+       client->request = ippNew();
+
+        while ((ipp_state = ippRead(client->http,
+                                    client->request)) != IPP_STATE_DATA)
+       {
+         if (ipp_state == IPP_STATE_ERROR)
+         {
+            fprintf(stderr, "%s IPP read error (%s).\n", client->hostname,
+                   cupsLastErrorString());
+           respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
+           return (0);
+         }
+       }
+
+       /*
+        * Now that we have the IPP request, process the request...
+       */
+
+        return (process_ipp(client));
+
+    default :
+        break; /* Anti-compiler-warning-code */
+  }
+
+  return (1);
+}
+
+
+/*
+ * 'process_ipp()' - Process an IPP request.
+ */
+
+static int                             /* O - 1 on success, 0 on error */
+process_ipp(_ipp_client_t *client)     /* I - Client */
+{
+  ipp_tag_t            group;          /* Current group tag */
+  ipp_attribute_t      *attr;          /* Current attribute */
+  ipp_attribute_t      *charset;       /* Character set attribute */
+  ipp_attribute_t      *language;      /* Language attribute */
+  ipp_attribute_t      *uri;           /* Printer URI attribute */
+  int                  major, minor;   /* Version number */
+  const char           *name;          /* Name of attribute */
+
+
+  debug_attributes("Request", client->request, 1);
+
+ /*
+  * First build an empty response message for this request...
+  */
+
+  client->operation_id = ippGetOperation(client->request);
+  client->response     = ippNewResponse(client->request);
+
+ /*
+  * Then validate the request header and required attributes...
+  */
+
+  major = ippGetVersion(client->request, &minor);
+
+  if (major < 1 || major > 2)
+  {
+   /*
+    * Return an error, since we only support IPP 1.x and 2.x.
+    */
+
+    respond_ipp(client, IPP_STATUS_ERROR_VERSION_NOT_SUPPORTED,
+                "Bad request version number %d.%d.", major, minor);
+  }
+  else if (ippGetRequestId(client->request) <= 0)
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Bad request-id %d.",
+                ippGetRequestId(client->request));
+  else if (!ippFirstAttribute(client->request))
+    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                "No attributes in request.");
+  else
+  {
+   /*
+    * Make sure that the attributes are provided in the correct order and
+    * don't repeat groups...
+    */
+
+    for (attr = ippFirstAttribute(client->request),
+             group = ippGetGroupTag(attr);
+        attr;
+        attr = ippNextAttribute(client->request))
+    {
+      if (ippGetGroupTag(attr) < group && ippGetGroupTag(attr) != IPP_TAG_ZERO)
+      {
+       /*
+       * Out of order; return an error...
+       */
+
+       respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                   "Attribute groups are out of order (%x < %x).",
+                   ippGetGroupTag(attr), group);
+       break;
+      }
+      else
+       group = ippGetGroupTag(attr);
+    }
+
+    if (!attr)
+    {
+     /*
+      * Then make sure that the first three attributes are:
+      *
+      *     attributes-charset
+      *     attributes-natural-language
+      *     printer-uri/job-uri
+      */
+
+      attr = ippFirstAttribute(client->request);
+      name = ippGetName(attr);
+      if (attr && name && !strcmp(name, "attributes-charset") &&
+         ippGetValueTag(attr) == IPP_TAG_CHARSET)
+       charset = attr;
+      else
+       charset = NULL;
+
+      attr = ippNextAttribute(client->request);
+      name = ippGetName(attr);
+
+      if (attr && name && !strcmp(name, "attributes-natural-language") &&
+         ippGetValueTag(attr) == IPP_TAG_LANGUAGE)
+       language = attr;
+      else
+       language = NULL;
+
+      if ((attr = ippFindAttribute(client->request, "printer-uri",
+                                   IPP_TAG_URI)) != NULL)
+       uri = attr;
+      else if ((attr = ippFindAttribute(client->request, "job-uri",
+                                        IPP_TAG_URI)) != NULL)
+       uri = attr;
+      else
+       uri = NULL;
+
+      if (charset &&
+          strcasecmp(ippGetString(charset, 0, NULL), "us-ascii") &&
+          strcasecmp(ippGetString(charset, 0, NULL), "utf-8"))
+      {
+       /*
+        * Bad character set...
+       */
+
+       respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                   "Unsupported character set \"%s\".",
+                   ippGetString(charset, 0, NULL));
+      }
+      else if (!charset || !language || !uri)
+      {
+       /*
+       * Return an error, since attributes-charset,
+       * attributes-natural-language, and printer-uri/job-uri are required
+       * for all operations.
+       */
+
+       respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
+                   "Missing required attributes.");
+      }
+      else
+      {
+        char           scheme[32],     /* URI scheme */
+                       userpass[32],   /* Username/password in URI */
+                       host[256],      /* Host name in URI */
+                       resource[256];  /* Resource path in URI */
+       int             port;           /* Port number in URI */
+
+        name = ippGetName(uri);
+
+        if (httpSeparateURI(HTTP_URI_CODING_ALL, ippGetString(uri, 0, NULL),
+                            scheme, sizeof(scheme),
+                            userpass, sizeof(userpass),
+                            host, sizeof(host), &port,
+                            resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
+         respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES,
+                     "Bad %s value '%s'.", name, ippGetString(uri, 0, NULL));
+        else if ((!strcmp(name, "job-uri") && strncmp(resource, "/ipp/print/", 11)) ||
+                 (!strcmp(name, "printer-uri") && strcmp(resource, "/ipp/print")))
+         respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "%s %s not found.",
+                     name, ippGetString(uri, 0, NULL));
+       else
+       {
+        /*
+         * Try processing the operation...
+         */
+
+         switch ((int)ippGetOperation(client->request))
+         {
+           case IPP_OP_PRINT_JOB :
+               ipp_print_job(client);
+               break;
+
+           case IPP_OP_PRINT_URI :
+               ipp_print_uri(client);
+               break;
+
+           case IPP_OP_VALIDATE_JOB :
+               ipp_validate_job(client);
+               break;
+
+           case IPP_OP_CREATE_JOB :
+               ipp_create_job(client);
+               break;
+
+           case IPP_OP_SEND_DOCUMENT :
+               ipp_send_document(client);
+               break;
+
+           case IPP_OP_SEND_URI :
+               ipp_send_uri(client);
+               break;
+
+           case IPP_OP_CANCEL_JOB :
+               ipp_cancel_job(client);
+               break;
+
+           case IPP_OP_CANCEL_MY_JOBS :
+               ipp_cancel_my_jobs(client);
+               break;
+
+           case IPP_OP_GET_JOB_ATTRIBUTES :
+               ipp_get_job_attributes(client);
+               break;
+
+           case IPP_OP_GET_JOBS :
+               ipp_get_jobs(client);
+               break;
+
+           case IPP_OP_GET_PRINTER_ATTRIBUTES :
+               ipp_get_printer_attributes(client);
+               break;
+
+           case IPP_OP_GET_PRINTER_SUPPORTED_VALUES :
+               ipp_get_printer_supported_values(client);
+               break;
+
+           case IPP_OP_CLOSE_JOB :
+               ipp_close_job(client);
+               break;
+
+           case IPP_OP_IDENTIFY_PRINTER :
+               ipp_identify_printer(client);
+               break;
+
+           case IPP_OP_CANCEL_SUBSCRIPTION :
+               ipp_cancel_subscription(client);
+               break;
+
+            case IPP_OP_CREATE_JOB_SUBSCRIPTIONS :
+           case IPP_OP_CREATE_PRINTER_SUBSCRIPTIONS :
+               ipp_create_xxx_subscriptions(client);
+               break;
+
+           case IPP_OP_GET_NOTIFICATIONS :
+               ipp_get_notifications(client);
+               break;
+
+           case IPP_OP_GET_SUBSCRIPTION_ATTRIBUTES :
+               ipp_get_subscription_attributes(client);
+               break;
+
+           case IPP_OP_GET_SUBSCRIPTIONS :
+               ipp_get_subscriptions(client);
+               break;
+
+           case IPP_OP_RENEW_SUBSCRIPTION :
+               ipp_renew_subscription(client);
+               break;
+
+           case IPP_OP_GET_DOCUMENT_ATTRIBUTES :
+               ipp_get_document_attributes(client);
+               break;
+
+           case IPP_OP_GET_DOCUMENTS :
+               ipp_get_documents(client);
+               break;
+
+           case IPP_OP_VALIDATE_DOCUMENT :
+               ipp_validate_document(client);
+               break;
+
+            case _IPP_OP_ACKNOWLEDGE_DOCUMENT :
+               ipp_acknowledge_document(client);
+               break;
+
+            case _IPP_OP_ACKNOWLEDGE_IDENTIFY_PRINTER :
+               ipp_acknowledge_identify_printer(client);
+               break;
+
+            case _IPP_OP_ACKNOWLEDGE_JOB :
+               ipp_acknowledge_job(client);
+               break;
+
+            case _IPP_OP_FETCH_DOCUMENT :
+               ipp_fetch_document(client);
+               break;
+
+            case _IPP_OP_FETCH_JOB :
+               ipp_fetch_job(client);
+               break;
+
+            case _IPP_OP_GET_OUTPUT_DEVICE_ATTRIBUTES :
+               ipp_get_output_device_attributes(client);
+               break;
+
+            case _IPP_OP_UPDATE_ACTIVE_JOBS :
+               ipp_update_active_jobs(client);
+               break;
+
+            case _IPP_OP_UPDATE_DOCUMENT_STATUS :
+               ipp_update_document_status(client);
+               break;
+
+            case _IPP_OP_UPDATE_JOB_STATUS :
+               ipp_update_job_status(client);
+               break;
+
+            case _IPP_OP_UPDATE_OUTPUT_DEVICE_ATTRIBUTES :
+               ipp_update_output_device_attributes(client);
+               break;
+
+           default :
+               respond_ipp(client, IPP_STATUS_ERROR_OPERATION_NOT_SUPPORTED,
+                           "Operation not supported.");
+               break;
+         }
+       }
+      }
+    }
+  }
+
+ /*
+  * Send the HTTP header and return...
+  */
+
+  if (httpGetState(client->http) != HTTP_STATE_POST_SEND)
+    httpFlush(client->http);           /* Flush trailing (junk) data */
+
+  return (respond_http(client, HTTP_STATUS_OK, NULL, "application/ipp",
+                       ippLength(client->response)));
+}
+
+
+/*
+ * 'process_job()' - Process a print job.
+ */
+
+static void *                          /* O - Thread exit status */
+process_job(_ipp_job_t *job)           /* I - Job */
+{
+  job->state                   = IPP_JSTATE_PROCESSING;
+  job->printer->state          = IPP_PSTATE_PROCESSING;
+  job->processing              = time(NULL);
+  job->printer->processing_job = job;
+
+  add_event(job->printer, job, _IPP_EVENT_JOB_STATE_CHANGED, "Job processing.");
+
+ /*
+  * TODO: Perform any preprocessing needed...
+  */
+
+  // job->state_reasons |= _IPP_JREASON_JOB_TRANSFORMING;
+  // job->state_reasons &= ~_IPP_JREASON_JOB_TRANSFORMING;
+
+ /*
+  * Set the state to processing-stopped, fetchable, then send a
+  * notification.
+  */
+
+  job->state         = IPP_JSTATE_STOPPED;
+  job->state_reasons |= _IPP_JREASON_JOB_FETCHABLE;
+
+  add_event(job->printer, job, _IPP_EVENT_JOB_STATE_CHANGED, "Job fetchable.");
+
+  return (NULL);
+}
+
+
+/*
+ * 'respond_http()' - Send a HTTP response.
+ */
+
+int                                    /* O - 1 on success, 0 on failure */
+respond_http(
+    _ipp_client_t *client,             /* I - Client */
+    http_status_t code,                        /* I - HTTP status of response */
+    const char    *content_encoding,   /* I - Content-Encoding of response */
+    const char    *type,               /* I - MIME media type of response */
+    size_t        length)              /* I - Length of response */
+{
+  char message[1024];                  /* Text message */
+
+
+  fprintf(stderr, "%s %s\n", client->hostname, httpStatus(code));
+
+  if (code == HTTP_STATUS_CONTINUE)
+  {
+   /*
+    * 100-continue doesn't send any headers...
+    */
+
+    return (httpWriteResponse(client->http, HTTP_STATUS_CONTINUE) == 0);
+  }
+
+ /*
+  * Format an error message...
+  */
+
+  if (!type && !length && code != HTTP_STATUS_OK && code != HTTP_STATUS_SWITCHING_PROTOCOLS)
+  {
+    snprintf(message, sizeof(message), "%d - %s\n", code, httpStatus(code));
+
+    type   = "text/plain";
+    length = strlen(message);
+  }
+  else
+    message[0] = '\0';
+
+ /*
+  * Send the HTTP response header...
+  */
+
+  httpClearFields(client->http);
+
+  if (code == HTTP_STATUS_METHOD_NOT_ALLOWED ||
+      client->operation == HTTP_STATE_OPTIONS)
+    httpSetField(client->http, HTTP_FIELD_ALLOW, "GET, HEAD, OPTIONS, POST");
+
+  if (type)
+  {
+    if (!strcmp(type, "text/html"))
+      httpSetField(client->http, HTTP_FIELD_CONTENT_TYPE,
+                   "text/html; charset=utf-8");
+    else
+      httpSetField(client->http, HTTP_FIELD_CONTENT_TYPE, type);
+
+    if (content_encoding)
+      httpSetField(client->http, HTTP_FIELD_CONTENT_ENCODING, content_encoding);
+  }
+
+  httpSetLength(client->http, length);
+
+  if (httpWriteResponse(client->http, code) < 0)
+    return (0);
+
+ /*
+  * Send the response data...
+  */
+
+  if (message[0])
+  {
+   /*
+    * Send a plain text message.
+    */
+
+    if (httpPrintf(client->http, "%s", message) < 0)
+      return (0);
+
+    if (httpWrite2(client->http, "", 0) < 0)
+      return (0);
+  }
+  else if (client->response)
+  {
+   /*
+    * Send an IPP response...
+    */
+
+    debug_attributes("Response", client->response, 2);
+
+    ippSetState(client->response, IPP_STATE_IDLE);
+
+    if (ippWrite(client->http, client->response) != IPP_STATE_DATA)
+      return (0);
+  }
+
+  return (1);
+}
+
+
+/*
+ * 'respond_ipp()' - Send an IPP response.
+ */
+
+static void
+respond_ipp(_ipp_client_t *client,     /* I - Client */
+            ipp_status_t  status,      /* I - status-code */
+           const char    *message,     /* I - printf-style status-message */
+           ...)                        /* I - Additional args as needed */
+{
+  const char   *formatted = NULL;      /* Formatted message */
+
+
+  ippSetStatusCode(client->response, status);
+
+  if (message)
+  {
+    va_list            ap;             /* Pointer to additional args */
+    ipp_attribute_t    *attr;          /* New status-message attribute */
+
+    va_start(ap, message);
+    if ((attr = ippFindAttribute(client->response, "status-message",
+                                IPP_TAG_TEXT)) != NULL)
+      ippSetStringfv(client->response, &attr, 0, message, ap);
+    else
+      attr = ippAddStringfv(client->response, IPP_TAG_OPERATION, IPP_TAG_TEXT,
+                           "status-message", NULL, message, ap);
+    va_end(ap);
+
+    formatted = ippGetString(attr, 0, NULL);
+  }
+
+  if (formatted)
+    fprintf(stderr, "%s %s %s (%s)\n", client->hostname,
+           ippOpString(client->operation_id), ippErrorString(status),
+           formatted);
+  else
+    fprintf(stderr, "%s %s %s\n", client->hostname,
+           ippOpString(client->operation_id), ippErrorString(status));
+}
+
+
+/*
+ * 'respond_unsupported()' - Respond with an unsupported attribute.
+ */
+
+static void
+respond_unsupported(
+    _ipp_client_t   *client,           /* I - Client */
+    ipp_attribute_t *attr)             /* I - Atribute */
+{
+  ipp_attribute_t      *temp;          /* Copy of attribute */
+
+
+  respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES,
+              "Unsupported %s %s%s value.", ippGetName(attr),
+              ippGetCount(attr) > 1 ? "1setOf " : "",
+             ippTagString(ippGetValueTag(attr)));
+
+  temp = ippCopyAttribute(client->response, attr, 0);
+  ippSetGroupTag(client->response, &temp, IPP_TAG_UNSUPPORTED_GROUP);
+}
+
+
+/*
+ * 'run_printer()' - Run the printer service.
+ */
+
+static void
+run_printer(_ipp_printer_t *printer)   /* I - Printer */
+{
+  int          num_fds;                /* Number of file descriptors */
+  struct pollfd        polldata[3];            /* poll() data */
+  int          timeout;                /* Timeout for poll() */
+  _ipp_client_t        *client;                /* New client */
+
+
+ /*
+  * Setup poll() data for the Bonjour service socket and IPv4/6 listeners...
+  */
+
+  polldata[0].fd     = printer->ipv4;
+  polldata[0].events = POLLIN;
+
+  polldata[1].fd     = printer->ipv6;
+  polldata[1].events = POLLIN;
+
+  num_fds = 2;
+
+ /*
+  * Loop until we are killed or have a hard error...
+  */
+
+  for (;;)
+  {
+    if (cupsArrayCount(printer->jobs))
+      timeout = 10;
+    else
+      timeout = -1;
+
+    if (poll(polldata, (nfds_t)num_fds, timeout) < 0 && errno != EINTR)
+    {
+      perror("poll() failed");
+      break;
+    }
+
+    if (polldata[0].revents & POLLIN)
+    {
+      if ((client = create_client(printer, printer->ipv4)) != NULL)
+      {
+       if (!_cupsThreadCreate((_cups_thread_func_t)process_client, client))
+       {
+         perror("Unable to create client thread");
+         delete_client(client);
+       }
+      }
+    }
+
+    if (polldata[1].revents & POLLIN)
+    {
+      if ((client = create_client(printer, printer->ipv6)) != NULL)
+      {
+       if (!_cupsThreadCreate((_cups_thread_func_t)process_client, client))
+       {
+         perror("Unable to create client thread");
+         delete_client(client);
+       }
+      }
+    }
+
+   /*
+    * Clean out old jobs...
+    */
+
+    clean_jobs(printer);
+  }
+}
+
+
+/*
+ * 'time_string()' - Return the local time in hours, minutes, and seconds.
+ */
+
+static char *
+time_string(time_t tv,                 /* I - Time value */
+            char   *buffer,            /* I - Buffer */
+           size_t bufsize)             /* I - Size of buffer */
+{
+  struct tm    *curtime = localtime(&tv);
+                                       /* Local time */
+
+  strftime(buffer, bufsize, "%X", curtime);
+  return (buffer);
+}
+
+
+/*
+ * 'usage()' - Show program usage.
+ */
+
+static void
+usage(int status)                      /* O - Exit status */
+{
+  if (!status)
+  {
+    puts(CUPS_SVERSION " - Copyright 2010-2014 by Apple Inc. All rights reserved.");
+    puts("");
+  }
+
+  puts("Usage: ippinfra [options] \"name\"");
+  puts("");
+  puts("Options:");
+  printf("-d spool-directory      Spool directory "
+         "(default=/tmp/ippserver.%d)\n", (int)getpid());
+  puts("-h                      Show program help");
+  puts("-k                      Keep job spool files");
+  puts("-n hostname             Hostname for printer");
+  puts("-p port                 Port number (default=auto)");
+  puts("-u user:pass            Set proxy username and password");
+  puts("-v[vvv]                 Be (very) verbose");
+
+  exit(status);
+}
+
+
+/*
+ * 'valid_doc_attributes()' - Determine whether the document attributes are
+ *                            valid.
+ *
+ * When one or more document attributes are invalid, this function adds a
+ * suitable response and attributes to the unsupported group.
+ */
+
+static int                             /* O - 1 if valid, 0 if not */
+valid_doc_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  int                  valid = 1;      /* Valid attributes? */
+  ipp_op_t             op = ippGetOperation(client->request);
+                                       /* IPP operation */
+  const char           *op_name = ippOpString(op);
+                                       /* IPP operation name */
+  ipp_attribute_t      *attr,          /* Current attribute */
+                       *supported;     /* xxx-supported attribute */
+  const char           *compression = NULL,
+                                       /* compression value */
+                       *format = NULL; /* document-format value */
+
+
+ /*
+  * Check operation attributes...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "compression", IPP_TAG_ZERO)) != NULL)
+  {
+   /*
+    * If compression is specified, only accept a supported value in a Print-Job
+    * or Send-Document request...
+    */
+
+    compression = ippGetString(attr, 0, NULL);
+    supported   = ippFindAttribute(client->printer->attrs,
+                                   "compression-supported", IPP_TAG_KEYWORD);
+
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD ||
+        ippGetGroupTag(attr) != IPP_TAG_OPERATION ||
+        (op != IPP_OP_PRINT_JOB && op != IPP_OP_SEND_DOCUMENT &&
+         op != IPP_OP_VALIDATE_JOB) ||
+        !ippContainsString(supported, compression))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    else
+    {
+      fprintf(stderr, "%s %s compression=\"%s\"\n", client->hostname, op_name, compression);
+
+      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "compression-supplied", NULL, compression);
+
+      if (strcmp(compression, "none"))
+      {
+       if (Verbosity)
+         fprintf(stderr, "Receiving job file with \"%s\" compression.\n", compression);
+        httpSetField(client->http, HTTP_FIELD_CONTENT_ENCODING, compression);
+      }
+    }
+  }
+
+ /*
+  * Is it a format we support?
+  */
+
+  if ((attr = ippFindAttribute(client->request, "document-format", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_MIMETYPE ||
+        ippGetGroupTag(attr) != IPP_TAG_OPERATION)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    else
+    {
+      format = ippGetString(attr, 0, NULL);
+
+      fprintf(stderr, "%s %s document-format=\"%s\"\n",
+             client->hostname, op_name, format);
+
+      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_MIMETYPE, "document-format-supplied", NULL, format);
+    }
+  }
+  else
+  {
+    format = ippGetString(ippFindAttribute(client->printer->attrs, "document-format-default", IPP_TAG_MIMETYPE), 0, NULL);
+    if (!format)
+      format = "application/octet-stream"; /* Should never happen */
+
+    attr = ippAddString(client->request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, format);
+  }
+
+  if (!strcmp(format, "application/octet-stream") && (ippGetOperation(client->request) == IPP_OP_PRINT_JOB || ippGetOperation(client->request) == IPP_OP_SEND_DOCUMENT))
+  {
+   /*
+    * Auto-type the file using the first 8 bytes of the file...
+    */
+
+    unsigned char      header[8];      /* First 8 bytes of file */
+
+    memset(header, 0, sizeof(header));
+    httpPeek(client->http, (char *)header, sizeof(header));
+
+    if (!memcmp(header, "%PDF", 4))
+      format = "application/pdf";
+    else if (!memcmp(header, "%!", 2))
+      format = "application/postscript";
+    else if (!memcmp(header, "\377\330\377", 3) && header[3] >= 0xe0 && header[3] <= 0xef)
+      format = "image/jpeg";
+    else if (!memcmp(header, "\211PNG", 4))
+      format = "image/png";
+    else if (!memcmp(header, "RAS2", 4))
+      format = "image/pwg-raster";
+    else if (!memcmp(header, "UNIRAST", 8))
+      format = "image/urf";
+    else
+      format = NULL;
+
+    if (format)
+    {
+      fprintf(stderr, "%s %s Auto-typed document-format=\"%s\"\n",
+             client->hostname, op_name, format);
+
+      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_MIMETYPE, "document-format-detected", NULL, format);
+    }
+  }
+
+  if (op != IPP_OP_CREATE_JOB && (supported = ippFindAttribute(client->printer->attrs, "document-format-supported", IPP_TAG_MIMETYPE)) != NULL && !ippContainsString(supported, format))
+  {
+    respond_unsupported(client, attr);
+    valid = 0;
+  }
+
+ /*
+  * document-name
+  */
+
+  if ((attr = ippFindAttribute(client->request, "document-name", IPP_TAG_NAME)) != NULL)
+    ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_NAME, "document-name-supplied", NULL, ippGetString(attr, 0, NULL));
+
+  return (valid);
+}
+
+
+/*
+ * 'valid_job_attributes()' - Determine whether the job attributes are valid.
+ *
+ * When one or more job attributes are invalid, this function adds a suitable
+ * response and attributes to the unsupported group.
+ */
+
+static int                             /* O - 1 if valid, 0 if not */
+valid_job_attributes(
+    _ipp_client_t *client)             /* I - Client */
+{
+  int                  i,              /* Looping var */
+                       valid = 1;      /* Valid attributes? */
+  ipp_attribute_t      *attr,          /* Current attribute */
+                       *supported;     /* xxx-supported attribute */
+
+
+ /*
+  * Check operation attributes...
+  */
+
+  valid = valid_doc_attributes(client);
+
+ /*
+  * Check the various job template attributes...
+  */
+
+  if ((attr = ippFindAttribute(client->request, "copies", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER ||
+        ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 999)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "ipp-attribute-fidelity", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_BOOLEAN)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "job-hold-until", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 ||
+        (ippGetValueTag(attr) != IPP_TAG_NAME &&
+        ippGetValueTag(attr) != IPP_TAG_NAMELANG &&
+        ippGetValueTag(attr) != IPP_TAG_KEYWORD) ||
+       strcmp(ippGetString(attr, 0, NULL), "no-hold"))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "job-impressions", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER || ippGetInteger(attr, 0) < 0)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "job-name", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 ||
+        (ippGetValueTag(attr) != IPP_TAG_NAME &&
+        ippGetValueTag(attr) != IPP_TAG_NAMELANG))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+
+    ippSetGroupTag(client->request, &attr, IPP_TAG_JOB);
+  }
+  else
+    ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_NAME, "job-name", NULL, "Untitled");
+
+  if ((attr = ippFindAttribute(client->request, "job-priority", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER ||
+        ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 100)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "job-sheets", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 ||
+        (ippGetValueTag(attr) != IPP_TAG_NAME &&
+        ippGetValueTag(attr) != IPP_TAG_NAMELANG &&
+        ippGetValueTag(attr) != IPP_TAG_KEYWORD) ||
+       strcmp(ippGetString(attr, 0, NULL), "none"))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "media", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 ||
+        (ippGetValueTag(attr) != IPP_TAG_NAME &&
+        ippGetValueTag(attr) != IPP_TAG_NAMELANG &&
+        ippGetValueTag(attr) != IPP_TAG_KEYWORD))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    else
+    {
+#if 0 /* TODO: Validate media */
+      for (i = 0;
+           i < (int)(sizeof(media_supported) / sizeof(media_supported[0]));
+          i ++)
+        if (!strcmp(ippGetString(attr, 0, NULL), media_supported[i]))
+         break;
+
+      if (i >= (int)(sizeof(media_supported) / sizeof(media_supported[0])))
+      {
+       respond_unsupported(client, attr);
+       valid = 0;
+      }
+#endif /* 0 */
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "media-col", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 ||
+        ippGetValueTag(attr) != IPP_TAG_BEGIN_COLLECTION)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    /* TODO: check for valid media-col */
+  }
+
+  if ((attr = ippFindAttribute(client->request, "multiple-document-handling", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD ||
+        (strcmp(ippGetString(attr, 0, NULL),
+               "separate-documents-uncollated-copies") &&
+        strcmp(ippGetString(attr, 0, NULL),
+               "separate-documents-collated-copies")))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "orientation-requested", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_ENUM ||
+        ippGetInteger(attr, 0) < IPP_ORIENT_PORTRAIT ||
+        ippGetInteger(attr, 0) > IPP_ORIENT_REVERSE_PORTRAIT)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "page-ranges", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetValueTag(attr) != IPP_TAG_RANGE)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "print-quality", IPP_TAG_ZERO)) != NULL)
+  {
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_ENUM ||
+        ippGetInteger(attr, 0) < IPP_QUALITY_DRAFT ||
+        ippGetInteger(attr, 0) > IPP_QUALITY_HIGH)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "printer-resolution", IPP_TAG_ZERO)) != NULL)
+  {
+    supported = ippFindAttribute(client->printer->dev_attrs, "printer-resolution-supported", IPP_TAG_RESOLUTION);
+
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_RESOLUTION ||
+        !supported)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    else
+    {
+      int      count,                  /* Number of supported values */
+               xdpi,                   /* Horizontal resolution for job template attribute */
+               ydpi,                   /* Vertical resolution for job template attribute */
+               sydpi;                  /* Vertical resolution for supported value */
+      ipp_res_t        units,                  /* Units for job template attribute */
+               sunits;                 /* Units for supported value */
+
+      xdpi  = ippGetResolution(attr, 0, &ydpi, &units);
+      count = ippGetCount(supported);
+
+      for (i = 0; i < count; i ++)
+      {
+        if (xdpi == ippGetResolution(supported, i, &sydpi, &sunits) && ydpi == sydpi && units == sunits)
+          break;
+      }
+
+      if (i >= count)
+      {
+       respond_unsupported(client, attr);
+       valid = 0;
+      }
+    }
+  }
+
+  if ((attr = ippFindAttribute(client->request, "sides", IPP_TAG_ZERO)) != NULL)
+  {
+    const char *sides = ippGetString(attr, 0, NULL);
+                                       /* "sides" value... */
+
+    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD)
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+    else if ((supported = ippFindAttribute(client->printer->dev_attrs, "sides-supported", IPP_TAG_KEYWORD)) != NULL)
+    {
+      if (!ippContainsString(supported, sides))
+      {
+       respond_unsupported(client, attr);
+       valid = 0;
+      }
+    }
+    else if (strcmp(sides, "one-sided"))
+    {
+      respond_unsupported(client, attr);
+      valid = 0;
+    }
+  }
+
+  return (valid);
+}
+
+
+/*
+ * End of "$Id$".
+ */
diff --git a/test/ippinfra.man b/test/ippinfra.man
new file mode 100644 (file)
index 0000000..46ac84e
--- /dev/null
@@ -0,0 +1,103 @@
+.\"
+.\" "$Id$"
+.\"
+.\" ippinfra man page for CUPS.
+.\"
+.\" Copyright 2014 by Apple Inc.
+.\"
+.\" These coded instructions, statements, and computer programs are the
+.\" property of Apple Inc. and are protected by Federal copyright
+.\" law.  Distribution and use rights are outlined in the file "LICENSE.txt"
+.\" which should have been included with this file.  If this file is
+.\" file is missing or damaged, see the license at "http://www.cups.org/".
+.\"
+.TH ippinfra 1 "CUPS" "16 September 2014" "Apple Inc."
+.SH NAME
+ippinfra \- a simple ipp infrastructure server
+.SH SYNOPSIS
+.B ippinfra
+[
+.B \-d
+.I spool-directory
+] [
+.B \-h
+] [
+.B \-k
+] [
+.B \-n
+.I hostname
+] [
+.B \-p
+.I port
+] [
+.B \-u
+.I username:password
+] [
+.B \-v[vvv]
+]
+.I service-name
+.SH DESCRIPTION
+.B ippinfra
+is a simple Internet Printing Protocol (IPP) infrastructure server conforming to the IPP Shared Infrastructure Extensions (INFRA) specification. It can be used as a very basic infrastructure server between standard IPP clients and IPP proxies conforming to the INFRA specification.
+.SH OPTIONS
+The following options are recognized by
+.B ippinfra:
+.TP 5
+\fB\-d \fIspool-directory\fR
+Specifies the directory that will hold the print files.
+The default is a directory under the user's current temporary directory.
+.TP 5
+.B \-h
+Shows program help.
+.TP 5
+.B \-k
+Keeps the print documents in the spool directory rather than deleting them.
+.TP 5
+\fB\-n \fIhostname\fR
+Specifies the hostname that is reported by the server.
+The default is the name returned by the
+.BR hostname (1)
+command.
+.TP 5
+\fB\-p \fIport\fR
+Specifies the port number to listen on.
+The default is a user-specific number from 8000 to 8999.
+.TP 5
+\fB\-u \fIusername:password\fR
+Specifies the username and password for the IPP proxy.
+The default is "test:test123".
+.TP 5
+.B \-v[vvv]
+Be (very) verbose when logging activity to the standard output.
+.SH EXIT STATUS
+The
+.B ippinfra
+program returns 1 if it is unable to process the command-line arguments.
+Otherwise
+.B ippinfra
+will run continuously until terminated.
+.SH CONFORMING TO
+The
+.B ippinfra
+program is unique to CUPS and conforms to the IPP Shared Infrastructure Extensions (INFRA) specification as an Infrastructure Printer.
+.SH EXAMPLES
+Run
+.B ippinfra
+with a service name of My Cool Printer:
+.nf
+
+    ippinfra "My Cool Printer"
+.fi
+.LP
+Specify a proxy username of "foo" and password of "bar":
+.nf
+
+    ippinfra \-u foo:bar "My Cool Printer"
+.fi
+.SH SEE ALSO
+PWG Internet Printing Protocol Workgroup (http://www.pwg.org/ipp)
+.SH COPYRIGHT
+Copyright \[co] 2007-2014 by Apple Inc.
+.\"
+.\" End of "$Id$".
+.\"
diff --git a/test/ippproxy.c b/test/ippproxy.c
new file mode 100644 (file)
index 0000000..176e403
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * "$Id$"
+ *
+ * CUPS Cloud Proxy for HP PCL and IPP Everywhere printers.
+ *
+ * Copyright 2014 by Apple Inc.
+ */
+
+#include <cups/cups.h>
+
+
+/*
+ * 'main()' - Main entry for cupsproxy.
+ */
+
+int                                    /* O - Exit status */
+main(int  argc,                                /* I - Number of command-line arguments */
+     char *argv[])                     /* I - Command-line arguments */
+{
+  (void)argc;
+  (void)argv;
+
+  return (0);
+}
+
+
+/*
+ * End of "$Id$".
+ */
diff --git a/test/ippproxy.man b/test/ippproxy.man
new file mode 100644 (file)
index 0000000..1d46c9a
--- /dev/null
@@ -0,0 +1,59 @@
+.\"
+.\" "$Id$"
+.\"
+.\" ippproxy man page for CUPS.
+.\"
+.\" Copyright 2014 by Apple Inc.
+.\"
+.\" These coded instructions, statements, and computer programs are the
+.\" property of Apple Inc. and are protected by Federal copyright
+.\" law.  Distribution and use rights are outlined in the file "LICENSE.txt"
+.\" which should have been included with this file.  If this file is
+.\" file is missing or damaged, see the license at "http://www.cups.org/".
+.\"
+.TH ippproxy 1 "CUPS" "16 September 2014" "Apple Inc."
+.SH NAME
+ippproxy \- a simple ipp proxy client
+.SH SYNOPSIS
+.B ippproxy
+[
+.B \-v[vvv]
+]
+.I infrastructure-printer-uri
+.I local-printer-uri
+.SH DESCRIPTION
+.B ippproxy
+is a simple IPP proxy client conforming to the IPP Shared Infrastructure Extensions (INFRA) specification. It can be used to proxy access to a local IPP printer through an Infrastructure Printer such as
+.BR ippinfra (1).
+.SH OPTIONS
+The following options are recognized by
+.B ippproxy:
+.TP 5
+.B \-v[vvv]
+Be (very) verbose when logging activity to the standard output.
+.SH EXIT STATUS
+The
+.B ippproxy
+program returns 1 if it is unable to process the command-line arguments or connect to either the infrastructure or local printers.
+Otherwise
+.B ippproxy
+will run continuously until terminated.
+.SH CONFORMING TO
+The
+.B ippproxy
+program is unique to CUPS and conforms to the IPP Shared Infrastructure Extensions (INFRA) specification.
+.SH EXAMPLE
+Run
+.B ippproxy
+with an infrastructure URI of "ipps://host.example.com/ipp/print" and a local URI of "ipp://10.0.1.2/ipp/print":
+.nf
+
+    ippproxy ipps://host.example.com/ipp/print ipp://10.0.1.2/ipp/print
+.fi
+.SH SEE ALSO
+PWG Internet Printing Protocol Workgroup (http://www.pwg.org/ipp)
+.SH COPYRIGHT
+Copyright \[co] 2007-2014 by Apple Inc.
+.\"
+.\" End of "$Id$".
+.\"