Netgate Discussion Forum
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Search
    • Register
    • Login

    Howto: AirPrint in pfSense 2.0

    Documentation
    4
    7
    18.7k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • kevevK
      kevev
      last edited by

      Adding AirPrint to pfSense 2.0

      iOS devices(iPhone, iPad, iETC…) can print to the CUPS service previosly set up in pfSense (See my other tutorial http://forum.pfsense.org/index.php/topic,44941.0.html ).

      Steps:

      1.) Install "Avahi" from the "Packages" screen in pfSense.

      2. ) Install dependencies:

      pkg_add -r gutenprint-cups py26-cups

      3.) Go to: https://github.com/tjfontaine/airprint-generate/blob/master/airprint-generate.py and copy the code to a text file "airprint-generate.py".

      a.) Change the first line in the file:

      #!/usr/bin/env python

      to

      #!/usr/local/bin/python

      If you did this in windows you may need to run it through a dos2unix conversion. I used.
                  The one available here. Just do dos2unix.exe airprint-generate.py in the windows console.

      4.) Generate your printer service config for Avahi:

      ./airprint-generate.py (This will output a file for every printer setup in your CUPS server)

      5.) Remove all default service files under: /usr/local/etc/avahi/services/ :

      rm -Rf /usr/local/etc/avahi/services/

      6.) Move your newly created *.service file to /usr/local/etc/avahi/services/ :

      mv *.service /usr/local/etc/avahi/services/

      7.) chmod 755 /usr/local/etc/avahi/services/*.service

      8.) Edit the default avahi-daemon.c template so that /usr/local/etc/avahi/avahi-daemon.conf is overwritten with our custom config whenever the service is started/stopped.

      vi /usr/local/pkg/avahi.inc

      Comment out:

      #host-name={$hostname}
      #domain-name={$domain}
      #browse-domains={$browsedomains}

      9.) Add "ServerAlias *" line without the double quotes to /usr/local/etc/cups/cupsd.conf like so:

      Allow remote access

      ServerAlias *
      Port 631

      10.) Now from here you can just reboot the router. Or you can continue with manually restarting services.

      11.) restart cups:

      /usr/local/etc/rc.d/cupsd stop

      /usr/local/etc/rc.d/cupsd onestart

      12.) Disable, then Enable Avahi from the Services menu.

      a.) Be sure that your local domain is listed in the "Browse domains" field. (mine is localdomain)

      b.) Select WAN in the "Deny Interfaces" field just to be safe.

      c.) uncheck "Enable" and save.

      d.) check "Enable" and save. (But you probably already guessed this part)  :p

      That should do it. Now try and print from your iOS device while it is on the pfSense router LAN.

      I do believe that you will need to have an iOS 4.2 or higher device from what I am reading.

      1 Reply Last reply Reply Quote 0
      • kevevK
        kevev
        last edited by

        Here is the most current version of airprint-generate.py from the time of this post. Just in case the original link goes away.

        Give props to Timothy J Fontaine for the script. I did not create it. https://github.com/tjfontaine   ;D

        
        #!/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 = """
         <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)
        
                        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()</tjfontaine@atxconsulting.com> 
        
        1 Reply Last reply Reply Quote 0
        • E
          eri--
          last edited by

          Can you please link this as a feature request on redmine.pfsense.org under the Pacakges section?

          1 Reply Last reply Reply Quote 0
          • M
            Metu69salemi
            last edited by

            posting such a long listings should be between```
            [ code ][ /code ]-brackets

            1 Reply Last reply Reply Quote 0
            • kevevK
              kevev
              last edited by

              @ermal:

              Can you please link this as a feature request on redmine.pfsense.org under the Pacakges section?

              Feature request created. Go vote for it.  :)

              http://redmine.pfsense.org/issues/2170

              1 Reply Last reply Reply Quote 0
              • kevevK
                kevev
                last edited by

                @Metu69salemi:

                posting such a long listings should be between```
                [ code ][ /code ]-brackets

                Fixed. Thank You.

                1 Reply Last reply Reply Quote 0
                • U
                  ulflun
                  last edited by

                  Thank you for this!

                  Anyone working on this as a package? been 9 months since it was added (http://redmine.pfsense.org/issues/2170)

                  1 Reply Last reply Reply Quote 0
                  • First post
                    Last post
                  Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.