mirror of
https://github.com/nikdoof/cups-avahi-airprint.git
synced 2025-12-11 17:02:21 +00:00
Initial commit
This commit is contained in:
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
# Add repos
|
||||
RUN echo 'deb http://us.archive.ubuntu.com/ubuntu/ xenial multiverse' >> /etc/apt/sources.list.d/multiverse.list && \
|
||||
echo 'deb-src http://us.archive.ubuntu.com/ubuntu/ xenial multiverse' >> /etc/apt/sources.list.d/multiverse.list && \
|
||||
echo 'deb http://us.archive.ubuntu.com/ubuntu/ xenial-updates multiverse' >> /etc/apt/sources.list.d/multiverse.list && \
|
||||
echo 'deb-src http://us.archive.ubuntu.com/ubuntu/ xenial-updates multiverse' >> /etc/apt/sources.list.d/multiverse.list && \
|
||||
echo 'deb http://archive.ubuntu.com/ubuntu/ xenial-security multiverse' >> /etc/apt/sources.list.d/multiverse.list && \
|
||||
echo 'deb-src http://archive.ubuntu.com/ubuntu/ xenial-security multiverse' >> /etc/apt/sources.list.d/multiverse.list
|
||||
|
||||
# Install the packages we need. Avahi will be included
|
||||
RUN apt-get update && apt-get install -y \
|
||||
brother-lpr-drivers-extra brother-cups-wrapper-extra \
|
||||
cups \
|
||||
cups-pdf \
|
||||
inotify-tools \
|
||||
python-cups \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# This will use port 631
|
||||
EXPOSE 631
|
||||
|
||||
# We want a mount for these
|
||||
VOLUME /config
|
||||
VOLUME /services
|
||||
|
||||
# Add scripts
|
||||
ADD root /
|
||||
RUN chmod +x /root/*
|
||||
CMD ["/root/run_cups.sh"]
|
||||
|
||||
# Baked-in config file changes
|
||||
RUN sed -i 's/Listen localhost:631/Listen 0.0.0.0:631/' /etc/cups/cupsd.conf && \
|
||||
sed -i 's/Browsing Off/Browsing On/' /etc/cups/cupsd.conf && \
|
||||
sed -i 's/<Location \/>/<Location \/>\n Allow All/' /etc/cups/cupsd.conf && \
|
||||
sed -i 's/<Location \/admin>/<Location \/admin>\n Allow All\n Require user @SYSTEM/' /etc/cups/cupsd.conf && \
|
||||
sed -i 's/<Location \/admin\/conf>/<Location \/admin\/conf>\n Allow All/' /etc/cups/cupsd.conf && \
|
||||
echo "ServerAlias *" >> /etc/cups/cupsd.conf && \
|
||||
echo "DefaultEncryption Never" >> /etc/cups/cupsd.conf
|
||||
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# QuadPortNick/cups-airprint
|
||||
This Ubuntu-based image runs a CUPS instance that is meant as an AirPrint relay for printers that are already on the network but not AirPrint capable. I'm using it on a Synology NAS because for whatever reason the built in functionality is broken. So here we are...
|
||||
|
||||
The Synology's CUPS is turned off and the local Avahi will be utilized for advertising the printers on the network.
|
||||
|
||||
## Setting it up
|
||||
~~~
|
||||
docker build -t QuadPortNick/cups-airprint .
|
||||
|
||||
mkdir -p /volume1/docker/cups-airprint
|
||||
docker create --name cups-airprint -e CUPSADMIN=cups -e CUPSPASSWORD=cupZZZ! -v /volume1/docker/cups-airprint:/config -v /etc/avahi/services:/services -p 631:631 QuadPortNick/cups-airprint
|
||||
~~~
|
||||
|
||||
Now set auto-start in the Synology DSM interface and start the container. CUPS will be configurable at http://[diskstation]:631 using the CUPSADMIN/CUPSPASSWORD when needed.
|
||||
|
||||
## Notes
|
||||
* CUPS doesn't like printers.conf being mounted directly as it appears to delete/recreate it with changes, so we copy it in on start and then watch for it to change to make a backup of it.
|
||||
* Watching for the printers.conf file changing also triggers generating the Avahi services. Thanks to @thfontaine for the script! <https://github.com/tjfontaine/airprint-generate>
|
||||
|
||||
|
||||
277
root/root/airprint-generate.py
Normal file
277
root/root/airprint-generate.py
Normal file
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2010 Timothy J Fontaine <tjfontaine@atxconsulting.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import cups, os, optparse, re, urlparse
|
||||
import os.path
|
||||
from StringIO import StringIO
|
||||
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.dom import minidom
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import lxml.etree as etree
|
||||
from lxml.etree import Element, ElementTree, tostring
|
||||
except:
|
||||
try:
|
||||
from xml.etree.ElementTree import Element, ElementTree, tostring
|
||||
etree = None
|
||||
except:
|
||||
try:
|
||||
from elementtree import Element, ElementTree, tostring
|
||||
etree = None
|
||||
except:
|
||||
raise 'Failed to find python libxml or elementtree, please install one of those or use python >= 2.5'
|
||||
|
||||
XML_TEMPLATE = """<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
||||
<service-group>
|
||||
<name replace-wildcards="yes"></name>
|
||||
<service>
|
||||
<type>_ipp._tcp</type>
|
||||
<subtype>_universal._sub._ipp._tcp</subtype>
|
||||
<port>631</port>
|
||||
<txt-record>txtvers=1</txt-record>
|
||||
<txt-record>qtotal=1</txt-record>
|
||||
<txt-record>Transparent=T</txt-record>
|
||||
<txt-record>URF=none</txt-record>
|
||||
</service>
|
||||
</service-group>"""
|
||||
|
||||
#TODO XXX FIXME
|
||||
#<txt-record>ty=AirPrint Ricoh Aficio MP 6000</txt-record>
|
||||
#<txt-record>Binary=T</txt-record>
|
||||
#<txt-record>Duplex=T</txt-record>
|
||||
#<txt-record>Copies=T</txt-record>
|
||||
|
||||
|
||||
DOCUMENT_TYPES = {
|
||||
# These content-types will be at the front of the list
|
||||
'application/pdf': True,
|
||||
'application/postscript': True,
|
||||
'application/vnd.cups-raster': True,
|
||||
'application/octet-stream': True,
|
||||
'image/urf': True,
|
||||
'image/png': True,
|
||||
'image/tiff': True,
|
||||
'image/png': True,
|
||||
'image/jpeg': True,
|
||||
'image/gif': True,
|
||||
'text/plain': True,
|
||||
'text/html': True,
|
||||
|
||||
# These content-types will never be reported
|
||||
'image/x-xwindowdump': False,
|
||||
'image/x-xpixmap': False,
|
||||
'image/x-xbitmap': False,
|
||||
'image/x-sun-raster': False,
|
||||
'image/x-sgi-rgb': False,
|
||||
'image/x-portable-pixmap': False,
|
||||
'image/x-portable-graymap': False,
|
||||
'image/x-portable-bitmap': False,
|
||||
'image/x-portable-anymap': False,
|
||||
'application/x-shell': False,
|
||||
'application/x-perl': False,
|
||||
'application/x-csource': False,
|
||||
'application/x-cshell': False,
|
||||
}
|
||||
|
||||
class AirPrintGenerate(object):
|
||||
def __init__(self, host=None, user=None, port=None, verbose=False,
|
||||
directory=None, prefix='AirPrint-', adminurl=False):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.port = port
|
||||
self.verbose = verbose
|
||||
self.directory = directory
|
||||
self.prefix = prefix
|
||||
self.adminurl = adminurl
|
||||
|
||||
if self.user:
|
||||
cups.setUser(self.user)
|
||||
|
||||
def generate(self):
|
||||
if not self.host:
|
||||
conn = cups.Connection()
|
||||
else:
|
||||
if not self.port:
|
||||
self.port = 631
|
||||
conn = cups.Connection(self.host, self.port)
|
||||
|
||||
printers = conn.getPrinters()
|
||||
|
||||
for p, v in printers.items():
|
||||
if v['printer-is-shared']:
|
||||
attrs = conn.getPrinterAttributes(p)
|
||||
uri = urlparse.urlparse(v['printer-uri-supported'])
|
||||
|
||||
tree = ElementTree()
|
||||
tree.parse(StringIO(XML_TEMPLATE.replace('\n', '').replace('\r', '').replace('\t', '')))
|
||||
|
||||
name = tree.find('name')
|
||||
name.text = 'AirPrint %s @ %%h' % (p)
|
||||
|
||||
service = tree.find('service')
|
||||
|
||||
port = service.find('port')
|
||||
port_no = None
|
||||
if hasattr(uri, 'port'):
|
||||
port_no = uri.port
|
||||
if not port_no:
|
||||
port_no = self.port
|
||||
if not port_no:
|
||||
port_no = cups.getPort()
|
||||
port.text = '%d' % port_no
|
||||
|
||||
if hasattr(uri, 'path'):
|
||||
rp = uri.path
|
||||
else:
|
||||
rp = uri[2]
|
||||
|
||||
re_match = re.match(r'^//(.*):(\d+)(/.*)', rp)
|
||||
if re_match:
|
||||
rp = re_match.group(3)
|
||||
|
||||
#Remove leading slashes from path
|
||||
#TODO XXX FIXME I'm worried this will match broken urlparse
|
||||
#results as well (for instance if they don't include a port)
|
||||
#the xml would be malform'd either way
|
||||
rp = re.sub(r'^/+', '', rp)
|
||||
|
||||
path = Element('txt-record')
|
||||
path.text = 'rp=%s' % (rp)
|
||||
service.append(path)
|
||||
|
||||
desc = Element('txt-record')
|
||||
desc.text = 'note=%s' % (v['printer-info'])
|
||||
service.append(desc)
|
||||
|
||||
product = Element('txt-record')
|
||||
product.text = 'product=(GPL Ghostscript)'
|
||||
service.append(product)
|
||||
|
||||
state = Element('txt-record')
|
||||
state.text = 'printer-state=%s' % (v['printer-state'])
|
||||
service.append(state)
|
||||
|
||||
ptype = Element('txt-record')
|
||||
ptype.text = 'printer-type=%s' % (hex(v['printer-type']))
|
||||
service.append(ptype)
|
||||
|
||||
pdl = Element('txt-record')
|
||||
fmts = []
|
||||
defer = []
|
||||
|
||||
for a in attrs['document-format-supported']:
|
||||
if a in DOCUMENT_TYPES:
|
||||
if DOCUMENT_TYPES[a]:
|
||||
fmts.append(a)
|
||||
else:
|
||||
defer.append(a)
|
||||
|
||||
if 'image/urf' not in fmts:
|
||||
sys.stderr.write('image/urf is not in mime types, %s may not be available on ios6 (see https://github.com/tjfontaine/airprint-generate/issues/5)%s' % (p, os.linesep))
|
||||
|
||||
fmts = ','.join(fmts+defer)
|
||||
|
||||
dropped = []
|
||||
|
||||
# TODO XXX FIXME all fields should be checked for 255 limit
|
||||
while len('pdl=%s' % (fmts)) >= 255:
|
||||
(fmts, drop) = fmts.rsplit(',', 1)
|
||||
dropped.append(drop)
|
||||
|
||||
if len(dropped) and self.verbose:
|
||||
sys.stderr.write('%s Losing support for: %s%s' % (p, ','.join(dropped), os.linesep))
|
||||
|
||||
pdl.text = 'pdl=%s' % (fmts)
|
||||
service.append(pdl)
|
||||
|
||||
if self.adminurl:
|
||||
admin = Element('txt-record')
|
||||
admin.text = 'adminurl=%s' % (v['printer-uri-supported'])
|
||||
service.append(admin)
|
||||
|
||||
fname = '%s%s.service' % (self.prefix, p)
|
||||
|
||||
if self.directory:
|
||||
fname = os.path.join(self.directory, fname)
|
||||
|
||||
f = open(fname, 'w')
|
||||
|
||||
if etree:
|
||||
tree.write(f, pretty_print=True, xml_declaration=True, encoding="UTF-8")
|
||||
else:
|
||||
xmlstr = tostring(tree.getroot())
|
||||
doc = parseString(xmlstr)
|
||||
dt= minidom.getDOMImplementation('').createDocumentType('service-group', None, 'avahi-service.dtd')
|
||||
doc.insertBefore(dt, doc.documentElement)
|
||||
doc.writexml(f)
|
||||
f.close()
|
||||
|
||||
if self.verbose:
|
||||
sys.stderr.write('Created: %s%s' % (fname, os.linesep))
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-H', '--host', action="store", type="string",
|
||||
dest='hostname', help='Hostname of CUPS server (optional)', metavar='HOSTNAME')
|
||||
parser.add_option('-P', '--port', action="store", type="int",
|
||||
dest='port', help='Port number of CUPS server', metavar='PORT')
|
||||
parser.add_option('-u', '--user', action="store", type="string",
|
||||
dest='username', help='Username to authenticate with against CUPS',
|
||||
metavar='USER')
|
||||
parser.add_option('-d', '--directory', action="store", type="string",
|
||||
dest='directory', help='Directory to create service files',
|
||||
metavar='DIRECTORY')
|
||||
parser.add_option('-v', '--verbose', action="store_true", dest="verbose",
|
||||
help="Print debugging information to STDERR")
|
||||
parser.add_option('-p', '--prefix', action="store", type="string",
|
||||
dest='prefix', help='Prefix all files with this string', metavar='PREFIX',
|
||||
default='AirPrint-')
|
||||
parser.add_option('-a', '--admin', action="store_true", dest="adminurl",
|
||||
help="Include the printer specified uri as the adminurl")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
# TODO XXX FIXME -- if cups login required, need to add
|
||||
# air=username,password
|
||||
from getpass import getpass
|
||||
cups.setPasswordCB(getpass)
|
||||
|
||||
if options.directory:
|
||||
if not os.path.exists(options.directory):
|
||||
os.mkdir(options.directory)
|
||||
|
||||
apg = AirPrintGenerate(
|
||||
user=options.username,
|
||||
host=options.hostname,
|
||||
port=options.port,
|
||||
verbose=options.verbose,
|
||||
directory=options.directory,
|
||||
prefix=options.prefix,
|
||||
adminurl=options.adminurl,
|
||||
)
|
||||
|
||||
apg.generate()
|
||||
9
root/root/printer-update.sh
Normal file
9
root/root/printer-update.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
inotifywait -m -e close_write,moved_to,create /etc/cups |
|
||||
while read -r directory events filename; do
|
||||
if [ "$filename" = "printers.conf" ]; then
|
||||
rm -rf /services/AirPrint-*.service
|
||||
/root/airprint-generate.py -d /services
|
||||
cp /etc/cups/printers.conf /config/printers.conf
|
||||
fi
|
||||
done
|
||||
22
root/root/run_cups.sh
Normal file
22
root/root/run_cups.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [ $(grep -ci $CUPSADMIN /etc/shadow) -eq 0 ]; then
|
||||
useradd -r -G lpadmin -M $CUPSADMIN
|
||||
fi
|
||||
echo $CUPSADMIN:$CUPSPASSWORD | chpasswd
|
||||
|
||||
mkdir -p /config/ppd
|
||||
mkdir -p /services
|
||||
rm -rf /etc/cups/ppd
|
||||
ln -s /config/ppd /etc/cups
|
||||
if [ ! -f /config/printers.conf ]; then
|
||||
touch /config/printers.conf
|
||||
fi
|
||||
cp /config/printers.conf /etc/cups/printers.conf
|
||||
|
||||
/etc/init.d/dbus start
|
||||
/etc/init.d/avahi-daemon start
|
||||
/root/printer-update.sh &
|
||||
exec /usr/sbin/cupsd -f
|
||||
Reference in New Issue
Block a user