pax_global_header00006660000000000000000000000064131120427400014504gustar00rootroot0000000000000052 comment=7f96e5894dbf9eaa67ce75bbfbabaece4ecba834 cloud-print-connector-1.12/000077500000000000000000000000001311204274000156575ustar00rootroot00000000000000cloud-print-connector-1.12/.travis.yml000066400000000000000000000006231311204274000177710ustar00rootroot00000000000000language: go go: - 1.5 - 1.6 - tip os: - linux - osx before_install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -y libcups2-dev libavahi-client-dev ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install bazaar ; fi cloud-print-connector-1.12/CONTRIBUTING.md000066400000000000000000000025631311204274000201160ustar00rootroot00000000000000Want to contribute? Great! First, read this page (including the small print at the end). ### Before you contribute Before we can use your code, you must sign the [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) (CLA), which you can do online. The CLA is necessary mainly because you own the copyright to your changes, even after your contribution becomes part of our codebase, so we need your permission to use and distribute your code. We also need to be sure of various other things—for instance that you'll tell us if you know that your code infringes on other people's patents. You don't have to sign the CLA until after you've submitted your code for review and a member has approved it, but you must do it before we can put your code into our codebase. Before you start working on a larger contribution, you should get in touch with us first through the issue tracker with your idea so that we can help out and possibly guide you. Coordinating up front makes it much easier to avoid frustration later on. ### Code reviews All submissions, including submissions by project members, require review. We use Github pull requests for this purpose. ### The small print Contributions made by corporations are covered by a different agreement than the one above, the Software Grant and Corporate Contributor License Agreement. cloud-print-connector-1.12/LICENSE000066400000000000000000000027001311204274000166630ustar00rootroot00000000000000Copyright 2015, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cloud-print-connector-1.12/README.md000066400000000000000000000021111311204274000171310ustar00rootroot00000000000000# Google Cloud Print Connector ## Introduction Share printers from your Windows, Linux, FreeBSD or OS X computer with ChromeOS and Android devices, using the Cloud Print Connector. The Connector is a purpose-built system process. It can share hundreds of printers on a powerful server, or one printer on a Raspberry Pi. Lots of help can be found in [the wiki](https://github.com/google/cloud-print-connector/wiki). ## Mailing list Please join the mailing list at https://groups.google.com/forum/#!forum/cloud-print-connector. Anyone can post and view messages. ## Build Status * Linux/OSX: [![Build Status](https://travis-ci.org/google/cloud-print-connector.svg?branch=master)](https://travis-ci.org/google/cloud-print-connector) * FreeBSD: [![Build Status](http://jenkins.mouf.net/job/cloud-print-connector/badge/icon)](http://jenkins.mouf.net/job/cloud-print-connector/) ## License Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd cloud-print-connector-1.12/cdd/000077500000000000000000000000001311204274000164115ustar00rootroot00000000000000cloud-print-connector-1.12/cdd/cdd.go000066400000000000000000000631341311204274000175010ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ // package cdd represents the Cloud Device Description format described here: // https://developers.google.com/cloud-print/docs/cdd // // Not-required fields are marked with the omitempty JSON attribute. package cdd import ( "strconv" "strings" ) type CloudDeviceDescription struct { Version string `json:"version"` Printer *PrinterDescriptionSection `json:"printer"` } type PrinterDescriptionSection struct { SupportedContentType *[]SupportedContentType `json:"supported_content_type,omitempty"` PrintingSpeed *PrintingSpeed `json:"printing_speed,omitempty"` PWGRasterConfig *PWGRasterConfig `json:"pwg_raster_config,omitempty"` InputTrayUnit *[]InputTrayUnit `json:"input_tray_unit,omitempty"` OutputBinUnit *[]OutputBinUnit `json:"output_bin_unit,omitempty"` Marker *[]Marker `json:"marker,omitempty"` Cover *[]Cover `json:"cover,omitempty"` MediaPath *[]MediaPath `json:"media_path,omitempty"` VendorCapability *[]VendorCapability `json:"vendor_capability,omitempty"` Color *Color `json:"color,omitempty"` Duplex *Duplex `json:"duplex,omitempty"` PageOrientation *PageOrientation `json:"page_orientation,omitempty"` Copies *Copies `json:"copies,omitempty"` Margins *Margins `json:"margins,omitempty"` DPI *DPI `json:"dpi,omitempty"` FitToPage *FitToPage `json:"fit_to_page,omitempty"` PageRange *PageRange `json:"page_range,omitempty"` MediaSize *MediaSize `json:"media_size,omitempty"` Collate *Collate `json:"collate,omitempty"` ReverseOrder *ReverseOrder `json:"reverse_order,omitempty"` } // Absorb copies all non-nil fields from the passed-in description. func (a *PrinterDescriptionSection) Absorb(b *PrinterDescriptionSection) { if b.SupportedContentType != nil { a.SupportedContentType = b.SupportedContentType } if b.PrintingSpeed != nil { a.PrintingSpeed = b.PrintingSpeed } if b.PWGRasterConfig != nil { a.PWGRasterConfig = b.PWGRasterConfig } if b.InputTrayUnit != nil { a.InputTrayUnit = b.InputTrayUnit } if b.OutputBinUnit != nil { a.OutputBinUnit = b.OutputBinUnit } if b.Marker != nil { a.Marker = b.Marker } if b.Cover != nil { a.Cover = b.Cover } if b.MediaPath != nil { a.MediaPath = b.MediaPath } if b.VendorCapability != nil { if a.VendorCapability == nil || len(*a.VendorCapability) == 0 { a.VendorCapability = b.VendorCapability } else { // Preserve vendor capabilities that already exist in a. aKeys := make(map[string]struct{}, len(*a.VendorCapability)) for _, v := range *a.VendorCapability { aKeys[v.ID] = struct{}{} } for _, v := range *b.VendorCapability { if _, exists := aKeys[v.ID]; !exists { *a.VendorCapability = append(*a.VendorCapability, v) } } } } if b.Color != nil { a.Color = b.Color } if b.Duplex != nil { a.Duplex = b.Duplex } if b.PageOrientation != nil { a.PageOrientation = b.PageOrientation } if b.Copies != nil { a.Copies = b.Copies } if b.Margins != nil { a.Margins = b.Margins } if b.DPI != nil { a.DPI = b.DPI } if b.FitToPage != nil { a.FitToPage = b.FitToPage } if b.PageRange != nil { a.PageRange = b.PageRange } if b.MediaSize != nil { a.MediaSize = b.MediaSize } if b.Collate != nil { a.Collate = b.Collate } if b.ReverseOrder != nil { a.ReverseOrder = b.ReverseOrder } } type SupportedContentType struct { ContentType string `json:"content_type"` MinVersion string `json:"min_version,omitempty"` MaxVersion string `json:"max_version,omitempty"` } func NewSupportedContentType(contentType string) *[]SupportedContentType { return &[]SupportedContentType{SupportedContentType{ContentType: contentType}} } type PrintingSpeed struct { Option []PrintingSpeedOption `json:"option,omitempty"` } type PrintingSpeedOption struct { SpeedPPM float32 `json:"speed_ppm"` ColorType *[]ColorType `json:"color_type,omitempty"` MediaSizeName *[]MediaSizeName `json:"media_size_name,omitempty"` } type PWGRasterConfig struct { DocumentResolutionSupported *[]PWGRasterConfigResolution `json:"document_resolution_supported,omitempty"` DocumentTypeSupported *[]string `json:"document_type_supported,omitempty"` // enum DocumentSheetBack string `json:"document_sheet_back,omitempty"` // enum; default = "ROTATED" ReverseOrderStreaming *bool `json:"reverse_order_streaming,omitempty"` RotateAllPages *bool `json:"rotate_all_pages,omitempty"` } type PWGRasterConfigResolution struct { CrossFeedDir int32 `json:"cross_feed_dir"` FeedDir int32 `json:"feed_dir"` } type InputTrayUnitType string const ( InputTrayUnitCustom InputTrayUnitType = "CUSTOM" InputTrayUnitInputTray InputTrayUnitType = "INPUT_TRAY" InputTrayUnitBypassTray InputTrayUnitType = "BYPASS_TRAY" InputTrayUnitManualFeedTray InputTrayUnitType = "MANUAL_FEED_TRAY" InputTrayUnitLCT InputTrayUnitType = "LCT" // Large capacity tray. InputTrayUnitEnvelopeTray InputTrayUnitType = "ENVELOPE_TRAY" InputTrayUnitRoll InputTrayUnitType = "ROLL" ) type InputTrayUnit struct { VendorID string `json:"vendor_id"` Type InputTrayUnitType `json:"type"` Index *SchizophrenicInt64 `json:"index,omitempty"` CustomDisplayName string `json:"custom_display_name,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type OutputBinUnitType string const ( OutputBinUnitCustom OutputBinUnitType = "CUSTOM" OutputBinUnitOutputBin OutputBinUnitType = "OUTPUT_BIN" OutputBinUnitMailbox OutputBinUnitType = "MAILBOX" OutputBinUnitStacker OutputBinUnitType = "STACKER" ) type OutputBinUnit struct { VendorID string `json:"vendor_id"` Type OutputBinUnitType `json:"type"` Index *SchizophrenicInt64 `json:"index,omitempty"` CustomDisplayName string `json:"custom_display_name,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type MarkerType string const ( MarkerCustom MarkerType = "CUSTOM" MarkerToner MarkerType = "TONER" MarkerInk MarkerType = "INK" MarkerStaples MarkerType = "STAPLES" ) type MarkerColorType string const ( MarkerColorCustom MarkerColorType = "CUSTOM" MarkerColorBlack MarkerColorType = "BLACK" MarkerColorColor MarkerColorType = "COLOR" MarkerColorCyan MarkerColorType = "CYAN" MarkerColorMagenta MarkerColorType = "MAGENTA" MarkerColorYellow MarkerColorType = "YELLOW" MarkerColorLightCyan MarkerColorType = "LIGHT_CYAN" MarkerColorLightMagenta MarkerColorType = "LIGHT_MAGENTA" MarkerColorGray MarkerColorType = "GRAY" MarkerColorLightGray MarkerColorType = "LIGHT_GRAY" MarkerColorPigmentBlack MarkerColorType = "PIGMENT_BLACK" MarkerColorMatteBlack MarkerColorType = "MATTE_BLACK" MarkerColorPhotoCyan MarkerColorType = "PHOTO_CYAN" MarkerColorPhotoMagenta MarkerColorType = "PHOTO_MAGENTA" MarkerColorPhotoYellow MarkerColorType = "PHOTO_YELLOW" MarkerColorPhotoGray MarkerColorType = "PHOTO_GRAY" MarkerColorRed MarkerColorType = "RED" MarkerColorGreen MarkerColorType = "GREEN" MarkerColorBlue MarkerColorType = "BLUE" ) type MarkerColor struct { Type MarkerColorType `json:"type"` CustomDisplayName string `json:"custom_display_name,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type Marker struct { VendorID string `json:"vendor_id"` Type MarkerType `json:"type"` Color *MarkerColor `json:"color,omitempty"` CustomDisplayName string `json:"custom_display_name,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type CoverType string const ( CoverTypeCustom CoverType = "CUSTOM" CoverTypeDoor CoverType = "DOOR" CoverTypeCover CoverType = "COVER" ) type Cover struct { VendorID string `json:"vendor_id"` Type CoverType `json:"type"` Index *SchizophrenicInt64 `json:"index,omitempty"` CustomDisplayName string `json:"custom_display_name,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type MediaPath struct { VendorID string `json:"vendor_id"` } type VendorCapabilityType string const ( VendorCapabilityRange VendorCapabilityType = "RANGE" VendorCapabilitySelect VendorCapabilityType = "SELECT" VendorCapabilityTypedValue VendorCapabilityType = "TYPED_VALUE" ) type VendorCapability struct { ID string `json:"id"` DisplayName string `json:"display_name,omitempty"` Type VendorCapabilityType `json:"type"` RangeCap *RangeCapability `json:"range_cap,omitempty"` SelectCap *SelectCapability `json:"select_cap,omitempty"` TypedValueCap *TypedValueCapability `json:"typed_value_cap,omitempty"` DisplayNameLocalized *[]LocalizedString `json:"display_name_localized,omitempty"` } type RangeCapabilityValueType string const ( RangeCapabilityValueFloat RangeCapabilityValueType = "FLOAT" RangeCapabilityValueInteger RangeCapabilityValueType = "INTEGER" ) type RangeCapability struct { ValueType RangeCapabilityValueType `json:"value_type"` Default string `json:"default,omitempty"` Min string `json:"min,omitempty"` Max string `json:"max,omitempty"` } type SelectCapability struct { Option []SelectCapabilityOption `json:"option"` } type SelectCapabilityOption struct { Value string `json:"value"` DisplayName string `json:"display_name,omitempty"` IsDefault bool `json:"is_default"` // default = false DisplayNameLocalized *[]LocalizedString `json:"display_name_localized,omitempty"` } type TypedValueCapabilityValueType string const ( TypedValueCapabilityTypeBoolean TypedValueCapabilityValueType = "BOOLEAN" TypedValueCapabilityTypeFloat TypedValueCapabilityValueType = "FLOAT" TypedValueCapabilityTypeInteger TypedValueCapabilityValueType = "INTEGER" TypedValueCapabilityTypeString TypedValueCapabilityValueType = "STRING" ) type TypedValueCapability struct { ValueType TypedValueCapabilityValueType `json:"value_type"` Default string `json:"default,omitempty"` } type Color struct { Option []ColorOption `json:"option"` } type ColorType string const ( ColorTypeStandardColor ColorType = "STANDARD_COLOR" ColorTypeStandardMonochrome ColorType = "STANDARD_MONOCHROME" ColorTypeCustomColor ColorType = "CUSTOM_COLOR" ColorTypeCustomMonochrome ColorType = "CUSTOM_MONOCHROME" ColorTypeAuto ColorType = "AUTO" ) type ColorOption struct { VendorID string `json:"vendor_id"` Type ColorType `json:"type"` CustomDisplayName string `json:"custom_display_name,omitempty"` IsDefault bool `json:"is_default"` // default = false CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type Duplex struct { Option []DuplexOption `json:"option"` } type DuplexType string const ( DuplexNoDuplex DuplexType = "NO_DUPLEX" DuplexLongEdge DuplexType = "LONG_EDGE" DuplexShortEdge DuplexType = "SHORT_EDGE" ) type DuplexOption struct { Type DuplexType `json:"type"` // default = "NO_DUPLEX" IsDefault bool `json:"is_default"` // default = false } type PageOrientation struct { Option []PageOrientationOption `json:"option"` } type PageOrientationType string const ( PageOrientationPortrait PageOrientationType = "PORTRAIT" PageOrientationLandscape PageOrientationType = "LANDSCAPE" PageOrientationAuto PageOrientationType = "AUTO" ) type PageOrientationOption struct { Type PageOrientationType `json:"type"` IsDefault bool `json:"is_default"` // default = false } type Copies struct { Default int32 `json:"default"` Max int32 `json:"max"` } type Margins struct { Option []MarginsOption `json:"option"` } type MarginsType string const ( MarginsBorderless MarginsType = "BORDERLESS" MarginsStandard MarginsType = "STANDARD" MarginsCustom MarginsType = "CUSTOM" ) type MarginsOption struct { Type MarginsType `json:"type"` TopMicrons int32 `json:"top_microns"` RightMicrons int32 `json:"right_microns"` BottomMicrons int32 `json:"bottom_microns"` LeftMicrons int32 `json:"left_microns"` IsDefault bool `json:"is_default"` // default = false } type DPI struct { Option []DPIOption `json:"option"` MinHorizontalDPI int32 `json:"min_horizontal_dpi,omitempty"` MaxHorizontalDPI int32 `json:"max_horizontal_dpi,omitempty"` MinVerticalDPI int32 `json:"min_vertical_dpi,omitempty"` MaxVerticalDPI int32 `json:"max_vertical_dpi,omitempty"` } type DPIOption struct { HorizontalDPI int32 `json:"horizontal_dpi"` VerticalDPI int32 `json:"vertical_dpi"` IsDefault bool `json:"is_default"` // default = false CustomDisplayName string `json:"custom_display_name,omitempty"` VendorID string `json:"vendor_id,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type FitToPage struct { Option []FitToPageOption `json:"option"` } type FitToPageType string const ( FitToPageNoFitting FitToPageType = "NO_FITTING" FitToPageFitToPage FitToPageType = "FIT_TO_PAGE" FitToPageGrowToPage FitToPageType = "GROW_TO_PAGE" FitToPageShrinkToPage FitToPageType = "SHRINK_TO_PAGE" FitToPageFillPage FitToPageType = "FILL_PAGE" ) type FitToPageOption struct { Type FitToPageType `json:"type"` IsDefault bool `json:"is_default"` // default = false } type PageRange struct { Interval []PageRangeInterval `json:"interval"` } type PageRangeInterval struct { Start int32 `json:"start"` End int32 `json:"end,omitempty"` } type MediaSize struct { Option []MediaSizeOption `json:"option"` MaxWidthMicrons int32 `json:"max_width_microns,omitempty"` MaxHeightMicrons int32 `json:"max_height_microns,omitempty"` MinWidthMicrons int32 `json:"min_width_microns,omitempty"` MinHeightMicrons int32 `json:"min_height_microns,omitempty"` } type MediaSizeOption struct { Name MediaSizeName `json:"name"` // default = "CUSTOM" WidthMicrons int32 `json:"width_microns,omitempty"` HeightMicrons int32 `json:"height_microns,omitempty"` IsContinuousFeed bool `json:"is_continuous_feed"` // default = false IsDefault bool `json:"is_default"` // default = false CustomDisplayName string `json:"custom_display_name,omitempty"` VendorID string `json:"vendor_id,omitempty"` CustomDisplayNameLocalized *[]LocalizedString `json:"custom_display_name_localized,omitempty"` } type MediaSizeName string const ( MediaSizeCustom MediaSizeName = "CUSTOM" MediaSizeNAIndex3x5 MediaSizeName = "NA_INDEX_3X5" MediaSizeNAPersonal MediaSizeName = "NA_PERSONAL" MediaSizeNAMonarch MediaSizeName = "NA_MONARCH" MediaSizeNANumber9 MediaSizeName = "NA_NUMBER_9" MediaSizeNAIndex4x6 MediaSizeName = "NA_INDEX_4X6" MediaSizeNANumber10 MediaSizeName = "NA_NUMBER_10" MediaSizeNAA2 MediaSizeName = "NA_A2" MediaSizeNANumber11 MediaSizeName = "NA_NUMBER_11" MediaSizeNANumber12 MediaSizeName = "NA_NUMBER_12" MediaSizeNA5x7 MediaSizeName = "NA_5X7" MediaSizeNAIndex5x8 MediaSizeName = "NA_INDEX_5X8" MediaSizeNANumber14 MediaSizeName = "NA_NUMBER_14" MediaSizeNAInvoice MediaSizeName = "NA_INVOICE" MediaSizeNAIndex4x6Ext MediaSizeName = "NA_INDEX_4X6_EXT" MediaSizeNA6x9 MediaSizeName = "NA_6X9" MediaSizeNAC5 MediaSizeName = "NA_C5" MediaSizeNA7x9 MediaSizeName = "NA_7X9" MediaSizeNAExecutive MediaSizeName = "NA_EXECUTIVE" MediaSizeNAGovtLetter MediaSizeName = "NA_GOVT_LETTER" MediaSizeNAGovtLegal MediaSizeName = "NA_GOVT_LEGAL" MediaSizeNAQuarto MediaSizeName = "NA_QUARTO" MediaSizeNALetter MediaSizeName = "NA_LETTER" MediaSizeNAFanfoldEur MediaSizeName = "NA_FANFOLD_EUR" MediaSizeNALetterPlus MediaSizeName = "NA_LETTER_PLUS" MediaSizeNAFoolscap MediaSizeName = "NA_FOOLSCAP" MediaSizeNALegal MediaSizeName = "NA_LEGAL" MediaSizeNASuperA MediaSizeName = "NA_SUPER_A" MediaSizeNA9x11 MediaSizeName = "NA_9X11" MediaSizeNAArchA MediaSizeName = "NA_ARCH_A" MediaSizeNALetterExtra MediaSizeName = "NA_LETTER_EXTRA" MediaSizeNALegalExtra MediaSizeName = "NA_LEGAL_EXTRA" MediaSizeNA10x11 MediaSizeName = "NA_10X11" MediaSizeNA10x13 MediaSizeName = "NA_10X13" MediaSizeNA10x14 MediaSizeName = "NA_10X14" MediaSizeNA10x15 MediaSizeName = "NA_10X15" MediaSizeNA11x12 MediaSizeName = "NA_11X12" MediaSizeNAEDP MediaSizeName = "NA_EDP" MediaSizeNAFanfoldUS MediaSizeName = "NA_FANFOLD_US" MediaSizeNA11x15 MediaSizeName = "NA_11X15" MediaSizeNALedger MediaSizeName = "NA_LEDGER" MediaSizeNAEurEDP MediaSizeName = "NA_EUR_EDP" MediaSizeNAArchB MediaSizeName = "NA_ARCH_B" MediaSizeNA12x19 MediaSizeName = "NA_12X19" MediaSizeNABPlus MediaSizeName = "NA_B_PLUS" MediaSizeNASuperB MediaSizeName = "NA_SUPER_B" MediaSizeNAC MediaSizeName = "NA_C" MediaSizeNAArchC MediaSizeName = "NA_ARCH_C" MediaSizeNAD MediaSizeName = "NA_D" MediaSizeNAArchD MediaSizeName = "NA_ARCH_D" MediaSizeNAAsmeF MediaSizeName = "NA_ASME_F" MediaSizeNAWideFormat MediaSizeName = "NA_WIDE_FORMAT" MediaSizeNAE MediaSizeName = "NA_E" MediaSizeNAArchE MediaSizeName = "NA_ARCH_E" MediaSizeNAF MediaSizeName = "NA_F" MediaSizeROC16k MediaSizeName = "ROC_16K" MediaSizeROC8k MediaSizeName = "ROC_8K" MediaSizePRC32k MediaSizeName = "PRC_32K" MediaSizePRC1 MediaSizeName = "PRC_1" MediaSizePRC2 MediaSizeName = "PRC_2" MediaSizePRC4 MediaSizeName = "PRC_4" MediaSizePRC5 MediaSizeName = "PRC_5" MediaSizePRC8 MediaSizeName = "PRC_8" MediaSizePRC6 MediaSizeName = "PRC_6" MediaSizePRC3 MediaSizeName = "PRC_3" MediaSizePRC16k MediaSizeName = "PRC_16K" MediaSizePRC7 MediaSizeName = "PRC_7" MediaSizeOMJuuroKuKai MediaSizeName = "OM_JUURO_KU_KAI" MediaSizeOMPaKai MediaSizeName = "OM_PA_KAI" MediaSizeOMDaiPaKai MediaSizeName = "OM_DAI_PA_KAI" MediaSizePRC10 MediaSizeName = "PRC_10" MediaSizeISOA10 MediaSizeName = "ISO_A10" MediaSizeISOA9 MediaSizeName = "ISO_A9" MediaSizeISOA8 MediaSizeName = "ISO_A8" MediaSizeISOA7 MediaSizeName = "ISO_A7" MediaSizeISOA6 MediaSizeName = "ISO_A6" MediaSizeISOA5 MediaSizeName = "ISO_A5" MediaSizeISOA5Extra MediaSizeName = "ISO_A5_EXTRA" MediaSizeISOA4 MediaSizeName = "ISO_A4" MediaSizeISOA4Tab MediaSizeName = "ISO_A4_TAB" MediaSizeISOA4Extra MediaSizeName = "ISO_A4_EXTRA" MediaSizeISOA3 MediaSizeName = "ISO_A3" MediaSizeISOA4x3 MediaSizeName = "ISO_A4X3" MediaSizeISOA4x4 MediaSizeName = "ISO_A4X4" MediaSizeISOA4x5 MediaSizeName = "ISO_A4X5" MediaSizeISOA4x6 MediaSizeName = "ISO_A4X6" MediaSizeISOA4x7 MediaSizeName = "ISO_A4X7" MediaSizeISOA4x8 MediaSizeName = "ISO_A4X8" MediaSizeISOA4x9 MediaSizeName = "ISO_A4X9" MediaSizeISOA3Extra MediaSizeName = "ISO_A3_EXTRA" MediaSizeISOA2 MediaSizeName = "ISO_A2" MediaSizeISOA3x3 MediaSizeName = "ISO_A3X3" MediaSizeISOA3x4 MediaSizeName = "ISO_A3X4" MediaSizeISOA3x5 MediaSizeName = "ISO_A3X5" MediaSizeISOA3x6 MediaSizeName = "ISO_A3X6" MediaSizeISOA3x7 MediaSizeName = "ISO_A3X7" MediaSizeISOA1 MediaSizeName = "ISO_A1" MediaSizeISOA2x3 MediaSizeName = "ISO_A2X3" MediaSizeISOA2x4 MediaSizeName = "ISO_A2X4" MediaSizeISOA2x5 MediaSizeName = "ISO_A2X5" MediaSizeISOA0 MediaSizeName = "ISO_A0" MediaSizeISOA1x3 MediaSizeName = "ISO_A1X3" MediaSizeISOA1x4 MediaSizeName = "ISO_A1X4" MediaSizeISO2A0 MediaSizeName = "ISO_2A0" MediaSizeISOA0x3 MediaSizeName = "ISO_A0X3" MediaSizeISOB10 MediaSizeName = "ISO_B10" MediaSizeISOB9 MediaSizeName = "ISO_B9" MediaSizeISOB8 MediaSizeName = "ISO_B8" MediaSizeISOB7 MediaSizeName = "ISO_B7" MediaSizeISOB6 MediaSizeName = "ISO_B6" MediaSizeISOB6C4 MediaSizeName = "ISO_B6C4" MediaSizeISOB5 MediaSizeName = "ISO_B5" MediaSizeISOB5Extra MediaSizeName = "ISO_B5_EXTRA" MediaSizeISOB4 MediaSizeName = "ISO_B4" MediaSizeISOB3 MediaSizeName = "ISO_B3" MediaSizeISOB2 MediaSizeName = "ISO_B2" MediaSizeISOB1 MediaSizeName = "ISO_B1" MediaSizeISOB0 MediaSizeName = "ISO_B0" MediaSizeISOC10 MediaSizeName = "ISO_C10" MediaSizeISOC9 MediaSizeName = "ISO_C9" MediaSizeISOC8 MediaSizeName = "ISO_C8" MediaSizeISOC7 MediaSizeName = "ISO_C7" MediaSizeISOC7c6 MediaSizeName = "ISO_C7C6" MediaSizeISOC6 MediaSizeName = "ISO_C6" MediaSizeISOC6c5 MediaSizeName = "ISO_C6C5" MediaSizeISOC5 MediaSizeName = "ISO_C5" MediaSizeISOC4 MediaSizeName = "ISO_C4" MediaSizeISOC3 MediaSizeName = "ISO_C3" MediaSizeISOC2 MediaSizeName = "ISO_C2" MediaSizeISOC1 MediaSizeName = "ISO_C1" MediaSizeISOC0 MediaSizeName = "ISO_C0" MediaSizeISODL MediaSizeName = "ISO_DL" MediaSizeISORA2 MediaSizeName = "ISO_RA2" MediaSizeISOSRA2 MediaSizeName = "ISO_SRA2" MediaSizeISORA1 MediaSizeName = "ISO_RA1" MediaSizeISOSRA1 MediaSizeName = "ISO_SRA1" MediaSizeISORA0 MediaSizeName = "ISO_RA0" MediaSizeISOSRA0 MediaSizeName = "ISO_SRA0" MediaSizeJISB10 MediaSizeName = "JIS_B10" MediaSizeJISB9 MediaSizeName = "JIS_B9" MediaSizeJISB8 MediaSizeName = "JIS_B8" MediaSizeJISB7 MediaSizeName = "JIS_B7" MediaSizeJISB6 MediaSizeName = "JIS_B6" MediaSizeJISB5 MediaSizeName = "JIS_B5" MediaSizeJISB4 MediaSizeName = "JIS_B4" MediaSizeJISB3 MediaSizeName = "JIS_B3" MediaSizeJISB2 MediaSizeName = "JIS_B2" MediaSizeJISB1 MediaSizeName = "JIS_B1" MediaSizeJISB0 MediaSizeName = "JIS_B0" MediaSizeJISExec MediaSizeName = "JIS_EXEC" MediaSizeJPNChou4 MediaSizeName = "JPN_CHOU4" MediaSizeJPNHagaki MediaSizeName = "JPN_HAGAKI" MediaSizeJPNYou4 MediaSizeName = "JPN_YOU4" MediaSizeJPNChou2 MediaSizeName = "JPN_CHOU2" MediaSizeJPNChou3 MediaSizeName = "JPN_CHOU3" MediaSizeJPNOufuku MediaSizeName = "JPN_OUFUKU" MediaSizeJPNKahu MediaSizeName = "JPN_KAHU" MediaSizeJPNKaku2 MediaSizeName = "JPN_KAKU2" MediaSizeOMSmallPhoto MediaSizeName = "OM_SMALL_PHOTO" MediaSizeOMItalian MediaSizeName = "OM_ITALIAN" MediaSizeOMPostfix MediaSizeName = "OM_POSTFIX" MediaSizeOMLargePhoto MediaSizeName = "OM_LARGE_PHOTO" MediaSizeOMFolio MediaSizeName = "OM_FOLIO" MediaSizeOMFolioSP MediaSizeName = "OM_FOLIO_SP" MediaSizeOMInvite MediaSizeName = "OM_INVITE" ) type Collate struct { Default bool `json:"default"` // default = true } type ReverseOrder struct { Default bool `json:"default"` // default = false } type LocalizedString struct { Locale string `json:"locale"` // enum; use "EN" Value string `json:"value"` } func NewLocalizedString(value string) *[]LocalizedString { return &[]LocalizedString{LocalizedString{"EN", value}} } // SchizophrenicInt64 is an int64 value that encodes to JSON without quotes, // but decodes with-or-without quotes. GCP requires this for int64 values. type SchizophrenicInt64 int64 func NewSchizophrenicInt64(i uint) *SchizophrenicInt64 { x := SchizophrenicInt64(i) return &x } // MarshalJSON marshals without quotes. func (i SchizophrenicInt64) MarshalJSON() ([]byte, error) { return []byte(i.String()), nil } // UnmarshalJSON unmarshals with or without quotes. func (i *SchizophrenicInt64) UnmarshalJSON(data []byte) error { s := string(data) if len(s) >= 2 && strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { s = s[1 : len(s)-1] } j, err := strconv.ParseInt(s, 10, 64) if err != nil { return err } *i = SchizophrenicInt64(j) return nil } func (i *SchizophrenicInt64) String() string { return strconv.FormatInt(int64(*i), 10) } cloud-print-connector-1.12/cdd/cds.go000066400000000000000000000114131311204274000175110ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package cdd type CloudConnectionStateType string const ( CloudConnectionStateUnknown CloudConnectionStateType = "UNKNOWN" CloudConnectionStateNotConfigured CloudConnectionStateType = "NOT_CONFIGURED" CloudConnectionStateOnline CloudConnectionStateType = "ONLINE" CloudConnectionStateOffline CloudConnectionStateType = "OFFLINE" ) type CloudDeviceState struct { Version string `json:"version"` CloudConnectionState *CloudConnectionStateType `json:"cloud_connection_state,omitempty"` Printer *PrinterStateSection `json:"printer"` } type CloudDeviceStateType string const ( CloudDeviceStateIdle CloudDeviceStateType = "IDLE" CloudDeviceStateProcessing CloudDeviceStateType = "PROCESSING" CloudDeviceStateStopped CloudDeviceStateType = "STOPPED" ) type PrinterStateSection struct { State CloudDeviceStateType `json:"state"` InputTrayState *InputTrayState `json:"input_tray_state,omitempty"` OutputBinState *OutputBinState `json:"output_bin_state,omitempty"` MarkerState *MarkerState `json:"marker_state,omitempty"` CoverState *CoverState `json:"cover_state,omitempty"` MediaPathState *MediaPathState `json:"media_path_state,omitempty"` VendorState *VendorState `json:"vendor_state,omitempty"` } type InputTrayState struct { Item []InputTrayStateItem `json:"item"` } type InputTrayStateType string const ( InputTrayStateOK InputTrayStateType = "OK" InputTrayStateEmpty InputTrayStateType = "EMPTY" InputTrayStateOpen InputTrayStateType = "OPEN" InputTrayStateOff InputTrayStateType = "OFF" InputTrayStateFailure InputTrayStateType = "FAILURE" ) type InputTrayStateItem struct { VendorID string `json:"vendor_id"` State InputTrayStateType `json:"state"` LevelPercent *int32 `json:"level_percent,omitempty"` VendorMessage string `json:"vendor_message,omitempty"` } type OutputBinState struct { Item []OutputBinStateItem `json:"item"` } type OutputBinStateType string const ( OutputBinStateOK OutputBinStateType = "OK" OutputBinStateFull OutputBinStateType = "FULL" OutputBinStateOpen OutputBinStateType = "OPEN" OutputBinStateOff OutputBinStateType = "OFF" OutputBinStateFailure OutputBinStateType = "FAILURE" ) type OutputBinStateItem struct { VendorID string `json:"vendor_id"` State OutputBinStateType `json:"state"` LevelPercent *int32 `json:"level_percent,omitempty"` VendorMessage string `json:"vendor_message,omitempty"` } type MarkerState struct { Item []MarkerStateItem `json:"item"` } type MarkerStateType string const ( MarkerStateOK MarkerStateType = "OK" MarkerStateExhausted MarkerStateType = "EXHAUSTED" MarkerStateRemoved MarkerStateType = "REMOVED" MarkerStateFailure MarkerStateType = "FAILURE" ) type MarkerStateItem struct { VendorID string `json:"vendor_id"` State MarkerStateType `json:"state"` LevelPercent *int32 `json:"level_percent,omitempty"` LevelPages *int32 `json:"level_pages,omitempty"` VendorMessage string `json:"vendor_message,omitempty"` } type CoverState struct { Item []CoverStateItem `json:"item"` } type CoverStateType string const ( CoverStateOK CoverStateType = "OK" CoverStateOpen CoverStateType = "OPEN" CoverStateFailure CoverStateType = "FAILURE" ) type CoverStateItem struct { VendorID string `json:"vendor_id"` State CoverStateType `json:"state"` VendorMessage string `json:"vendor_message,omitempty"` } type MediaPathState struct { Item []MediaPathStateItem `json:"item"` } type MediaPathStateType string const ( MediaPathStateOK MediaPathStateType = "OK" MediaPathStateMediaJam MediaPathStateType = "MEDIA_JAM" MediaPathStateFailure MediaPathStateType = "FAILURE" ) type MediaPathStateItem struct { VendorID string `json:"vendor_id"` State MediaPathStateType `json:"state"` VendorMessage string `json:"vendor_message,omitempty"` } type VendorState struct { Item []VendorStateItem `json:"item"` } type VendorStateType string const ( VendorStateError VendorStateType = "ERROR" VendorStateWarning VendorStateType = "WARNING" VendorStateInfo VendorStateType = "INFO" ) type VendorStateItem struct { State VendorStateType `json:"state"` Description string `json:"description,omitempty"` DescriptionLocalized *[]LocalizedString `json:"description_localized,omitempty"` } cloud-print-connector-1.12/cdd/cjs.go000066400000000000000000000101631311204274000175200ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package cdd type PrintJobState struct { Version string `json:"version"` State JobState `json:"state"` PagesPrinted *int32 `json:"pages_printed,omitempty"` DeliveryAttempts *int32 `json:"delivery_attempts,omitempty"` } type PrintJobStateDiff struct { State *JobState `json:"state,omitempty"` PagesPrinted *int32 `json:"pages_printed,omitempty"` } type JobStateType string const ( JobStateDraft JobStateType = "DRAFT" JobStateHeld JobStateType = "HELD" JobStateQueued JobStateType = "QUEUED" JobStateInProgress JobStateType = "IN_PROGRESS" JobStateStopped JobStateType = "STOPPED" JobStateDone JobStateType = "DONE" JobStateAborted JobStateType = "ABORTED" ) type JobState struct { Type JobStateType `json:"type"` UserActionCause *UserActionCause `json:"user_action_cause,omitempty"` DeviceStateCause *DeviceStateCause `json:"device_state_cause,omitempty"` DeviceActionCause *DeviceActionCause `json:"device_action_cause,omitempty"` ServiceActionCause *ServiceActionCause `json:"service_action_cause,omitempty"` } type UserActionCauseCode string const ( UserActionCauseCanceled UserActionCauseCode = "CANCELLED" // Two L's UserActionCausePaused UserActionCauseCode = "PAUSED" UserActionCauseOther UserActionCauseCode = "OTHER" ) type UserActionCause struct { ActionCode UserActionCauseCode `json:"action_code"` } type DeviceStateCauseCode string const ( DeviceStateCauseInputTray DeviceStateCauseCode = "INPUT_TRAY" DeviceStateCauseMarker DeviceStateCauseCode = "MARKER" DeviceStateCauseMediaPath DeviceStateCauseCode = "MEDIA_PATH" DeviceStateCauseMediaSize DeviceStateCauseCode = "MEDIA_SIZE" DeviceStateCauseMediaType DeviceStateCauseCode = "MEDIA_TYPE" DeviceStateCauseOther DeviceStateCauseCode = "OTHER" ) type DeviceStateCause struct { ErrorCode DeviceStateCauseCode `json:"error_code"` } type DeviceActionCauseCode string const ( DeviceActionCauseDownloadFailure DeviceActionCauseCode = "DOWNLOAD_FAILURE" DeviceActionCauseInvalidTicket DeviceActionCauseCode = "INVALID_TICKET" DeviceActionCausePrintFailure DeviceActionCauseCode = "PRINT_FAILURE" DeviceActionCauseOther DeviceActionCauseCode = "OTHER" ) type DeviceActionCause struct { ErrorCode DeviceActionCauseCode `json:"error_code"` } type ServiceActionCauseCode string const ( ServiceActionCauseCommunication ServiceActionCauseCode = "COMMUNICATION_WITH_DEVICE_ERROR" ServiceActionCauseConversionError ServiceActionCauseCode = "CONVERSION_ERROR" ServiceActionCauseConversionFileTooBig ServiceActionCauseCode = "CONVERSION_FILE_TOO_BIG" ServiceActionCauseConversionType ServiceActionCauseCode = "CONVERSION_UNSUPPORTED_CONTENT_TYPE" ServiceActionCauseDeliveryFailure ServiceActionCauseCode = "DELIVERY_FAILURE" ServiceActionCauseExpiration ServiceActionCauseCode = "EXPIRATION" ServiceActionCauseFetchForbidden ServiceActionCauseCode = "FETCH_DOCUMENT_FORBIDDEN" ServiceActionCauseFetchNotFound ServiceActionCauseCode = "FETCH_DOCUMENT_NOT_FOUND" ServiceActionCauseDriveQuota ServiceActionCauseCode = "GOOGLE_DRIVE_QUOTA" ServiceActionCauseInconsistentJob ServiceActionCauseCode = "INCONSISTENT_JOB" ServiceActionCauseInconsistentPrinter ServiceActionCauseCode = "INCONSISTENT_PRINTER" ServiceActionCausePrinterDeleted ServiceActionCauseCode = "PRINTER_DELETED" ServiceActionCauseRemoteJobNoExist ServiceActionCauseCode = "REMOTE_JOB_NO_LONGER_EXISTS" ServiceActionCauseRemoteJobError ServiceActionCauseCode = "REMOTE_JOB_ERROR" ServiceActionCauseRemoteJobTimeout ServiceActionCauseCode = "REMOTE_JOB_TIMEOUT" ServiceActionCauseRemoteJobAborted ServiceActionCauseCode = "REMOTE_JOB_ABORTED" ServiceActionCauseOther ServiceActionCauseCode = "OTHER" ) type ServiceActionCause struct { ErrorCode ServiceActionCauseCode `json:"error_code"` } cloud-print-connector-1.12/cdd/cjt.go000066400000000000000000000050561311204274000175260ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package cdd type CloudJobTicket struct { Version string `json:"version"` Print PrintTicketSection `json:"print"` } type PrintTicketSection struct { VendorTicketItem []VendorTicketItem `json:"vendor_ticket_item,omitempty"` Color *ColorTicketItem `json:"color,omitempty"` Duplex *DuplexTicketItem `json:"duplex,omitempty"` PageOrientation *PageOrientationTicketItem `json:"page_orientation,omitempty"` Copies *CopiesTicketItem `json:"copies,omitempty"` Margins *MarginsTicketItem `json:"margins,omitempty"` DPI *DPITicketItem `json:"dpi,omitempty"` FitToPage *FitToPageTicketItem `json:"fit_to_page,omitempty"` PageRange *PageRangeTicketItem `json:"page_range,omitempty"` MediaSize *MediaSizeTicketItem `json:"media_size,omitempty"` Collate *CollateTicketItem `json:"collate,omitempty"` ReverseOrder *ReverseOrderTicketItem `json:"reverse_order,omitempty"` } type VendorTicketItem struct { ID string `json:"id"` Value string `json:"value"` } type ColorTicketItem struct { VendorID string `json:"vendor_id"` Type ColorType `json:"type"` } type DuplexTicketItem struct { Type DuplexType `json:"type"` } type PageOrientationTicketItem struct { Type PageOrientationType `json:"type"` } type CopiesTicketItem struct { Copies int32 `json:"copies"` } type MarginsTicketItem struct { TopMicrons int32 `json:"top_microns"` RightMicrons int32 `json:"right_microns"` BottomMicrons int32 `json:"bottom_microns"` LeftMicrons int32 `json:"left_microns"` } type DPITicketItem struct { HorizontalDPI int32 `json:"horizontal_dpi"` VerticalDPI int32 `json:"vertical_dpi"` VendorID string `json:"vendor_id"` } type FitToPageTicketItem struct { Type FitToPageType `json:"type"` } type PageRangeTicketItem struct { Interval []PageRangeInterval `json:"interval"` } type MediaSizeTicketItem struct { WidthMicrons int32 `json:"width_microns"` HeightMicrons int32 `json:"height_microns"` IsContinuousFeed bool `json:"is_continuous_feed"` // default = false VendorID string `json:"vendor_id"` } type CollateTicketItem struct { Collate bool `json:"collate"` } type ReverseOrderTicketItem struct { ReverseOrder bool `json:"reverse_order"` } cloud-print-connector-1.12/cups/000077500000000000000000000000001311204274000166315ustar00rootroot00000000000000cloud-print-connector-1.12/cups/core.go000066400000000000000000000235701311204274000201170ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups /* #cgo freebsd CFLAGS: -I/usr/local/include #cgo freebsd LDFLAGS: -L/usr/local/lib #include "cups.h" */ import "C" import ( "errors" "fmt" "os" "runtime" "strings" "time" "unsafe" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" ) const ( // jobURIFormat is the string format required by the CUPS API // to do things like query the state of a job. jobURIFormat = "/jobs/%d" // filePathMaxLength varies by operating system and file system. // This value should be large enough to be useful and small enough // to work on any platform. filePathMaxLength = 1024 ) // cupsCore handles CUPS API interaction and connection management. type cupsCore struct { host *C.char port C.int encryption C.http_encryption_t connectTimeout C.int // connectionSemaphore limits the quantity of open CUPS connections. connectionSemaphore *lib.Semaphore // connectionPool allows a connection to be reused instead of closed. connectionPool chan *C.http_t hostIsLocal bool } func newCUPSCore(maxConnections uint, connectTimeout time.Duration) (*cupsCore, error) { host := C.cupsServer() port := C.ippPort() encryption := C.cupsEncryption() timeout := C.int(connectTimeout / time.Millisecond) var e string switch encryption { case C.HTTP_ENCRYPTION_ALWAYS: e = "encrypting ALWAYS" case C.HTTP_ENCRYPTION_IF_REQUESTED: e = "encrypting IF REQUESTED" case C.HTTP_ENCRYPTION_NEVER: e = "encrypting NEVER" case C.HTTP_ENCRYPTION_REQUIRED: e = "encryption REQUIRED" default: encryption = C.HTTP_ENCRYPTION_REQUIRED e = "encrypting REQUIRED" } var hostIsLocal bool if h := C.GoString(host); strings.HasPrefix(h, "/") || h == "localhost" { hostIsLocal = true } cs := lib.NewSemaphore(maxConnections) cp := make(chan *C.http_t) cc := &cupsCore{host, port, encryption, timeout, cs, cp, hostIsLocal} log.Infof("Connecting to CUPS server at %s:%d %s", C.GoString(host), int(port), e) // This connection isn't used, just checks that a connection is possible // before returning from the constructor. http, err := cc.connect() if err != nil { return nil, err } cc.disconnect(http) log.Info("Connected to CUPS server successfully") return cc, nil } // printFile prints by calling C.cupsPrintFile2(). // Returns the CUPS job ID, which is 0 (and meaningless) when err // is not nil. func (cc *cupsCore) printFile(user, printername, filename, title *C.char, numOptions C.int, options *C.cups_option_t) (C.int, error) { http, err := cc.connect() if err != nil { return 0, err } defer cc.disconnect(http) C.cupsSetUser(user) jobID := C.cupsPrintFile2(http, printername, filename, title, numOptions, options) if jobID == 0 { return 0, fmt.Errorf("Failed to call cupsPrintFile2() for file %s: %d %s", C.GoString(filename), int(C.cupsLastError()), C.GoString(C.cupsLastErrorString())) } return jobID, nil } // getPrinters gets the current list and state of printers by calling // C.doRequest (IPP_OP_CUPS_GET_PRINTERS). // // The caller is responsible to C.ippDelete the returned *C.ipp_t response. func (cc *cupsCore) getPrinters(attributes **C.char, attrSize C.int) (*C.ipp_t, error) { // ippNewRequest() returns ipp_t pointer which does not need explicit free. request := C.ippNewRequest(C.IPP_OP_CUPS_GET_PRINTERS) C.ippAddStrings(request, C.IPP_TAG_OPERATION, C.IPP_TAG_KEYWORD, C.REQUESTED_ATTRIBUTES, attrSize, nil, attributes) response, err := cc.doRequest(request, []C.ipp_status_t{C.IPP_STATUS_OK, C.IPP_STATUS_ERROR_NOT_FOUND}) if err != nil { err = fmt.Errorf("Failed to call cupsDoRequest() [IPP_OP_CUPS_GET_PRINTERS]: %s", err) return nil, err } return response, nil } // getPPD gets the filename of the PPD for a printer by calling // C.cupsGetPPD3. If the PPD hasn't changed since the time indicated // by modtime, then the returned filename is a nil pointer. // // Note that modtime is a pointer whose value is changed by this // function. // // The caller is responsible to C.free the returned *C.char filename // if the returned filename is not nil. func (cc *cupsCore) getPPD(printername *C.char, modtime *C.time_t) (*C.char, error) { bufsize := C.size_t(filePathMaxLength) buffer := (*C.char)(C.malloc(bufsize)) if buffer == nil { return nil, errors.New("Failed to malloc; out of memory?") } C.memset(unsafe.Pointer(buffer), 0, bufsize) var http *C.http_t if !cc.hostIsLocal { // Don't need a connection or corresponding semaphore if the PPD // is on the local filesystem. // Still need OS thread lock; see else. var err error http, err = cc.connect() if err != nil { return nil, err } defer cc.disconnect(http) } else { // Lock the OS thread so that thread-local storage is available to // cupsLastError() and cupsLastErrorString(). runtime.LockOSThread() defer runtime.UnlockOSThread() } httpStatus := C.cupsGetPPD3(http, printername, modtime, buffer, bufsize) switch httpStatus { case C.HTTP_STATUS_NOT_MODIFIED: // Cache hit. if len(C.GoString(buffer)) > 0 { os.Remove(C.GoString(buffer)) } C.free(unsafe.Pointer(buffer)) return nil, nil case C.HTTP_STATUS_OK: // Cache miss. return buffer, nil default: if len(C.GoString(buffer)) > 0 { os.Remove(C.GoString(buffer)) } C.free(unsafe.Pointer(buffer)) cupsLastError := C.cupsLastError() if cupsLastError != C.IPP_STATUS_OK { return nil, fmt.Errorf("Failed to call cupsGetPPD3(): %d %s", int(cupsLastError), C.GoString(C.cupsLastErrorString())) } return nil, fmt.Errorf("Failed to call cupsGetPPD3(); HTTP status: %d", int(httpStatus)) } } // getJobAttributes gets the requested attributes for a job by calling // C.doRequest (IPP_OP_GET_JOB_ATTRIBUTES). // // The caller is responsible to C.ippDelete the returned *C.ipp_t response. func (cc *cupsCore) getJobAttributes(jobID C.int, attributes **C.char) (*C.ipp_t, error) { uri, err := createJobURI(jobID) if err != nil { return nil, err } defer C.free(unsafe.Pointer(uri)) // ippNewRequest() returns ipp_t pointer does not need explicit free. request := C.ippNewRequest(C.IPP_OP_GET_JOB_ATTRIBUTES) C.ippAddString(request, C.IPP_TAG_OPERATION, C.IPP_TAG_URI, C.JOB_URI_ATTRIBUTE, nil, uri) C.ippAddStrings(request, C.IPP_TAG_OPERATION, C.IPP_TAG_KEYWORD, C.REQUESTED_ATTRIBUTES, C.int(0), nil, attributes) response, err := cc.doRequest(request, []C.ipp_status_t{C.IPP_STATUS_OK}) if err != nil { err = fmt.Errorf("Failed to call cupsDoRequest() [IPP_OP_GET_JOB_ATTRIBUTES]: %s", err) return nil, err } return response, nil } // createJobURI creates a uri string for the job-uri attribute, used to get the // state of a CUPS job. func createJobURI(jobID C.int) (*C.char, error) { length := C.size_t(urlMaxLength) uri := (*C.char)(C.malloc(length)) if uri == nil { return nil, errors.New("Failed to malloc; out of memory?") } resource := C.CString(fmt.Sprintf(jobURIFormat, uint32(jobID))) defer C.free(unsafe.Pointer(resource)) C.httpAssembleURI(C.HTTP_URI_CODING_ALL, uri, C.int(length), C.IPP, nil, C.cupsServer(), C.ippPort(), resource) return uri, nil } // doRequest calls cupsDoRequest(). func (cc *cupsCore) doRequest(request *C.ipp_t, acceptableStatusCodes []C.ipp_status_t) (*C.ipp_t, error) { http, err := cc.connect() if err != nil { return nil, err } defer cc.disconnect(http) if C.ippValidateAttributes(request) != 1 { return nil, fmt.Errorf("Bad IPP request: %s", C.GoString(C.cupsLastErrorString())) } response := C.cupsDoRequest(http, request, C.POST_RESOURCE) if response == nil { return nil, fmt.Errorf("cupsDoRequest failed: %d %s", int(C.cupsLastError()), C.GoString(C.cupsLastErrorString())) } statusCode := C.getIPPRequestStatusCode(response) for _, sc := range acceptableStatusCodes { if statusCode == sc { return response, nil } } return nil, fmt.Errorf("IPP status code %d", int(statusCode)) } // connect calls C.httpConnect2 to create a new, open connection to // the CUPS server specified by environment variables, client.conf, etc. // // connect also acquires the connection semaphore and locks the OS // thread to allow the CUPS API to use thread-local storage cleanly. // // The caller is responsible to close the connection when finished // using cupsCore.disconnect. func (cc *cupsCore) connect() (*C.http_t, error) { cc.connectionSemaphore.Acquire() // Lock the OS thread so that thread-local storage is available to // cupsLastError() and cupsLastErrorString(). runtime.LockOSThread() var http *C.http_t select { case h := <-cc.connectionPool: // Reuse another connection. http = h default: // No connection available for reuse; create a new one. http = C.httpConnect2(cc.host, cc.port, nil, C.AF_UNSPEC, cc.encryption, 1, cc.connectTimeout, nil) if http == nil { defer cc.disconnect(http) return nil, fmt.Errorf("Failed to connect to CUPS server %s:%d because %d %s", C.GoString(cc.host), int(cc.port), int(C.cupsLastError()), C.GoString(C.cupsLastErrorString())) } } return http, nil } // disconnect calls C.httpClose to close an open CUPS connection, then // unlocks the OS thread and the connection semaphore. // // The http argument may be nil; the OS thread and semaphore are still // treated the same as described above. func (cc *cupsCore) disconnect(http *C.http_t) { go func() { select { case cc.connectionPool <- http: // Hand this connection to the next guy who needs it. case <-time.After(time.Second): // Don't wait very long; stale connections are no fun. C.httpClose(http) } }() runtime.UnlockOSThread() cc.connectionSemaphore.Release() } func (cc *cupsCore) connQtyOpen() uint { return cc.connectionSemaphore.Count() } func (cc *cupsCore) connQtyMax() uint { return cc.connectionSemaphore.Size() } cloud-print-connector-1.12/cups/cups.c000066400000000000000000000055661311204274000177630ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ #include "cups.h" const char *JOB_STATE = "job-state", *JOB_MEDIA_SHEETS_COMPLETED = "job-media-sheets-completed", *POST_RESOURCE = "/", *REQUESTED_ATTRIBUTES = "requested-attributes", *JOB_URI_ATTRIBUTE = "job-uri", *IPP = "ipp"; // Allocates a new char**, initializes the values to NULL. char **newArrayOfStrings(int size) { return calloc(size, sizeof(char *)); } // Sets one value in a char**. void setStringArrayValue(char **stringArray, int index, char *value) { stringArray[index] = value; } // Frees a char** and associated non-NULL char*. void freeStringArrayAndStrings(char **stringArray, int size) { int i; for (i = 0; i < size; i++) { if (stringArray[i] != NULL) free(stringArray[i]); } free(stringArray); } // getIPPRequestStatusCode gets the status_code field. // This field is not visible to cgo (don't know why). ipp_status_t getIPPRequestStatusCode(ipp_t *ipp) { return ipp->request.status.status_code; } // getAttributeDateValue gets the ith date value from attr. const ipp_uchar_t *getAttributeDateValue(ipp_attribute_t *attr, int i) { return attr->values[i].date; } // getAttributeIntegerValue gets the ith integer value from attr. int getAttributeIntegerValue(ipp_attribute_t *attr, int i) { return attr->values[i].integer; } // getAttributeStringValue gets the ith string value from attr. const char *getAttributeStringValue(ipp_attribute_t *attr, int i) { return attr->values[i].string.text; } // getAttributeValueRange gets the ith range value from attr. void getAttributeValueRange(ipp_attribute_t *attr, int i, int *lower, int *upper) { *lower = attr->values[i].range.lower; *upper = attr->values[i].range.upper; } // getAttributeValueResolution gets the ith resolution value from attr. // The values returned are always "per inch" not "per centimeter". void getAttributeValueResolution(ipp_attribute_t *attr, int i, int *xres, int *yres) { if (IPP_RES_PER_CM == attr->values[i].resolution.units) { *xres = attr->values[i].resolution.xres * 2.54; *yres = attr->values[i].resolution.yres * 2.54; } else { *xres = attr->values[i].resolution.xres; *yres = attr->values[i].resolution.yres; } } #ifndef _CUPS_API_1_7 // Skip attribute validation with older clients. int ippValidateAttributes(ipp_t *ipp) { return 1; } // Ignore some fields with older clients. // The connector doesn't use addrlist anyways. // Older clients use msec = 30000. http_t *httpConnect2(const char *host, int port, http_addrlist_t *addrlist, int family, http_encryption_t encryption, int blocking, int msec, int *cancel) { return httpConnectEncrypt(host, port, encryption); } #endif cloud-print-connector-1.12/cups/cups.go000066400000000000000000000442461311204274000201440ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups /* #cgo LDFLAGS: -lcups #include "cups.h" */ import "C" import ( "bytes" "encoding/binary" "fmt" "os" "runtime" "strconv" "strings" "sync" "syscall" "time" "unsafe" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" ) const ( // CUPS "URL" length are always less than 40. For example: /job/1234567 urlMaxLength = 100 // Attributes that CUPS uses to describe printers. attrCUPSVersion = "cups-version" attrCopiesDefault = "copies-default" attrCopiesSupported = "copies-supported" attrDeviceURI = "device-uri" attrDocumentFormatSupported = "document-format-supported" attrMarkerLevels = "marker-levels" attrMarkerNames = "marker-names" attrMarkerTypes = "marker-types" attrNumberUpDefault = "number-up-default" attrNumberUpSupported = "number-up-supported" attrOrientationRequestedDefault = "orientation-requested-default" attrOrientationRequestedSupported = "orientation-requested-supported" attrPDFVersionsSupported = "pdf-versions-supported" attrPrintColorModeDefault = "print-color-mode-default" attrPrintColorModeSupported = "print-color-mode-supported" attrPrinterInfo = "printer-info" attrPrinterName = "printer-name" attrPrinterState = "printer-state" attrPrinterStateReasons = "printer-state-reasons" attrPrinterUUID = "printer-uuid" // Attributes that the connector uses to describe print jobs to CUPS. attrCopies = "copies" attrCollate = "collate" attrFalse = "false" attrFitToPage = "fit-to-page" attrMediaBottomMargin = "media-bottom-margin" attrMediaLeftMargin = "media-left-margin" attrMediaRightMargin = "media-right-margin" attrMediaTopMargin = "media-top-margin" attrNormal = "normal" attrNumberUp = "number-up" attrOrientationRequested = "orientation-requested" attrOutputOrder = "outputorder" attrPrintColorMode = "print-color-mode" attrReverse = "reverse" attrTrue = "true" // Attributes that CUPS uses to describe job state. attrJobMediaSheetsCompleted = "job-media-sheets-completed" attrJobState = "job-state" ) var ( requiredPrinterAttributes []string = []string{ attrCopiesDefault, attrCopiesSupported, attrDeviceURI, attrDocumentFormatSupported, attrMarkerLevels, attrMarkerNames, attrMarkerTypes, attrNumberUpDefault, attrNumberUpSupported, attrOrientationRequestedDefault, attrOrientationRequestedSupported, attrPDFVersionsSupported, attrPrintColorModeDefault, attrPrintColorModeSupported, attrPrinterInfo, attrPrinterName, attrPrinterState, attrPrinterStateReasons, attrPrinterUUID, } jobAttributes []string = []string{ attrJobState, attrJobMediaSheetsCompleted, } // cupsPDS represents capabilities that CUPS always provides. cupsPDS = cdd.PrinterDescriptionSection{ FitToPage: &cdd.FitToPage{ Option: []cdd.FitToPageOption{ cdd.FitToPageOption{ Type: cdd.FitToPageNoFitting, IsDefault: true, }, cdd.FitToPageOption{ Type: cdd.FitToPageFitToPage, IsDefault: false, }, }, }, ReverseOrder: &cdd.ReverseOrder{Default: false}, Collate: &cdd.Collate{Default: true}, } ) // Interface between Go and the CUPS API. type CUPS struct { cc *cupsCore pc *ppdCache infoToDisplayName bool prefixJobIDToJobTitle bool displayNamePrefix string printerAttributes []string systemTags map[string]string printerBlacklist map[string]interface{} printerWhitelist map[string]interface{} ignoreRawPrinters bool ignoreClassPrinters bool } func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix string, printerAttributes, vendorPPDOptions []string, maxConnections uint, connectTimeout time.Duration, printerBlacklist, printerWhitelist []string, ignoreRawPrinters bool, ignoreClassPrinters bool) (*CUPS, error) { if err := checkPrinterAttributes(printerAttributes); err != nil { return nil, err } cc, err := newCUPSCore(maxConnections, connectTimeout) if err != nil { return nil, err } pc := newPPDCache(cc, vendorPPDOptions) systemTags, err := getSystemTags() if err != nil { return nil, err } pb := map[string]interface{}{} for _, p := range printerBlacklist { pb[p] = struct{}{} } pw := map[string]interface{}{} for _, p := range printerWhitelist { pw[p] = struct{}{} } c := &CUPS{ cc: cc, pc: pc, infoToDisplayName: infoToDisplayName, displayNamePrefix: displayNamePrefix, printerAttributes: printerAttributes, systemTags: systemTags, printerBlacklist: pb, printerWhitelist: pw, ignoreRawPrinters: ignoreRawPrinters, ignoreClassPrinters: ignoreClassPrinters, } return c, nil } func (c *CUPS) Quit() { c.pc.quit() } // ConnQtyOpen gets the current quantity of open CUPS connections. func (c *CUPS) ConnQtyOpen() uint { return c.cc.connQtyOpen() } // ConnQtyOpen gets the maximum quantity of open CUPS connections. func (c *CUPS) ConnQtyMax() uint { return c.cc.connQtyMax() } // GetPrinters gets all CUPS printers found on the CUPS server. func (c *CUPS) GetPrinters() ([]lib.Printer, error) { pa := C.newArrayOfStrings(C.int(len(c.printerAttributes))) defer C.freeStringArrayAndStrings(pa, C.int(len(c.printerAttributes))) for i, a := range c.printerAttributes { C.setStringArrayValue(pa, C.int(i), C.CString(a)) } response, err := c.cc.getPrinters(pa, C.int(len(c.printerAttributes))) if err != nil { return nil, err } // cupsDoRequest() returns ipp_t pointer which needs explicit free. defer C.ippDelete(response) if C.getIPPRequestStatusCode(response) == C.IPP_STATUS_ERROR_NOT_FOUND { // Normal error when there are no printers. return make([]lib.Printer, 0), nil } printers := c.responseToPrinters(response) printers = lib.FilterBlacklistPrinters(printers, c.printerBlacklist) printers = lib.FilterWhitelistPrinters(printers, c.printerWhitelist) if c.ignoreRawPrinters { printers = filterRawPrinters(printers) } if c.ignoreClassPrinters { printers = filterClassPrinters(printers) } printers = c.addPPDDescriptionToPrinters(printers) printers = addStaticDescriptionToPrinters(printers) printers = c.addSystemTagsToPrinters(printers) return printers, nil } // responseToPrinters converts a C.ipp_t to a slice of lib.Printers. func (c *CUPS) responseToPrinters(response *C.ipp_t) []lib.Printer { printers := make([]lib.Printer, 0, 1) for a := response.attrs; a != nil; a = a.next { if a.group_tag != C.IPP_TAG_PRINTER { continue } attributes := make([]*C.ipp_attribute_t, 0, C.int(len(c.printerAttributes))) for ; a != nil && a.group_tag == C.IPP_TAG_PRINTER; a = a.next { attributes = append(attributes, a) } mAttributes := attributesToMap(attributes) pds, pss, name, defaultDisplayName, uuid, tags := translateAttrs(mAttributes) if !c.infoToDisplayName || defaultDisplayName == "" { defaultDisplayName = name } defaultDisplayName = c.displayNamePrefix + defaultDisplayName p := lib.Printer{ Name: name, DefaultDisplayName: defaultDisplayName, UUID: uuid, State: pss, Description: pds, Tags: tags, } printers = append(printers, p) if a == nil { break } } return printers } // filterClassPrinters removes class printers from the slice. func filterClassPrinters(printers []lib.Printer) []lib.Printer { result := make([]lib.Printer, 0, len(printers)) for i := range printers { if !lib.PrinterIsClass(printers[i]) { result = append(result, printers[i]) } } return result } // filterRawPrinters removes raw printers from the slice. func filterRawPrinters(printers []lib.Printer) []lib.Printer { result := make([]lib.Printer, 0, len(printers)) for i := range printers { if !lib.PrinterIsRaw(printers[i]) { result = append(result, printers[i]) } } return result } // addPPDDescriptionToPrinters fetches description, PPD hash, manufacturer, model // for argument printers, concurrently. These are the fields derived from PPD. func (c *CUPS) addPPDDescriptionToPrinters(printers []lib.Printer) []lib.Printer { var wg sync.WaitGroup ch := make(chan *lib.Printer, len(printers)) for i := range printers { wg.Add(1) go func(p *lib.Printer) { if description, manufacturer, model, duplexMap, err := c.pc.getPPDCacheEntry(p.Name); err == nil { p.Description.Absorb(description) p.Manufacturer = manufacturer p.Model = model if duplexMap != nil { p.DuplexMap = duplexMap } ch <- p } else { log.ErrorPrinter(p.Name, err) } wg.Done() }(&printers[i]) } wg.Wait() close(ch) result := make([]lib.Printer, 0, len(ch)) for printer := range ch { result = append(result, *printer) } return result } // addStaticDescriptionToPrinters adds information that is true for all // printers to printers. func addStaticDescriptionToPrinters(printers []lib.Printer) []lib.Printer { for i := range printers { printers[i].GCPVersion = lib.GCPAPIVersion printers[i].SetupURL = lib.ConnectorHomeURL printers[i].SupportURL = lib.ConnectorHomeURL printers[i].UpdateURL = lib.ConnectorHomeURL printers[i].ConnectorVersion = lib.ShortName printers[i].Description.Absorb(&cupsPDS) } return printers } func (c *CUPS) addSystemTagsToPrinters(printers []lib.Printer) []lib.Printer { for i := range printers { for k, v := range c.systemTags { printers[i].Tags[k] = v } } return printers } // uname returns strings similar to the Unix uname command: // sysname, nodename, release, version, machine func uname() (string, string, string, string, string, error) { var name C.struct_utsname _, err := C.uname(&name) if err != nil { var errno syscall.Errno = err.(syscall.Errno) return "", "", "", "", "", fmt.Errorf("Failed to call uname: %s", errno) } return C.GoString(&name.sysname[0]), C.GoString(&name.nodename[0]), C.GoString(&name.release[0]), C.GoString(&name.version[0]), C.GoString(&name.machine[0]), nil } func getSystemTags() (map[string]string, error) { tags := make(map[string]string) tags["connector-version"] = lib.BuildDate hostname, err := os.Hostname() if err == nil { tags["system-hostname"] = hostname } tags["system-arch"] = runtime.GOARCH tags["system-golang-version"] = runtime.Version() sysname, nodename, release, version, machine, err := uname() if err != nil { return nil, fmt.Errorf("CUPS failed to call uname while initializing: %s", err) } tags["system-uname-sysname"] = sysname tags["system-uname-nodename"] = nodename tags["system-uname-release"] = release tags["system-uname-version"] = version tags["system-uname-machine"] = machine tags["connector-cups-client-version"] = fmt.Sprintf("%d.%d.%d", C.CUPS_VERSION_MAJOR, C.CUPS_VERSION_MINOR, C.CUPS_VERSION_PATCH) return tags, nil } // RemoveCachedPPD removes a printer's PPD from the cache. func (c *CUPS) RemoveCachedPPD(printername string) { c.pc.removePPD(printername) } // GetJobState gets the current state of the job indicated by jobID. func (c *CUPS) GetJobState(_ string, jobID uint32) (*cdd.PrintJobStateDiff, error) { ja := C.newArrayOfStrings(C.int(len(jobAttributes))) defer C.freeStringArrayAndStrings(ja, C.int(len(jobAttributes))) for i, attribute := range jobAttributes { C.setStringArrayValue(ja, C.int(i), C.CString(attribute)) } response, err := c.cc.getJobAttributes(C.int(jobID), ja) if err != nil { return nil, err } // cupsDoRequest() returned ipp_t pointer needs explicit free. defer C.ippDelete(response) s := C.ippFindAttribute(response, C.JOB_STATE, C.IPP_TAG_ENUM) state := int32(C.getAttributeIntegerValue(s, C.int(0))) return convertJobState(state), nil } // convertJobState converts CUPS job state to cdd.PrintJobStateDiff. func convertJobState(cupsState int32) *cdd.PrintJobStateDiff { var state cdd.PrintJobStateDiff switch cupsState { case 3, 4, 5: // PENDING, HELD, PROCESSING state.State = &cdd.JobState{Type: cdd.JobStateInProgress} case 6: // STOPPED state.State = &cdd.JobState{ Type: cdd.JobStateStopped, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseOther}, } case 7: // CANCELED state.State = &cdd.JobState{ Type: cdd.JobStateAborted, UserActionCause: &cdd.UserActionCause{ActionCode: cdd.UserActionCauseCanceled}, } case 8: // ABORTED state.State = &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCausePrintFailure}, } case 9: // COMPLETED state.State = &cdd.JobState{Type: cdd.JobStateDone} } return &state } // Print sends a new print job to the specified printer. The job ID // is returned. func (c *CUPS) Print(printer *lib.Printer, filename, title, user, gcpJobID string, ticket *cdd.CloudJobTicket) (uint32, error) { printer.NativeJobSemaphore.Acquire() defer printer.NativeJobSemaphore.Release() pn := C.CString(printer.Name) defer C.free(unsafe.Pointer(pn)) fn := C.CString(filename) defer C.free(unsafe.Pointer(fn)) var t *C.char if c.prefixJobIDToJobTitle { title = fmt.Sprintf("gcp:%s %s", gcpJobID, title) } if len(title) > 255 { t = C.CString(title[:255]) } else { t = C.CString(title) } defer C.free(unsafe.Pointer(t)) options, err := translateTicket(printer, ticket) if err != nil { return 0, err } numOptions := C.int(0) var o *C.cups_option_t = nil for key, value := range options { k, v := C.CString(key), C.CString(value) numOptions = C.cupsAddOption(k, v, numOptions, &o) C.free(unsafe.Pointer(k)) C.free(unsafe.Pointer(v)) } defer C.cupsFreeOptions(numOptions, o) u := C.CString(user) defer C.free(unsafe.Pointer(u)) cupsJobID, err := c.cc.printFile(u, pn, fn, t, numOptions, o) if err != nil { return 0, err } return uint32(cupsJobID), nil } // convertIPPDateToTime converts an RFC 2579 date to a time.Time object. func convertIPPDateToTime(date *C.ipp_uchar_t) time.Time { r := bytes.NewReader(C.GoBytes(unsafe.Pointer(date), 11)) var year uint16 var month, day, hour, min, sec, dsec uint8 binary.Read(r, binary.BigEndian, &year) binary.Read(r, binary.BigEndian, &month) binary.Read(r, binary.BigEndian, &day) binary.Read(r, binary.BigEndian, &hour) binary.Read(r, binary.BigEndian, &min) binary.Read(r, binary.BigEndian, &sec) binary.Read(r, binary.BigEndian, &dsec) var utcDirection, utcHour, utcMin uint8 binary.Read(r, binary.BigEndian, &utcDirection) binary.Read(r, binary.BigEndian, &utcHour) binary.Read(r, binary.BigEndian, &utcMin) var utcOffset time.Duration utcOffset += time.Duration(utcHour) * time.Hour utcOffset += time.Duration(utcMin) * time.Minute var loc *time.Location if utcDirection == '-' { loc = time.FixedZone("", -int(utcOffset.Seconds())) } else { loc = time.FixedZone("", int(utcOffset.Seconds())) } nsec := int(dsec) * 100 * int(time.Millisecond) return time.Date(int(year), time.Month(month), int(day), int(hour), int(min), int(sec), nsec, loc) } // attributesToMap converts a slice of C.ipp_attribute_t to a // string:string "tag" map. func attributesToMap(attributes []*C.ipp_attribute_t) map[string][]string { m := make(map[string][]string) for _, a := range attributes { key := C.GoString(a.name) count := int(a.num_values) values := make([]string, count) switch a.value_tag { case C.IPP_TAG_NOVALUE, C.IPP_TAG_NOTSETTABLE: // No value means no value. case C.IPP_TAG_INTEGER, C.IPP_TAG_ENUM: for i := 0; i < count; i++ { values[i] = strconv.FormatInt(int64(C.getAttributeIntegerValue(a, C.int(i))), 10) } case C.IPP_TAG_BOOLEAN: for i := 0; i < count; i++ { if int(C.getAttributeIntegerValue(a, C.int(i))) == 0 { values[i] = "false" } else { values[i] = "true" } } case C.IPP_TAG_TEXTLANG, C.IPP_TAG_NAMELANG, C.IPP_TAG_TEXT, C.IPP_TAG_NAME, C.IPP_TAG_KEYWORD, C.IPP_TAG_URI, C.IPP_TAG_URISCHEME, C.IPP_TAG_CHARSET, C.IPP_TAG_LANGUAGE, C.IPP_TAG_MIMETYPE: for i := 0; i < count; i++ { values[i] = C.GoString(C.getAttributeStringValue(a, C.int(i))) } case C.IPP_TAG_DATE: for i := 0; i < count; i++ { date := C.getAttributeDateValue(a, C.int(i)) t := convertIPPDateToTime(date) values[i] = strconv.FormatInt(t.Unix(), 10) } case C.IPP_TAG_RESOLUTION: for i := 0; i < count; i++ { xres, yres := C.int(0), C.int(0) C.getAttributeValueResolution(a, C.int(i), &xres, &yres) values[i] = fmt.Sprintf("%dx%dppi", int(xres), int(yres)) } case C.IPP_TAG_RANGE: for i := 0; i < count; i++ { upper, lower := C.int(0), C.int(0) C.getAttributeValueRange(a, C.int(i), &lower, &upper) values[i] = fmt.Sprintf("%d~%d", int(lower), int(upper)) } default: if count > 0 { values = []string{"unknown or unsupported type"} } } if len(values) == 1 && (values[0] == "none" || len(values[0]) == 0) { values = []string{} } m[key] = values } return m } func contains(haystack []string, needle string) bool { for _, h := range haystack { if needle == h { return true } } return false } func findMissing(haystack, needles []string) []string { missing := make([]string, 0) for _, n := range needles { if !contains(haystack, n) { missing = append(missing, n) } } return missing } func checkPrinterAttributes(printerAttributes []string) error { if !contains(printerAttributes, "all") { missing := findMissing(printerAttributes, requiredPrinterAttributes) if len(missing) > 0 { return fmt.Errorf("Printer attributes missing from config file: %s", strings.Join(missing, ",")) } } return nil } // The following functions are not relevant to CUPS printing, but are required by the NativePrintSystem interface. func (c *CUPS) ReleaseJob(printerName string, jobID uint32) error { return nil } cloud-print-connector-1.12/cups/cups.h000066400000000000000000000042751311204274000177640ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ // Since CUPS 1.6, the ipp struct properties have been private, with accessor // functions added (STR #3928). This line makes the properties not private, so // that the connector can be compiled against pre and post 1.6 libraries. #define _IPP_PRIVATE_STRUCTURES 1 #include #include #include // size_t #include // free, calloc, malloc #include // AF_UNSPEC #include // uname #include // time_t extern const char *JOB_STATE, *JOB_MEDIA_SHEETS_COMPLETED, *POST_RESOURCE, *REQUESTED_ATTRIBUTES, *JOB_URI_ATTRIBUTE, *IPP; char **newArrayOfStrings(int size); void setStringArrayValue(char **stringArray, int index, char *value); void freeStringArrayAndStrings(char **stringArray, int size); ipp_status_t getIPPRequestStatusCode(ipp_t *ipp); const ipp_uchar_t *getAttributeDateValue(ipp_attribute_t *attr, int i); int getAttributeIntegerValue(ipp_attribute_t *attr, int i); const char *getAttributeStringValue(ipp_attribute_t *attr, int i); void getAttributeValueRange(ipp_attribute_t *attr, int i, int *lower, int *upper); void getAttributeValueResolution(ipp_attribute_t *attr, int i, int *xres, int *yres); #ifndef _CUPS_API_1_7 int ippValidateAttributes(ipp_t *ipp); http_t *httpConnect2(const char *host, int port, http_addrlist_t *addrlist, int family, http_encryption_t encryption, int blocking, int msec, int *cancel); # define HTTP_ENCRYPTION_IF_REQUESTED HTTP_ENCRYPT_IF_REQUESTED # define HTTP_ENCRYPTION_NEVER HTTP_ENCRYPT_NEVER # define HTTP_ENCRYPTION_REQUIRED HTTP_ENCRYPT_REQUIRED # define HTTP_ENCRYPTION_ALWAYS HTTP_ENCRYPT_ALWAYS # define HTTP_STATUS_OK HTTP_OK # define HTTP_STATUS_NOT_MODIFIED HTTP_NOT_MODIFIED # define IPP_OP_CUPS_GET_PRINTERS CUPS_GET_PRINTERS # define IPP_OP_GET_JOB_ATTRIBUTES IPP_GET_JOB_ATTRIBUTES # define IPP_STATUS_OK IPP_OK # define IPP_STATUS_ERROR_NOT_FOUND IPP_NOT_FOUND #endif cloud-print-connector-1.12/cups/ppdcache.go000066400000000000000000000120611311204274000207270ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups /* #include "cups.h" */ import "C" import ( "bytes" "errors" "os" "sync" "unsafe" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" ) // This isn't really a cache, but an interface to CUPS' quirky PPD interface. // The connector needs to know when a PPD changes, but the CUPS API can only: // (1) fetch a PPD to a file // (2) indicate whether a PPD file is up-to-date. // So, this "cache": // (1) maintains temporary file copies of PPDs for each printer // (2) updates those PPD files as necessary type ppdCache struct { cc *cupsCore vendorPPDOptions []string cache map[string]*ppdCacheEntry cacheMutex sync.RWMutex } func newPPDCache(cc *cupsCore, vendorPPDOptions []string) *ppdCache { cache := make(map[string]*ppdCacheEntry) pc := ppdCache{ cc: cc, vendorPPDOptions: vendorPPDOptions, cache: cache, } return &pc } func (pc *ppdCache) quit() { pc.cacheMutex.Lock() defer pc.cacheMutex.Unlock() for printername, pce := range pc.cache { pce.free() delete(pc.cache, printername) } } // removePPD removes a cache entry from the cache. func (pc *ppdCache) removePPD(printername string) { pc.cacheMutex.Lock() defer pc.cacheMutex.Unlock() if pce, exists := pc.cache[printername]; exists { pce.free() delete(pc.cache, printername) } } func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap, error) { pc.cacheMutex.RLock() pce, exists := pc.cache[printername] pc.cacheMutex.RUnlock() if !exists { pce, err := createPPDCacheEntry(printername) if err != nil { return nil, "", "", nil, err } if err = pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { pce.free() return nil, "", "", nil, err } pc.cacheMutex.Lock() defer pc.cacheMutex.Unlock() if firstPCE, exists := pc.cache[printername]; exists { // Two entries were created at the same time. Remove the older one. delete(pc.cache, printername) go firstPCE.free() } pc.cache[printername] = pce description, manufacturer, model, duplexMap := pce.getFields() return &description, manufacturer, model, duplexMap, nil } else { if err := pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { delete(pc.cache, printername) pce.free() return nil, "", "", nil, err } description, manufacturer, model, duplexMap := pce.getFields() return &description, manufacturer, model, duplexMap, nil } } // Holds persistent data needed for calling C.cupsGetPPD3. type ppdCacheEntry struct { printername *C.char modtime C.time_t description cdd.PrinterDescriptionSection manufacturer string model string duplexMap lib.DuplexVendorMap mutex sync.Mutex } // createPPDCacheEntry creates an instance of ppdCache with the name field set, // all else empty. The caller must free the name and buffer fields with // ppdCacheEntry.free() func createPPDCacheEntry(name string) (*ppdCacheEntry, error) { pce := &ppdCacheEntry{ printername: C.CString(name), modtime: C.time_t(0), } return pce, nil } // getFields gets externally-interesting fields from this ppdCacheEntry under // a lock. The description is passed as a value (copy), to protect the cached copy. func (pce *ppdCacheEntry) getFields() (cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap) { pce.mutex.Lock() defer pce.mutex.Unlock() return pce.description, pce.manufacturer, pce.model, pce.duplexMap } // free frees the memory that stores the name and buffer fields, and deletes // the file named by the buffer field. If the file doesn't exist, no error is // returned. func (pce *ppdCacheEntry) free() { pce.mutex.Lock() defer pce.mutex.Unlock() C.free(unsafe.Pointer(pce.printername)) } // refresh calls cupsGetPPD3() to refresh this PPD information, in // case CUPS has a new PPD for the printer. func (pce *ppdCacheEntry) refresh(cc *cupsCore, vendorPPDOptions []string) error { pce.mutex.Lock() defer pce.mutex.Unlock() ppdFilename, err := cc.getPPD(pce.printername, &pce.modtime) if err != nil { return err } if ppdFilename == nil { // Cache hit. return nil } // (else) Cache miss. defer C.free(unsafe.Pointer(ppdFilename)) defer os.Remove(C.GoString(ppdFilename)) // Read from CUPS temporary file. r, err := os.Open(C.GoString(ppdFilename)) if err != nil { return err } defer r.Close() // Write to a buffer string for translation. var w bytes.Buffer if _, err := w.ReadFrom(r); err != nil { return err } description, manufacturer, model, duplexMap := translatePPD(w.String(), vendorPPDOptions) if description == nil || manufacturer == "" || model == "" { return errors.New("Failed to parse PPD") } pce.description = *description pce.manufacturer = manufacturer pce.model = model pce.duplexMap = duplexMap return nil } cloud-print-connector-1.12/cups/translate-attrs.go000066400000000000000000000352611311204274000223170ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "fmt" "reflect" "sort" "strconv" "strings" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/log" ) // translateAttrs extracts a PrinterDescriptionSection, PrinterStateSection, name, default diplay name, UUID, and tags from maps of tags (CUPS attributes) func translateAttrs(printerTags map[string][]string) (*cdd.PrinterDescriptionSection, *cdd.PrinterStateSection, string, string, string, map[string]string) { var name, info string if n, ok := printerTags[attrPrinterName]; ok && len(n) > 0 { name = n[0] } if i, ok := printerTags[attrPrinterInfo]; ok && len(i) > 0 { info = i[0] } uuid := getUUID(printerTags) var desc = cdd.PrinterDescriptionSection{VendorCapability: &[]cdd.VendorCapability{}} var state cdd.PrinterStateSection desc.SupportedContentType = convertSupportedContentType(printerTags) desc.Marker, state.MarkerState = convertMarkers(printerTags) desc.PageOrientation = convertPageOrientation(printerTags) desc.Copies = convertCopies(printerTags) desc.Color = convertColorAttrs(printerTags) if vc := convertPagesPerSheet(printerTags); vc != nil { *desc.VendorCapability = append(*desc.VendorCapability, *vc) } state.State = getState(printerTags) state.VendorState = getVendorState(printerTags) tags := make(map[string]string, len(printerTags)) for k, v := range printerTags { tags[k] = strings.Join(v, ",") } return &desc, &state, name, info, uuid, tags } func getUUID(printerTags map[string][]string) string { var uuid string if u, ok := printerTags[attrPrinterUUID]; ok { uuid = u[0] uuid = strings.TrimPrefix(uuid, "urn:") uuid = strings.TrimPrefix(uuid, "uuid:") } else if u, ok := printerTags[attrPrinterName]; ok { // CUPS < 1.5 doesn't send a printer-uuid attribute. uuid = u[0] } return uuid } func getState(printerTags map[string][]string) cdd.CloudDeviceStateType { // Some CUPS backends (e.g. usb-darwin) add offline-report // to printer-state-reasons when the printer is offline/disconnected reasons, exists := printerTags[attrPrinterStateReasons] if exists && len(reasons) > 0 { for _, reason := range reasons { if reason == "offline-report" { return cdd.CloudDeviceStateStopped } } } if s, ok := printerTags[attrPrinterState]; ok { switch s[0] { case "3": return cdd.CloudDeviceStateIdle case "4": return cdd.CloudDeviceStateProcessing case "5": return cdd.CloudDeviceStateStopped default: return cdd.CloudDeviceStateIdle } } return cdd.CloudDeviceStateIdle } func getVendorState(printerTags map[string][]string) *cdd.VendorState { reasons, exists := printerTags[attrPrinterStateReasons] if !exists || len(reasons) < 1 { return nil } sort.Strings(reasons) vendorState := &cdd.VendorState{Item: make([]cdd.VendorStateItem, len(reasons))} for i, reason := range reasons { vs := cdd.VendorStateItem{DescriptionLocalized: cdd.NewLocalizedString(reason)} if strings.HasSuffix(reason, "-error") { vs.State = cdd.VendorStateError } else if strings.HasSuffix(reason, "-warning") { vs.State = cdd.VendorStateWarning } else if strings.HasSuffix(reason, "-report") { vs.State = cdd.VendorStateInfo } else { vs.State = cdd.VendorStateError } vendorState.Item[i] = vs } return vendorState } func getAdobeVersionRange(pdfVersionsSupported []string) (string, string) { var min, max string for _, pdfVersion := range pdfVersionsSupported { var v string if _, err := fmt.Sscanf(pdfVersion, "adobe-%s", &v); err != nil || len(v) < 1 { continue } if min == "" || v < min { min = v } if max == "" || v > max { max = v } } return min, max } // CUPS can accept some unsafe types, like application/x-perl, so limit to these. var mimeTypesAllowed = map[string]struct{}{ "application/pdf": struct{}{}, "application/postscript": struct{}{}, "image/pwg-raster": struct{}{}, "image/gif": struct{}{}, "image/jp2": struct{}{}, "image/jpeg": struct{}{}, "image/png": struct{}{}, "image/tiff": struct{}{}, "text/plain": struct{}{}, "text/rtf": struct{}{}, } func convertSupportedContentType(printerTags map[string][]string) *[]cdd.SupportedContentType { mimeTypes, exists := printerTags[attrDocumentFormatSupported] if !exists || len(mimeTypes) < 1 { return nil } pdfMin, pdfMax := getAdobeVersionRange(printerTags[attrPDFVersionsSupported]) pdf := cdd.SupportedContentType{ContentType: "application/pdf"} if pdfMin != "" && pdfMax != "" { pdf.MinVersion = pdfMin pdf.MaxVersion = pdfMax } sct := []cdd.SupportedContentType{pdf, cdd.SupportedContentType{ContentType: "application/postscript"}} // Preferred order: // 1) PDF (vector and small). // 2) Postscript (vector). // 3) Any CUPS-supported formats (don't need conversion in client or cloud). // 4) PWG-Raster (all clients support but it's huge). for _, mimeType := range mimeTypes { if mimeType == "application/pdf" || // Already added. mimeType == "application/postscript" || // Already added. mimeType == "image/pwg-raster" { // Added last, if at all. continue } if _, exists = mimeTypesAllowed[mimeType]; !exists { continue } sct = append(sct, cdd.SupportedContentType{ContentType: mimeType}) } /* TODO: Consider adding pwg-raster with config option to enable/disable. - All clients authored by Google do not create PWG Raster jobs. - cups-filters only supports pwg-raster input in recent versions. https://www.cups.org/pipermail/cups/2015-July/026927.html */ return &sct } var cupsMarkerNameToGCP map[string]cdd.MarkerColorType = map[string]cdd.MarkerColorType{ "black": cdd.MarkerColorBlack, "color": cdd.MarkerColorColor, "cyan": cdd.MarkerColorCyan, "magenta": cdd.MarkerColorMagenta, "yellow": cdd.MarkerColorYellow, "lightcyan": cdd.MarkerColorLightCyan, "lightmagenta": cdd.MarkerColorLightMagenta, "gray": cdd.MarkerColorGray, "lightgray": cdd.MarkerColorLightGray, "pigmentblack": cdd.MarkerColorPigmentBlack, "matteblack": cdd.MarkerColorMatteBlack, "photocyan": cdd.MarkerColorPhotoCyan, "photomagenta": cdd.MarkerColorPhotoMagenta, "photoyellow": cdd.MarkerColorPhotoYellow, "photogray": cdd.MarkerColorPhotoGray, "red": cdd.MarkerColorRed, "green": cdd.MarkerColorGreen, "blue": cdd.MarkerColorBlue, } // convertMarkers converts CUPS marker-(names|types|levels) to *[]cdd.Marker and *cdd.MarkerState. // // Normalizes marker type: toner(Cartridge|-cartridge) => toner, // ink(Cartridge|-cartridge|Ribbon|-ribbon) => ink func convertMarkers(printerTags map[string][]string) (*[]cdd.Marker, *cdd.MarkerState) { names, types, levels := printerTags[attrMarkerNames], printerTags[attrMarkerTypes], printerTags[attrMarkerLevels] if len(names) == 0 || len(types) == 0 || len(levels) == 0 { return nil, nil } if len(names) != len(levels) { newNames := fixMarkers(names) if len(newNames) != len(levels) { log.Warningf("Received badly-formatted marker-names from CUPS: %s, %s, %s", strings.Join(names, ";"), strings.Join(types, ";"), strings.Join(levels, ";")) return nil, nil } names = newNames } { nameSet := make(map[string]struct{}, len(names)) for _, name := range names { if _, exists := nameSet[name]; exists { return nil, nil } nameSet[name] = struct{}{} } } if len(types) != len(levels) { newTypes := fixMarkers(types) if len(newTypes) != len(levels) { log.Warningf("Received badly-formatted marker-types from CUPS: %s, %s, %s", strings.Join(names, ";"), strings.Join(types, ";"), strings.Join(levels, ";")) return nil, nil } types = newTypes } markers := make([]cdd.Marker, 0, len(names)) states := cdd.MarkerState{make([]cdd.MarkerStateItem, 0, len(names))} for i := 0; i < len(names); i++ { if len(names[i]) == 0 { return nil, nil } var markerType cdd.MarkerType switch strings.ToLower(types[i]) { case "toner", "tonercartridge", "toner-cartridge": markerType = cdd.MarkerToner case "ink", "inkcartridge", "ink-cartridge", "ink-ribbon", "inkribbon": markerType = cdd.MarkerInk case "staples": markerType = cdd.MarkerStaples default: continue } var color *cdd.MarkerColor if markerType == cdd.MarkerToner || markerType == cdd.MarkerInk { nameStripped := strings.Replace(strings.Replace(strings.ToLower(names[i]), " ", "", -1), "-", "", -1) colorType := cdd.MarkerColorCustom for k, v := range cupsMarkerNameToGCP { if strings.HasPrefix(nameStripped, k) { colorType = v break } } color = &cdd.MarkerColor{Type: colorType} if colorType == cdd.MarkerColorCustom { name := names[i] name = strings.TrimSuffix(name, " Cartridge") name = strings.TrimSuffix(name, " cartridge") name = strings.TrimSuffix(name, " Ribbon") name = strings.TrimSuffix(name, " ribbon") name = strings.TrimSuffix(name, " Toner") name = strings.TrimSuffix(name, " toner") name = strings.TrimSuffix(name, " Ink") name = strings.TrimSuffix(name, " ink") name = strings.Replace(name, "-", " ", -1) color.CustomDisplayNameLocalized = cdd.NewLocalizedString(name) } } marker := cdd.Marker{ VendorID: names[i], Type: markerType, Color: color, } level, err := strconv.ParseInt(levels[i], 10, 32) if err != nil { log.Warningf("Failed to parse CUPS marker state %s=%s: %s", names[i], levels[i], err) return nil, nil } if level > 100 { // Lop off extra (proprietary?) bits. level = level & 0x7f } if level < 0 || level > 100 { return nil, nil } var state cdd.MarkerStateType if level > 10 { state = cdd.MarkerStateOK } else { state = cdd.MarkerStateExhausted } level32 := int32(level) markerState := cdd.MarkerStateItem{ VendorID: names[i], State: state, LevelPercent: &level32, } markers = append(markers, marker) states.Item = append(states.Item, markerState) } return &markers, &states } //fixMarkers corrects some drivers' marker names/types where CUPS detects names/types with a comma // as two separate values. The second value of these pairs contain a space, so it's easy to detect. func fixMarkers(values []string) []string { var newValues []string for i := range values { if i > 0 && len(values[i]) > 1 && values[i][0] == ' ' { newValues[len(newValues)-1] = strings.Join([]string{newValues[len(newValues)-1], values[i]}, ",") } else { newValues = append(newValues, values[i]) } } return newValues } func convertPagesPerSheet(printerTags map[string][]string) *cdd.VendorCapability { numberUpSupported, exists := printerTags[attrNumberUpSupported] if !exists { return nil } c := cdd.VendorCapability{ ID: attrNumberUp, Type: cdd.VendorCapabilitySelect, SelectCap: &cdd.SelectCapability{}, DisplayNameLocalized: cdd.NewLocalizedString("Pages per sheet"), } def, exists := printerTags[attrNumberUpDefault] if !exists { def = []string{"1"} } for _, number := range numberUpSupported { option := cdd.SelectCapabilityOption{ Value: number, IsDefault: reflect.DeepEqual(number, def[0]), DisplayNameLocalized: cdd.NewLocalizedString(number), } c.SelectCap.Option = append(c.SelectCap.Option, option) } return &c } var ( pageOrientationByValue map[string]cdd.PageOrientationType = map[string]cdd.PageOrientationType{ "3": cdd.PageOrientationPortrait, "4": cdd.PageOrientationLandscape, "auto": cdd.PageOrientationAuto, // custom value, not CUPS standard } orientationValueByType map[cdd.PageOrientationType]string = map[cdd.PageOrientationType]string{ cdd.PageOrientationPortrait: "3", cdd.PageOrientationLandscape: "4", } ) func convertPageOrientation(printerTags map[string][]string) *cdd.PageOrientation { orientationDefault, exists := printerTags[attrOrientationRequestedDefault] if !exists || len(orientationDefault) != 1 { orientationDefault = []string{"auto"} } orientationSupported, exists := printerTags[attrOrientationRequestedSupported] if !exists { return nil } pageOrientation := cdd.PageOrientation{} for _, orientation := range append([]string{"auto"}, orientationSupported...) { if po, exists := pageOrientationByValue[orientation]; exists { option := cdd.PageOrientationOption{ Type: po, IsDefault: orientation == orientationDefault[0], } pageOrientation.Option = append(pageOrientation.Option, option) } } return &pageOrientation } func convertCopies(printerTags map[string][]string) *cdd.Copies { var err error var def int64 if copiesDefault, exists := printerTags[attrCopiesDefault]; !exists || len(copiesDefault) != 1 { def = 1 } else { def, err = strconv.ParseInt(copiesDefault[0], 10, 32) if err != nil { def = 1 } } var max int64 if copiesSupported, exists := printerTags[attrCopiesSupported]; !exists || len(copiesSupported) != 1 { return nil } else { c := strings.SplitN(copiesSupported[0], "~", 2) max, err = strconv.ParseInt(c[1], 10, 32) if err != nil { return nil } } return &cdd.Copies{ Default: int32(def), Max: int32(max), } } var colorByKeyword = map[string]cdd.ColorOption{ "auto": cdd.ColorOption{ VendorID: attrPrintColorMode + internalKeySeparator + "auto", Type: cdd.ColorTypeAuto, CustomDisplayNameLocalized: cdd.NewLocalizedString("Auto"), }, "color": cdd.ColorOption{ VendorID: attrPrintColorMode + internalKeySeparator + "color", Type: cdd.ColorTypeStandardColor, CustomDisplayNameLocalized: cdd.NewLocalizedString("Color"), }, "monochrome": cdd.ColorOption{ VendorID: attrPrintColorMode + internalKeySeparator + "monochrome", Type: cdd.ColorTypeStandardMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString("Monochrome"), }, } func convertColorAttrs(printerTags map[string][]string) *cdd.Color { colorSupported, exists := printerTags[attrPrintColorModeSupported] if !exists { return nil } c := cdd.Color{} colorDefault, exists := printerTags[attrPrintColorModeDefault] if !exists || len(colorDefault) != 1 { colorDefault = colorSupported[:1] } for _, color := range colorSupported { var co cdd.ColorOption var exists bool if co, exists = colorByKeyword[color]; !exists { co = cdd.ColorOption{ VendorID: attrPrintColorMode + internalKeySeparator + color, Type: cdd.ColorTypeCustomColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(color), } } if color == colorDefault[0] { co.IsDefault = true } c.Option = append(c.Option, co) } return &c } cloud-print-connector-1.12/cups/translate-attrs_test.go000066400000000000000000000344461311204274000233620ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "encoding/json" "reflect" "testing" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/log" ) func TestGetUUID(t *testing.T) { u := getUUID(nil) if u != "" { t.Logf("expected empty string, got %s", u) t.Fail() } pt := map[string][]string{} u = getUUID(pt) if u != "" { t.Logf("expected empty string, got %s", u) t.Fail() } pt = map[string][]string{attrPrinterUUID: []string{"123"}} expected := "123" u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{attrPrinterUUID: []string{"abc:123"}} expected = "abc:123" u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{attrPrinterUUID: []string{"urn:123"}} expected = "123" u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{attrPrinterUUID: []string{"uuid:123"}} u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{attrPrinterUUID: []string{"urn:uuid:123"}} u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{ attrPrinterUUID: []string{"urn:uuid:123"}, attrPrinterName: []string{"my-name"}, } u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } pt = map[string][]string{ attrPrinterName: []string{"my-name"}, } expected = "my-name" u = getUUID(pt) if u != expected { t.Logf("expected %s, got %s", expected, u) t.Fail() } } func TestGetState(t *testing.T) { state := getState(nil) if cdd.CloudDeviceStateIdle != state { t.Logf("expected %+v, got %+v", cdd.CloudDeviceStateIdle, state) t.Fail() } pt := map[string][]string{} state = getState(pt) if cdd.CloudDeviceStateIdle != state { t.Logf("expected %+v, got %+v", cdd.CloudDeviceStateIdle, state) t.Fail() } pt = map[string][]string{attrPrinterState: []string{"1"}} state = getState(pt) if cdd.CloudDeviceStateIdle != state { t.Logf("expected %+v, got %+v", cdd.CloudDeviceStateIdle, state) t.Fail() } pt = map[string][]string{attrPrinterState: []string{"4"}} state = getState(pt) if cdd.CloudDeviceStateProcessing != state { t.Logf("expected %+v, got %+v", cdd.CloudDeviceStateProcessing, state) t.Fail() } } func TestGetVendorState(t *testing.T) { vs := getVendorState(nil) if nil != vs { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} vs = getVendorState(pt) if nil != vs { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ attrPrinterStateReasons: []string{"broken-arrow", "peanut-butter-jam-warning"}, } expected := &cdd.VendorState{ Item: []cdd.VendorStateItem{ cdd.VendorStateItem{ DescriptionLocalized: cdd.NewLocalizedString("broken-arrow"), State: cdd.VendorStateError, }, cdd.VendorStateItem{ DescriptionLocalized: cdd.NewLocalizedString("peanut-butter-jam-warning"), State: cdd.VendorStateWarning, }, }, } vs = getVendorState(pt) if !reflect.DeepEqual(expected, vs) { t.Logf("expected\n %+v\ngot\n %+v", expected, vs) t.Fail() } } func TestConvertSupportedContentType(t *testing.T) { sct := convertSupportedContentType(nil) if sct != nil { t.Logf("expected nil, got %+v", sct) t.Fail() } pt := map[string][]string{} sct = convertSupportedContentType(pt) if sct != nil { t.Logf("expected nil, got %+v", sct) t.Fail() } pt = map[string][]string{ attrDocumentFormatSupported: []string{ "image/png", "image/pwg-raster", "application/octet-stream", "application/pdf", "application/postscript"}, "pdf-versions-supported": []string{"adobe-1.3", "adobe-1.4", "adobe-1.6", "dingbat"}, } expected := &[]cdd.SupportedContentType{ cdd.SupportedContentType{ ContentType: "application/pdf", MinVersion: "1.3", MaxVersion: "1.6", }, cdd.SupportedContentType{ContentType: "application/postscript"}, cdd.SupportedContentType{ContentType: "image/png"}, } sct = convertSupportedContentType(pt) if !reflect.DeepEqual(*expected, *sct) { t.Logf("expected\n %+v\ngot\n %+v", *expected, *sct) t.Fail() } pt = map[string][]string{ attrDocumentFormatSupported: []string{"image/png"}, } expected = &[]cdd.SupportedContentType{ cdd.SupportedContentType{ContentType: "application/pdf"}, cdd.SupportedContentType{ContentType: "application/postscript"}, cdd.SupportedContentType{ContentType: "image/png"}, } sct = convertSupportedContentType(pt) if !reflect.DeepEqual(*expected, *sct) { t.Logf("expected %+v, got %+v", *expected, *sct) t.Fail() } } func TestConvertMarkers(t *testing.T) { log.SetLevel(log.ERROR) m, ms := convertMarkers(nil) if m != nil { t.Logf("expected nil") t.Fail() } if ms != nil { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} m, ms = convertMarkers(pt) if m != nil { t.Logf("expected nil") t.Fail() } if ms != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ attrMarkerNames: []string{"black", "black", "black"}, attrMarkerTypes: []string{"toner", "toner", "ink"}, attrMarkerLevels: []string{"10", "11", "12"}, } m, ms = convertMarkers(pt) if m != nil { t.Logf("expected nil") t.Fail() } if ms != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ attrMarkerNames: []string{"black", "color"}, attrMarkerTypes: []string{"toner", "toner", "ink"}, attrMarkerLevels: []string{"10", "11", "12"}, } m, ms = convertMarkers(pt) if m != nil { t.Logf("expected nil") t.Fail() } if ms != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ attrMarkerNames: []string{"black", "color", "rainbow"}, attrMarkerTypes: []string{"toner", "toner"}, attrMarkerLevels: []string{"10", "11", "12"}, } m, ms = convertMarkers(pt) if m != nil { t.Logf("expected nil") t.Fail() } if ms != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ attrMarkerNames: []string{"black", " Reorder Part #12345", "color", "rainbow", "zebra", "pony"}, attrMarkerTypes: []string{"toner", "toner", "ink", "staples", "water", " Reorder H2O"}, attrMarkerLevels: []string{"10", "11", "12", "208", "13"}, } mExpected := &[]cdd.Marker{ cdd.Marker{ VendorID: "black, Reorder Part #12345", Type: cdd.MarkerToner, Color: &cdd.MarkerColor{Type: cdd.MarkerColorBlack}, }, cdd.Marker{ VendorID: "color", Type: cdd.MarkerToner, Color: &cdd.MarkerColor{Type: cdd.MarkerColorColor}, }, cdd.Marker{ VendorID: "rainbow", Type: cdd.MarkerInk, Color: &cdd.MarkerColor{ Type: cdd.MarkerColorCustom, CustomDisplayNameLocalized: cdd.NewLocalizedString("rainbow"), }, }, cdd.Marker{ VendorID: "zebra", Type: cdd.MarkerStaples, }, } ten, eleven, twelve, eighty := int32(10), int32(11), int32(12), int32(80) msExpected := &cdd.MarkerState{ Item: []cdd.MarkerStateItem{ cdd.MarkerStateItem{ VendorID: "black, Reorder Part #12345", State: cdd.MarkerStateExhausted, LevelPercent: &ten, }, cdd.MarkerStateItem{ VendorID: "color", State: cdd.MarkerStateOK, LevelPercent: &eleven, }, cdd.MarkerStateItem{ VendorID: "rainbow", State: cdd.MarkerStateOK, LevelPercent: &twelve, }, cdd.MarkerStateItem{ VendorID: "zebra", State: cdd.MarkerStateOK, LevelPercent: &eighty, }, }, } m, ms = convertMarkers(pt) if !reflect.DeepEqual(mExpected, m) { e, _ := json.Marshal(mExpected) f, _ := json.Marshal(m) t.Logf("expected\n %s\ngot\n %s", e, f) t.Fail() } if !reflect.DeepEqual(msExpected, ms) { e, _ := json.Marshal(msExpected) f, _ := json.Marshal(ms) t.Logf("expected\n %s\ngot\n %s", e, f) t.Fail() } pt = map[string][]string{ attrMarkerNames: []string{"black", "color", "rainbow", "zebra", "pony"}, attrMarkerTypes: []string{"toner", "toner", "ink", "staples", "water"}, attrMarkerLevels: []string{"10", "11", "12", "208", "13"}, } mExpected = &[]cdd.Marker{ cdd.Marker{ VendorID: "black", Type: cdd.MarkerToner, Color: &cdd.MarkerColor{Type: cdd.MarkerColorBlack}, }, cdd.Marker{ VendorID: "color", Type: cdd.MarkerToner, Color: &cdd.MarkerColor{Type: cdd.MarkerColorColor}, }, cdd.Marker{ VendorID: "rainbow", Type: cdd.MarkerInk, Color: &cdd.MarkerColor{ Type: cdd.MarkerColorCustom, CustomDisplayNameLocalized: cdd.NewLocalizedString("rainbow"), }, }, cdd.Marker{ VendorID: "zebra", Type: cdd.MarkerStaples, }, } msExpected = &cdd.MarkerState{ Item: []cdd.MarkerStateItem{ cdd.MarkerStateItem{ VendorID: "black", State: cdd.MarkerStateExhausted, LevelPercent: &ten, }, cdd.MarkerStateItem{ VendorID: "color", State: cdd.MarkerStateOK, LevelPercent: &eleven, }, cdd.MarkerStateItem{ VendorID: "rainbow", State: cdd.MarkerStateOK, LevelPercent: &twelve, }, cdd.MarkerStateItem{ VendorID: "zebra", State: cdd.MarkerStateOK, LevelPercent: &eighty, }, }, } m, ms = convertMarkers(pt) if !reflect.DeepEqual(mExpected, m) { e, _ := json.Marshal(mExpected) f, _ := json.Marshal(m) t.Logf("expected\n %s\ngot\n %s", e, f) t.Fail() } if !reflect.DeepEqual(msExpected, ms) { e, _ := json.Marshal(msExpected) f, _ := json.Marshal(ms) t.Logf("expected\n %s\ngot\n %s", e, f) t.Fail() } } func TestConvertPagesPerSheet(t *testing.T) { vc := convertPagesPerSheet(nil) if vc != nil { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} vc = convertPagesPerSheet(pt) if vc != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ "number-up-default": []string{"4"}, "number-up-supported": []string{"1", "2", "4"}, } expected := &cdd.VendorCapability{ ID: "number-up", Type: cdd.VendorCapabilitySelect, SelectCap: &cdd.SelectCapability{ Option: []cdd.SelectCapabilityOption{ cdd.SelectCapabilityOption{ Value: "1", IsDefault: false, DisplayNameLocalized: cdd.NewLocalizedString("1"), }, cdd.SelectCapabilityOption{ Value: "2", IsDefault: false, DisplayNameLocalized: cdd.NewLocalizedString("2"), }, cdd.SelectCapabilityOption{ Value: "4", IsDefault: true, DisplayNameLocalized: cdd.NewLocalizedString("4"), }, }, }, DisplayNameLocalized: cdd.NewLocalizedString("Pages per sheet"), } vc = convertPagesPerSheet(pt) if !reflect.DeepEqual(expected, vc) { e, _ := json.Marshal(expected) f, _ := json.Marshal(vc) t.Logf("expected\n %s\ngot\n %s", e, f) t.Fail() } } func TestConvertPageOrientation(t *testing.T) { po := convertPageOrientation(nil) if po != nil { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} po = convertPageOrientation(pt) if po != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ "orientation-requested-default": []string{"4"}, "orientation-requested-supported": []string{"3", "4"}, } expected := &cdd.PageOrientation{ Option: []cdd.PageOrientationOption{ cdd.PageOrientationOption{ Type: cdd.PageOrientationAuto, IsDefault: false, }, cdd.PageOrientationOption{ Type: cdd.PageOrientationPortrait, IsDefault: false, }, cdd.PageOrientationOption{ Type: cdd.PageOrientationLandscape, IsDefault: true, }, }, } po = convertPageOrientation(pt) if !reflect.DeepEqual(expected, po) { t.Logf("expected %+v, got %+v", expected, po) t.Fail() } pt = map[string][]string{ "orientation-requested-supported": []string{"3", "4"}, } expected = &cdd.PageOrientation{ Option: []cdd.PageOrientationOption{ cdd.PageOrientationOption{ Type: cdd.PageOrientationAuto, IsDefault: true, }, cdd.PageOrientationOption{ Type: cdd.PageOrientationPortrait, IsDefault: false, }, cdd.PageOrientationOption{ Type: cdd.PageOrientationLandscape, IsDefault: false, }, }, } po = convertPageOrientation(pt) if !reflect.DeepEqual(expected, po) { t.Logf("expected %+v, got %+v", expected, po) t.Fail() } pt = map[string][]string{ "orientation-requested-default": []string{}, "orientation-requested-supported": []string{"3", "4"}, } po = convertPageOrientation(pt) if !reflect.DeepEqual(expected, po) { t.Logf("expected %+v, got %+v", expected, po) t.Fail() } } func TestConvertCopies(t *testing.T) { c := convertCopies(nil) if c != nil { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} c = convertCopies(pt) if c != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ "copies-default": []string{"2"}, "copies-supported": []string{"1~101"}, } expected := &cdd.Copies{ Default: int32(2), Max: int32(101), } c = convertCopies(pt) if !reflect.DeepEqual(expected, c) { t.Logf("expected %+v, got %+v", expected, c) t.Fail() } } func TestConvertColorAttrs(t *testing.T) { c := convertColorAttrs(nil) if c != nil { t.Logf("expected nil") t.Fail() } pt := map[string][]string{} c = convertColorAttrs(pt) if c != nil { t.Logf("expected nil") t.Fail() } pt = map[string][]string{ "print-color-mode-default": []string{"auto"}, "print-color-mode-supported": []string{"color", "monochrome", "auto", "zebra"}, } expected := &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{"print-color-mode:color", cdd.ColorTypeStandardColor, "", false, cdd.NewLocalizedString("Color")}, cdd.ColorOption{"print-color-mode:monochrome", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Monochrome")}, cdd.ColorOption{"print-color-mode:auto", cdd.ColorTypeAuto, "", true, cdd.NewLocalizedString("Auto")}, cdd.ColorOption{"print-color-mode:zebra", cdd.ColorTypeCustomColor, "", false, cdd.NewLocalizedString("zebra")}, }, } c = convertColorAttrs(pt) if !reflect.DeepEqual(expected, c) { t.Logf("expected %+v, got %+v", expected, c) t.Fail() } } cloud-print-connector-1.12/cups/translate-ppd.go000066400000000000000000001644641311204274000217550ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "fmt" "regexp" "strconv" "strings" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" ) const ( ppdBoolean = "Boolean" ppdBRDuplex = "BRDuplex" ppdCMAndResolution = "CMAndResolution" ppdCloseGroup = "CloseGroup" ppdCloseSubGroup = "CloseSubGroup" ppdCloseUI = "CloseUI" ppdColorModel = "ColorModel" ppdDefault = "Default" ppdDuplex = "Duplex" ppdDuplexNoTumble = "DuplexNoTumble" ppdDuplexTumble = "DuplexTumble" ppdEnd = "End" ppdFalse = "False" ppdHWMargins = "HWMargins" ppdInstallableOptions = "InstallableOptions" ppdJCLCloseUI = "JCLCloseUI" ppdJCLOpenUI = "JCLOpenUI" ppdJobType = "JobType" ppdKMDuplex = "KMDuplex" ppdKMDuplexBooklet = "Booklet" ppdKMDuplexDouble = "Double" ppdKMDuplexSingle = "Single" ppdLockedPrint = "LockedPrint" ppdLockedPrintPassword = "LockedPrintPassword" ppdManufacturer = "Manufacturer" ppdMediaType = "MediaType" ppdNickName = "NickName" ppdNone = "None" ppdOpenGroup = "OpenGroup" ppdOpenSubGroup = "OpenSubGroup" ppdOpenUI = "OpenUI" ppdOutputBin = "OutputBin" ppdPageSize = "PageSize" ppdPickMany = "PickMany" ppdPickOne = "PickOne" ppdPrintQualityTranslation = "Print Quality" ppdResolution = "Resolution" ppdSelectColor = "SelectColor" ppdThroughput = "Throughput" ppdTrue = "True" ppdUIConstraints = "UIConstraints" // These characters are not allowed in PPD main keywords or option keywords, // so they are safe to use as separators in CDD strings. // A:B/C is interpreted as 2 CUPS/IPP options: A=B and C=[VendorTicketItem.Value]. internalKeySeparator = ":" internalValueSeparator = "/" ) var ( rLineSplit = regexp.MustCompile(`(?:\r\n|\r|\n)\*`) rStatement = regexp.MustCompile(`` + `^([^\s:/]+)` + // Main keyword; not optional. `(?:\s+([^/:]+))?` + // Option keyword. `(?:/([^:]*))?` + // Translation string. `(?::\s*(?:"([^"]*)"|(.*)))?\s*$`) // Value. rConstraint = regexp.MustCompile(`^\*([^\s\*]+)\s+(\S+)\s+\*([^\s\*]+)\s+(\S+)$`) // rModel is for removing superfluous information from *ModelName or *NickName. rModel *regexp.Regexp = regexp.MustCompile(`\s+(` + `(w/)?PS2?3?(\(P\))?(,\s+[0-9\.]+)?|` + `pcl3?(,\s+\d+(\.\d+))*|` + `-|PXL|PDF|cups-team|CUPS\+Gutenprint\s+v\S+|\(?recommended\)?|` + `(A4|Letter)(\+Duplex)?|` + `Post[Ss]cript|BR-Script2?3?J?|` + `v[0-9\.]+|` + `\(?KPDL(-2)?\)?|` + `Foomatic/\S+|Epson Inkjet Printer Driver \(ESC/P-R\) for \S+|` + `(hpcups|hpijs|HPLIP),?\s+\d+(\.\d+)*|requires proprietary plugin` + `)\s*$`) rPageSize = regexp.MustCompile(`([\d.]+)(?:mm|in)?x([\d.]+)(mm|in)?`) rPageSizeInPoints = regexp.MustCompile(`w([\d.]+)h([\d.]+)`) rColor = regexp.MustCompile(`(?i)^(?:cmy|rgb|color)`) rGray = regexp.MustCompile(`(?i)^(?:gray|black|mono)`) rCMAndResolutionPrefix = regexp.MustCompile(`(?i)^(?:on|off)\s*-?\s*`) rResolution = regexp.MustCompile(`^(\d+)(?:x(\d+))?dpi$`) rHWMargins = regexp.MustCompile(`^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$`) ricohPasswordVendorID = fmt.Sprintf("%s%s%s%s%s", ppdJobType, internalKeySeparator, ppdLockedPrint, internalValueSeparator, ppdLockedPrintPassword) rRicohPasswordFormat = regexp.MustCompile(`^\d{4}$`) ) // statement represents a PPD statement. type statement struct { mainKeyword string optionKeyword string translation string value string } type entryType uint8 const ( entryTypePickOne entryType = iota entryTypeBoolean entryType = iota ) // entry represents a PPD OpenUI entry. type entry struct { mainKeyword string translation string entryType entryType defaultValue string options []statement } // translatePPD extracts a PrinterDescriptionSection, manufacturer string, model string, and DuplexVendorMap // from a PPD string. func translatePPD(ppd string, vendorPPDOptions []string) (*cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap) { statements := ppdToStatements(ppd) openUIStatements, installables, uiConstraints, standAlones := groupStatements(statements) openUIStatements = filterConstraints(openUIStatements, installables, uiConstraints) entriesByMainKeyword, entriesByTranslation := openUIStatementsToEntries(openUIStatements) consideredMainKeywords := make(map[string]struct{}) pds := cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{}, } var duplexMap lib.DuplexVendorMap if e, exists := entriesByMainKeyword[ppdPageSize]; exists { pds.MediaSize = convertMediaSize(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdColorModel]; exists { pds.Color = convertColorPPD(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdCMAndResolution]; exists { pds.Color = convertColorPPD(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdSelectColor]; exists { pds.Color = convertColorPPD(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdDuplex]; exists { pds.Duplex, duplexMap = convertDuplex(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdBRDuplex]; exists { pds.Duplex, duplexMap = convertDuplex(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdKMDuplex]; exists { pds.Duplex, duplexMap = convertDuplex(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdResolution]; exists { pds.DPI = convertDPI(e) consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdOutputBin]; exists { *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) consideredMainKeywords[e.mainKeyword] = struct{}{} } if jobType, exists := entriesByMainKeyword[ppdJobType]; exists { if lockedPrintPassword, exists := entriesByMainKeyword[ppdLockedPrintPassword]; exists { vc := convertRicohLockedPrintPassword(jobType, lockedPrintPassword) if vc != nil { *pds.VendorCapability = append(*pds.VendorCapability, *vc) consideredMainKeywords[jobType.mainKeyword] = struct{}{} consideredMainKeywords[lockedPrintPassword.mainKeyword] = struct{}{} } } } if e, exists := entriesByTranslation[ppdPrintQualityTranslation]; exists { *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) consideredMainKeywords[e.mainKeyword] = struct{}{} } vendorPPDOptionsMap := make(map[string]struct{}, len(vendorPPDOptions)) for _, o := range vendorPPDOptions { vendorPPDOptionsMap[o] = struct{}{} } _, translateAllVendorPPDOptions := vendorPPDOptionsMap["all"] for _, e := range entriesByMainKeyword { if _, exists := consideredMainKeywords[e.mainKeyword]; exists { continue } if !translateAllVendorPPDOptions { if _, exists := vendorPPDOptionsMap[e.mainKeyword]; !exists { continue } } *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) } if len(*pds.VendorCapability) == 0 { // Don't generate invalid CDD JSON. pds.VendorCapability = nil } var manufacturer, model string for _, s := range standAlones { switch s.mainKeyword { case ppdManufacturer: manufacturer = s.value case ppdNickName: model = cleanupModel(s.value) case ppdHWMargins: pds.Margins = convertMargins(s.value) case ppdThroughput: pds.PrintingSpeed = convertPrintingSpeed(s.value, pds.Color) } } model = strings.TrimLeft(strings.TrimPrefix(model, manufacturer), " ") return &pds, manufacturer, model, duplexMap } // ppdToStatements converts a PPD file to a slice of statements. func ppdToStatements(ppd string) []statement { var statements []statement for _, line := range rLineSplit.Split(ppd, -1) { if strings.HasPrefix(line, "%") || strings.HasPrefix(line, "?") { // Ignore comments and query statements. continue } found := rStatement.FindStringSubmatch(line) if found == nil { continue } mainKeyword, optionKeyword, translation := found[1], found[2], found[3] if mainKeyword == ppdEnd { // Ignore End statements. continue } found[4] = strings.TrimSpace(found[4]) found[5] = strings.TrimSpace(found[5]) var value string if found[4] != "" { value = found[4] } else { value = found[5] } statements = append(statements, statement{mainKeyword, optionKeyword, translation, value}) } return statements } // groupStatements groups statements into: // 1) OpenUI entries // 2) InstallableOptions OpenUI entries // 3) UIConstraints statements // 4) other stand-alone statements // Other Groups and SubGroups structures are thrown away. func groupStatements(statements []statement) ([][]statement, [][]statement, []statement, []statement) { var openUIs, installables [][]statement var uiConstraints, standAlones []statement var insideOpenUI, insideInstallable bool for _, s := range statements { switch s.mainKeyword { case ppdOpenUI, ppdJCLOpenUI: insideOpenUI = true if insideInstallable { installables = append(installables, []statement{s}) } else { openUIs = append(openUIs, []statement{s}) } case ppdCloseUI, ppdJCLCloseUI: insideOpenUI = false case ppdOpenGroup: if strings.HasPrefix(s.value, ppdInstallableOptions) { insideInstallable = true } case ppdCloseGroup: if strings.HasPrefix(s.value, ppdInstallableOptions) { insideInstallable = false } case ppdOpenSubGroup: case ppdCloseSubGroup: case ppdUIConstraints: uiConstraints = append(uiConstraints, s) default: if insideInstallable { if len(installables) > 0 { installables[len(installables)-1] = append(installables[len(installables)-1], s) } } else if insideOpenUI { if len(openUIs) > 0 { openUIs[len(openUIs)-1] = append(openUIs[len(openUIs)-1], s) } } else { standAlones = append(standAlones, s) } } } return openUIs, installables, uiConstraints, standAlones } type pair struct { first string second string } func filterConstraints(openUIs, installables [][]statement, uiConstraints []statement) [][]statement { installedDefaults := make(map[pair]struct{}, len(installables)) for _, installable := range installables { for _, s := range installable { if strings.HasPrefix(s.mainKeyword, ppdDefault) { uiKeyword := strings.TrimPrefix(s.mainKeyword, ppdDefault) installedDefaults[pair{uiKeyword, s.value}] = struct{}{} } } } constraints := map[pair]struct{}{} for _, s := range uiConstraints { constraint := rConstraint.FindStringSubmatch(s.value) if constraint == nil || len(constraint) != 5 { continue } if _, exists := installedDefaults[pair{constraint[1], constraint[2]}]; exists { constraints[pair{constraint[3], constraint[4]}] = struct{}{} } } var newOpenUIs [][]statement for _, openUI := range openUIs { newOpenUI := make([]statement, 0, len(openUI)) for _, s := range openUI { if _, exists := constraints[pair{s.mainKeyword, s.optionKeyword}]; !exists { newOpenUI = append(newOpenUI, s) } } newOpenUIs = append(newOpenUIs, newOpenUI) } return newOpenUIs } func openUIStatementsToEntries(statements [][]statement) (map[string]entry, map[string]entry) { byMainKeyword, byTranslation := make(map[string]entry), make(map[string]entry) for _, openUI := range statements { var e entry e.mainKeyword = strings.TrimPrefix(openUI[0].optionKeyword, "*") e.translation = openUI[0].translation if len(e.translation) < 1 { e.translation = e.mainKeyword } switch openUI[0].value { case ppdPickOne: e.entryType = entryTypePickOne case ppdBoolean: e.entryType = entryTypeBoolean case ppdPickMany: log.Warning("This PPD file contains a PickMany OpenUI entry, which is not supported") continue default: continue } optionValues := map[string]struct{}{} for _, s := range openUI { if strings.HasPrefix(s.mainKeyword, ppdDefault) { e.defaultValue = s.value } else if strings.HasPrefix(s.mainKeyword, e.mainKeyword) { e.options = append(e.options, s) optionValues[s.optionKeyword] = struct{}{} } } if len(e.options) < 1 { continue } if _, exists := optionValues[e.defaultValue]; !exists { e.defaultValue = e.options[0].value } byMainKeyword[e.mainKeyword] = e byTranslation[e.translation] = e } return byMainKeyword, byTranslation } func cleanupModel(model string) string { for { newModel := rModel.ReplaceAllString(model, "") // rModel looks for whitespace in the prefix. These don't have whitespace prefixes. newModel = strings.TrimSuffix(newModel, ",") newModel = strings.TrimSuffix(newModel, "(PS)") if model == newModel { break } model = newModel } return model } func convertMargins(hwMargins string) *cdd.Margins { found := rHWMargins.FindStringSubmatch(hwMargins) if found == nil { return nil } marginsType := cdd.MarginsBorderless marginsMicrons := make([]int32, 4) for i := 1; i < len(found); i++ { intValue, err := strconv.ParseInt(found[i], 10, 32) if err != nil { return nil } if intValue > 0 { marginsType = cdd.MarginsStandard } marginsMicrons[i-1] = pointsToMicrons(float32(intValue)) } // HWResolution format: left, bottom, right, top. return &cdd.Margins{ []cdd.MarginsOption{ cdd.MarginsOption{ Type: marginsType, LeftMicrons: marginsMicrons[0], BottomMicrons: marginsMicrons[1], RightMicrons: marginsMicrons[2], TopMicrons: marginsMicrons[3], IsDefault: true, }, }, } } func convertPrintingSpeed(throughput string, color *cdd.Color) *cdd.PrintingSpeed { speedPPM, err := strconv.ParseInt(throughput, 10, 32) if err != nil { return nil } var colorTypes *[]cdd.ColorType if color != nil { mColorTypes := make(map[cdd.ColorType]struct{}, len(color.Option)) for _, co := range color.Option { mColorTypes[co.Type] = struct{}{} } ct := make([]cdd.ColorType, 0, len(mColorTypes)) for colorType := range mColorTypes { ct = append(ct, colorType) } colorTypes = &ct } return &cdd.PrintingSpeed{ []cdd.PrintingSpeedOption{ cdd.PrintingSpeedOption{ SpeedPPM: float32(speedPPM), ColorType: colorTypes, }, }, } } func cleanupColorName(colorValue, colorName string) string { newColorName := rCMAndResolutionPrefix.ReplaceAllString(colorName, "") if colorName == newColorName { return colorName } if rGray.MatchString(colorValue) || rGray.MatchString(newColorName) { if len(newColorName) > 0 { return "Gray, " + newColorName } else { return "Gray" } } if rColor.MatchString(colorValue) || rColor.MatchString(newColorName) { if len(newColorName) > 0 { return "Color, " + newColorName } else { return "Color" } } return newColorName } func convertColorPPD(e entry) *cdd.Color { var colorOptions, grayOptions, otherOptions []statement for _, o := range e.options { if rGray.MatchString(o.optionKeyword) { grayOptions = append(grayOptions, o) } else if rColor.MatchString(o.optionKeyword) { colorOptions = append(colorOptions, o) } else { otherOptions = append(otherOptions, o) } } c := cdd.Color{} if len(colorOptions) == 1 { colorName := cleanupColorName(colorOptions[0].optionKeyword, colorOptions[0].translation) co := cdd.ColorOption{ VendorID: e.mainKeyword + internalKeySeparator + colorOptions[0].optionKeyword, Type: cdd.ColorTypeStandardColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } c.Option = append(c.Option, co) } else { for _, o := range colorOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } c.Option = append(c.Option, co) } } if len(grayOptions) == 1 { colorName := cleanupColorName(grayOptions[0].optionKeyword, grayOptions[0].translation) co := cdd.ColorOption{ VendorID: e.mainKeyword + internalKeySeparator + grayOptions[0].optionKeyword, Type: cdd.ColorTypeStandardMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } c.Option = append(c.Option, co) } else { for _, o := range grayOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } c.Option = append(c.Option, co) } } for _, o := range otherOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } c.Option = append(c.Option, co) } if len(c.Option) == 0 { return nil } for i := range c.Option { if c.Option[i].VendorID == e.mainKeyword+internalKeySeparator+e.defaultValue { c.Option[i].IsDefault = true return &c } } c.Option[0].IsDefault = true return &c } func convertDuplex(e entry) (*cdd.Duplex, lib.DuplexVendorMap) { d := cdd.Duplex{} duplexMap := lib.DuplexVendorMap{} var foundDefault bool for _, o := range e.options { def := o.optionKeyword == e.defaultValue switch o.optionKeyword { case ppdNone, ppdFalse, ppdKMDuplexSingle: d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def}) duplexMap[cdd.DuplexNoDuplex] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def case ppdDuplexNoTumble, ppdTrue, ppdKMDuplexDouble: d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def}) duplexMap[cdd.DuplexLongEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def case ppdDuplexTumble, ppdKMDuplexBooklet: d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexShortEdge, def}) duplexMap[cdd.DuplexShortEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def default: if strings.HasPrefix(o.optionKeyword, "1") { d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def}) duplexMap[cdd.DuplexNoDuplex] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def } else if strings.HasPrefix(o.optionKeyword, "2") { d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def}) duplexMap[cdd.DuplexLongEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def } } } if len(d.Option) == 0 { return nil, nil } if !foundDefault { d.Option[0].IsDefault = true } return &d, duplexMap } func convertDPI(e entry) *cdd.DPI { d := cdd.DPI{} for _, o := range e.options { found := rResolution.FindStringSubmatch(o.optionKeyword) if found == nil { continue } h, err := strconv.ParseInt(found[1], 10, 32) if err != nil { continue } v, err := strconv.ParseInt(found[2], 10, 32) if err != nil { v = h } do := cdd.DPIOption{ HorizontalDPI: int32(h), VerticalDPI: int32(v), VendorID: o.optionKeyword, CustomDisplayNameLocalized: cdd.NewLocalizedString(o.translation), } d.Option = append(d.Option, do) } if len(d.Option) == 0 { return nil } for i := range d.Option { if d.Option[i].VendorID == e.defaultValue { d.Option[i].IsDefault = true return &d } } d.Option[0].IsDefault = true return &d } // Convert 2 entries, JobType and LockedPrintPassword, to one CDD VendorCapability. // http://www.linuxfoundation.org/collaborate/workgroups/openprinting/databasericohfaq#What_is_JobType_.22Locked_Print.22.3F_How_do_I_use_it.3F func convertRicohLockedPrintPassword(jobType, lockedPrintPassword entry) *cdd.VendorCapability { var foundLockedPrint bool for _, s := range jobType.options { if s.optionKeyword == ppdLockedPrint { foundLockedPrint = true break } } if !foundLockedPrint { return nil } return &cdd.VendorCapability{ ID: ricohPasswordVendorID, Type: cdd.VendorCapabilityTypedValue, TypedValueCap: &cdd.TypedValueCapability{ ValueType: cdd.TypedValueCapabilityTypeString, }, DisplayNameLocalized: cdd.NewLocalizedString("Password (4 numbers)"), } } func convertVendorCapability(e entry) *cdd.VendorCapability { vc := cdd.VendorCapability{ ID: e.mainKeyword, DisplayNameLocalized: cdd.NewLocalizedString(e.translation), } if e.entryType == entryTypePickOne { vc.Type = cdd.VendorCapabilitySelect vc.SelectCap = &cdd.SelectCapability{} var foundDefault bool for _, o := range e.options { sco := cdd.SelectCapabilityOption{ Value: o.optionKeyword, DisplayNameLocalized: cdd.NewLocalizedString(o.translation), } if e.defaultValue == o.optionKeyword { foundDefault = true sco.IsDefault = true } vc.SelectCap.Option = append(vc.SelectCap.Option, sco) } if !foundDefault { vc.SelectCap.Option[0].IsDefault = true } } else { vc.Type = cdd.VendorCapabilityTypedValue vc.TypedValueCap = &cdd.TypedValueCapability{ ValueType: cdd.TypedValueCapabilityTypeBoolean, Default: e.defaultValue, } } return &vc } func convertMediaSize(e entry) *cdd.MediaSize { foundDefault := false ms := cdd.MediaSize{} for _, option := range e.options { if strings.HasSuffix(option.optionKeyword, ".FullBleed") { continue } var o cdd.MediaSizeOption var exists bool if o, exists = ppdMediaSizes[option.optionKeyword]; !exists { op := getCustomMediaSizeOption(option.optionKeyword, option.translation) if op == nil { // Failed to make a custom media size. continue } o = *op } if e.defaultValue == option.optionKeyword { o.IsDefault = true foundDefault = true } ms.Option = append(ms.Option, o) } if len(ms.Option) == 0 { return nil } if !foundDefault { ms.Option[0].IsDefault = true } return &ms } func getCustomMediaSizeOption(optionKeyword, translation string) *cdd.MediaSizeOption { found := rPageSize.FindStringSubmatch(optionKeyword) if found == nil { found = rPageSize.FindStringSubmatch(translation) } if found == nil { found = rPageSizeInPoints.FindStringSubmatch(optionKeyword) if found == nil { return nil } found = append(found, "points") } if len(found) != 4 { return nil } width, err := strconv.ParseFloat(found[1], 32) if err != nil { return nil } height, err := strconv.ParseFloat(found[2], 32) if err != nil { return nil } var toMicrons func(float32) int32 switch found[3] { case "mm": toMicrons = mmToMicrons case "points": toMicrons = pointsToMicrons default: toMicrons = inchesToMicrons } return &cdd.MediaSizeOption{ Name: cdd.MediaSizeCustom, WidthMicrons: toMicrons(float32(width)), HeightMicrons: toMicrons(float32(height)), VendorID: optionKeyword, CustomDisplayNameLocalized: cdd.NewLocalizedString(translation), } } func inchesToMicrons(inches float32) int32 { return int32(inches*25400 + 0.5) } func mmToMicrons(mm float32) int32 { return int32(mm*1000 + 0.5) } func pointsToMicrons(points float32) int32 { return int32(points*25400/72 + 0.5) } var ppdMediaSizes = map[string]cdd.MediaSizeOption{ "3x5": {Name: cdd.MediaSizeNAIndex3x5, WidthMicrons: inchesToMicrons(3), HeightMicrons: inchesToMicrons(5), VendorID: "3x5", CustomDisplayNameLocalized: cdd.NewLocalizedString("3x5")}, "4x6": {Name: cdd.MediaSizeNAIndex4x6, WidthMicrons: inchesToMicrons(4), HeightMicrons: inchesToMicrons(6), VendorID: "4x6", CustomDisplayNameLocalized: cdd.NewLocalizedString("4x6")}, "5x7": {Name: cdd.MediaSizeNA5x7, WidthMicrons: inchesToMicrons(5), HeightMicrons: inchesToMicrons(7), VendorID: "5x7", CustomDisplayNameLocalized: cdd.NewLocalizedString("5x7")}, "5x8": {Name: cdd.MediaSizeNAIndex5x8, WidthMicrons: inchesToMicrons(5), HeightMicrons: inchesToMicrons(8), VendorID: "5x8", CustomDisplayNameLocalized: cdd.NewLocalizedString("5x8")}, "6x9": {Name: cdd.MediaSizeNA6x9, WidthMicrons: inchesToMicrons(6), HeightMicrons: inchesToMicrons(9), VendorID: "6x9", CustomDisplayNameLocalized: cdd.NewLocalizedString("6x9")}, "6.5x9.5": {Name: cdd.MediaSizeNAC5, WidthMicrons: inchesToMicrons(6.5), HeightMicrons: inchesToMicrons(9.5), VendorID: "6.5x9.5", CustomDisplayNameLocalized: cdd.NewLocalizedString("6.5x9.5")}, "7x9": {Name: cdd.MediaSizeNA7x9, WidthMicrons: inchesToMicrons(7), HeightMicrons: inchesToMicrons(9), VendorID: "7x9", CustomDisplayNameLocalized: cdd.NewLocalizedString("7x9")}, "8x10": {Name: cdd.MediaSizeNAGovtLetter, WidthMicrons: inchesToMicrons(8), HeightMicrons: inchesToMicrons(10), VendorID: "8x10", CustomDisplayNameLocalized: cdd.NewLocalizedString("8x10")}, "8x13": {Name: cdd.MediaSizeNAGovtLegal, WidthMicrons: inchesToMicrons(8), HeightMicrons: inchesToMicrons(13), VendorID: "8x13", CustomDisplayNameLocalized: cdd.NewLocalizedString("8x13")}, "9x11": {Name: cdd.MediaSizeNA9x11, WidthMicrons: inchesToMicrons(9), HeightMicrons: inchesToMicrons(11), VendorID: "9x11", CustomDisplayNameLocalized: cdd.NewLocalizedString("9x11")}, "10x11": {Name: cdd.MediaSizeNA10x11, WidthMicrons: inchesToMicrons(10), HeightMicrons: inchesToMicrons(11), VendorID: "10x11", CustomDisplayNameLocalized: cdd.NewLocalizedString("10x11")}, "10x13": {Name: cdd.MediaSizeNA10x13, WidthMicrons: inchesToMicrons(10), HeightMicrons: inchesToMicrons(13), VendorID: "10x13", CustomDisplayNameLocalized: cdd.NewLocalizedString("10x13")}, "10x14": {Name: cdd.MediaSizeNA10x14, WidthMicrons: inchesToMicrons(10), HeightMicrons: inchesToMicrons(14), VendorID: "10x14", CustomDisplayNameLocalized: cdd.NewLocalizedString("10x14")}, "10x15": {Name: cdd.MediaSizeNA10x15, WidthMicrons: inchesToMicrons(10), HeightMicrons: inchesToMicrons(15), VendorID: "10x15", CustomDisplayNameLocalized: cdd.NewLocalizedString("10x15")}, "11x12": {Name: cdd.MediaSizeNA11x12, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(12), VendorID: "11x12", CustomDisplayNameLocalized: cdd.NewLocalizedString("11x12")}, "11x14": {Name: cdd.MediaSizeNAEDP, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(14), VendorID: "11x14", CustomDisplayNameLocalized: cdd.NewLocalizedString("11x14")}, "11x15": {Name: cdd.MediaSizeNA11x15, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(15), VendorID: "11x15", CustomDisplayNameLocalized: cdd.NewLocalizedString("11x15")}, "11x17": {Name: cdd.MediaSizeNALedger, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(17), VendorID: "11x17", CustomDisplayNameLocalized: cdd.NewLocalizedString("11x17")}, "12x18": {Name: cdd.MediaSizeNAArchB, WidthMicrons: inchesToMicrons(12), HeightMicrons: inchesToMicrons(18), VendorID: "12x18", CustomDisplayNameLocalized: cdd.NewLocalizedString("12x18")}, "12x19": {Name: cdd.MediaSizeNA12x19, WidthMicrons: inchesToMicrons(12), HeightMicrons: inchesToMicrons(19), VendorID: "12x19", CustomDisplayNameLocalized: cdd.NewLocalizedString("12x19")}, "13x19": {Name: cdd.MediaSizeNASuperB, WidthMicrons: inchesToMicrons(13), HeightMicrons: inchesToMicrons(19), VendorID: "13x19", CustomDisplayNameLocalized: cdd.NewLocalizedString("13x19")}, "EnvPersonal": {Name: cdd.MediaSizeNAPersonal, WidthMicrons: inchesToMicrons(3.625), HeightMicrons: inchesToMicrons(6.5), VendorID: "EnvPersonal", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPersonal")}, "Monarch": {Name: cdd.MediaSizeNAMonarch, WidthMicrons: inchesToMicrons(3.875), HeightMicrons: inchesToMicrons(7.5), VendorID: "Monarch", CustomDisplayNameLocalized: cdd.NewLocalizedString("Monarch")}, "EnvMonarch": {Name: cdd.MediaSizeNAMonarch, WidthMicrons: inchesToMicrons(3.875), HeightMicrons: inchesToMicrons(7.5), VendorID: "EnvMonarch", CustomDisplayNameLocalized: cdd.NewLocalizedString("Monarch")}, "Comm10": {Name: cdd.MediaSizeNANumber10, WidthMicrons: inchesToMicrons(4.125), HeightMicrons: inchesToMicrons(9.5), VendorID: "Comm10", CustomDisplayNameLocalized: cdd.NewLocalizedString("Comm10")}, "EnvA2": {Name: cdd.MediaSizeNAA2, WidthMicrons: inchesToMicrons(4.375), HeightMicrons: inchesToMicrons(5.75), VendorID: "EnvA2", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvA2")}, "Env9": {Name: cdd.MediaSizeNANumber9, WidthMicrons: inchesToMicrons(3.875), HeightMicrons: inchesToMicrons(8.875), VendorID: "Env9", CustomDisplayNameLocalized: cdd.NewLocalizedString("Env9")}, "Env10": {Name: cdd.MediaSizeNANumber10, WidthMicrons: inchesToMicrons(4.125), HeightMicrons: inchesToMicrons(9.5), VendorID: "Env10", CustomDisplayNameLocalized: cdd.NewLocalizedString("Env10")}, "Env11": {Name: cdd.MediaSizeNANumber11, WidthMicrons: inchesToMicrons(4.5), HeightMicrons: inchesToMicrons(10.375), VendorID: "Env11", CustomDisplayNameLocalized: cdd.NewLocalizedString("Env11")}, "Env12": {Name: cdd.MediaSizeNANumber12, WidthMicrons: inchesToMicrons(4.75), HeightMicrons: inchesToMicrons(11), VendorID: "Env12", CustomDisplayNameLocalized: cdd.NewLocalizedString("Env12")}, "Env14": {Name: cdd.MediaSizeNANumber14, WidthMicrons: inchesToMicrons(5), HeightMicrons: inchesToMicrons(11.5), VendorID: "Env14", CustomDisplayNameLocalized: cdd.NewLocalizedString("Env14")}, "Statement": {Name: cdd.MediaSizeNAInvoice, WidthMicrons: inchesToMicrons(5.5), HeightMicrons: inchesToMicrons(8.5), VendorID: "Statement", CustomDisplayNameLocalized: cdd.NewLocalizedString("Statement")}, "Executive": {Name: cdd.MediaSizeNAExecutive, WidthMicrons: inchesToMicrons(7.25), HeightMicrons: inchesToMicrons(10.5), VendorID: "Executive", CustomDisplayNameLocalized: cdd.NewLocalizedString("Executive")}, "Quarto": {Name: cdd.MediaSizeNAQuarto, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(10.83), VendorID: "Quarto", CustomDisplayNameLocalized: cdd.NewLocalizedString("Quarto")}, "EngQuatro": {Name: cdd.MediaSizeCustom, WidthMicrons: inchesToMicrons(8), HeightMicrons: inchesToMicrons(10), VendorID: "EngQuatro", CustomDisplayNameLocalized: cdd.NewLocalizedString("English Quatro 8x10")}, "Letter": {Name: cdd.MediaSizeNALetter, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(11), VendorID: "Letter", CustomDisplayNameLocalized: cdd.NewLocalizedString("Letter")}, "LetterExtra": {Name: cdd.MediaSizeNALetterExtra, WidthMicrons: inchesToMicrons(9.5), HeightMicrons: inchesToMicrons(12), VendorID: "LetterExtra", CustomDisplayNameLocalized: cdd.NewLocalizedString("Letter Extra")}, "LetterPlus": {Name: cdd.MediaSizeNALetterPlus, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(12.69), VendorID: "LetterPlus", CustomDisplayNameLocalized: cdd.NewLocalizedString("Letter Plus")}, "Legal": {Name: cdd.MediaSizeNALegal, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(14), VendorID: "Legal", CustomDisplayNameLocalized: cdd.NewLocalizedString("Legal")}, "LegalExtra": {Name: cdd.MediaSizeNALegalExtra, WidthMicrons: inchesToMicrons(9.5), HeightMicrons: inchesToMicrons(15), VendorID: "LegalExtra", CustomDisplayNameLocalized: cdd.NewLocalizedString("Legal Extra")}, "FanFoldGerman": {Name: cdd.MediaSizeNAFanfoldEur, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(12), VendorID: "FanFoldGerman", CustomDisplayNameLocalized: cdd.NewLocalizedString("FanFoldGerman")}, "Foolscap": {Name: cdd.MediaSizeNAFoolscap, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(13), VendorID: "Foolscap", CustomDisplayNameLocalized: cdd.NewLocalizedString("Foolscap")}, "FanFoldGermanLegal": {Name: cdd.MediaSizeNAFoolscap, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(13), VendorID: "FanFoldGermanLegal", CustomDisplayNameLocalized: cdd.NewLocalizedString("Fan Fold German Legal")}, "GovernmentLG": {Name: cdd.MediaSizeNAFoolscap, WidthMicrons: inchesToMicrons(8.5), HeightMicrons: inchesToMicrons(13), VendorID: "GovernmentLG", CustomDisplayNameLocalized: cdd.NewLocalizedString("GovernmentLG")}, "SuperA": {Name: cdd.MediaSizeNASuperA, WidthMicrons: inchesToMicrons(8.94), HeightMicrons: inchesToMicrons(14), VendorID: "SuperA", CustomDisplayNameLocalized: cdd.NewLocalizedString("Super A")}, "SuperB": {Name: cdd.MediaSizeNABPlus, WidthMicrons: inchesToMicrons(12), HeightMicrons: inchesToMicrons(19.17), VendorID: "SuperB", CustomDisplayNameLocalized: cdd.NewLocalizedString("Super B")}, "Tabloid": {Name: cdd.MediaSizeNALedger, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(17), VendorID: "Tabloid", CustomDisplayNameLocalized: cdd.NewLocalizedString("Tabloid")}, "Ledger": {Name: cdd.MediaSizeNALedger, WidthMicrons: inchesToMicrons(11), HeightMicrons: inchesToMicrons(17), VendorID: "Ledger", CustomDisplayNameLocalized: cdd.NewLocalizedString("Ledger")}, "ARCHA": {Name: cdd.MediaSizeNAArchA, WidthMicrons: inchesToMicrons(9), HeightMicrons: inchesToMicrons(12), VendorID: "ARCHA", CustomDisplayNameLocalized: cdd.NewLocalizedString("Arch A")}, "ARCHB": {Name: cdd.MediaSizeNAArchB, WidthMicrons: inchesToMicrons(12), HeightMicrons: inchesToMicrons(18), VendorID: "ARCHB", CustomDisplayNameLocalized: cdd.NewLocalizedString("Arch B")}, "ARCHC": {Name: cdd.MediaSizeNAArchC, WidthMicrons: inchesToMicrons(18), HeightMicrons: inchesToMicrons(24), VendorID: "ARCHC", CustomDisplayNameLocalized: cdd.NewLocalizedString("Arch C")}, "ARCHD": {Name: cdd.MediaSizeNAArchD, WidthMicrons: inchesToMicrons(24), HeightMicrons: inchesToMicrons(36), VendorID: "ARCHD", CustomDisplayNameLocalized: cdd.NewLocalizedString("Arch D")}, "ARCHE": {Name: cdd.MediaSizeNAArchE, WidthMicrons: inchesToMicrons(36), HeightMicrons: inchesToMicrons(48), VendorID: "ARCHE", CustomDisplayNameLocalized: cdd.NewLocalizedString("Arch E")}, "AnsiC": {Name: cdd.MediaSizeNAC, WidthMicrons: inchesToMicrons(17), HeightMicrons: inchesToMicrons(22), VendorID: "AnsiC", CustomDisplayNameLocalized: cdd.NewLocalizedString("ANSI C")}, "AnsiD": {Name: cdd.MediaSizeNAD, WidthMicrons: inchesToMicrons(22), HeightMicrons: inchesToMicrons(34), VendorID: "AnsiD", CustomDisplayNameLocalized: cdd.NewLocalizedString("ANSI D")}, "AnsiE": {Name: cdd.MediaSizeNAE, WidthMicrons: inchesToMicrons(34), HeightMicrons: inchesToMicrons(44), VendorID: "AnsiE", CustomDisplayNameLocalized: cdd.NewLocalizedString("ANSI E")}, "AnsiF": {Name: cdd.MediaSizeNAF, WidthMicrons: inchesToMicrons(44), HeightMicrons: inchesToMicrons(68), VendorID: "AnsiF", CustomDisplayNameLocalized: cdd.NewLocalizedString("ANSI F")}, "F": {Name: cdd.MediaSizeNAF, WidthMicrons: inchesToMicrons(44), HeightMicrons: inchesToMicrons(68), VendorID: "F", CustomDisplayNameLocalized: cdd.NewLocalizedString("ANSI F")}, "roc16k": {Name: cdd.MediaSizeROC16k, WidthMicrons: inchesToMicrons(7.75), HeightMicrons: inchesToMicrons(10.75), VendorID: "roc16k", CustomDisplayNameLocalized: cdd.NewLocalizedString("16K (ROC)")}, "roc8k": {Name: cdd.MediaSizeROC8k, WidthMicrons: inchesToMicrons(10.75), HeightMicrons: inchesToMicrons(15.5), VendorID: "roc8k", CustomDisplayNameLocalized: cdd.NewLocalizedString("8K (ROC)")}, "PRC32K": {Name: cdd.MediaSizePRC32k, WidthMicrons: mmToMicrons(97), HeightMicrons: mmToMicrons(151), VendorID: "PRC32K", CustomDisplayNameLocalized: cdd.NewLocalizedString("32K (PRC)")}, "EnvPRC1": {Name: cdd.MediaSizePRC1, WidthMicrons: mmToMicrons(102), HeightMicrons: mmToMicrons(165), VendorID: "EnvPRC1", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC1")}, "EnvPRC2": {Name: cdd.MediaSizePRC2, WidthMicrons: mmToMicrons(102), HeightMicrons: mmToMicrons(176), VendorID: "EnvPRC2", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC2")}, "EnvPRC4": {Name: cdd.MediaSizePRC4, WidthMicrons: mmToMicrons(110), HeightMicrons: mmToMicrons(208), VendorID: "EnvPRC4", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC4")}, "EnvPRC5": {Name: cdd.MediaSizePRC5, WidthMicrons: mmToMicrons(110), HeightMicrons: mmToMicrons(220), VendorID: "EnvPRC5", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC5")}, "EnvPRC8": {Name: cdd.MediaSizePRC8, WidthMicrons: mmToMicrons(120), HeightMicrons: mmToMicrons(309), VendorID: "EnvPRC8", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC8")}, "EnvPRC6": {Name: cdd.MediaSizePRC6, WidthMicrons: mmToMicrons(120), HeightMicrons: mmToMicrons(230), VendorID: "EnvPRC6", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC6")}, "EnvPRC3": {Name: cdd.MediaSizePRC3, WidthMicrons: mmToMicrons(125), HeightMicrons: mmToMicrons(176), VendorID: "EnvPRC3", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC3")}, "PRC16K": {Name: cdd.MediaSizePRC16k, WidthMicrons: mmToMicrons(146), HeightMicrons: mmToMicrons(215), VendorID: "PRC16K", CustomDisplayNameLocalized: cdd.NewLocalizedString("PRC16K")}, "EnvPRC7": {Name: cdd.MediaSizePRC7, WidthMicrons: mmToMicrons(160), HeightMicrons: mmToMicrons(230), VendorID: "EnvPRC7", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvPRC7")}, "A0": {Name: cdd.MediaSizeISOA0, WidthMicrons: mmToMicrons(841), HeightMicrons: mmToMicrons(1189), VendorID: "A0", CustomDisplayNameLocalized: cdd.NewLocalizedString("A0")}, "A1": {Name: cdd.MediaSizeISOA1, WidthMicrons: mmToMicrons(594), HeightMicrons: mmToMicrons(841), VendorID: "A1", CustomDisplayNameLocalized: cdd.NewLocalizedString("A1")}, "A2": {Name: cdd.MediaSizeISOA2, WidthMicrons: mmToMicrons(420), HeightMicrons: mmToMicrons(594), VendorID: "A2", CustomDisplayNameLocalized: cdd.NewLocalizedString("A2")}, "A3": {Name: cdd.MediaSizeISOA3, WidthMicrons: mmToMicrons(297), HeightMicrons: mmToMicrons(420), VendorID: "A3", CustomDisplayNameLocalized: cdd.NewLocalizedString("A3")}, "A3Extra": {Name: cdd.MediaSizeISOA3Extra, WidthMicrons: mmToMicrons(322), HeightMicrons: mmToMicrons(445), VendorID: "A3Extra", CustomDisplayNameLocalized: cdd.NewLocalizedString("A3 Extra")}, "A4": {Name: cdd.MediaSizeISOA4, WidthMicrons: mmToMicrons(210), HeightMicrons: mmToMicrons(297), VendorID: "A4", CustomDisplayNameLocalized: cdd.NewLocalizedString("A4")}, "A4Extra": {Name: cdd.MediaSizeISOA4Extra, WidthMicrons: mmToMicrons(235.5), HeightMicrons: mmToMicrons(322.3), VendorID: "A4Extra", CustomDisplayNameLocalized: cdd.NewLocalizedString("A4 Extra")}, "A4Tab": {Name: cdd.MediaSizeISOA4Tab, WidthMicrons: mmToMicrons(225), HeightMicrons: mmToMicrons(297), VendorID: "A4Tab", CustomDisplayNameLocalized: cdd.NewLocalizedString("A4 Tab")}, "A5": {Name: cdd.MediaSizeISOA5, WidthMicrons: mmToMicrons(148), HeightMicrons: mmToMicrons(210), VendorID: "A5", CustomDisplayNameLocalized: cdd.NewLocalizedString("A5")}, "A5Extra": {Name: cdd.MediaSizeISOA5Extra, WidthMicrons: mmToMicrons(174), HeightMicrons: mmToMicrons(235), VendorID: "A5Extra", CustomDisplayNameLocalized: cdd.NewLocalizedString("A5 Extra")}, "A6": {Name: cdd.MediaSizeISOA6, WidthMicrons: mmToMicrons(105), HeightMicrons: mmToMicrons(148), VendorID: "A6", CustomDisplayNameLocalized: cdd.NewLocalizedString("A6")}, "A7": {Name: cdd.MediaSizeISOA7, WidthMicrons: mmToMicrons(74), HeightMicrons: mmToMicrons(105), VendorID: "A7", CustomDisplayNameLocalized: cdd.NewLocalizedString("A7")}, "A8": {Name: cdd.MediaSizeISOA8, WidthMicrons: mmToMicrons(52), HeightMicrons: mmToMicrons(74), VendorID: "A8", CustomDisplayNameLocalized: cdd.NewLocalizedString("A8")}, "A9": {Name: cdd.MediaSizeISOA9, WidthMicrons: mmToMicrons(37), HeightMicrons: mmToMicrons(52), VendorID: "A9", CustomDisplayNameLocalized: cdd.NewLocalizedString("A9")}, "A10": {Name: cdd.MediaSizeISOA10, WidthMicrons: mmToMicrons(26), HeightMicrons: mmToMicrons(37), VendorID: "A10", CustomDisplayNameLocalized: cdd.NewLocalizedString("A10")}, "ISOB0": {Name: cdd.MediaSizeISOB0, WidthMicrons: mmToMicrons(1000), HeightMicrons: mmToMicrons(1414), VendorID: "ISOB0", CustomDisplayNameLocalized: cdd.NewLocalizedString("B0 (ISO)")}, "ISOB1": {Name: cdd.MediaSizeISOB1, WidthMicrons: mmToMicrons(707), HeightMicrons: mmToMicrons(1000), VendorID: "ISOB1", CustomDisplayNameLocalized: cdd.NewLocalizedString("B1 (ISO)")}, "ISOB2": {Name: cdd.MediaSizeISOB2, WidthMicrons: mmToMicrons(500), HeightMicrons: mmToMicrons(707), VendorID: "ISOB2", CustomDisplayNameLocalized: cdd.NewLocalizedString("B2 (ISO)")}, "ISOB3": {Name: cdd.MediaSizeISOB3, WidthMicrons: mmToMicrons(353), HeightMicrons: mmToMicrons(500), VendorID: "ISOB3", CustomDisplayNameLocalized: cdd.NewLocalizedString("B3 (ISO)")}, "ISOB4": {Name: cdd.MediaSizeISOB4, WidthMicrons: mmToMicrons(250), HeightMicrons: mmToMicrons(353), VendorID: "ISOB4", CustomDisplayNameLocalized: cdd.NewLocalizedString("B4 (ISO)")}, "ISOB5": {Name: cdd.MediaSizeISOB5, WidthMicrons: mmToMicrons(176), HeightMicrons: mmToMicrons(250), VendorID: "ISOB5", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 (ISO)")}, "EnvISOB5": {Name: cdd.MediaSizeISOB5, WidthMicrons: mmToMicrons(176), HeightMicrons: mmToMicrons(250), VendorID: "EnvISOB5", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 Envelope (ISO)")}, "ISOB5Extra": {Name: cdd.MediaSizeISOB5Extra, WidthMicrons: mmToMicrons(201), HeightMicrons: mmToMicrons(276), VendorID: "ISOB5Extra", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 Extra (ISO)")}, "ISOB6": {Name: cdd.MediaSizeISOB6, WidthMicrons: mmToMicrons(125), HeightMicrons: mmToMicrons(176), VendorID: "ISOB6", CustomDisplayNameLocalized: cdd.NewLocalizedString("B6 (ISO)")}, "ISOB7": {Name: cdd.MediaSizeISOB7, WidthMicrons: mmToMicrons(88), HeightMicrons: mmToMicrons(125), VendorID: "ISOB7", CustomDisplayNameLocalized: cdd.NewLocalizedString("B7 (ISO)")}, "ISOB8": {Name: cdd.MediaSizeISOB8, WidthMicrons: mmToMicrons(62), HeightMicrons: mmToMicrons(88), VendorID: "ISOB8", CustomDisplayNameLocalized: cdd.NewLocalizedString("B8 (ISO)")}, "ISOB9": {Name: cdd.MediaSizeISOB9, WidthMicrons: mmToMicrons(44), HeightMicrons: mmToMicrons(62), VendorID: "ISOB9", CustomDisplayNameLocalized: cdd.NewLocalizedString("B9 (ISO)")}, "ISOB10": {Name: cdd.MediaSizeISOB10, WidthMicrons: mmToMicrons(31), HeightMicrons: mmToMicrons(44), VendorID: "ISOB10", CustomDisplayNameLocalized: cdd.NewLocalizedString("B10 (ISO)")}, "EnvC0": {Name: cdd.MediaSizeISOC0, WidthMicrons: mmToMicrons(917), HeightMicrons: mmToMicrons(1297), VendorID: "EnvC0", CustomDisplayNameLocalized: cdd.NewLocalizedString("C0 (ISO)")}, "EnvC1": {Name: cdd.MediaSizeISOC1, WidthMicrons: mmToMicrons(648), HeightMicrons: mmToMicrons(917), VendorID: "EnvC1", CustomDisplayNameLocalized: cdd.NewLocalizedString("C1 (ISO)")}, "EnvC2": {Name: cdd.MediaSizeISOC2, WidthMicrons: mmToMicrons(458), HeightMicrons: mmToMicrons(648), VendorID: "EnvC2", CustomDisplayNameLocalized: cdd.NewLocalizedString("C2 (ISO)")}, "EnvC3": {Name: cdd.MediaSizeISOC3, WidthMicrons: mmToMicrons(324), HeightMicrons: mmToMicrons(458), VendorID: "EnvC3", CustomDisplayNameLocalized: cdd.NewLocalizedString("C3 (ISO)")}, "EnvC4": {Name: cdd.MediaSizeISOC4, WidthMicrons: mmToMicrons(229), HeightMicrons: mmToMicrons(324), VendorID: "EnvC4", CustomDisplayNameLocalized: cdd.NewLocalizedString("C4 (ISO)")}, "EnvC5": {Name: cdd.MediaSizeISOC5, WidthMicrons: mmToMicrons(162), HeightMicrons: mmToMicrons(229), VendorID: "EnvC5", CustomDisplayNameLocalized: cdd.NewLocalizedString("C5 (ISO)")}, "EnvC6": {Name: cdd.MediaSizeISOC6, WidthMicrons: mmToMicrons(114), HeightMicrons: mmToMicrons(162), VendorID: "EnvC6", CustomDisplayNameLocalized: cdd.NewLocalizedString("C6 (ISO)")}, "EnvC65": {Name: cdd.MediaSizeISOC6c5, WidthMicrons: mmToMicrons(114), HeightMicrons: mmToMicrons(229), VendorID: "EnvC65", CustomDisplayNameLocalized: cdd.NewLocalizedString("C6c5 (ISO)")}, "EnvC7": {Name: cdd.MediaSizeISOC7, WidthMicrons: mmToMicrons(81), HeightMicrons: mmToMicrons(114), VendorID: "EnvC7", CustomDisplayNameLocalized: cdd.NewLocalizedString("C7 (ISO)")}, "EnvDL": {Name: cdd.MediaSizeISODL, WidthMicrons: mmToMicrons(110), HeightMicrons: mmToMicrons(220), VendorID: "EnvDL", CustomDisplayNameLocalized: cdd.NewLocalizedString("DL Envelope")}, "DLEnv": {Name: cdd.MediaSizeISODL, WidthMicrons: mmToMicrons(110), HeightMicrons: mmToMicrons(220), VendorID: "DLEnv", CustomDisplayNameLocalized: cdd.NewLocalizedString("DL Envelope")}, "RA0": {Name: cdd.MediaSizeISORA0, WidthMicrons: mmToMicrons(860), HeightMicrons: mmToMicrons(1220), VendorID: "RA0", CustomDisplayNameLocalized: cdd.NewLocalizedString("RA0")}, "RA1": {Name: cdd.MediaSizeISORA1, WidthMicrons: mmToMicrons(610), HeightMicrons: mmToMicrons(860), VendorID: "RA1", CustomDisplayNameLocalized: cdd.NewLocalizedString("RA1")}, "RA2": {Name: cdd.MediaSizeISORA2, WidthMicrons: mmToMicrons(430), HeightMicrons: mmToMicrons(610), VendorID: "RA2", CustomDisplayNameLocalized: cdd.NewLocalizedString("RA2")}, "RA3": {Name: cdd.MediaSizeCustom, WidthMicrons: mmToMicrons(305), HeightMicrons: mmToMicrons(430), VendorID: "RA3", CustomDisplayNameLocalized: cdd.NewLocalizedString("RA3")}, "RA4": {Name: cdd.MediaSizeCustom, WidthMicrons: mmToMicrons(215), HeightMicrons: mmToMicrons(305), VendorID: "RA4", CustomDisplayNameLocalized: cdd.NewLocalizedString("RA4")}, "SRA0": {Name: cdd.MediaSizeISOSRA0, WidthMicrons: mmToMicrons(900), HeightMicrons: mmToMicrons(1280), VendorID: "SRA0", CustomDisplayNameLocalized: cdd.NewLocalizedString("SRA0")}, "SRA1": {Name: cdd.MediaSizeISOSRA1, WidthMicrons: mmToMicrons(640), HeightMicrons: mmToMicrons(900), VendorID: "SRA1", CustomDisplayNameLocalized: cdd.NewLocalizedString("SRA1")}, "SRA2": {Name: cdd.MediaSizeISOSRA2, WidthMicrons: mmToMicrons(450), HeightMicrons: mmToMicrons(640), VendorID: "SRA2", CustomDisplayNameLocalized: cdd.NewLocalizedString("SRA2")}, "SRA3": {Name: cdd.MediaSizeCustom, WidthMicrons: mmToMicrons(320), HeightMicrons: mmToMicrons(450), VendorID: "SRA3", CustomDisplayNameLocalized: cdd.NewLocalizedString("SRA3")}, "SRA4": {Name: cdd.MediaSizeCustom, WidthMicrons: mmToMicrons(225), HeightMicrons: mmToMicrons(320), VendorID: "SRA4", CustomDisplayNameLocalized: cdd.NewLocalizedString("SRA4")}, "JISB0": {Name: cdd.MediaSizeJISB0, WidthMicrons: mmToMicrons(1030), HeightMicrons: mmToMicrons(1456), VendorID: "JISB0", CustomDisplayNameLocalized: cdd.NewLocalizedString("B0 (JIS)")}, "B0JIS": {Name: cdd.MediaSizeJISB0, WidthMicrons: mmToMicrons(1030), HeightMicrons: mmToMicrons(1456), VendorID: "B0JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B0 (JIS)")}, "B0": {Name: cdd.MediaSizeJISB0, WidthMicrons: mmToMicrons(1030), HeightMicrons: mmToMicrons(1456), VendorID: "B0", CustomDisplayNameLocalized: cdd.NewLocalizedString("B0 (JIS)")}, "JISB1": {Name: cdd.MediaSizeJISB1, WidthMicrons: mmToMicrons(728), HeightMicrons: mmToMicrons(1030), VendorID: "JISB1", CustomDisplayNameLocalized: cdd.NewLocalizedString("B1 (JIS)")}, "B1JIS": {Name: cdd.MediaSizeJISB1, WidthMicrons: mmToMicrons(728), HeightMicrons: mmToMicrons(1030), VendorID: "B1JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B1 (JIS)")}, "B1": {Name: cdd.MediaSizeJISB1, WidthMicrons: mmToMicrons(728), HeightMicrons: mmToMicrons(1030), VendorID: "B1", CustomDisplayNameLocalized: cdd.NewLocalizedString("B1 (JIS)")}, "JISB2": {Name: cdd.MediaSizeJISB2, WidthMicrons: mmToMicrons(515), HeightMicrons: mmToMicrons(728), VendorID: "JISB2", CustomDisplayNameLocalized: cdd.NewLocalizedString("B2 (JIS)")}, "B2JIS": {Name: cdd.MediaSizeJISB2, WidthMicrons: mmToMicrons(515), HeightMicrons: mmToMicrons(728), VendorID: "B2JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B2 (JIS)")}, "B2": {Name: cdd.MediaSizeJISB2, WidthMicrons: mmToMicrons(515), HeightMicrons: mmToMicrons(728), VendorID: "B2", CustomDisplayNameLocalized: cdd.NewLocalizedString("B2 (JIS)")}, "JISB3": {Name: cdd.MediaSizeJISB3, WidthMicrons: mmToMicrons(364), HeightMicrons: mmToMicrons(515), VendorID: "JISB3", CustomDisplayNameLocalized: cdd.NewLocalizedString("B3 (JIS)")}, "B3JIS": {Name: cdd.MediaSizeJISB3, WidthMicrons: mmToMicrons(364), HeightMicrons: mmToMicrons(515), VendorID: "B3JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B3 (JIS)")}, "B3": {Name: cdd.MediaSizeJISB3, WidthMicrons: mmToMicrons(364), HeightMicrons: mmToMicrons(515), VendorID: "B3", CustomDisplayNameLocalized: cdd.NewLocalizedString("B3 (JIS)")}, "JISB4": {Name: cdd.MediaSizeJISB4, WidthMicrons: mmToMicrons(257), HeightMicrons: mmToMicrons(364), VendorID: "JISB4", CustomDisplayNameLocalized: cdd.NewLocalizedString("B4 (JIS)")}, "B4JIS": {Name: cdd.MediaSizeJISB4, WidthMicrons: mmToMicrons(257), HeightMicrons: mmToMicrons(364), VendorID: "B4JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B4 (JIS)")}, "B4": {Name: cdd.MediaSizeJISB4, WidthMicrons: mmToMicrons(257), HeightMicrons: mmToMicrons(364), VendorID: "B4", CustomDisplayNameLocalized: cdd.NewLocalizedString("B4 (JIS)")}, "JISB5": {Name: cdd.MediaSizeJISB5, WidthMicrons: mmToMicrons(182), HeightMicrons: mmToMicrons(257), VendorID: "JISB5", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 (JIS)")}, "B5JIS": {Name: cdd.MediaSizeJISB5, WidthMicrons: mmToMicrons(182), HeightMicrons: mmToMicrons(257), VendorID: "B5JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 (JIS)")}, "B5": {Name: cdd.MediaSizeJISB5, WidthMicrons: mmToMicrons(182), HeightMicrons: mmToMicrons(257), VendorID: "B5", CustomDisplayNameLocalized: cdd.NewLocalizedString("B5 (JIS)")}, "JISB6": {Name: cdd.MediaSizeJISB6, WidthMicrons: mmToMicrons(128), HeightMicrons: mmToMicrons(182), VendorID: "JISB6", CustomDisplayNameLocalized: cdd.NewLocalizedString("B6 (JIS)")}, "B6JIS": {Name: cdd.MediaSizeJISB6, WidthMicrons: mmToMicrons(128), HeightMicrons: mmToMicrons(182), VendorID: "B6JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B6 (JIS)")}, "B6": {Name: cdd.MediaSizeJISB6, WidthMicrons: mmToMicrons(128), HeightMicrons: mmToMicrons(182), VendorID: "B6", CustomDisplayNameLocalized: cdd.NewLocalizedString("B6 (JIS)")}, "JISB7": {Name: cdd.MediaSizeJISB7, WidthMicrons: mmToMicrons(91), HeightMicrons: mmToMicrons(128), VendorID: "JISB7", CustomDisplayNameLocalized: cdd.NewLocalizedString("B7 (JIS)")}, "B7JIS": {Name: cdd.MediaSizeJISB7, WidthMicrons: mmToMicrons(91), HeightMicrons: mmToMicrons(128), VendorID: "B7JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B7 (JIS)")}, "B7": {Name: cdd.MediaSizeJISB7, WidthMicrons: mmToMicrons(91), HeightMicrons: mmToMicrons(128), VendorID: "B7", CustomDisplayNameLocalized: cdd.NewLocalizedString("B7 (JIS)")}, "JISB8": {Name: cdd.MediaSizeJISB8, WidthMicrons: mmToMicrons(64), HeightMicrons: mmToMicrons(91), VendorID: "JISB8", CustomDisplayNameLocalized: cdd.NewLocalizedString("B8 (JIS)")}, "B8JIS": {Name: cdd.MediaSizeJISB8, WidthMicrons: mmToMicrons(64), HeightMicrons: mmToMicrons(91), VendorID: "B8JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B8 (JIS)")}, "B8": {Name: cdd.MediaSizeJISB8, WidthMicrons: mmToMicrons(64), HeightMicrons: mmToMicrons(91), VendorID: "B8", CustomDisplayNameLocalized: cdd.NewLocalizedString("B8 (JIS)")}, "JISB9": {Name: cdd.MediaSizeJISB9, WidthMicrons: mmToMicrons(45), HeightMicrons: mmToMicrons(64), VendorID: "JISB9", CustomDisplayNameLocalized: cdd.NewLocalizedString("B9 (JIS)")}, "B9JIS": {Name: cdd.MediaSizeJISB9, WidthMicrons: mmToMicrons(45), HeightMicrons: mmToMicrons(64), VendorID: "B9JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B9 (JIS)")}, "B9": {Name: cdd.MediaSizeJISB9, WidthMicrons: mmToMicrons(45), HeightMicrons: mmToMicrons(64), VendorID: "B9", CustomDisplayNameLocalized: cdd.NewLocalizedString("B9 (JIS)")}, "JISB10": {Name: cdd.MediaSizeJISB10, WidthMicrons: mmToMicrons(32), HeightMicrons: mmToMicrons(45), VendorID: "JISB10", CustomDisplayNameLocalized: cdd.NewLocalizedString("B10 (JIS)")}, "B10JIS": {Name: cdd.MediaSizeJISB10, WidthMicrons: mmToMicrons(32), HeightMicrons: mmToMicrons(45), VendorID: "B10JIS", CustomDisplayNameLocalized: cdd.NewLocalizedString("B10 (JIS)")}, "B10": {Name: cdd.MediaSizeJISB10, WidthMicrons: mmToMicrons(32), HeightMicrons: mmToMicrons(45), VendorID: "B10", CustomDisplayNameLocalized: cdd.NewLocalizedString("B10 (JIS)")}, "EnvChou4": {Name: cdd.MediaSizeJPNChou4, WidthMicrons: mmToMicrons(90), HeightMicrons: mmToMicrons(205), VendorID: "EnvChou4", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvChou4")}, "Hagaki": {Name: cdd.MediaSizeJPNHagaki, WidthMicrons: mmToMicrons(100), HeightMicrons: mmToMicrons(148), VendorID: "Hagaki", CustomDisplayNameLocalized: cdd.NewLocalizedString("Hagaki")}, "JapanesePostCard": {Name: cdd.MediaSizeJPNHagaki, WidthMicrons: mmToMicrons(100), HeightMicrons: mmToMicrons(148), VendorID: "JapanesePostCard", CustomDisplayNameLocalized: cdd.NewLocalizedString("Japanese Postcard")}, "Postcard": {Name: cdd.MediaSizeJPNHagaki, WidthMicrons: mmToMicrons(100), HeightMicrons: mmToMicrons(148), VendorID: "Postcard", CustomDisplayNameLocalized: cdd.NewLocalizedString("Postcard")}, "EnvYou4": {Name: cdd.MediaSizeJPNYou4, WidthMicrons: mmToMicrons(105), HeightMicrons: mmToMicrons(235), VendorID: "EnvYou4", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvYou4")}, "EnvChou3": {Name: cdd.MediaSizeJPNChou3, WidthMicrons: mmToMicrons(120), HeightMicrons: mmToMicrons(235), VendorID: "EnvChou3", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvChou3")}, "Oufuku": {Name: cdd.MediaSizeJPNOufuku, WidthMicrons: mmToMicrons(148), HeightMicrons: mmToMicrons(200), VendorID: "Oufuku", CustomDisplayNameLocalized: cdd.NewLocalizedString("Oufuku")}, "DoublePostcardRotated": {Name: cdd.MediaSizeJPNOufuku, WidthMicrons: mmToMicrons(148), HeightMicrons: mmToMicrons(200), VendorID: "DoublePostcardRotated", CustomDisplayNameLocalized: cdd.NewLocalizedString("Double Postcard Rotated")}, "EnvKaku2": {Name: cdd.MediaSizeJPNKaku2, WidthMicrons: mmToMicrons(240), HeightMicrons: mmToMicrons(332), VendorID: "EnvKaku2", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvKaku2")}, "om_small-photo": {Name: cdd.MediaSizeOMSmallPhoto, WidthMicrons: mmToMicrons(100), HeightMicrons: mmToMicrons(150), VendorID: "om_small-photo", CustomDisplayNameLocalized: cdd.NewLocalizedString("Small Photo")}, "EnvItalian": {Name: cdd.MediaSizeOMItalian, WidthMicrons: mmToMicrons(110), HeightMicrons: mmToMicrons(230), VendorID: "EnvItalian", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvItalian")}, "om_large-photo": {Name: cdd.MediaSizeOMLargePhoto, WidthMicrons: mmToMicrons(200), HeightMicrons: mmToMicrons(300), VendorID: "om_large-photo", CustomDisplayNameLocalized: cdd.NewLocalizedString("Large Photo")}, "Folio": {Name: cdd.MediaSizeOMFolio, WidthMicrons: mmToMicrons(210), HeightMicrons: mmToMicrons(330), VendorID: "Folio", CustomDisplayNameLocalized: cdd.NewLocalizedString("Folio")}, "FolioSP": {Name: cdd.MediaSizeOMFolioSP, WidthMicrons: mmToMicrons(215), HeightMicrons: mmToMicrons(315), VendorID: "FolioSP", CustomDisplayNameLocalized: cdd.NewLocalizedString("FolioSP")}, "EnvInvite": {Name: cdd.MediaSizeOMInvite, WidthMicrons: mmToMicrons(220), HeightMicrons: mmToMicrons(220), VendorID: "EnvInvite", CustomDisplayNameLocalized: cdd.NewLocalizedString("EnvInvite")}, "8Kai": {Name: cdd.MediaSizeCustom, WidthMicrons: inchesToMicrons(10.5), HeightMicrons: inchesToMicrons(15.375), VendorID: "8Kai", CustomDisplayNameLocalized: cdd.NewLocalizedString("8 Kai")}, "8K": {Name: cdd.MediaSizeCustom, WidthMicrons: inchesToMicrons(10.5), HeightMicrons: inchesToMicrons(15.375), VendorID: "8K", CustomDisplayNameLocalized: cdd.NewLocalizedString("8 Kai")}, "16Kai": {Name: cdd.MediaSizeCustom, WidthMicrons: inchesToMicrons(7.6875), HeightMicrons: inchesToMicrons(10.5), VendorID: "16Kai", CustomDisplayNameLocalized: cdd.NewLocalizedString("16 Kai")}, "16K": {Name: cdd.MediaSizeCustom, WidthMicrons: inchesToMicrons(7.6875), HeightMicrons: inchesToMicrons(10.5), VendorID: "16K", CustomDisplayNameLocalized: cdd.NewLocalizedString("16 Kai")}, } cloud-print-connector-1.12/cups/translate-ppd_test.go000066400000000000000000000415671311204274000230120ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "encoding/json" "reflect" "testing" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" ) type testdata struct { Pds *cdd.PrinterDescriptionSection Dm lib.DuplexVendorMap } func translationTest(t *testing.T, ppd string, vendorPPDOptions []string, expected testdata) { description, _, _, dm := translatePPD(ppd, vendorPPDOptions) actual := testdata{description, dm} if !reflect.DeepEqual(expected, actual) { e, _ := json.Marshal(expected) d, _ := json.Marshal(actual) t.Logf("expected\n %s\ngot\n %s", e, d) t.Fail() } } func TestTrPrintingSpeed(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *Throughput: "30"` expected := testdata{ &cdd.PrinterDescriptionSection{ PrintingSpeed: &cdd.PrintingSpeed{ []cdd.PrintingSpeedOption{ cdd.PrintingSpeedOption{ SpeedPPM: 30.0, }, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestTrMediaSize(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *PageSize: PickOne *DefaultPageSize: Letter *PageSize A3/A3: "" *PageSize ISOB5/B5 - ISO: "" *PageSize B5/B5 - JIS: "" *PageSize Letter/Letter: "" *PageSize HalfLetter/5.5x8.5: "" *PageSize w81h252/Address - 1 1/8 x 3 1/2": "<>setpagedevice" *CloseUI: *PageSize` expected := testdata{ &cdd.PrinterDescriptionSection{ MediaSize: &cdd.MediaSize{ Option: []cdd.MediaSizeOption{ cdd.MediaSizeOption{cdd.MediaSizeISOA3, mmToMicrons(297), mmToMicrons(420), false, false, "", "A3", cdd.NewLocalizedString("A3")}, cdd.MediaSizeOption{cdd.MediaSizeISOB5, mmToMicrons(176), mmToMicrons(250), false, false, "", "ISOB5", cdd.NewLocalizedString("B5 (ISO)")}, cdd.MediaSizeOption{cdd.MediaSizeJISB5, mmToMicrons(182), mmToMicrons(257), false, false, "", "B5", cdd.NewLocalizedString("B5 (JIS)")}, cdd.MediaSizeOption{cdd.MediaSizeNALetter, inchesToMicrons(8.5), inchesToMicrons(11), false, true, "", "Letter", cdd.NewLocalizedString("Letter")}, cdd.MediaSizeOption{cdd.MediaSizeCustom, inchesToMicrons(5.5), inchesToMicrons(8.5), false, false, "", "HalfLetter", cdd.NewLocalizedString("5.5x8.5")}, cdd.MediaSizeOption{cdd.MediaSizeCustom, pointsToMicrons(81), pointsToMicrons(252), false, false, "", "w81h252", cdd.NewLocalizedString(`Address - 1 1/8 x 3 1/2"`)}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestTrColor(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *ColorModel/Color Mode: PickOne *DefaultColorModel: Gray *ColorModel CMYK/Color: "(cmyk) RCsetdevicecolor" *ColorModel Gray/Black and White: "(gray) RCsetdevicecolor" *CloseUI: *ColorModel` expected := testdata{ &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{"ColorModel:CMYK", cdd.ColorTypeStandardColor, "", false, cdd.NewLocalizedString("Color")}, cdd.ColorOption{"ColorModel:Gray", cdd.ColorTypeStandardMonochrome, "", true, cdd.NewLocalizedString("Black and White")}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *CMAndResolution/Print Color as Gray: PickOne *OrderDependency: 20 AnySetup *CMAndResolution *DefaultCMAndResolution: CMYKImageRET3600 *CMAndResolution CMYKImageRET3600/Off: " <> setpagedevice" *End *CMAndResolution Gray600x600dpi/On: " <> setpagedevice" *End *CloseUI: *CMAndResolution ` expected = testdata{ &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{"CMAndResolution:CMYKImageRET3600", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, cdd.ColorOption{"CMAndResolution:Gray600x600dpi", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Gray")}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *CMAndResolution/Print Color as Gray: PickOne *OrderDependency: 20 AnySetup *CMAndResolution *DefaultCMAndResolution: CMYKImageRET2400 *CMAndResolution CMYKImageRET2400/Off - ImageRET 2400: "<< /ProcessColorModel /DeviceCMYK /HWResolution [600 600] >> setpagedevice" *CMAndResolution Gray1200x1200dpi/On - ProRes 1200: "<> setpagedevice" *CMAndResolution Gray600x600dpi/On - 600 dpi: "<> setpagedevice" *CloseUI: *CMAndResolution ` expected = testdata{ &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{"CMAndResolution:CMYKImageRET2400", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color, ImageRET 2400")}, cdd.ColorOption{"CMAndResolution:Gray1200x1200dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, ProRes 1200")}, cdd.ColorOption{"CMAndResolution:Gray600x600dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, 600 dpi")}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *SelectColor/Select Color: PickOne *OrderDependency: 10 AnySetup *SelectColor *DefaultSelectColor: Color *SelectColor Color/Color: "<> setpagedevice" *SelectColor Grayscale/Grayscale: "<> setpagedevice" *CloseUI: *SelectColor ` expected = testdata{ &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{"SelectColor:Color", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, cdd.ColorOption{"SelectColor:Grayscale", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Grayscale")}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestTrDuplex(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *Duplex/Duplex: PickOne *DefaultDuplex: None *Duplex None/Off: "" *Duplex DuplexNoTumble/Long Edge: "" *CloseUI: *Duplex` expected := testdata{ &cdd.PrinterDescriptionSection{ Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{cdd.DuplexNoDuplex, true}, cdd.DuplexOption{cdd.DuplexLongEdge, false}, }, }, }, lib.DuplexVendorMap{ cdd.DuplexNoDuplex: "Duplex:None", cdd.DuplexLongEdge: "Duplex:DuplexNoTumble", }, } translationTest(t, ppd, []string{}, expected) } func TestTrKMDuplex(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *KMDuplex/Print Type: PickOne *OrderDependency: 5 AnySetup *KMDuplex *DefaultKMDuplex: Double *KMDuplex Single/1-Sided: "<< /Duplex false >> setpagedevice << /Layout 0 >> /KMOptions /ProcSet findresource /setKMoptions get exec" *End *KMDuplex Double/2-Sided: "<< /Duplex true >> setpagedevice << /Layout 0 >> /KMOptions /ProcSet findresource /setKMoptions get exec" *End *KMDuplex Booklet/Booklet: "<< /Duplex true >> setpagedevice << /Layout 1 >> /KMOptions /ProcSet findresource /setKMoptions get exec" *End *CloseUI: *KMDuplex ` expected := testdata{ &cdd.PrinterDescriptionSection{ Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{cdd.DuplexNoDuplex, false}, cdd.DuplexOption{cdd.DuplexLongEdge, true}, cdd.DuplexOption{cdd.DuplexShortEdge, false}, }, }, }, lib.DuplexVendorMap{ cdd.DuplexNoDuplex: "KMDuplex:Single", cdd.DuplexLongEdge: "KMDuplex:Double", cdd.DuplexShortEdge: "KMDuplex:Booklet", }, } translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *KMDuplex/Duplex: Boolean *OrderDependency: 15 AnySetup *KMDuplex *DefaultKMDuplex: False *KMDuplex False/Off: "<< /Duplex false >> setpagedevice" *KMDuplex True/On: "<< /Duplex true >> setpagedevice" *CloseUI: *KMDuplex ` expected = testdata{ &cdd.PrinterDescriptionSection{ Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{cdd.DuplexNoDuplex, true}, cdd.DuplexOption{cdd.DuplexLongEdge, false}, }, }, }, lib.DuplexVendorMap{ cdd.DuplexNoDuplex: "KMDuplex:False", cdd.DuplexLongEdge: "KMDuplex:True", }, } translationTest(t, ppd, []string{}, expected) } func TestTrBRDuplex(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *BRDuplex/Two-Sided Printing: PickOne *OrderDependency: 25 AnySetup *BRDuplex *DefaultBRDuplex: None *BRDuplex DuplexTumble/DuplexTumble: " " *BRDuplex DuplexNoTumble/DuplexNoTumble: " " *BRDuplex None/OFF: " " *CloseUI: *BRDuplex ` expected := testdata{ &cdd.PrinterDescriptionSection{ Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{cdd.DuplexShortEdge, false}, cdd.DuplexOption{cdd.DuplexLongEdge, false}, cdd.DuplexOption{cdd.DuplexNoDuplex, true}, }, }, }, lib.DuplexVendorMap{ cdd.DuplexNoDuplex: "BRDuplex:None", cdd.DuplexLongEdge: "BRDuplex:DuplexNoTumble", cdd.DuplexShortEdge: "BRDuplex:DuplexTumble", }, } translationTest(t, ppd, []string{}, expected) } func TestTrDPI(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *Resolution/Resolution: PickOne *DefaultResolution: 600dpi *Resolution 600dpi/600 dpi: "" *Resolution 1200x600dpi/1200x600 dpi: "" *Resolution 1200x1200dpi/1200 dpi: "" *CloseUI: *Resolution` expected := testdata{ &cdd.PrinterDescriptionSection{ DPI: &cdd.DPI{ Option: []cdd.DPIOption{ cdd.DPIOption{600, 600, true, "", "600dpi", cdd.NewLocalizedString("600 dpi")}, cdd.DPIOption{1200, 600, false, "", "1200x600dpi", cdd.NewLocalizedString("1200x600 dpi")}, cdd.DPIOption{1200, 1200, false, "", "1200x1200dpi", cdd.NewLocalizedString("1200 dpi")}, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestTrInputSlot(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *OutputBin/Destination: PickOne *OrderDependency: 210 AnySetup *OutputBin *DefaultOutputBin: FinProof *OutputBin Standard/Internal Tray 1: "" *OutputBin Bin1/Internal Tray 2: "" *OutputBin External/External Tray: "" *CloseUI: *OutputBin` expected := testdata{ &cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{ cdd.VendorCapability{ ID: "OutputBin", Type: cdd.VendorCapabilitySelect, DisplayNameLocalized: cdd.NewLocalizedString("Destination"), SelectCap: &cdd.SelectCapability{ Option: []cdd.SelectCapabilityOption{ cdd.SelectCapabilityOption{"Standard", "", true, cdd.NewLocalizedString("Internal Tray 1")}, cdd.SelectCapabilityOption{"Bin1", "", false, cdd.NewLocalizedString("Internal Tray 2")}, cdd.SelectCapabilityOption{"External", "", false, cdd.NewLocalizedString("External Tray")}, }, }, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestTrPrintQuality(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *HPPrintQuality/Print Quality: PickOne *DefaultHPPrintQuality: FastRes1200 *HPPrintQuality FastRes1200/FastRes 1200: "" *HPPrintQuality 600dpi/600 dpi: "" *HPPrintQuality ProRes1200/ProRes 1200: "" *CloseUI: *HPPrintQuality` expected := testdata{ &cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{ cdd.VendorCapability{ ID: "HPPrintQuality", Type: cdd.VendorCapabilitySelect, DisplayNameLocalized: cdd.NewLocalizedString("Print Quality"), SelectCap: &cdd.SelectCapability{ Option: []cdd.SelectCapabilityOption{ cdd.SelectCapabilityOption{"FastRes1200", "", true, cdd.NewLocalizedString("FastRes 1200")}, cdd.SelectCapabilityOption{"600dpi", "", false, cdd.NewLocalizedString("600 dpi")}, cdd.SelectCapabilityOption{"ProRes1200", "", false, cdd.NewLocalizedString("ProRes 1200")}, }, }, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func TestRicohLockedPrint(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *JobType/JobType: PickOne *FoomaticRIPOption JobType: enum CmdLine B *OrderDependency: 255 AnySetup *JobType *DefaultJobType: Normal *JobType Normal/Normal: "%% FoomaticRIPOptionSetting: JobType=Normal" *JobType SamplePrint/Sample Print: "%% FoomaticRIPOptionSetting: JobType=SamplePrint" *JobType LockedPrint/Locked Print: "" *JobType DocServer/Document Server: "" *CloseUI: *JobType *OpenUI *LockedPrintPassword/Locked Print Password (4-8 digits): PickOne *FoomaticRIPOption LockedPrintPassword: password CmdLine C *FoomaticRIPOptionMaxLength LockedPrintPassword:8 *FoomaticRIPOptionAllowedChars LockedPrintPassword: "0-9" *OrderDependency: 255 AnySetup *LockedPrintPassword *DefaultLockedPrintPassword: None *LockedPrintPassword None/None: "" *LockedPrintPassword 4001/4001: "%% FoomaticRIPOptionSetting: LockedPrintPassword=4001" *LockedPrintPassword 4002/4002: "%% FoomaticRIPOptionSetting: LockedPrintPassword=4002" *LockedPrintPassword 4003/4003: "%% FoomaticRIPOptionSetting: LockedPrintPassword=4003" *CloseUI: *LockedPrintPassword *CustomLockedPrintPassword True/Custom Password: "" *ParamCustomLockedPrintPassword Password: 1 passcode 4 8 ` expected := testdata{ &cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{ cdd.VendorCapability{ ID: "JobType:LockedPrint/LockedPrintPassword", Type: cdd.VendorCapabilityTypedValue, DisplayNameLocalized: cdd.NewLocalizedString("Password (4 numbers)"), TypedValueCap: &cdd.TypedValueCapability{ ValueType: cdd.TypedValueCapabilityTypeString, }, }, }, }, nil, } translationTest(t, ppd, []string{}, expected) } func easyModelTest(t *testing.T, input, expected string) { got := cleanupModel(input) if expected != got { t.Logf("expected %s got %s", expected, got) t.Fail() } } func TestCleanupModel(t *testing.T) { easyModelTest(t, "C451 PS(P)", "C451") easyModelTest(t, "MD-1000 Foomatic/md2k", "MD-1000") easyModelTest(t, "M24 Foomatic/epson (recommended)", "M24") easyModelTest(t, "LaserJet 2 w/PS Foomatic/Postscript (recommended)", "LaserJet 2") easyModelTest(t, "8445 PS2", "8445") easyModelTest(t, "AL-2600 PS3 v3016.103", "AL-2600") easyModelTest(t, "AR-163FG PS, 1.1", "AR-163FG") easyModelTest(t, "3212 PXL", "3212") easyModelTest(t, "Aficio SP C431DN PDF cups-team recommended", "Aficio SP C431DN") easyModelTest(t, "PIXMA Pro9000 - CUPS+Gutenprint v5.2.8-pre1", "PIXMA Pro9000") easyModelTest(t, "LaserJet M401dne PS A4 cups-team recommended", "LaserJet M401dne") easyModelTest(t, "LaserJet 4250 PS v3010.107 cups-team Letter+Duplex", "LaserJet 4250") easyModelTest(t, "Designjet Z5200 PostScript - PS", "Designjet Z5200") easyModelTest(t, "DCP-7025 BR-Script3", "DCP-7025") easyModelTest(t, "HL-5070DN BR-Script3J", "HL-5070DN") easyModelTest(t, "HL-1450 BR-Script2", "HL-1450") easyModelTest(t, "FS-600 (KPDL-2) Foomatic/Postscript (recommended)", "FS-600") easyModelTest(t, "XP-750 Series, Epson Inkjet Printer Driver (ESC/P-R) for Linux", "XP-750 Series") easyModelTest(t, "C5700(PS)", "C5700") easyModelTest(t, "OfficeJet 7400 Foomatic/hpijs (recommended) - HPLIP 0.9.7", "OfficeJet 7400") easyModelTest(t, "LaserJet p4015n, hpcups 3.13.9", "LaserJet p4015n") easyModelTest(t, "Color LaserJet 3600 hpijs, 3.13.9, requires proprietary plugin", "Color LaserJet 3600") easyModelTest(t, "LaserJet 4250 pcl3, hpcups 3.13.9", "LaserJet 4250") easyModelTest(t, "DesignJet T790 pcl, 1.0", "DesignJet T790") } func TestTrVendorPPDOptions(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *OpenUI *CustomKey/Custom Translation: PickOne *DefaultCustomKey: Some *CustomKey None/Off: "" *CustomKey Some/On: "" *CustomKey Yes/Petunia: "" *CloseUI: *CustomKey` expected := testdata{ &cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{ cdd.VendorCapability{ ID: "CustomKey", Type: cdd.VendorCapabilitySelect, DisplayNameLocalized: cdd.NewLocalizedString("Custom Translation"), SelectCap: &cdd.SelectCapability{ Option: []cdd.SelectCapabilityOption{ cdd.SelectCapabilityOption{"None", "", false, cdd.NewLocalizedString("Off")}, cdd.SelectCapabilityOption{"Some", "", true, cdd.NewLocalizedString("On")}, cdd.SelectCapabilityOption{"Yes", "", false, cdd.NewLocalizedString("Petunia")}, }, }, }, }, }, nil, } translationTest(t, ppd, []string{"CustomKey", "AnyOtherKey"}, expected) translationTest(t, ppd, []string{"all"}, expected) translationTest(t, ppd, []string{"CustomKey", "all"}, expected) translationTest(t, ppd, []string{"AnyOtherKey", "all"}, expected) expected = testdata{ &cdd.PrinterDescriptionSection{}, nil, } translationTest(t, ppd, []string{}, expected) translationTest(t, ppd, []string{"AnyOtherKey"}, expected) } cloud-print-connector-1.12/cups/translate-ticket.go000066400000000000000000000112301311204274000224330ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "errors" "fmt" "regexp" "strconv" "strings" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" ) var rVendorIDKeyValue = regexp.MustCompile( `^([^\` + internalKeySeparator + `]+)(?:` + internalKeySeparator + `(.+))?$`) // translateTicket converts a CloudJobTicket to a map of options, suitable for a new CUPS print job. func translateTicket(printer *lib.Printer, ticket *cdd.CloudJobTicket) (map[string]string, error) { if printer == nil || ticket == nil { return map[string]string{}, nil } m := map[string]string{} for _, vti := range ticket.Print.VendorTicketItem { if vti.ID == ricohPasswordVendorID { if vti.Value == "" { // do not add specific map of options for Ricoh vendor like ppdLockedPrintPassword or ppdJobType when password is empty continue; } if !rRicohPasswordFormat.MatchString(vti.Value) { return map[string]string{}, errors.New("Invalid password format") } } for _, option := range strings.Split(vti.ID, internalValueSeparator) { var key, value string parts := rVendorIDKeyValue.FindStringSubmatch(option) if parts == nil || parts[2] == "" { key, value = option, vti.Value } else { key, value = parts[1], parts[2] } m[key] = value } } if ticket.Print.Color != nil && printer.Description.Color != nil { var colorString string if ticket.Print.Color.VendorID != "" { colorString = ticket.Print.Color.VendorID } else { // The ticket doesn't provide the VendorID. Let's find it by Type. for _, colorOption := range printer.Description.Color.Option { if ticket.Print.Color.Type == colorOption.Type { colorString = colorOption.VendorID break } } } parts := rVendorIDKeyValue.FindStringSubmatch(colorString) if parts != nil && parts[2] != "" { m[parts[1]] = parts[2] } } if ticket.Print.Duplex != nil && printer.Description.Duplex != nil { duplexString := printer.DuplexMap[ticket.Print.Duplex.Type] parts := rVendorIDKeyValue.FindStringSubmatch(duplexString) if parts != nil && parts[2] != "" { m[parts[1]] = parts[2] } } if ticket.Print.PageOrientation != nil && printer.Description.PageOrientation != nil { if orientation, exists := orientationValueByType[ticket.Print.PageOrientation.Type]; exists { m[attrOrientationRequested] = orientation } } if ticket.Print.Copies != nil && printer.Description.Copies != nil { m[attrCopies] = strconv.FormatInt(int64(ticket.Print.Copies.Copies), 10) } if ticket.Print.Margins != nil && printer.Description.Margins != nil { m[attrMediaLeftMargin] = micronsToPoints(ticket.Print.Margins.LeftMicrons) m[attrMediaRightMargin] = micronsToPoints(ticket.Print.Margins.RightMicrons) m[attrMediaTopMargin] = micronsToPoints(ticket.Print.Margins.TopMicrons) m[attrMediaBottomMargin] = micronsToPoints(ticket.Print.Margins.BottomMicrons) } if ticket.Print.DPI != nil && printer.Description.DPI != nil { if ticket.Print.DPI.VendorID != "" { m[ppdResolution] = ticket.Print.DPI.VendorID } else { for _, dpiOption := range printer.Description.DPI.Option { if ticket.Print.DPI.HorizontalDPI == dpiOption.HorizontalDPI && ticket.Print.DPI.VerticalDPI == dpiOption.VerticalDPI { m[ppdResolution] = dpiOption.VendorID } } } } if ticket.Print.FitToPage != nil && printer.Description.FitToPage != nil { switch ticket.Print.FitToPage.Type { case cdd.FitToPageFitToPage: m[attrFitToPage] = attrTrue case cdd.FitToPageNoFitting: m[attrFitToPage] = attrFalse } } if ticket.Print.MediaSize != nil && printer.Description.MediaSize != nil { if ticket.Print.MediaSize.VendorID != "" { m[ppdPageSize] = ticket.Print.MediaSize.VendorID } else { widthPoints := micronsToPoints(ticket.Print.MediaSize.WidthMicrons) heightPoints := micronsToPoints(ticket.Print.MediaSize.HeightMicrons) m[ppdPageSize] = fmt.Sprintf("Custom.%sx%s", widthPoints, heightPoints) } } if ticket.Print.Collate != nil && printer.Description.Collate != nil { if ticket.Print.Collate.Collate { m[attrCollate] = attrTrue } else { m[attrCollate] = attrFalse } } if ticket.Print.ReverseOrder != nil && printer.Description.ReverseOrder != nil { if ticket.Print.ReverseOrder.ReverseOrder { m[attrOutputOrder] = "reverse" } else { m[attrOutputOrder] = "normal" } } return m, nil } func micronsToPoints(microns int32) string { return strconv.Itoa(int(float32(microns)*72/25400 + 0.5)) } cloud-print-connector-1.12/cups/translate-ticket_test.go000066400000000000000000000217531311204274000235050ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package cups import ( "fmt" "reflect" "sort" "strings" "testing" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" ) func TestTranslateTicket(t *testing.T) { printer := lib.Printer{} expected := map[string]string{} o, err := translateTicket(&printer, nil) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected %+v, got %+v", expected, o) t.Fail() } ticket := cdd.CloudJobTicket{} o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected %+v, got %+v", expected, o) t.Fail() } printer = lib.Printer{ Description: &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: "ColorModel:zebra-stripes", Type: cdd.ColorTypeCustomMonochrome, }, }, }, Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{ Type: cdd.DuplexNoDuplex, }, }, }, PageOrientation: &cdd.PageOrientation{}, Copies: &cdd.Copies{}, Margins: &cdd.Margins{}, DPI: &cdd.DPI{ Option: []cdd.DPIOption{ cdd.DPIOption{ HorizontalDPI: 100, VerticalDPI: 100, VendorID: "q", }, }, }, FitToPage: &cdd.FitToPage{}, MediaSize: &cdd.MediaSize{}, Collate: &cdd.Collate{}, ReverseOrder: &cdd.ReverseOrder{}, }, DuplexMap: lib.DuplexVendorMap{ cdd.DuplexNoDuplex: "Duplex:None", }, } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"number-up", "a"}, cdd.VendorTicketItem{"a:b/c:d/e", "f"}, }, Color: &cdd.ColorTicketItem{VendorID: "ColorModel:zebra-stripes", Type: cdd.ColorTypeCustomMonochrome}, Duplex: &cdd.DuplexTicketItem{Type: cdd.DuplexNoDuplex}, PageOrientation: &cdd.PageOrientationTicketItem{Type: cdd.PageOrientationAuto}, Copies: &cdd.CopiesTicketItem{Copies: 2}, Margins: &cdd.MarginsTicketItem{100000, 100000, 100000, 100000}, DPI: &cdd.DPITicketItem{100, 100, "q"}, FitToPage: &cdd.FitToPageTicketItem{cdd.FitToPageNoFitting}, MediaSize: &cdd.MediaSizeTicketItem{100000, 100000, false, "r"}, Collate: &cdd.CollateTicketItem{false}, ReverseOrder: &cdd.ReverseOrderTicketItem{false}, } expected = map[string]string{ "number-up": "a", "a": "b", "c": "d", "e": "f", "ColorModel": "zebra-stripes", "Duplex": "None", "copies": "2", "media-left-margin": micronsToPoints(100000), "media-right-margin": micronsToPoints(100000), "media-top-margin": micronsToPoints(100000), "media-bottom-margin": micronsToPoints(100000), "Resolution": "q", "fit-to-page": "false", "PageSize": "r", "collate": "false", "outputorder": "normal", } o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { eSorted := make([]string, 0, len(expected)) for k := range expected { eSorted = append(eSorted, fmt.Sprintf("%s:%s", k, expected[k])) } sort.Strings(eSorted) oSorted := make([]string, 0, len(o)) for k := range o { oSorted = append(oSorted, fmt.Sprintf("%s:%s", k, o[k])) } sort.Strings(oSorted) t.Logf("expected\n %+v\ngot\n %+v", strings.Join(eSorted, ","), strings.Join(oSorted, ",")) t.Fail() } printer.Description = &cdd.PrinterDescriptionSection{ Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: "print-color-mode:color", Type: cdd.ColorTypeStandardColor, }, }, }, Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{ Type: cdd.DuplexLongEdge, }, }, }, PageOrientation: &cdd.PageOrientation{}, DPI: &cdd.DPI{ Option: []cdd.DPIOption{ cdd.DPIOption{ HorizontalDPI: 100, VerticalDPI: 100, VendorID: "q", }, }, }, MediaSize: &cdd.MediaSize{}, } printer.DuplexMap = lib.DuplexVendorMap{ cdd.DuplexLongEdge: "KMDuplex:Single", } ticket.Print = cdd.PrintTicketSection{ Color: &cdd.ColorTicketItem{VendorID: "print-color-mode:color", Type: cdd.ColorTypeStandardColor}, Duplex: &cdd.DuplexTicketItem{Type: cdd.DuplexLongEdge}, PageOrientation: &cdd.PageOrientationTicketItem{Type: cdd.PageOrientationLandscape}, DPI: &cdd.DPITicketItem{100, 100, ""}, MediaSize: &cdd.MediaSizeTicketItem{100000, 100000, false, ""}, } expected = map[string]string{ "print-color-mode": "color", "KMDuplex": "Single", "orientation-requested": "4", "Resolution": "q", "PageSize": "Custom.283x283", } o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: "CMAndResolution:Gray600x600dpi", Type: cdd.ColorTypeStandardColor, }, }, } ticket.Print = cdd.PrintTicketSection{ Color: &cdd.ColorTicketItem{VendorID: "CMAndResolution:Gray600x600dpi", Type: cdd.ColorTypeStandardColor}, } expected = map[string]string{ "CMAndResolution": "Gray600x600dpi", } o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: "SelectColor:Color", Type: cdd.ColorTypeStandardColor, }, }, } ticket.Print = cdd.PrintTicketSection{ Color: &cdd.ColorTicketItem{VendorID: "SelectColor:Color"}, } expected = map[string]string{ "SelectColor": "Color", } o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } ticket.Print = cdd.PrintTicketSection{ Color: &cdd.ColorTicketItem{Type: cdd.ColorTypeStandardColor}, } o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } } func TestTranslateTicket_RicohLockedPrint(t *testing.T) { printer := lib.Printer{} ticket := cdd.CloudJobTicket{} ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"JobType:LockedPrint/LockedPrintPassword", "1234"}, }, } expected := map[string]string{ "JobType": "LockedPrint", "LockedPrintPassword": "1234", } o, err := translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"JobType:LockedPrint/LockedPrintPassword", ""}, }, } expected = map[string]string{} o, err = translateTicket(&printer, &ticket) if err != nil { t.Logf("did not expect error %s", err) t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"JobType:LockedPrint/LockedPrintPassword", "123"}, }, } o, err = translateTicket(&printer, &ticket) if err == nil { t.Log("expected error") t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"JobType:LockedPrint/LockedPrintPassword", "12345"}, }, } o, err = translateTicket(&printer, &ticket) if err == nil { t.Log("expected error") t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"JobType:LockedPrint/LockedPrintPassword", "1bc3"}, }, } o, err = translateTicket(&printer, &ticket) if err == nil { t.Log("expected error") t.Fail() } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() } } cloud-print-connector-1.12/gcp-connector-util/000077500000000000000000000000001311204274000213735ustar00rootroot00000000000000cloud-print-connector-1.12/gcp-connector-util/gcp-cups-connector-util.go000066400000000000000000000306621311204274000264150ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "strings" "sync" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/urfave/cli" ) var commonCommands = []cli.Command{ cli.Command{ Name: "delete-all-gcp-printers", Usage: "Delete all printers associated with this connector", Action: deleteAllGCPPrinters, }, cli.Command{ Name: "backfill-config-file", Usage: "Add all keys, with default values, to the config file", Action: backfillConfigFile, }, cli.Command{ Name: "sparse-config-file", Usage: "Remove all keys, with non-default values, from the config file", Action: sparseConfigFile, }, cli.Command{ Name: "delete-gcp-job", Usage: "Deletes one GCP job", Action: deleteGCPJob, Flags: []cli.Flag{ cli.StringFlag{ Name: "job-id", }, }, }, cli.Command{ Name: "cancel-gcp-job", Usage: "Cancels one GCP job", Action: cancelGCPJob, Flags: []cli.Flag{ cli.StringFlag{ Name: "job-id", }, }, }, cli.Command{ Name: "delete-all-gcp-printer-jobs", Usage: "Delete all queued jobs associated with a printer", Action: deleteAllGCPPrinterJobs, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", }, }, }, cli.Command{ Name: "cancel-all-gcp-printer-jobs", Usage: "Cancels all queued jobs associated with a printer", Action: cancelAllGCPPrinterJobs, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", }, }, }, cli.Command{ Name: "show-gcp-printer-status", Usage: "Shows the current status of a printer and its jobs", Action: showGCPPrinterStatus, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", }, }, }, cli.Command{ Name: "share-gcp-printer", Usage: "Shares a printer with user or group", Action: shareGCPPrinter, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", Usage: "Printer to share", }, cli.StringFlag{ Name: "email", Usage: "Group or user to share with", }, cli.StringFlag{ Name: "role", Value: "USER", Usage: "Role granted. user or manager", }, cli.BoolTFlag{ Name: "skip-notification", Usage: "Skip sending email notice. Defaults to true", }, cli.BoolFlag{ Name: "public", Usage: "Make the printer public (anyone can print)", }, }, }, cli.Command{ Name: "unshare-gcp-printer", Usage: "Removes user or group access to printer", Action: unshareGCPPrinter, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", Usage: "Printer to unshare", }, cli.StringFlag{ Name: "email", Usage: "Group or user to remove", }, cli.BoolFlag{ Name: "public", Usage: "Remove public printer access", }, }, }, cli.Command{ Name: "update-gcp-printer", Usage: "Modifies settings for a printer", Action: updateGCPPrinter, Flags: []cli.Flag{ cli.StringFlag{ Name: "printer-id", Usage: "Printer to update", }, cli.BoolFlag{ Name: "enable-quota", Usage: "Set a daily per-user quota", }, cli.BoolFlag{ Name: "disable-quota", Usage: "Disable daily per-user quota", }, cli.IntFlag{ Name: "daily-quota", Usage: "Pages per-user per-day", }, }, }, } // getConfig returns a config object func getConfig(context *cli.Context) (*lib.Config, error) { config, _, err := lib.GetConfig(context) if err != nil { return nil, err } return config, nil } // getGCP returns a GoogleCloudPrint object func getGCP(config *lib.Config) (*gcp.GoogleCloudPrint, error) { return gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, 0, nil) } // backfillConfigFile opens the config file, adds all missing keys // and default values, then writes the config file back. func backfillConfigFile(context *cli.Context) error { config, cfBefore, err := lib.GetConfig(context) if err != nil { return err } if cfBefore == "" { return fmt.Errorf("Could not find a config file to backfill") } // Same config in []byte format. configRaw, err := ioutil.ReadFile(cfBefore) if err != nil { return err } // Same config in map format so that we can detect missing keys. var configMap map[string]interface{} if err = json.Unmarshal(configRaw, &configMap); err != nil { return err } if cfWritten, err := config.Backfill(configMap).ToFile(context); err != nil { return fmt.Errorf("Failed to write config file: %s", err) } else { fmt.Printf("Wrote %s\n", cfWritten) } return nil } // sparseConfigFile opens the config file, removes most keys // that have default values, then writes the config file back. func sparseConfigFile(context *cli.Context) error { config, cfBefore, err := lib.GetConfig(context) if err != nil { return err } if cfBefore == "" { return errors.New("Could not find a config file to sparse") } if cfWritten, err := config.Sparse(context).ToFile(context); err != nil { return fmt.Errorf("Failed to write config file: %s\n", err) } else { fmt.Printf("Wrote %s\n", cfWritten) } return nil } // deleteAllGCPPrinters finds all GCP printers associated with this // connector, deletes them from GCP. func deleteAllGCPPrinters(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } printers, err := gcp.List() if err != nil { return err } var wg sync.WaitGroup for gcpID, name := range printers { wg.Add(1) go func(gcpID, name string) { defer wg.Done() err := gcp.Delete(gcpID) if err != nil { fmt.Printf("Failed to delete %s \"%s\": %s\n", gcpID, name, err) } else { fmt.Printf("Deleted %s \"%s\" from GCP\n", gcpID, name) } }(gcpID, name) } wg.Wait() return nil } // deleteGCPJob deletes one GCP job func deleteGCPJob(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } err = gcp.DeleteJob(context.String("job-id")) if err != nil { return fmt.Errorf("Failed to delete GCP job %s: %s\n", context.String("job-id"), err) } fmt.Printf("Deleted GCP job %s\n", context.String("job-id")) return nil } // cancelGCPJob cancels one GCP job func cancelGCPJob(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } cancelState := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, UserActionCause: &cdd.UserActionCause{ActionCode: cdd.UserActionCauseCanceled}, }, } err = gcp.Control(context.String("job-id"), &cancelState) if err != nil { return fmt.Errorf("Failed to cancel GCP job %s: %s", context.String("job-id"), err) } fmt.Printf("Canceled GCP job %s\n", context.String("job-id")) return nil } // deleteAllGCPPrinterJobs finds all GCP printer jobs associated with a // a given printer id and deletes them. func deleteAllGCPPrinterJobs(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } jobs, err := gcp.Fetch(context.String("printer-id")) if err != nil { return err } if len(jobs) == 0 { fmt.Printf("No queued jobs\n") } ch := make(chan bool) for _, job := range jobs { go func(gcpJobID string) { err := gcp.DeleteJob(gcpJobID) if err != nil { fmt.Printf("Failed to delete GCP job %s: %s\n", gcpJobID, err) } else { fmt.Printf("Deleted GCP job %s\n", gcpJobID) } ch <- true }(job.GCPJobID) } for _ = range jobs { <-ch } return nil } // cancelAllGCPPrinterJobs finds all GCP printer jobs associated with a // a given printer id and cancels them. func cancelAllGCPPrinterJobs(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } jobs, err := gcp.Fetch(context.String("printer-id")) if err != nil { return err } if len(jobs) == 0 { fmt.Printf("No queued jobs\n") } cancelState := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, UserActionCause: &cdd.UserActionCause{ActionCode: cdd.UserActionCauseCanceled}, }, } ch := make(chan bool) for _, job := range jobs { go func(gcpJobID string) { err := gcp.Control(gcpJobID, &cancelState) if err != nil { fmt.Printf("Failed to cancel GCP job %s: %s\n", gcpJobID, err) } else { fmt.Printf("Cancelled GCP job %s\n", gcpJobID) } ch <- true }(job.GCPJobID) } for _ = range jobs { <-ch } return nil } // showGCPPrinterStatus shows the current status of a GCP printer and it's jobs func showGCPPrinterStatus(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcp, err := getGCP(config) if err != nil { return err } printer, _, err := gcp.Printer(context.String("printer-id")) if err != nil { return err } fmt.Println("Name:", printer.DefaultDisplayName) fmt.Println("State:", printer.State.State) jobs, err := gcp.Jobs(context.String("printer-id")) if err != nil { return err } // Only init common states. Unusual states like DRAFT will only be shown // if there are jobs in that state. jobStateCounts := map[string]int{ "DONE": 0, "ABORTED": 0, "QUEUED": 0, "STOPPED": 0, "IN_PROGRESS": 0, } for _, job := range jobs { jobState := string(job.SemanticState.State.Type) jobStateCounts[jobState]++ } fmt.Println("Printer jobs:") for state, count := range jobStateCounts { fmt.Println(" ", state, ":", count) } return nil } // shareGCPPrinter shares a GCP printer func shareGCPPrinter(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcpConn, err := getGCP(config) if err != nil { return err } var role gcp.Role switch strings.ToUpper(context.String("role")) { case "USER": role = gcp.User case "MANAGER": role = gcp.Manager default: return fmt.Errorf("role should be user or manager.") } err = gcpConn.Share(context.String("printer-id"), context.String("email"), role, context.Bool("skip-notification"), context.Bool("public")) var sharedWith string if context.Bool("public") { sharedWith = "public" } else { sharedWith = context.String("email") } if err != nil { return fmt.Errorf("Failed to share GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) } fmt.Printf("Shared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) return nil } // unshareGCPPrinter unshares a GCP printer. func unshareGCPPrinter(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcpConn, err := getGCP(config) if err != nil { return err } err = gcpConn.Unshare(context.String("printer-id"), context.String("email"), context.Bool("public")) var sharedWith string if context.Bool("public") { sharedWith = "public" } else { sharedWith = context.String("email") } if err != nil { return fmt.Errorf("Failed to unshare GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) } fmt.Printf("Unshared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) return nil } // updateGCPPrinter updates settings for a GCP printer. func updateGCPPrinter(context *cli.Context) error { config, err := getConfig(context) if err != nil { return err } gcpConn, err := getGCP(config) if err != nil { return err } var diff lib.PrinterDiff diff.Printer = lib.Printer{GCPID: context.String("printer-id")} if context.Bool("enable-quota") { diff.Printer.QuotaEnabled = true diff.QuotaEnabledChanged = true } else if context.Bool("disable-quota") { diff.Printer.QuotaEnabled = false diff.QuotaEnabledChanged = true } if context.Int("daily-quota") > 0 { diff.Printer.DailyQuota = context.Int("daily-quota") diff.DailyQuotaChanged = true } err = gcpConn.Update(&diff) if err != nil { return fmt.Errorf("Failed to update GCP printer %s: %s", context.String("printer-id"), err) } else { fmt.Printf("Updated GCP printer %s", context.String("printer-id")) } return nil } cloud-print-connector-1.12/gcp-connector-util/init.go000066400000000000000000000315201311204274000226660ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package main import ( "bufio" "encoding/json" "fmt" "net/http" "net/url" "os" "path/filepath" "runtime" "strings" "time" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/satori/go.uuid" "github.com/urfave/cli" "golang.org/x/oauth2" ) const ( gcpOAuthDeviceCodeURL = "https://accounts.google.com/o/oauth2/device/code" gcpOAuthTokenPollURL = "https://www.googleapis.com/oauth2/v3/token" gcpOAuthGrantTypeDevice = "http://oauth.net/grant_type/device/1.0" ) var commonInitFlags = []cli.Flag{ cli.DurationFlag{ Name: "gcp-api-timeout", Usage: "GCP API timeout, for debugging", Value: 30 * time.Second, }, cli.StringFlag{ Name: "gcp-oauth-client-id", Usage: "Identifies the CUPS Connector to the Google Cloud Print cloud service", Value: lib.DefaultConfig.GCPOAuthClientID, }, cli.StringFlag{ Name: "gcp-oauth-client-secret", Usage: "Goes along with the Client ID. Not actually secret", Value: lib.DefaultConfig.GCPOAuthClientSecret, }, cli.StringFlag{ Name: "gcp-user-refresh-token", Usage: "GCP user refresh token, useful when managing many connectors", }, cli.StringFlag{ Name: "share-scope", Usage: "Scope (user or group email address) to automatically share printers with", }, cli.StringFlag{ Name: "proxy-name", Usage: "Name for this connector instance. Should be unique per Google user account", }, cli.IntFlag{ Name: "xmpp-port", Usage: "XMPP port number", Value: int(lib.DefaultConfig.XMPPPort), }, cli.StringFlag{ Name: "xmpp-ping-timeout", Usage: "XMPP ping timeout (give up waiting for ping response after this)", Value: lib.DefaultConfig.XMPPPingTimeout, }, cli.StringFlag{ Name: "xmpp-ping-interval", Usage: "XMPP ping interval (ping every this often)", Value: lib.DefaultConfig.XMPPPingInterval, }, cli.IntFlag{ Name: "gcp-max-concurrent-downloads", Usage: "Maximum quantity of PDFs to download concurrently from GCP cloud service", Value: int(lib.DefaultConfig.GCPMaxConcurrentDownloads), }, cli.IntFlag{ Name: "native-job-queue-size", Usage: "Native job queue size", Value: int(lib.DefaultConfig.NativeJobQueueSize), }, cli.StringFlag{ Name: "native-printer-poll-interval", Usage: "Interval, in seconds, between native printer state polls", Value: lib.DefaultConfig.NativePrinterPollInterval, }, cli.BoolFlag{ Name: "prefix-job-id-to-job-title", Usage: "Whether to add the job ID to the beginning of the job title", }, cli.StringFlag{ Name: "display-name-prefix", Usage: "Prefix to add to GCP printer's display name", Value: lib.DefaultConfig.DisplayNamePrefix, }, cli.BoolFlag{ Name: "local-printing-enable", Usage: "Enable local discovery and printing (aka GCP 2.0 or Privet)", }, cli.BoolFlag{ Name: "cloud-printing-enable", Usage: "Enable cloud discovery and printing", }, cli.StringFlag{ Name: "log-level", Usage: "Minimum event severity to log: FATAL, ERROR, WARNING, INFO, DEBUG", Value: lib.DefaultConfig.LogLevel, }, cli.IntFlag{ Name: "local-port-low", Usage: "Local HTTP API server port range, low", Value: int(lib.DefaultConfig.LocalPortLow), }, cli.IntFlag{ Name: "local-port-high", Usage: "Local HTTP API server port range, high", Value: int(lib.DefaultConfig.LocalPortHigh), }, } func postWithRetry(url string, data url.Values) (*http.Response, error) { backoff := lib.Backoff{} for { response, err := http.PostForm(url, data) if err == nil { return response, err } fmt.Printf("POST to %s failed with error: %s\n", url, err) p, retryAgain := backoff.Pause() if !retryAgain { return response, err } fmt.Printf("retrying POST to %s in %s\n", url, p) time.Sleep(p) } } // getUserClientFromUser follows the token acquisition steps outlined here: // https://developers.google.com/identity/protocols/OAuth2ForDevices func getUserClientFromUser(context *cli.Context) (*http.Client, string, error) { form := url.Values{ "client_id": {context.String("gcp-oauth-client-id")}, "scope": {gcp.ScopeCloudPrint}, } response, err := postWithRetry(gcpOAuthDeviceCodeURL, form) if err != nil { return nil, "", err } var r struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` VerificationURL string `json:"verification_url"` ExpiresIn int `json:"expires_in"` Interval int `json:"interval"` } json.NewDecoder(response.Body).Decode(&r) fmt.Printf("Visit %s, and enter this code. I'll wait for you.\n%s\n", r.VerificationURL, r.UserCode) return pollOAuthConfirmation(context, r.DeviceCode, r.Interval) } func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int) (*http.Client, string, error) { config := oauth2.Config{ ClientID: context.String("gcp-oauth-client-id"), ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, }, RedirectURL: gcp.RedirectURL, Scopes: []string{gcp.ScopeCloudPrint}, } for { time.Sleep(time.Duration(interval) * time.Second) form := url.Values{ "client_id": {context.String("gcp-oauth-client-id")}, "client_secret": {context.String("gcp-oauth-client-secret")}, "code": {deviceCode}, "grant_type": {gcpOAuthGrantTypeDevice}, } response, err := postWithRetry(gcpOAuthTokenPollURL, form) if err != nil { return nil, "", err } var r struct { Error string `json:"error"` AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` } json.NewDecoder(response.Body).Decode(&r) switch r.Error { case "": token := &oauth2.Token{RefreshToken: r.RefreshToken} client := config.Client(oauth2.NoContext, token) client.Timeout = context.Duration("gcp-api-timeout") return client, r.RefreshToken, nil case "authorization_pending": case "slow_down": interval *= 2 default: return nil, "", err } } panic("unreachable") } // getUserClientFromToken creates a user client with just a refresh token. func getUserClientFromToken(context *cli.Context) *http.Client { config := &oauth2.Config{ ClientID: context.String("gcp-oauth-client-id"), ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, }, RedirectURL: gcp.RedirectURL, Scopes: []string{gcp.ScopeCloudPrint}, } token := &oauth2.Token{RefreshToken: context.String("gcp-user-refresh-token")} client := config.Client(oauth2.NoContext, token) client.Timeout = context.Duration("gcp-api-timeout") return client } // initRobotAccount creates a GCP robot account for this connector. func initRobotAccount(context *cli.Context, userClient *http.Client) (string, string, error) { params := url.Values{} params.Set("oauth_client_id", context.String("gcp-oauth-client-id")) url := fmt.Sprintf("%s%s?%s", lib.DefaultConfig.GCPBaseURL, "createrobot", params.Encode()) response, err := userClient.Get(url) if err != nil { return "", "", err } if response.StatusCode != http.StatusOK { return "", "", fmt.Errorf("Failed to initialize robot account: %s", response.Status) } var robotInit struct { Success bool `json:"success"` Message string `json:"message"` XMPPJID string `json:"xmpp_jid"` AuthCode string `json:"authorization_code"` } if err = json.NewDecoder(response.Body).Decode(&robotInit); err != nil { return "", "", err } if !robotInit.Success { return "", "", fmt.Errorf("Failed to initialize robot account: %s", robotInit.Message) } return robotInit.XMPPJID, robotInit.AuthCode, nil } func verifyRobotAccount(context *cli.Context, authCode string) (string, error) { config := &oauth2.Config{ ClientID: context.String("gcp-oauth-client-id"), ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, }, RedirectURL: gcp.RedirectURL, Scopes: []string{gcp.ScopeCloudPrint, gcp.ScopeGoogleTalk}, } token, err := config.Exchange(oauth2.NoContext, authCode) if err != nil { return "", err } return token.RefreshToken, nil } func createRobotAccount(context *cli.Context, userClient *http.Client) (string, string, error) { xmppJID, authCode, err := initRobotAccount(context, userClient) if err != nil { return "", "", err } token, err := verifyRobotAccount(context, authCode) if err != nil { return "", "", err } return xmppJID, token, nil } func scanString(prompt string) (string, error) { fmt.Println(prompt) reader := bufio.NewReader(os.Stdin) if answer, err := reader.ReadString('\n'); err != nil { return "", err } else { answer = strings.TrimSpace(answer) // remove newline fmt.Println("") return answer, nil } } func scanYesOrNo(question string) (bool, error) { for { var answer string fmt.Println(question) if _, err := fmt.Scan(&answer); err != nil { return false, err } else if parsed, value := stringToBool(answer); parsed { fmt.Println("") return value, nil } } panic("unreachable") } // The first return value is true if a boolean value could be parsed. // The second return value is the parsed boolean value if the first return value is true. func stringToBool(val string) (bool, bool) { if len(val) > 0 { switch strings.ToLower(val[0:1]) { case "y", "t", "1": return true, true case "n", "f", "0": return true, false default: return false, true } } return false, false } func initConfigFile(context *cli.Context) error { var err error var localEnable bool if runtime.GOOS == "windows" { // Remove this if block when Privet support is added to Windows. localEnable = false } else if context.IsSet("local-printing-enable") { localEnable = context.Bool("local-printing-enable") } else { fmt.Println("\"Local printing\" means that clients print directly to the connector via") fmt.Println("local subnet, and that an Internet connection is neither necessary nor used.") localEnable, err = scanYesOrNo("Enable local printing?") if err != nil { return err } } var cloudEnable bool if runtime.GOOS == "windows" { // Remove this if block when Privet support is added to Windows. cloudEnable = true } else if localEnable == false { cloudEnable = true } else if context.IsSet("cloud-printing-enable") { cloudEnable = context.Bool("cloud-printing-enable") } else { fmt.Println("\"Cloud printing\" means that clients can print from anywhere on the Internet,") fmt.Println("and that printers must be explicitly shared with users.") cloudEnable, err = scanYesOrNo("Enable cloud printing?") if err != nil { return err } } var config *lib.Config var xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string if cloudEnable { if context.IsSet("proxy-name") { proxyName = context.String("proxy-name") } else { proxyName = uuid.NewV4().String() } var userClient *http.Client var urt string if context.IsSet("gcp-user-refresh-token") { userClient = getUserClientFromToken(context) } else { userClient, urt, err = getUserClientFromUser(context) if err != nil { return err } } xmppJID, robotRefreshToken, err = createRobotAccount(context, userClient) if err != nil { return err } fmt.Println("Acquired OAuth credentials for robot account") fmt.Println("") if context.IsSet("share-scope") { shareScope = context.String("share-scope") } else { shareScope, err = scanString("Enter the email address of a user or group with whom all printers will automatically be shared or leave blank to disable automatic sharing:") if err != nil { return err } } if shareScope != "" { if context.IsSet("gcp-user-refresh-token") { userRefreshToken = context.String("gcp-user-refresh-token") } else { userRefreshToken = urt } } config = createCloudConfig(context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName, localEnable) } else { config = createLocalConfig(context) } configFilename, err := config.Sparse(context).ToFile(context) if err != nil { return err } fmt.Printf("The config file %s is ready to rock.\n", configFilename) if cloudEnable { fmt.Println("Keep it somewhere safe, as it contains an OAuth refresh token.") } socketDirectory := filepath.Dir(context.String("monitor-socket-filename")) if _, err := os.Stat(socketDirectory); os.IsNotExist(err) { fmt.Println("") fmt.Printf("When the connector runs, be sure the socket directory %s exists.\n", socketDirectory) } else if err != nil { return err } return nil } cloud-print-connector-1.12/gcp-connector-util/main_unix.go000066400000000000000000000164551311204274000237240ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package main import ( "os" "time" "github.com/google/cloud-print-connector/lib" "github.com/urfave/cli" ) var unixInitFlags = []cli.Flag{ cli.StringFlag{ Name: "log-file-name", Usage: "Log file name, full path", Value: lib.DefaultConfig.LogFileName, }, cli.IntFlag{ Name: "log-file-max-megabytes", Usage: "Log file max size, in megabytes", Value: int(lib.DefaultConfig.LogFileMaxMegabytes), }, cli.IntFlag{ Name: "log-max-files", Usage: "Maximum log file quantity before rollover", Value: int(lib.DefaultConfig.LogMaxFiles), }, cli.BoolFlag{ Name: "log-to-journal", Usage: "Log to the systemd journal (if available) instead of to log-file-name", }, cli.StringFlag{ Name: "monitor-socket-filename", Usage: "Filename of unix socket for connector-check to talk to connector", Value: lib.DefaultConfig.MonitorSocketFilename, }, cli.IntFlag{ Name: "cups-max-connections", Usage: "Max connections to CUPS server", Value: int(lib.DefaultConfig.CUPSMaxConnections), }, cli.StringFlag{ Name: "cups-connect-timeout", Usage: "CUPS timeout for opening a new connection", Value: lib.DefaultConfig.CUPSConnectTimeout, }, cli.BoolFlag{ Name: "cups-job-full-username", Usage: "Whether to use the full username (joe@example.com) in CUPS jobs", }, cli.BoolTFlag{ Name: "cups-ignore-raw-printers", Usage: "Whether to ignore CUPS raw printers", }, cli.BoolTFlag{ Name: "cups-ignore-class-printers", Usage: "Whether to ignore CUPS class printers", }, cli.BoolTFlag{ Name: "copy-printer-info-to-display-name", Usage: "Whether to copy the CUPS printer's printer-info attribute to the GCP printer's defaultDisplayName", }, } var unixCommands = []cli.Command{ cli.Command{ Name: "init", ShortName: "i", Usage: "Creates a config file", Action: initConfigFile, Flags: append(commonInitFlags, unixInitFlags...), }, cli.Command{ Name: "monitor", ShortName: "m", Usage: "Read stats from a running connector", Action: monitorConnector, Flags: []cli.Flag{ cli.DurationFlag{ Name: "monitor-timeout", Usage: "wait for a monitor response no more than this long", Value: 10 * time.Second, }, }, }, } func main() { app := cli.NewApp() app.Name = "gcp-connector-util" app.Usage = lib.ConnectorName + " for CUPS utility tools" app.Version = lib.BuildDate app.Flags = []cli.Flag{ lib.ConfigFilenameFlag, } app.Commands = append(unixCommands, commonCommands...) app.Run(os.Args) } // createCloudConfig creates a config object that supports cloud and (optionally) local mode. func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { return &lib.Config{ LocalPrintingEnable: localEnable, CloudPrintingEnable: true, XMPPJID: xmppJID, RobotRefreshToken: robotRefreshToken, UserRefreshToken: userRefreshToken, ShareScope: shareScope, ProxyName: proxyName, XMPPServer: lib.DefaultConfig.XMPPServer, XMPPPort: uint16(context.Int("xmpp-port")), XMPPPingTimeout: context.String("xmpp-ping-timeout"), XMPPPingInterval: context.String("xmpp-ping-interval"), GCPBaseURL: lib.DefaultConfig.GCPBaseURL, GCPOAuthClientID: context.String("gcp-oauth-client-id"), GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), GCPOAuthAuthURL: lib.DefaultConfig.GCPOAuthAuthURL, GCPOAuthTokenURL: lib.DefaultConfig.GCPOAuthTokenURL, GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), LocalPortLow: uint16(context.Int("local-port-low")), LocalPortHigh: uint16(context.Int("local-port-high")), LogFileName: context.String("log-file-name"), LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), LogMaxFiles: uint(context.Int("log-max-files")), LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), MonitorSocketFilename: context.String("monitor-socket-filename"), CUPSMaxConnections: uint(context.Int("cups-max-connections")), CUPSConnectTimeout: context.String("cups-connect-timeout"), CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), } } // createLocalConfig creates a config object that supports local mode. func createLocalConfig(context *cli.Context) *lib.Config { return &lib.Config{ LocalPrintingEnable: true, CloudPrintingEnable: false, NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), LocalPortLow: uint16(context.Int("local-port-low")), LocalPortHigh: uint16(context.Int("local-port-high")), LogFileName: context.String("log-file-name"), LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), LogMaxFiles: uint(context.Int("log-max-files")), LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), MonitorSocketFilename: context.String("monitor-socket-filename"), CUPSMaxConnections: uint(context.Int("cups-max-connections")), CUPSConnectTimeout: context.String("cups-connect-timeout"), CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), } } cloud-print-connector-1.12/gcp-connector-util/main_windows.go000066400000000000000000000165071311204274000244310ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package main import ( "fmt" "os" "path/filepath" "github.com/google/cloud-print-connector/lib" "github.com/urfave/cli" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" ) var windowsCommands = []cli.Command{ cli.Command{ Name: "init", ShortName: "i", Usage: "Create a config file", Action: initConfigFile, Flags: commonInitFlags, }, cli.Command{ Name: "install-event-log", Usage: "Install registry entries for the event log", Action: installEventLog, }, cli.Command{ Name: "remove-event-log", Usage: "Remove registry entries for the event log", Action: removeEventLog, }, cli.Command{ Name: "create-service", Usage: "Create a service in the local service control manager", Action: createService, }, cli.Command{ Name: "delete-service", Usage: "Delete an existing service in the local service control manager", Action: deleteService, }, cli.Command{ Name: "start-service", Usage: "Start the service in the local service control manager", Action: startService, }, cli.Command{ Name: "stop-service", Usage: "Stop the service in the local service control manager", Action: stopService, }, } func installEventLog(c *cli.Context) error { err := eventlog.InstallAsEventCreate(lib.ConnectorName, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { return fmt.Errorf("Failed to install event log registry entries: %s", err) } fmt.Println("Event log registry entries installed successfully") return nil } func removeEventLog(c *cli.Context) error { err := eventlog.Remove(lib.ConnectorName) if err != nil { return fmt.Errorf("Failed to remove event log registry entries: %s\n", err) } fmt.Println("Event log registry entries removed successfully") return nil } func createService(c *cli.Context) error { exePath, err := filepath.Abs("gcp-windows-connector.exe") if err != nil { return fmt.Errorf("Failed to find the connector executable: %s\n", err) } m, err := mgr.Connect() if err != nil { return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() config := mgr.Config{ DisplayName: lib.ConnectorName, Description: "Shares printers with Google Cloud Print", Dependencies: []string{"Spooler"}, StartType: mgr.StartAutomatic, } service, err := m.CreateService(lib.ConnectorName, exePath, config) if err != nil { return fmt.Errorf("Failed to create service: %s\n", err) } defer service.Close() fmt.Println("Service created successfully") return nil } func deleteService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() err = service.Delete() if err != nil { return fmt.Errorf("Failed to delete service: %s\n", err) } fmt.Println("Service deleted successfully") return nil } func startService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() err = service.Start() if err != nil { return fmt.Errorf("Failed to start service: %s\n", err) } fmt.Println("Service started successfully") return nil } func stopService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() _, err = service.Control(svc.Stop) if err != nil { return fmt.Errorf("Failed to stop service: %s\n", err) } fmt.Printf("Service stopped successfully") return nil } func main() { app := cli.NewApp() app.Name = "gcp-connector-util" app.Usage = lib.ConnectorName + " for Windows utility tools" app.Version = lib.BuildDate app.Flags = []cli.Flag{ lib.ConfigFilenameFlag, } app.Commands = append(windowsCommands, commonCommands...) app.Run(os.Args) } // createCloudConfig creates a config object that supports cloud and (optionally) local mode. func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { return &lib.Config{ LocalPrintingEnable: localEnable, CloudPrintingEnable: true, XMPPJID: xmppJID, RobotRefreshToken: robotRefreshToken, UserRefreshToken: userRefreshToken, ShareScope: shareScope, ProxyName: proxyName, XMPPServer: lib.DefaultConfig.XMPPServer, XMPPPort: uint16(context.Int("xmpp-port")), XMPPPingTimeout: context.String("xmpp-ping-timeout"), XMPPPingInterval: context.String("xmpp-ping-interval"), GCPBaseURL: lib.DefaultConfig.GCPBaseURL, GCPOAuthClientID: context.String("gcp-oauth-client-id"), GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), GCPOAuthAuthURL: lib.DefaultConfig.GCPOAuthAuthURL, GCPOAuthTokenURL: lib.DefaultConfig.GCPOAuthTokenURL, GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), LocalPortLow: uint16(context.Int("local-port-low")), LocalPortHigh: uint16(context.Int("local-port-high")), } } // createLocalConfig creates a config object that supports local mode. func createLocalConfig(context *cli.Context) *lib.Config { return &lib.Config{ LocalPrintingEnable: true, CloudPrintingEnable: false, NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), LocalPortLow: uint16(context.Int("local-port-low")), LocalPortHigh: uint16(context.Int("local-port-high")), } } cloud-print-connector-1.12/gcp-connector-util/monitor.go000066400000000000000000000026711311204274000234170ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package main import ( "fmt" "io/ioutil" "net" "os" "time" "github.com/google/cloud-print-connector/lib" "github.com/urfave/cli" ) func monitorConnector(context *cli.Context) error { config, filename, err := lib.GetConfig(context) if err != nil { return fmt.Errorf("Failed to read config file: %s", err) } if filename == "" { fmt.Println("No config file was found, so using defaults") } if _, err := os.Stat(config.MonitorSocketFilename); err != nil { if !os.IsNotExist(err) { return err } return fmt.Errorf( "No connector is running, or the monitoring socket %s is mis-configured", config.MonitorSocketFilename) } timer := time.AfterFunc(context.Duration("monitor-timeout"), func() { fmt.Fprintf(os.Stderr, "Monitor check timed out after %s", context.Duration("monitor-timeout").String()) os.Exit(1) }) conn, err := net.DialTimeout("unix", config.MonitorSocketFilename, time.Second) if err != nil { return fmt.Errorf( "No connector is running, or it is not listening to socket %s", config.MonitorSocketFilename) } defer conn.Close() buf, err := ioutil.ReadAll(conn) if err != nil { return err } timer.Stop() fmt.Printf(string(buf)) return nil } cloud-print-connector-1.12/gcp-cups-connector/000077500000000000000000000000001311204274000213705ustar00rootroot00000000000000cloud-print-connector-1.12/gcp-cups-connector/gcp-cups-connector.go000066400000000000000000000157301311204274000254360ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package main import ( "encoding/json" "fmt" "io" "io/ioutil" "os" "os/signal" "syscall" "time" "github.com/coreos/go-systemd/journal" "github.com/google/cloud-print-connector/cups" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" "github.com/google/cloud-print-connector/manager" "github.com/google/cloud-print-connector/monitor" "github.com/google/cloud-print-connector/privet" "github.com/google/cloud-print-connector/xmpp" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "gcp-cups-connector" app.Usage = lib.ConnectorName + " for CUPS" app.Version = lib.BuildDate app.Flags = []cli.Flag{ lib.ConfigFilenameFlag, cli.BoolFlag{ Name: "log-to-console", Usage: "Log to STDERR, in addition to configured logging", }, } app.Action = connector app.Run(os.Args) } func connector(context *cli.Context) error { config, configFilename, err := lib.GetConfig(context) if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to read config file: %s", err), 1) } logToJournal := *config.LogToJournal && journal.Enabled() logToConsole := context.Bool("log-to-console") if logToJournal { log.SetJournalEnabled(true) if logToConsole { log.SetWriter(os.Stderr) } else { log.SetWriter(ioutil.Discard) } } else { logFileMaxBytes := config.LogFileMaxMegabytes * 1024 * 1024 var logWriter io.Writer logWriter, err = log.NewLogRoller(config.LogFileName, logFileMaxBytes, config.LogMaxFiles) if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to start log roller: %s", err), 1) } if logToConsole { logWriter = io.MultiWriter(logWriter, os.Stderr) } log.SetWriter(logWriter) } logLevel, ok := log.LevelFromString(config.LogLevel) if !ok { return cli.NewExitError(fmt.Sprintf("Log level %s is not recognized", config.LogLevel), 1) } log.SetLevel(logLevel) if configFilename == "" { log.Info("No config file was found, so using defaults") } else { log.Infof("Using config file %s", configFilename) } completeConfig, _ := json.MarshalIndent(config, "", " ") log.Debugf("Config: %s", string(completeConfig)) log.Info(lib.FullName) fmt.Println(lib.FullName) if !config.CloudPrintingEnable && !config.LocalPrintingEnable { errStr := "Cannot run connector with both local_printing_enable and cloud_printing_enable set to false" log.Fatal(errStr) return cli.NewExitError(errStr, 1) } if _, err := os.Stat(config.MonitorSocketFilename); !os.IsNotExist(err) { var errStr string if err != nil { errStr = fmt.Sprintf("Failed to stat monitor socket: %s", err) } else { errStr = fmt.Sprintf( "A connector is already running, or the monitoring socket %s wasn't cleaned up properly", config.MonitorSocketFilename) } log.Fatal(errStr) return cli.NewExitError(errStr, 1) } jobs := make(chan *lib.Job, 10) xmppNotifications := make(chan xmpp.PrinterNotification, 5) var g *gcp.GoogleCloudPrint var x *xmpp.XMPP if config.CloudPrintingEnable { xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) if err != nil { errStr := fmt.Sprintf("Failed to parse xmpp ping timeout: %s", err) log.Fatal(errStr) return cli.NewExitError(errStr, 1) } xmppPingInterval, err := time.ParseDuration(config.XMPPPingInterval) if err != nil { errStr := fmt.Sprintf("Failed to parse xmpp ping interval default: %s", err) log.Fatalf(errStr) return cli.NewExitError(errStr, 1) } g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, config.GCPMaxConcurrentDownloads, jobs) if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, xmppNotifications) if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } defer x.Quit() } cupsConnectTimeout, err := time.ParseDuration(config.CUPSConnectTimeout) if err != nil { errStr := fmt.Sprintf("Failed to parse CUPS connect timeout: %s", err) log.Fatalf(errStr) return cli.NewExitError(errStr, 1) } c, err := cups.NewCUPS(*config.CUPSCopyPrinterInfoToDisplayName, *config.PrefixJobIDToJobTitle, config.DisplayNamePrefix, config.CUPSPrinterAttributes, config.CUPSVendorPPDOptions, config.CUPSMaxConnections, cupsConnectTimeout, config.PrinterBlacklist, config.PrinterWhitelist, *config.CUPSIgnoreRawPrinters, *config.CUPSIgnoreClassPrinters) if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } defer c.Quit() var priv *privet.Privet if config.LocalPrintingEnable { if g == nil { priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, nil) } else { priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, g.ProximityToken) } if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } defer priv.Quit() } nativePrinterPollInterval, err := time.ParseDuration(config.NativePrinterPollInterval) if err != nil { errStr := fmt.Sprintf("Failed to parse CUPS printer poll interval: %s", err) log.Fatal(errStr) return cli.NewExitError(errStr, 1) } pm, err := manager.NewPrinterManager(c, g, priv, nativePrinterPollInterval, config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, jobs, xmppNotifications) if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } defer pm.Quit() m, err := monitor.NewMonitor(c, g, priv, pm, config.MonitorSocketFilename) if err != nil { log.Fatal(err) return cli.NewExitError(err.Error(), 1) } defer m.Quit() if config.CloudPrintingEnable { if config.LocalPrintingEnable { log.Infof("Ready to rock as proxy '%s' and in local mode", config.ProxyName) fmt.Printf("Ready to rock as proxy '%s' and in local mode\n", config.ProxyName) } else { log.Infof("Ready to rock as proxy '%s'", config.ProxyName) fmt.Printf("Ready to rock as proxy '%s'\n", config.ProxyName) } } else { log.Info("Ready to rock in local-only mode") fmt.Println("Ready to rock in local-only mode") } waitIndefinitely() log.Info("Shutting down") fmt.Println("") fmt.Println("Shutting down") return nil } // Blocks until Ctrl-C or SIGTERM. func waitIndefinitely() { ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) <-ch go func() { // In case the process doesn't die quickly, wait for a second termination request. <-ch fmt.Println("Second termination request received") os.Exit(1) }() } cloud-print-connector-1.12/gcp-windows-connector/000077500000000000000000000000001311204274000221105ustar00rootroot00000000000000cloud-print-connector-1.12/gcp-windows-connector/gcp-windows-connector.go000066400000000000000000000137341311204274000267000ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package main import ( "encoding/json" "fmt" "os" "time" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" "github.com/google/cloud-print-connector/manager" "github.com/google/cloud-print-connector/winspool" "github.com/google/cloud-print-connector/xmpp" "github.com/urfave/cli" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" ) func main() { app := cli.NewApp() app.Name = "gcp-windows-connector" app.Usage = lib.ConnectorName + " for Windows" app.Version = lib.BuildDate app.Flags = []cli.Flag{ lib.ConfigFilenameFlag, } app.Action = runService app.Run(os.Args) } var ( runningStatus = svc.Status{ State: svc.Running, Accepts: svc.AcceptStop, } stoppingStatus = svc.Status{ State: svc.StopPending, Accepts: svc.AcceptStop, } ) type service struct { context *cli.Context interactive bool } func runService(context *cli.Context) error { interactive, err := svc.IsAnInteractiveSession() if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to detect interactive session: %s", err), 1) } s := service{context, interactive} if interactive { err = debug.Run(lib.ConnectorName, &s) } else { err = svc.Run(lib.ConnectorName, &s) } if err != nil { err = cli.NewExitError(err.Error(), 1) } return err } func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { if service.interactive { if err := log.Start(true); err != nil { fmt.Fprintf(os.Stderr, "Failed to start event log: %s\n", err) return false, 1 } } else { if err := log.Start(false); err != nil { fmt.Fprintf(os.Stderr, "Failed to start event log: %s\n", err) return false, 1 } } defer log.Stop() config, configFilename, err := lib.GetConfig(service.context) if err != nil { fmt.Fprintf(os.Stderr, "Failed to read config file: %s\n", err) return false, 1 } logLevel, ok := log.LevelFromString(config.LogLevel) if !ok { fmt.Fprintf(os.Stderr, "Log level %s is not recognized\n", config.LogLevel) return false, 1 } log.SetLevel(logLevel) if configFilename == "" { log.Info("No config file was found, so using defaults") } else { log.Infof("Using config file %s", configFilename) } completeConfig, _ := json.MarshalIndent(config, "", " ") log.Debugf("Config: %s", string(completeConfig)) log.Info(lib.FullName) if !config.CloudPrintingEnable && !config.LocalPrintingEnable { log.Fatal("Cannot run connector with both local_printing_enable and cloud_printing_enable set to false") return false, 1 } else if config.LocalPrintingEnable { log.Fatal("Local printing has not been implemented in this version of the Windows connector.") return false, 1 } jobs := make(chan *lib.Job, 10) xmppNotifications := make(chan xmpp.PrinterNotification, 5) var g *gcp.GoogleCloudPrint var x *xmpp.XMPP if config.CloudPrintingEnable { xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) if err != nil { log.Fatalf("Failed to parse xmpp ping timeout: %s", err) return false, 1 } xmppPingInterval, err := time.ParseDuration(config.XMPPPingInterval) if err != nil { log.Fatalf("Failed to parse xmpp ping interval default: %s", err) return false, 1 } g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, config.GCPMaxConcurrentDownloads, jobs) if err != nil { log.Fatal(err) return false, 1 } x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, xmppNotifications) if err != nil { log.Fatal(err) return false, 1 } defer x.Quit() } ws, err := winspool.NewWinSpool(*config.PrefixJobIDToJobTitle, config.DisplayNamePrefix, config.PrinterBlacklist, config.PrinterWhitelist) if err != nil { log.Fatal(err) return false, 1 } nativePrinterPollInterval, err := time.ParseDuration(config.NativePrinterPollInterval) if err != nil { log.Fatalf("Failed to parse printer poll interval: %s", err) return false, 1 } pm, err := manager.NewPrinterManager(ws, g, nil, nativePrinterPollInterval, config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, jobs, xmppNotifications) if err != nil { log.Fatal(err) return false, 1 } defer pm.Quit() statusHandle := svc.StatusHandle() if statusHandle != 0 { err = ws.StartPrinterNotifications(statusHandle) if err != nil { log.Error(err) } else { log.Info("Successfully registered for device notifications.") } } if config.CloudPrintingEnable { if config.LocalPrintingEnable { log.Infof("Ready to rock as proxy '%s' and in local mode", config.ProxyName) } else { log.Infof("Ready to rock as proxy '%s'", config.ProxyName) } } else { log.Info("Ready to rock in local-only mode") } s <- runningStatus for { request := <-r switch request.Cmd { case svc.Interrogate: s <- runningStatus case svc.Stop: s <- stoppingStatus log.Info("Shutting down") time.AfterFunc(time.Second*30, func() { log.Fatal("Failed to stop quickly; stopping forcefully") os.Exit(1) }) return false, 0 case svc.DeviceEvent: log.Infof("Printers change notification received %d.", request.EventType) // Delay the action to let the OS finish the process or we might // not see the new printer. Even if we miss it eventually the timed updates // will pick it up. time.AfterFunc(time.Second*5, func() { pm.SyncPrinters(false) }) default: log.Errorf("Received unsupported service command from service control manager: %d", request.Cmd) } } } cloud-print-connector-1.12/gcp/000077500000000000000000000000001311204274000164305ustar00rootroot00000000000000cloud-print-connector-1.12/gcp/gcp.go000066400000000000000000000512531311204274000175360ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ // Package gcp is the Google Cloud Print API client. package gcp import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "sort" "strconv" "strings" "time" "golang.org/x/oauth2" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" ) const ( // This prefix tickles a magic spell in GCP so that, for example, // the GCP UI shows location as the string found in the // printer-location CUPS attribute. gcpTagPrefix = "__cp__" // OAuth constants. RedirectURL = "oob" ScopeCloudPrint = "https://www.googleapis.com/auth/cloudprint" ScopeGoogleTalk = "https://www.googleapis.com/auth/googletalk" AccessType = "offline" ) // GoogleCloudPrint is the interface between Go and the Google Cloud Print API. type GoogleCloudPrint struct { baseURL string robotClient *http.Client userClient *http.Client proxyName string jobs chan<- *lib.Job downloadSemaphore *lib.Semaphore } // NewGoogleCloudPrint establishes a connection with GCP, returns a new GoogleCloudPrint object. func NewGoogleCloudPrint(baseURL, robotRefreshToken, userRefreshToken, proxyName, oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL string, maxConcurrentDownload uint, jobs chan<- *lib.Job) (*GoogleCloudPrint, error) { robotClient, err := newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, robotRefreshToken, ScopeCloudPrint, ScopeGoogleTalk) if err != nil { return nil, err } var userClient *http.Client if userRefreshToken != "" { userClient, err = newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, userRefreshToken, ScopeCloudPrint) if err != nil { return nil, err } } gcp := &GoogleCloudPrint{ baseURL: baseURL, robotClient: robotClient, userClient: userClient, proxyName: proxyName, jobs: jobs, downloadSemaphore: lib.NewSemaphore(maxConcurrentDownload), } return gcp, nil } func (gcp *GoogleCloudPrint) GetRobotAccessToken() (string, error) { token, err := gcp.robotClient.Transport.(*oauth2.Transport).Source.Token() if err != nil { return "", err } return token.AccessToken, nil } // CanShare answers the question "can we share printers when they are registered?" func (gcp *GoogleCloudPrint) CanShare() bool { return gcp.userClient != nil } // Control calls google.com/cloudprint/control to set the state of a // GCP print job. func (gcp *GoogleCloudPrint) Control(jobID string, state *cdd.PrintJobStateDiff) error { semanticState, err := json.Marshal(state) if err != nil { return err } form := url.Values{} form.Set("jobid", jobID) form.Set("semantic_state_diff", string(semanticState)) if _, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"control", form); err != nil { return err } return nil } // Delete calls google.com/cloudprint/delete to delete a printer from GCP. func (gcp *GoogleCloudPrint) Delete(gcpID string) error { form := url.Values{} form.Set("printerid", gcpID) if _, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"delete", form); err != nil { return err } return nil } // DeleteJob calls google.com/cloudprint/deletejob to delete a print job. func (gcp *GoogleCloudPrint) DeleteJob(gcpJobID string) error { form := url.Values{} form.Set("jobid", gcpJobID) if _, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"deletejob", form); err != nil { return err } return nil } // Fetch calls google.com/cloudprint/fetch to get the outstanding print jobs for // a GCP printer. func (gcp *GoogleCloudPrint) Fetch(gcpID string) ([]Job, error) { form := url.Values{} form.Set("printerid", gcpID) responseBody, errorCode, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"fetch", form) if err != nil { if errorCode == 413 { log.Debugf("No jobs returned by fetch (413 error)") // 413 means "Zero print jobs returned", which isn't really an error. return []Job{}, nil } return nil, err } var jobsData struct { Jobs []struct { ID string Title string FileURL string OwnerID string } } if err = json.Unmarshal(responseBody, &jobsData); err != nil { return nil, err } jobs := make([]Job, len(jobsData.Jobs)) for i, jobData := range jobsData.Jobs { jobs[i] = Job{ GCPPrinterID: gcpID, GCPJobID: jobData.ID, FileURL: jobData.FileURL, OwnerID: jobData.OwnerID, Title: jobData.Title, } } log.Debugf("Fetched jobs: %+v", jobs) return jobs, nil } // Jobs calls google.com/cloudprint/jobs to get print jobs for a GCP printer. func (gcp *GoogleCloudPrint) Jobs(gcpID string) ([]Job, error) { form := url.Values{} form.Set("printerid", gcpID) responseBody, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"jobs", form) if err != nil { return nil, err } var jobsData struct { Jobs []struct { ID string Title string OwnerID string SemanticState *cdd.PrintJobState } } if err = json.Unmarshal(responseBody, &jobsData); err != nil { return nil, err } jobs := make([]Job, len(jobsData.Jobs)) for i, jobData := range jobsData.Jobs { jobs[i] = Job{ GCPPrinterID: gcpID, GCPJobID: jobData.ID, OwnerID: jobData.OwnerID, Title: jobData.Title, SemanticState: jobData.SemanticState, } } return jobs, nil } // List calls google.com/cloudprint/list to get all GCP printers assigned // to this connector. // // Returns map of GCPID => printer name. GCPID is unique to GCP; printer name // should be unique to CUPS. Use Printer to get details about each printer. func (gcp *GoogleCloudPrint) List() (map[string]string, error) { form := url.Values{} form.Set("proxy", gcp.proxyName) form.Set("extra_fields", "-tags") responseBody, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"list", form) if err != nil { return nil, err } var listData struct { Printers []struct { ID string `json:"id"` Name string `json:"name"` } } if err = json.Unmarshal(responseBody, &listData); err != nil { return nil, err } printers := make(map[string]string, len(listData.Printers)) for _, p := range listData.Printers { printers[p.ID] = p.Name } return printers, nil } // Register calls google.com/cloudprint/register to register a GCP printer. // // Sets the GCPID field in the printer arg. func (gcp *GoogleCloudPrint) Register(printer *lib.Printer) error { capabilities, err := marshalCapabilities(printer.Description) if err != nil { return err } semanticState, err := json.Marshal(cdd.CloudDeviceState{Printer: printer.State}) if err != nil { return err } form := url.Values{} form.Set("name", printer.Name) form.Set("default_display_name", printer.DefaultDisplayName) form.Set("proxy", gcp.proxyName) form.Set("uuid", printer.UUID) form.Set("manufacturer", printer.Manufacturer) form.Set("model", printer.Model) form.Set("gcp_version", printer.GCPVersion) form.Set("setup_url", printer.SetupURL) form.Set("support_url", printer.SupportURL) form.Set("update_url", printer.UpdateURL) form.Set("firmware", printer.ConnectorVersion) form.Set("semantic_state", string(semanticState)) form.Set("use_cdd", "true") form.Set("capabilities", capabilities) form.Set("capsHash", printer.CapsHash) sortedKeys := make([]string, 0, len(printer.Tags)) for key := range printer.Tags { sortedKeys = append(sortedKeys, key) } sort.Strings(sortedKeys) for _, key := range sortedKeys { form.Add("tag", fmt.Sprintf("%s%s=%s", gcpTagPrefix, key, printer.Tags[key])) } responseBody, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"register", form) if err != nil { return err } var registerData struct { Printers []struct { ID string } } if err = json.Unmarshal(responseBody, ®isterData); err != nil { return err } printer.GCPID = registerData.Printers[0].ID return nil } // Update calls google.com/cloudprint/update to update a GCP printer. func (gcp *GoogleCloudPrint) Update(diff *lib.PrinterDiff) error { // Ignores Name field because it never changes. form := url.Values{} form.Set("printerid", diff.Printer.GCPID) form.Set("proxy", gcp.proxyName) if diff.DefaultDisplayNameChanged { form.Set("default_display_name", diff.Printer.DefaultDisplayName) } if diff.ManufacturerChanged { form.Set("manufacturer", diff.Printer.Manufacturer) } if diff.ModelChanged { form.Set("model", diff.Printer.Model) } if diff.GCPVersionChanged { form.Set("gcp_version", diff.Printer.GCPVersion) } if diff.SetupURLChanged { form.Set("setup_url", diff.Printer.SetupURL) } if diff.SupportURLChanged { form.Set("support_url", diff.Printer.SupportURL) } if diff.UpdateURLChanged { form.Set("update_url", diff.Printer.UpdateURL) } if diff.ConnectorVersionChanged { form.Set("firmware", diff.Printer.ConnectorVersion) } if diff.StateChanged || diff.DescriptionChanged || diff.GCPVersionChanged { semanticState, err := json.Marshal(cdd.CloudDeviceState{Printer: diff.Printer.State}) if err != nil { return err } form.Set("semantic_state", string(semanticState)) } if diff.CapsHashChanged || diff.DescriptionChanged || diff.GCPVersionChanged { capabilities, err := marshalCapabilities(diff.Printer.Description) if err != nil { return err } form.Set("use_cdd", "true") form.Set("capabilities", capabilities) form.Set("capsHash", diff.Printer.CapsHash) } if diff.TagsChanged { sortedKeys := make([]string, 0, len(diff.Printer.Tags)) for key := range diff.Printer.Tags { sortedKeys = append(sortedKeys, key) } sort.Strings(sortedKeys) for _, key := range sortedKeys { form.Add("tag", fmt.Sprintf("%s%s=%s", gcpTagPrefix, key, diff.Printer.Tags[key])) } form.Set("remove_tag", gcpTagPrefix+".*") } if diff.QuotaEnabledChanged { form.Set("quota_enabled", strconv.FormatBool(diff.Printer.QuotaEnabled)) } if diff.DailyQuotaChanged { form.Set("daily_quota", strconv.Itoa(diff.Printer.DailyQuota)) } if _, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"update", form); err != nil { return err } return nil } // Printer gets the printer identified by it's GCPID. // // The second return value is queued print job quantity. func (gcp *GoogleCloudPrint) Printer(gcpID string) (*lib.Printer, uint, error) { form := url.Values{} form.Set("printerid", gcpID) form.Set("use_cdd", "true") form.Set("extra_fields", "queuedJobsCount,semanticState") responseBody, _, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"printer", form) if err != nil { return nil, 0, err } var printersData struct { Printers []struct { ID string `json:"id"` Name string `json:"name"` DefaultDisplayName string `json:"defaultDisplayName"` UUID string `json:"uuid"` Manufacturer string `json:"manufacturer"` Model string `json:"model"` GCPVersion string `json:"gcpVersion"` SetupURL string `json:"setupUrl"` SupportURL string `json:"supportUrl"` UpdateURL string `json:"updateUrl"` Firmware string `json:"firmware"` Capabilities cdd.CloudDeviceDescription `json:"capabilities"` CapsHash string `json:"capsHash"` Tags []string `json:"tags"` QueuedJobsCount uint `json:"queuedJobsCount"` SemanticState cdd.CloudDeviceState `json:"semanticState"` } } if err = json.Unmarshal(responseBody, &printersData); err != nil { return nil, 0, err } p := printersData.Printers[0] // If the slice were empty, postWithRetry would have returned an error. tags := make(map[string]string) for _, tag := range p.Tags { s := strings.SplitN(tag[len(gcpTagPrefix):], "=", 2) key := s[0] var value string if len(s) > 1 { value = s[1] } tags[key] = value } printer := &lib.Printer{ GCPID: p.ID, Name: p.Name, DefaultDisplayName: p.DefaultDisplayName, UUID: p.UUID, Manufacturer: p.Manufacturer, Model: p.Model, GCPVersion: p.GCPVersion, SetupURL: p.SetupURL, SupportURL: p.SupportURL, UpdateURL: p.UpdateURL, ConnectorVersion: p.Firmware, State: p.SemanticState.Printer, Description: p.Capabilities.Printer, CapsHash: p.CapsHash, Tags: tags, } return printer, p.QueuedJobsCount, err } func marshalCapabilities(description *cdd.PrinterDescriptionSection) (string, error) { capabilities := cdd.CloudDeviceDescription{ Version: "1.0", Printer: description, } cdd, err := json.Marshal(capabilities) if err != nil { return "", fmt.Errorf("Failed to remarshal translated CDD: %s", err) } return string(cdd), nil } // Role is the role a user or group is granted when sharing a printer. type Role string const ( User Role = "USER" Manager Role = "MANAGER" Owner Role = "OWNER" ) // Share calls google.com/cloudprint/share to share a registered GCP printer. func (gcp *GoogleCloudPrint) Share(gcpID, shareScope string, role Role, skip_notification bool, public bool) error { if gcp.userClient == nil { return errors.New("Cannot share because user OAuth credentials not provided.") } form := url.Values{} form.Set("printerid", gcpID) if public { form.Set("public", "true") } else { form.Set("skip_notification", strconv.FormatBool(skip_notification)) form.Set("role", string(role)) form.Set("scope", shareScope) } if _, _, _, err := postWithRetry(gcp.userClient, gcp.baseURL+"share", form); err != nil { return err } return nil } // Unshare calls google.com/cloudprint/unshare to unshare a registered GCP printer. func (gcp *GoogleCloudPrint) Unshare(gcpID, shareScope string, public bool) error { if gcp.userClient == nil { return errors.New("Cannot unshare because user OAuth credentials not provided.") } form := url.Values{} form.Set("printerid", gcpID) if public { form.Set("public", "true") } else { form.Set("scope", shareScope) } if _, _, _, err := postWithRetry(gcp.userClient, gcp.baseURL+"unshare", form); err != nil { return err } return nil } // Download downloads a URL (a print job data file) directly to a Writer. func (gcp *GoogleCloudPrint) Download(dst io.Writer, url string) error { response, err := getWithRetry(gcp.robotClient, url) if err != nil { return err } defer response.Body.Close() _, err = io.Copy(dst, response.Body) if err != nil { return err } return nil } // Ticket gets a ticket, aka print job options. func (gcp *GoogleCloudPrint) Ticket(gcpJobID string) (*cdd.CloudJobTicket, error) { form := url.Values{} form.Set("jobid", gcpJobID) form.Set("use_cjt", "true") responseBody, _, httpStatusCode, err := postWithRetry(gcp.robotClient, gcp.baseURL+"ticket", form) // The /ticket API is different than others, because it only returns the // standard GCP error information on success=false. if httpStatusCode != http.StatusOK { return nil, err } d := json.NewDecoder(bytes.NewReader(responseBody)) d.UseNumber() // Force large numbers not to be formatted with scientific notation. var ticket cdd.CloudJobTicket err = d.Decode(&ticket) if err != nil { return nil, fmt.Errorf("Failed to unmarshal ticket: %s", err) } return &ticket, nil } // ProximityToken gets a proximity token for Privet users to access a printer // through the cloud. // // Returns byte array of raw JSON to preserve any/all returned fields // and returned HTTP status code. func (gcp *GoogleCloudPrint) ProximityToken(gcpID, user string) ([]byte, int, error) { form := url.Values{} form.Set("printerid", gcpID) form.Set("user", user) responseBody, _, httpStatus, err := postWithRetry(gcp.robotClient, gcp.baseURL+"proximitytoken", form) return responseBody, httpStatus, err } // ListPrinters calls gcp.List, then calls gcp.Printer, one goroutine per // printer. This is a fast way to fetch all printers with corresponding CDD // info, which the List API does not provide. // // The second return value is a map of GCPID -> queued print job quantity. func (gcp *GoogleCloudPrint) ListPrinters() ([]lib.Printer, map[string]uint, error) { ids, err := gcp.List() if err != nil { return nil, nil, err } type response struct { printer *lib.Printer queuedJobsCount uint err error } ch := make(chan response) for id := range ids { go func(id string) { printer, queuedJobsCount, err := gcp.Printer(id) ch <- response{printer, queuedJobsCount, err} }(id) } errs := make([]error, 0) printers := make([]lib.Printer, 0, len(ids)) queuedJobsCount := make(map[string]uint) for _ = range ids { r := <-ch if r.err != nil { errs = append(errs, r.err) continue } printers = append(printers, *r.printer) if r.queuedJobsCount > 0 { queuedJobsCount[r.printer.GCPID] = r.queuedJobsCount } } if len(errs) == 0 { return printers, queuedJobsCount, nil } else if len(errs) == 1 { return nil, nil, errs[0] } else { // Return an error that is somewhat human-readable. b := bytes.NewBufferString(fmt.Sprintf("%d errors: ", len(errs))) for i, err := range errs { if i > 0 { b.WriteString(", ") } b.WriteString(err.Error()) } return nil, nil, errors.New(b.String()) } } // HandleJobs gets and processes jobs waiting on a printer. func (gcp *GoogleCloudPrint) HandleJobs(printer *lib.Printer, reportJobFailed func()) { jobs, err := gcp.Fetch(printer.GCPID) if err != nil { log.Errorf("Failed to fetch jobs for GCP printer %s: %s", printer.GCPID, err) } else { for i := range jobs { go gcp.processJob(&jobs[i], printer, reportJobFailed) } } } // processJob performs these steps: // // 1) Assembles the job resources (printer, ticket, data) // 2) Creates a new job in CUPS. // 3) Follows up with the job state until done or error. // 4) Deletes temporary file. // // Nothing is returned; intended for use as goroutine. func (gcp *GoogleCloudPrint) processJob(job *Job, printer *lib.Printer, reportJobFailed func()) { log.InfoJobf(job.GCPJobID, "Received from cloud") ticket, filename, message, state := gcp.assembleJob(job) if message != "" { reportJobFailed() log.ErrorJob(job.GCPJobID, message) if err := gcp.Control(job.GCPJobID, state); err != nil { log.ErrorJob(job.GCPJobID, err) } return } gcp.jobs <- &lib.Job{ NativePrinterName: printer.Name, Filename: filename, Title: job.Title, User: job.OwnerID, JobID: job.GCPJobID, Ticket: ticket, UpdateJob: gcp.Control, } } // assembleJob prepares for printing a job by fetching the job's ticket and payload. // // The caller is responsible to remove the returned file. // // Errors are returned as a string (last return value), for reporting // to GCP and local log. func (gcp *GoogleCloudPrint) assembleJob(job *Job) (*cdd.CloudJobTicket, string, string, *cdd.PrintJobStateDiff) { ticket, err := gcp.Ticket(job.GCPJobID) if err != nil { return nil, "", fmt.Sprintf("Failed to get a ticket: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseInvalidTicket}, }, } } file, err := ioutil.TempFile("", "cloud-print-connector-") if err != nil { return nil, "", fmt.Sprintf("Failed to create a temporary file: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseOther}, }, } } gcp.downloadSemaphore.Acquire() t := time.Now() // Do not check err until semaphore is released and timer is stopped. err = gcp.Download(file, job.FileURL) dt := time.Since(t) gcp.downloadSemaphore.Release() if err != nil { // Clean up this temporary file so the caller doesn't need extra logic. os.Remove(file.Name()) return nil, "", fmt.Sprintf("Failed to download data: %s", err), &cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseDownloadFailure}, }, } } log.InfoJobf(job.GCPJobID, "Downloaded in %s", dt.String()) defer file.Close() log.DebugJobf(job.GCPJobID, "Assembled with file %s: %+v", file.Name(), ticket.Print.Color) return ticket, file.Name(), "", &cdd.PrintJobStateDiff{} } cloud-print-connector-1.12/gcp/http.go000066400000000000000000000125571311204274000177500ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package gcp import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" "time" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" "golang.org/x/oauth2" ) /* glibc < 2.20 and OSX 10.10 have problems when C.getaddrinfo is called many times concurrently. When the connector shares more than about 230 printers, and GCP is called once per printer in concurrent goroutines, http.Client.Do starts to fail with a lookup error. This solution, a semaphore, limits the quantity of concurrent HTTP requests, which also limits the quantity of concurrent calls to net.LookupHost (which calls C.getaddrinfo()). I would rather wait for the Go compiler to solve this problem than make this a configurable option, hence this long-winded comment. https://github.com/golang/go/issues/3575 https://github.com/golang/go/issues/6336 */ var lock *lib.Semaphore = lib.NewSemaphore(100) // newClient creates an instance of http.Client, wrapped with OAuth credentials. func newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, refreshToken string, scopes ...string) (*http.Client, error) { config := oauth2.Config{ ClientID: oauthClientID, ClientSecret: oauthClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: oauthAuthURL, TokenURL: oauthTokenURL, }, RedirectURL: RedirectURL, Scopes: scopes, } token := oauth2.Token{RefreshToken: refreshToken} client := config.Client(oauth2.NoContext, &token) return client, nil } // getWithRetry calls get() and retries on HTTP temp failure // (response code 500-599). func getWithRetry(hc *http.Client, url string) (*http.Response, error) { backoff := lib.Backoff{} for { response, err := get(hc, url) if response != nil && response.StatusCode == http.StatusOK { return response, err } else if response != nil && response.StatusCode >= 500 && response.StatusCode <= 599 { p, retryAgain := backoff.Pause() if !retryAgain { log.Debugf("HTTP error %s, retry timeout hit", err) return response, err } log.Debugf("HTTP error %s, retrying after %s", err, p) time.Sleep(p) } else { log.Debugf("Permanent HTTP error %s, will not retry", err) return response, err } } } // get GETs a URL. Returns the response object (not body), in case the body // is very large. // // The caller must close the returned Response.Body object if err == nil. func get(hc *http.Client, url string) (*http.Response, error) { request, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } request.Header.Set("X-CloudPrint-Proxy", lib.ShortName) lock.Acquire() response, err := hc.Do(request) lock.Release() if err != nil { return response, fmt.Errorf("GET failure: %s", err) } if response.StatusCode != http.StatusOK { return response, fmt.Errorf("GET HTTP-level failure: %s %s", url, response.Status) } return response, nil } // postWithRetry calls post() and retries on HTTP temp failure // (response code 500-599). func postWithRetry(hc *http.Client, url string, form url.Values) ([]byte, uint, int, error) { backoff := lib.Backoff{} for { responseBody, gcpErrorCode, httpStatusCode, err := post(hc, url, form) if responseBody != nil && httpStatusCode == http.StatusOK { return responseBody, gcpErrorCode, httpStatusCode, err } else if responseBody != nil && httpStatusCode >= 500 && httpStatusCode <= 599 { p, retryAgain := backoff.Pause() if !retryAgain { log.Debugf("HTTP error %s, retry timeout hit", err) return responseBody, gcpErrorCode, httpStatusCode, err } log.Debugf("HTTP error %s, retrying after %s", err, p) time.Sleep(p) } else { log.Debugf("Permanent HTTP error %s, will not retry", err) return responseBody, gcpErrorCode, httpStatusCode, err } } } // post POSTs to a URL. Returns the body of the response. // // Returns the response body, GCP error code, HTTP status, and error. // None of the returned fields is guaranteed to be non-zero. func post(hc *http.Client, url string, form url.Values) ([]byte, uint, int, error) { requestBody := strings.NewReader(form.Encode()) request, err := http.NewRequest("POST", url, requestBody) if err != nil { return nil, 0, 0, err } request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("X-CloudPrint-Proxy", lib.ShortName) lock.Acquire() response, err := hc.Do(request) lock.Release() if err != nil { return nil, 0, 0, fmt.Errorf("POST failure: %s", err) } defer response.Body.Close() responseBody, err := ioutil.ReadAll(response.Body) if err != nil { return nil, 0, response.StatusCode, err } if response.StatusCode != http.StatusOK { return responseBody, 0, response.StatusCode, fmt.Errorf("/%s POST HTTP-level failure: %s", url, response.Status) } var responseStatus struct { Success bool Message string ErrorCode uint } if err = json.Unmarshal(responseBody, &responseStatus); err != nil { return responseBody, 0, response.StatusCode, err } if !responseStatus.Success { return responseBody, responseStatus.ErrorCode, response.StatusCode, fmt.Errorf( "%s call failed: %s", url, responseStatus.Message) } return responseBody, responseStatus.ErrorCode, response.StatusCode, nil } cloud-print-connector-1.12/gcp/job.go000066400000000000000000000006751311204274000175410ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package gcp import "github.com/google/cloud-print-connector/cdd" type Job struct { GCPPrinterID string GCPJobID string FileURL string OwnerID string Title string SemanticState *cdd.PrintJobState } cloud-print-connector-1.12/lib/000077500000000000000000000000001311204274000164255ustar00rootroot00000000000000cloud-print-connector-1.12/lib/backoff.go000066400000000000000000000026411311204274000203520ustar00rootroot00000000000000/* Copyright 2016 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "math/rand" "time" ) const ( initialRetryInterval = 500 * time.Millisecond maxInterval = 1 * time.Minute maxElapsedTime = 15 * time.Minute multiplier = 1.5 randomizationFactor = 0.5 ) // Backoff provides a mechanism for determining a good amount of time before // retrying an operation. type Backoff struct { interval time.Duration elapsedTime time.Duration } // Pause returns the amount of time to wait before retrying an operation and true if // it is ok to try again or false if the operation should be abandoned. func (b *Backoff) Pause() (time.Duration, bool) { if b.interval == 0 { // first time b.interval = initialRetryInterval b.elapsedTime = 0 } // interval from [1 - randomizationFactor, 1 + randomizationFactor) randomizedInterval := time.Duration((rand.Float64()*(2*randomizationFactor) + (1 - randomizationFactor)) * float64(b.interval)) b.elapsedTime += randomizedInterval if b.elapsedTime > maxElapsedTime { return 0, false } // Increase interval up to the interval cap b.interval = time.Duration(float64(b.interval) * multiplier) if b.interval > maxInterval { b.interval = maxInterval } return randomizedInterval, true } cloud-print-connector-1.12/lib/backoff_test.go000066400000000000000000000020721311204274000214070ustar00rootroot00000000000000/* Copyright 2016 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "testing" "time" ) func TestBackoffMultiple(t *testing.T) { b := &Backoff{} // with the current parameters, we will be able to wait at least 19 times before hitting the max for i := 0; i < 19; i++ { p, ok := b.Pause() t.Logf("iteration %d pausing for %s", i, p) if !ok { t.Fatalf("hit the pause timeout after %d pauses", i) } } } func TestBackoffTimeout(t *testing.T) { var elapsed time.Duration b := &Backoff{} // with the current parameters, we will hit the timeout at or before 40 pauses for i := 0; i < 40; i++ { p, ok := b.Pause() elapsed += p t.Logf("iteration %d pausing for %s (total %s)", i, p, elapsed) if !ok { break } } if _, ok := b.Pause(); ok { t.Fatalf("did not hit the pause timeout") } if elapsed > maxElapsedTime { t.Fatalf("waited too long: %s > %s", elapsed, maxElapsedTime) } } cloud-print-connector-1.12/lib/concprintermap.go000066400000000000000000000042511311204274000220020ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import "sync" // ConcurrentPrinterMap is a map-like data structure that is also // thread-safe. Printers are keyed by Printer.Name and Printer.GCPID. type ConcurrentPrinterMap struct { byNativeName map[string]Printer byGCPID map[string]Printer mutex sync.RWMutex } // NewConcurrentPrinterMap initializes an empty ConcurrentPrinterMap. func NewConcurrentPrinterMap(printers []Printer) *ConcurrentPrinterMap { cpm := ConcurrentPrinterMap{} // TODO will this fail on nil? cpm.Refresh(printers) return &cpm } // Refresh replaces the internal (non-concurrent) map with newPrinters. func (cpm *ConcurrentPrinterMap) Refresh(newPrinters []Printer) { c := make(map[string]Printer, len(newPrinters)) for _, printer := range newPrinters { c[printer.Name] = printer } g := make(map[string]Printer, len(newPrinters)) for _, printer := range newPrinters { if len(printer.GCPID) > 0 { g[printer.GCPID] = printer } } cpm.mutex.Lock() defer cpm.mutex.Unlock() cpm.byNativeName = c cpm.byGCPID = g } // Get gets a printer, using the native name as key. // // The second return value is true if the entry exists. func (cpm *ConcurrentPrinterMap) GetByNativeName(name string) (Printer, bool) { cpm.mutex.RLock() defer cpm.mutex.RUnlock() if p, exists := cpm.byNativeName[name]; exists { return p, true } return Printer{}, false } // Get gets a printer, using the GCP ID as key. // // The second return value is true if the entry exists. func (cpm *ConcurrentPrinterMap) GetByGCPID(gcpID string) (Printer, bool) { cpm.mutex.RLock() defer cpm.mutex.RUnlock() if p, exists := cpm.byGCPID[gcpID]; exists { return p, true } return Printer{}, false } // GetAll returns a slice of all printers. func (cpm *ConcurrentPrinterMap) GetAll() []Printer { cpm.mutex.RLock() defer cpm.mutex.RUnlock() printers := make([]Printer, len(cpm.byNativeName)) i := 0 for _, printer := range cpm.byNativeName { printers[i] = printer i++ } return printers } cloud-print-connector-1.12/lib/config.go000066400000000000000000000155621311204274000202320ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "encoding/json" "io/ioutil" "reflect" "runtime" "github.com/urfave/cli" ) const ( ConnectorName = "Google Cloud Print Connector" // A website with user-friendly information. ConnectorHomeURL = "https://github.com/google/cloud-print-connector" GCPAPIVersion = "2.0" ) var ( ConfigFilenameFlag = cli.StringFlag{ Name: "config-filename", Usage: "Connector config filename", Value: defaultConfigFilename, } // To be populated by something like: // go install -ldflags "-X github.com/google/cloud-print-connector/lib.BuildDate=`date +%Y.%m.%d`" BuildDate = "DEV" ShortName = platformName + " Connector " + BuildDate + "-" + runtime.GOOS FullName = ConnectorName + " for " + platformName + " version " + BuildDate + "-" + runtime.GOOS ) // PointerToBool converts a boolean value (constant) to a pointer-to-bool. func PointerToBool(b bool) *bool { return &b } // GetConfig reads a Config object from the config file indicated by the config // filename flag. If no such file exists, then DefaultConfig is returned. func GetConfig(context *cli.Context) (*Config, string, error) { cf, exists := getConfigFilename(context) if !exists { return &DefaultConfig, "", nil } configRaw, err := ioutil.ReadFile(cf) if err != nil { return nil, "", err } config := new(Config) if err = json.Unmarshal(configRaw, config); err != nil { return nil, "", err } // Same config as a map so that we can detect missing keys. var configMap map[string]interface{} if err = json.Unmarshal(configRaw, &configMap); err != nil { return nil, "", err } b := config.Backfill(configMap) return b, cf, nil } // ToFile writes this Config object to the config file indicated by ConfigFile. func (c *Config) ToFile(context *cli.Context) (string, error) { b, err := json.MarshalIndent(c, "", " ") if err != nil { return "", err } cf, _ := getConfigFilename(context) if err = ioutil.WriteFile(cf, b, 0600); err != nil { return "", err } return cf, nil } func (c *Config) commonSparse(context *cli.Context) *Config { s := *c if s.XMPPServer == DefaultConfig.XMPPServer { s.XMPPServer = "" } if !context.IsSet("xmpp-port") && s.XMPPPort == DefaultConfig.XMPPPort { s.XMPPPort = 0 } if !context.IsSet("xmpp-ping-timeout") && s.XMPPPingTimeout == DefaultConfig.XMPPPingTimeout { s.XMPPPingTimeout = "" } if !context.IsSet("xmpp-ping-interval") && s.XMPPPingInterval == DefaultConfig.XMPPPingInterval { s.XMPPPingInterval = "" } if s.GCPBaseURL == DefaultConfig.GCPBaseURL { s.GCPBaseURL = "" } if s.GCPOAuthClientID == DefaultConfig.GCPOAuthClientID { s.GCPOAuthClientID = "" } if s.GCPOAuthClientSecret == DefaultConfig.GCPOAuthClientSecret { s.GCPOAuthClientSecret = "" } if s.GCPOAuthAuthURL == DefaultConfig.GCPOAuthAuthURL { s.GCPOAuthAuthURL = "" } if s.GCPOAuthTokenURL == DefaultConfig.GCPOAuthTokenURL { s.GCPOAuthTokenURL = "" } if !context.IsSet("gcp-max-concurrent-downloads") && s.GCPMaxConcurrentDownloads == DefaultConfig.GCPMaxConcurrentDownloads { s.GCPMaxConcurrentDownloads = 0 } if !context.IsSet("native-job-queue-size") && s.NativeJobQueueSize == DefaultConfig.NativeJobQueueSize { s.NativeJobQueueSize = 0 } if !context.IsSet("native-printer-poll-interval") && s.NativePrinterPollInterval == DefaultConfig.NativePrinterPollInterval { s.NativePrinterPollInterval = "" } if !context.IsSet("cups-job-full-username") && reflect.DeepEqual(s.CUPSJobFullUsername, DefaultConfig.CUPSJobFullUsername) { s.CUPSJobFullUsername = nil } if !context.IsSet("prefix-job-id-to-job-title") && reflect.DeepEqual(s.PrefixJobIDToJobTitle, DefaultConfig.PrefixJobIDToJobTitle) { s.PrefixJobIDToJobTitle = nil } if !context.IsSet("display-name-prefix") && s.DisplayNamePrefix == DefaultConfig.DisplayNamePrefix { s.DisplayNamePrefix = "" } if !context.IsSet("local-port-low") && s.LocalPortLow == DefaultConfig.LocalPortLow { s.LocalPortLow = 0 } if !context.IsSet("local-port-high") && s.LocalPortHigh == DefaultConfig.LocalPortHigh { s.LocalPortHigh = 0 } return &s } func (c *Config) commonBackfill(configMap map[string]interface{}) *Config { b := *c if _, exists := configMap["xmpp_server"]; !exists { b.XMPPServer = DefaultConfig.XMPPServer } if _, exists := configMap["xmpp_port"]; !exists { b.XMPPPort = DefaultConfig.XMPPPort } if _, exists := configMap["gcp_xmpp_ping_timeout"]; !exists { b.XMPPPingTimeout = DefaultConfig.XMPPPingTimeout } if _, exists := configMap["gcp_xmpp_ping_interval_default"]; !exists { b.XMPPPingInterval = DefaultConfig.XMPPPingInterval } if _, exists := configMap["gcp_base_url"]; !exists { b.GCPBaseURL = DefaultConfig.GCPBaseURL } if _, exists := configMap["gcp_oauth_client_id"]; !exists { b.GCPOAuthClientID = DefaultConfig.GCPOAuthClientID } if _, exists := configMap["gcp_oauth_client_secret"]; !exists { b.GCPOAuthClientSecret = DefaultConfig.GCPOAuthClientSecret } if _, exists := configMap["gcp_oauth_auth_url"]; !exists { b.GCPOAuthAuthURL = DefaultConfig.GCPOAuthAuthURL } if _, exists := configMap["gcp_oauth_token_url"]; !exists { b.GCPOAuthTokenURL = DefaultConfig.GCPOAuthTokenURL } if _, exists := configMap["gcp_max_concurrent_downloads"]; !exists { b.GCPMaxConcurrentDownloads = DefaultConfig.GCPMaxConcurrentDownloads } if _, exists := configMap["cups_job_queue_size"]; !exists { b.NativeJobQueueSize = DefaultConfig.NativeJobQueueSize } if _, exists := configMap["cups_printer_poll_interval"]; !exists { b.NativePrinterPollInterval = DefaultConfig.NativePrinterPollInterval } if _, exists := configMap["cups_job_full_username"]; !exists { b.CUPSJobFullUsername = DefaultConfig.CUPSJobFullUsername } if _, exists := configMap["prefix_job_id_to_job_title"]; !exists { b.PrefixJobIDToJobTitle = DefaultConfig.PrefixJobIDToJobTitle } if _, exists := configMap["display_name_prefix"]; !exists { b.DisplayNamePrefix = DefaultConfig.DisplayNamePrefix } if _, exists := configMap["printer_blacklist"]; !exists { b.PrinterBlacklist = DefaultConfig.PrinterBlacklist } if _, exists := configMap["printer_whitelist"]; !exists { b.PrinterWhitelist = DefaultConfig.PrinterWhitelist } if _, exists := configMap["local_printing_enable"]; !exists { b.LocalPrintingEnable = DefaultConfig.LocalPrintingEnable } if _, exists := configMap["cloud_printing_enable"]; !exists { b.CloudPrintingEnable = DefaultConfig.CloudPrintingEnable } if _, exists := configMap["log_level"]; !exists { b.LogLevel = DefaultConfig.LogLevel } if _, exists := configMap["local_port_low"]; !exists { b.LocalPortLow = DefaultConfig.LocalPortLow } if _, exists := configMap["local_port_high"]; !exists { b.LocalPortHigh = DefaultConfig.LocalPortHigh } return &b } cloud-print-connector-1.12/lib/config_unix.go000066400000000000000000000302221311204274000212630ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package lib import ( "os" "path/filepath" "reflect" "github.com/urfave/cli" "launchpad.net/go-xdg/v0" ) const ( platformName = "CUPS" defaultConfigFilename = "gcp-cups-connector.config.json" ) type Config struct { // Enable local discovery and printing. LocalPrintingEnable bool `json:"local_printing_enable"` // Enable cloud discovery and printing. CloudPrintingEnable bool `json:"cloud_printing_enable"` // Associated with root account. XMPP credential. XMPPJID string `json:"xmpp_jid,omitempty"` // Associated with robot account. Used for acquiring OAuth access tokens. RobotRefreshToken string `json:"robot_refresh_token,omitempty"` // Associated with user account. Used for sharing GCP printers; may be omitted. UserRefreshToken string `json:"user_refresh_token,omitempty"` // Scope (user, group, domain) to share printers with. ShareScope string `json:"share_scope,omitempty"` // User-chosen name of this proxy. Should be unique per Google user account. ProxyName string `json:"proxy_name,omitempty"` // XMPP server FQDN. XMPPServer string `json:"xmpp_server,omitempty"` // XMPP server port number. XMPPPort uint16 `json:"xmpp_port,omitempty"` // XMPP ping timeout (give up waiting after this time). // TODO: Rename with "gcp_" removed. XMPPPingTimeout string `json:"gcp_xmpp_ping_timeout,omitempty"` // XMPP ping interval (time between ping attempts). // TODO: Rename with "gcp_" removed. // TODO: Rename with "_default" removed. XMPPPingInterval string `json:"gcp_xmpp_ping_interval_default,omitempty"` // GCP API URL prefix. GCPBaseURL string `json:"gcp_base_url,omitempty"` // OAuth2 client ID (not unique per client). GCPOAuthClientID string `json:"gcp_oauth_client_id,omitempty"` // OAuth2 client secret (not unique per client). GCPOAuthClientSecret string `json:"gcp_oauth_client_secret,omitempty"` // OAuth2 auth URL. GCPOAuthAuthURL string `json:"gcp_oauth_auth_url,omitempty"` // OAuth2 token URL. GCPOAuthTokenURL string `json:"gcp_oauth_token_url,omitempty"` // Maximum quantity of jobs (data) to download concurrently. GCPMaxConcurrentDownloads uint `json:"gcp_max_concurrent_downloads,omitempty"` // CUPS job queue size, must be greater than zero. // TODO: rename without cups_ prefix NativeJobQueueSize uint `json:"cups_job_queue_size,omitempty"` // Interval (eg 10s, 1m) between CUPS printer state polls. // TODO: rename without cups_ prefix NativePrinterPollInterval string `json:"cups_printer_poll_interval,omitempty"` // Use the full username (joe@example.com) in job. // TODO: rename without cups_ prefix CUPSJobFullUsername *bool `json:"cups_job_full_username,omitempty"` // Add the job ID to the beginning of the job title. Useful for debugging. PrefixJobIDToJobTitle *bool `json:"prefix_job_id_to_job_title,omitempty"` // Prefix for all GCP printers hosted by this connector. DisplayNamePrefix string `json:"display_name_prefix,omitempty"` // Ignore printers with native names. PrinterBlacklist []string `json:"printer_blacklist,omitempty"` // Allow printers with native names. PrinterWhitelist []string `json:"printer_whitelist,omitempty"` // Least severity to log. LogLevel string `json:"log_level"` // Local only: HTTP API port range, low. LocalPortLow uint16 `json:"local_port_low,omitempty"` // Local only: HTTP API port range, high. LocalPortHigh uint16 `json:"local_port_high,omitempty"` // CUPS only: Where to place log file. LogFileName string `json:"log_file_name"` // CUPS only: Maximum log file size. LogFileMaxMegabytes uint `json:"log_file_max_megabytes,omitempty"` // CUPS only: Maximum log file quantity. LogMaxFiles uint `json:"log_max_files,omitempty"` // CUPS only: Log to the systemd journal instead of to files? LogToJournal *bool `json:"log_to_journal,omitempty"` // CUPS only: Filename of unix socket for connector-check to talk to connector. MonitorSocketFilename string `json:"monitor_socket_filename,omitempty"` // CUPS only: Maximum quantity of open CUPS connections. CUPSMaxConnections uint `json:"cups_max_connections,omitempty"` // CUPS only: timeout for opening a new connection. CUPSConnectTimeout string `json:"cups_connect_timeout,omitempty"` // CUPS only: printer attributes to copy to GCP. CUPSPrinterAttributes []string `json:"cups_printer_attributes,omitempty"` // CUPS only: non-standard PPD options to add as GCP vendor capabilities. CUPSVendorPPDOptions []string `json:"cups_vendor_ppd_options,omitempty"` // CUPS only: ignore printers with make/model 'Local Raw Printer'. CUPSIgnoreRawPrinters *bool `json:"cups_ignore_raw_printers,omitempty"` // CUPS only: ignore printers with make/model 'Local Printer Class'. CUPSIgnoreClassPrinters *bool `json:"cups_ignore_class_printers,omitempty"` // CUPS only: copy the CUPS printer's printer-info attribute to the GCP printer's defaultDisplayName. // TODO: rename with cups_ prefix CUPSCopyPrinterInfoToDisplayName *bool `json:"copy_printer_info_to_display_name,omitempty"` } // DefaultConfig represents reasonable default values for Config fields. // Omitted Config fields are omitted on purpose; they are unique per // connector instance. var DefaultConfig = Config{ LocalPrintingEnable: true, CloudPrintingEnable: false, XMPPServer: "talk.google.com", XMPPPort: 443, XMPPPingTimeout: "5s", XMPPPingInterval: "2m", GCPBaseURL: "https://www.google.com/cloudprint/", GCPOAuthClientID: "539833558011-35iq8btpgas80nrs3o7mv99hm95d4dv6.apps.googleusercontent.com", GCPOAuthClientSecret: "V9BfPOvdiYuw12hDx5Y5nR0a", GCPOAuthAuthURL: "https://accounts.google.com/o/oauth2/auth", GCPOAuthTokenURL: "https://accounts.google.com/o/oauth2/token", GCPMaxConcurrentDownloads: 5, NativeJobQueueSize: 3, NativePrinterPollInterval: "1m", PrefixJobIDToJobTitle: PointerToBool(false), DisplayNamePrefix: "", PrinterBlacklist: []string{}, PrinterWhitelist: []string{}, LogLevel: "INFO", LocalPortLow: 26000, LocalPortHigh: 26999, LogFileName: "/tmp/cloud-print-connector", LogFileMaxMegabytes: 1, LogMaxFiles: 3, LogToJournal: PointerToBool(false), MonitorSocketFilename: "/tmp/cloud-print-connector-monitor.sock", CUPSMaxConnections: 50, CUPSConnectTimeout: "5s", CUPSPrinterAttributes: []string{ "cups-version", "device-uri", "document-format-supported", "print-color-mode-default", "print-color-mode-supported", "printer-name", "printer-info", "printer-location", "printer-make-and-model", "printer-state", "printer-state-reasons", "printer-uuid", "marker-names", "marker-types", "marker-levels", "copies-default", "copies-supported", "number-up-default", "number-up-supported", "orientation-requested-default", "orientation-requested-supported", "pdf-versions-supported", }, CUPSJobFullUsername: PointerToBool(false), CUPSIgnoreRawPrinters: PointerToBool(true), CUPSIgnoreClassPrinters: PointerToBool(true), CUPSCopyPrinterInfoToDisplayName: PointerToBool(true), } // getConfigFilename gets the absolute filename of the config file specified by // the ConfigFilename flag, and whether it exists. // // If the (relative or absolute) ConfigFilename exists, then it is returned. // If the ConfigFilename exists in a valid XDG path, then it is returned. // If neither of those exist, the (relative or absolute) ConfigFilename is returned. func getConfigFilename(context *cli.Context) (string, bool) { cf := context.GlobalString("config-filename") if filepath.IsAbs(cf) { // Absolute path specified; user knows what they want. _, err := os.Stat(cf) return cf, err == nil } absCF, err := filepath.Abs(cf) if err != nil { // syscall failure; treat as if file doesn't exist. return cf, false } if _, err := os.Stat(absCF); err == nil { // File exists on relative path. return absCF, true } if xdgCF, err := xdg.Config.Find(cf); err == nil { // File exists in an XDG directory. return xdgCF, true } // Default to relative path. This is probably what the user expects if // it wasn't found anywhere else. return absCF, false } // Backfill returns a copy of this config with all missing keys set to default values. func (c *Config) Backfill(configMap map[string]interface{}) *Config { b := *c.commonBackfill(configMap) if _, exists := configMap["log_file_name"]; !exists { b.LogFileName = DefaultConfig.LogFileName } if _, exists := configMap["log_file_max_megabytes"]; !exists { b.LogFileMaxMegabytes = DefaultConfig.LogFileMaxMegabytes } if _, exists := configMap["log_max_files"]; !exists { b.LogMaxFiles = DefaultConfig.LogMaxFiles } if _, exists := configMap["log_to_journal"]; !exists { b.LogToJournal = DefaultConfig.LogToJournal } if _, exists := configMap["monitor_socket_filename"]; !exists { b.MonitorSocketFilename = DefaultConfig.MonitorSocketFilename } if _, exists := configMap["cups_max_connections"]; !exists { b.CUPSMaxConnections = DefaultConfig.CUPSMaxConnections } if _, exists := configMap["cups_connect_timeout"]; !exists { b.CUPSConnectTimeout = DefaultConfig.CUPSConnectTimeout } if _, exists := configMap["cups_printer_attributes"]; !exists { b.CUPSPrinterAttributes = DefaultConfig.CUPSPrinterAttributes } else { // Make sure all required attributes are present. s := make(map[string]struct{}, len(b.CUPSPrinterAttributes)) for _, a := range b.CUPSPrinterAttributes { s[a] = struct{}{} } for _, a := range DefaultConfig.CUPSPrinterAttributes { if _, exists := s[a]; !exists { b.CUPSPrinterAttributes = append(b.CUPSPrinterAttributes, a) } } } if _, exists := configMap["cups_job_full_username"]; !exists { b.CUPSJobFullUsername = DefaultConfig.CUPSJobFullUsername } if _, exists := configMap["cups_ignore_raw_printers"]; !exists { b.CUPSIgnoreRawPrinters = DefaultConfig.CUPSIgnoreRawPrinters } if _, exists := configMap["cups_ignore_class_printers"]; !exists { b.CUPSIgnoreClassPrinters = DefaultConfig.CUPSIgnoreClassPrinters } if _, exists := configMap["copy_printer_info_to_display_name"]; !exists { b.CUPSCopyPrinterInfoToDisplayName = DefaultConfig.CUPSCopyPrinterInfoToDisplayName } return &b } // Sparse returns a copy of this config with obvious values removed. func (c *Config) Sparse(context *cli.Context) *Config { s := *c.commonSparse(context) if !context.IsSet("log-file-max-megabytes") && s.LogFileMaxMegabytes == DefaultConfig.LogFileMaxMegabytes { s.LogFileMaxMegabytes = 0 } if !context.IsSet("log-max-files") && s.LogMaxFiles == DefaultConfig.LogMaxFiles { s.LogMaxFiles = 0 } if !context.IsSet("log-to-journal") && reflect.DeepEqual(s.LogToJournal, DefaultConfig.LogToJournal) { s.LogToJournal = nil } if !context.IsSet("monitor-socket-filename") && s.MonitorSocketFilename == DefaultConfig.MonitorSocketFilename { s.MonitorSocketFilename = "" } if !context.IsSet("cups-max-connections") && s.CUPSMaxConnections == DefaultConfig.CUPSMaxConnections { s.CUPSMaxConnections = 0 } if !context.IsSet("cups-connect-timeout") && s.CUPSConnectTimeout == DefaultConfig.CUPSConnectTimeout { s.CUPSConnectTimeout = "" } if reflect.DeepEqual(s.CUPSPrinterAttributes, DefaultConfig.CUPSPrinterAttributes) { s.CUPSPrinterAttributes = nil } if !context.IsSet("cups-job-full-username") && reflect.DeepEqual(s.CUPSJobFullUsername, DefaultConfig.CUPSJobFullUsername) { s.CUPSJobFullUsername = nil } if !context.IsSet("cups-ignore-raw-printers") && reflect.DeepEqual(s.CUPSIgnoreRawPrinters, DefaultConfig.CUPSIgnoreRawPrinters) { s.CUPSIgnoreRawPrinters = nil } if !context.IsSet("cups-ignore-class-printers") && reflect.DeepEqual(s.CUPSIgnoreClassPrinters, DefaultConfig.CUPSIgnoreClassPrinters) { s.CUPSIgnoreClassPrinters = nil } if !context.IsSet("copy-printer-info-to-display-name") && reflect.DeepEqual(s.CUPSCopyPrinterInfoToDisplayName, DefaultConfig.CUPSCopyPrinterInfoToDisplayName) { s.CUPSCopyPrinterInfoToDisplayName = nil } return &s } cloud-print-connector-1.12/lib/config_windows.go000066400000000000000000000142531311204274000220000ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package lib import ( "os" "path/filepath" "github.com/urfave/cli" ) const ( platformName = "Windows" defaultConfigFilename = "gcp-windows-connector.config.json" ) type Config struct { // Enable local discovery and printing. LocalPrintingEnable bool `json:"local_printing_enable"` // Enable cloud discovery and printing. CloudPrintingEnable bool `json:"cloud_printing_enable"` // Associated with root account. XMPP credential. XMPPJID string `json:"xmpp_jid,omitempty"` // Associated with robot account. Used for acquiring OAuth access tokens. RobotRefreshToken string `json:"robot_refresh_token,omitempty"` // Associated with user account. Used for sharing GCP printers; may be omitted. UserRefreshToken string `json:"user_refresh_token,omitempty"` // Scope (user, group, domain) to share printers with. ShareScope string `json:"share_scope,omitempty"` // User-chosen name of this proxy. Should be unique per Google user account. ProxyName string `json:"proxy_name,omitempty"` // XMPP server FQDN. XMPPServer string `json:"xmpp_server,omitempty"` // XMPP server port number. XMPPPort uint16 `json:"xmpp_port,omitempty"` // XMPP ping timeout (give up waiting after this time). // TODO: Rename with "gcp_" removed. XMPPPingTimeout string `json:"gcp_xmpp_ping_timeout,omitempty"` // XMPP ping interval (time between ping attempts). // TODO: Rename with "gcp_" removed. // TODO: Rename with "_default" removed. XMPPPingInterval string `json:"gcp_xmpp_ping_interval_default,omitempty"` // GCP API URL prefix. GCPBaseURL string `json:"gcp_base_url,omitempty"` // OAuth2 client ID (not unique per client). GCPOAuthClientID string `json:"gcp_oauth_client_id,omitempty"` // OAuth2 client secret (not unique per client). GCPOAuthClientSecret string `json:"gcp_oauth_client_secret,omitempty"` // OAuth2 auth URL. GCPOAuthAuthURL string `json:"gcp_oauth_auth_url,omitempty"` // OAuth2 token URL. GCPOAuthTokenURL string `json:"gcp_oauth_token_url,omitempty"` // Maximum quantity of jobs (data) to download concurrently. GCPMaxConcurrentDownloads uint `json:"gcp_max_concurrent_downloads,omitempty"` // Windows Spooler job queue size, must be greater than zero. // TODO: rename without cups_ prefix NativeJobQueueSize uint `json:"cups_job_queue_size,omitempty"` // Interval (eg 10s, 1m) between Windows Spooler printer state polls. // TODO: rename without cups_ prefix NativePrinterPollInterval string `json:"cups_printer_poll_interval,omitempty"` // Use the full username (joe@example.com) in job. // TODO: rename without cups_ prefix CUPSJobFullUsername *bool `json:"cups_job_full_username,omitempty"` // Add the job ID to the beginning of the job title. Useful for debugging. PrefixJobIDToJobTitle *bool `json:"prefix_job_id_to_job_title,omitempty"` // Prefix for all GCP printers hosted by this connector. DisplayNamePrefix string `json:"display_name_prefix,omitempty"` // Ignore printers with native names. PrinterBlacklist []string `json:"printer_blacklist,omitempty"` // Allow printers with native names. PrinterWhitelist []string `json:"printer_whitelist,omitempty"` // Least severity to log. LogLevel string `json:"log_level"` // Local only: HTTP API port range, low. LocalPortLow uint16 `json:"local_port_low,omitempty"` // Local only: HTTP API port range, high. LocalPortHigh uint16 `json:"local_port_high,omitempty"` } // DefaultConfig represents reasonable default values for Config fields. // Omitted Config fields are omitted on purpose; they are unique per // connector instance. var DefaultConfig = Config{ XMPPServer: "talk.google.com", XMPPPort: 443, XMPPPingTimeout: "5s", XMPPPingInterval: "2m", GCPBaseURL: "https://www.google.com/cloudprint/", GCPOAuthClientID: "539833558011-35iq8btpgas80nrs3o7mv99hm95d4dv6.apps.googleusercontent.com", GCPOAuthClientSecret: "V9BfPOvdiYuw12hDx5Y5nR0a", GCPOAuthAuthURL: "https://accounts.google.com/o/oauth2/auth", GCPOAuthTokenURL: "https://accounts.google.com/o/oauth2/token", GCPMaxConcurrentDownloads: 5, NativeJobQueueSize: 3, NativePrinterPollInterval: "1m", CUPSJobFullUsername: PointerToBool(false), PrefixJobIDToJobTitle: PointerToBool(false), DisplayNamePrefix: "", PrinterBlacklist: []string{ "Fax", "CutePDF Writer", "Microsoft XPS Document Writer", "Google Cloud Printer", }, PrinterWhitelist: []string{}, LocalPrintingEnable: true, CloudPrintingEnable: false, LogLevel: "INFO", LocalPortLow: 26000, LocalPortHigh: 26999, } // getConfigFilename gets the absolute filename of the config file specified by // the ConfigFilename flag, and whether it exists. // // If the ConfigFilename exists, then it is returned as an absolute path. // If neither of those exist, the absolute ConfigFilename is returned. func getConfigFilename(context *cli.Context) (string, bool) { cf := context.GlobalString("config-filename") if filepath.IsAbs(cf) { // Absolute path specified; user knows what they want. _, err := os.Stat(cf) return cf, err == nil } absCF, err := filepath.Abs(cf) if err != nil { // syscall failure; treat as if file doesn't exist. return cf, false } if _, err := os.Stat(absCF); err == nil { // File exists on relative path. return absCF, true } // Check for config file on path relative to executable. exeFile := os.Args[0] exeDir := filepath.Dir(exeFile) absCF = filepath.Join(exeDir, cf) if _, err := os.Stat(absCF); err == nil { return absCF, true } // This is probably what the user expects if it wasn't found anywhere else. return absCF, false } // Backfill returns a copy of this config with all missing keys set to default values. func (c *Config) Backfill(configMap map[string]interface{}) *Config { return c.commonBackfill(configMap) } // Sparse returns a copy of this config with obvious values removed. func (c *Config) Sparse(context *cli.Context) *Config { return c.commonSparse(context) } cloud-print-connector-1.12/lib/deephash.go000066400000000000000000000070731311204274000205440ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "encoding/binary" "fmt" "hash" "io" "reflect" "sort" ) // DeepHash writes an object's values to h. // Struct member names are ignored, values are written to h. // Map keys and values are written to h. // Slice inde and values are written to h. // Pointers are followed once. // Recursive pointer references cause panic. func DeepHash(data interface{}, h hash.Hash) { visited := map[uintptr]struct{}{} deepHash(h, reflect.ValueOf(data), visited) } func binWrite(h hash.Hash, d interface{}) { binary.Write(h, binary.BigEndian, d) } type sortableValues []reflect.Value func (sv sortableValues) Len() int { return len(sv) } func (sv sortableValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] } func (sv sortableValues) Less(i, j int) bool { switch sv[i].Kind() { case reflect.Bool: return sv[i].Bool() == false && sv[j].Bool() == true case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return sv[i].Int() < sv[i].Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return sv[i].Uint() < sv[i].Uint() case reflect.Float32, reflect.Float64: return sv[i].Float() < sv[i].Float() case reflect.String: return sv[i].String() < sv[j].String() case reflect.Ptr: return sv[i].Pointer() < sv[i].Pointer() default: panic(fmt.Sprintf("Cannot compare type %s", sv[i].Kind().String())) } } func deepHash(h hash.Hash, v reflect.Value, visited map[uintptr]struct{}) { switch v.Kind() { case reflect.Invalid: h.Write([]byte{0}) case reflect.Bool: if v.Bool() { binWrite(h, uint8(1)) } else { binWrite(h, uint8(0)) } case reflect.Int: binWrite(h, int(v.Int())) case reflect.Int8: binWrite(h, int8(v.Int())) case reflect.Int16: binWrite(h, int16(v.Int())) case reflect.Int32: binWrite(h, int32(v.Int())) case reflect.Int64: binWrite(h, int64(v.Int())) case reflect.Uint: binWrite(h, uint(v.Uint())) case reflect.Uint8: binWrite(h, uint8(v.Uint())) case reflect.Uint16: binWrite(h, uint16(v.Uint())) case reflect.Uint32: binWrite(h, uint32(v.Uint())) case reflect.Uint64: binWrite(h, uint64(v.Uint())) case reflect.Float32: binWrite(h, float32(v.Float())) case reflect.Float64: binWrite(h, float64(v.Float())) case reflect.Complex64: binWrite(h, float32(real(v.Complex()))) binWrite(h, float32(imag(v.Complex()))) case reflect.Complex128: binWrite(h, float64(real(v.Complex()))) binWrite(h, float64(imag(v.Complex()))) case reflect.Map: keys := make(sortableValues, 0, v.Len()) for _, key := range v.MapKeys() { keys = append(keys, key) } sort.Sort(keys) for _, key := range keys { io.WriteString(h, key.String()) deepHash(h, v.MapIndex(key), visited) } case reflect.Ptr: if _, exists := visited[v.Pointer()]; exists { panic("Cannot hash recursive structure") } else { visited[v.Pointer()] = struct{}{} deepHash(h, v.Elem(), visited) delete(visited, v.Pointer()) } case reflect.Slice, reflect.Array: for i, l := 0, v.Len(); i < l; i++ { binWrite(h, i) deepHash(h, v.Index(i), visited) } case reflect.String: io.WriteString(h, v.String()) case reflect.Struct: for i, n := 0, v.NumField(); i < n; i++ { io.WriteString(h, v.Type().Field(i).Name) deepHash(h, v.Field(i), visited) } default: message := fmt.Sprintf("DeepHash not implemented for '%s' type", v.Kind().String()) panic(message) } } cloud-print-connector-1.12/lib/deephash_test.go000066400000000000000000000073151311204274000216020ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "bytes" "crypto/md5" "encoding/binary" "io" "testing" ) func check(t *testing.T, expected []byte, data interface{}) { h := md5.New() DeepHash(data, h) got := h.Sum(nil) if bytes.Compare(expected, got) != 0 { t.Logf("expected %x got %x", expected, got) t.Fail() } } func TestBool(t *testing.T) { h := md5.New() binary.Write(h, binary.BigEndian, uint8(0)) expected := h.Sum(nil) check(t, expected, false) h = md5.New() binary.Write(h, binary.BigEndian, uint8(1)) expected2 := h.Sum(nil) check(t, expected2, true) } func TestInt(t *testing.T) { i := int(123456789) h := md5.New() binary.Write(h, binary.BigEndian, i) expected := h.Sum(nil) check(t, expected, i) i8 := int8(123) h = md5.New() binary.Write(h, binary.BigEndian, i8) expected = h.Sum(nil) check(t, expected, i8) // byte is an alias for uint8 b := byte('Q') h = md5.New() binary.Write(h, binary.BigEndian, b) expected = h.Sum(nil) check(t, expected, b) // rune is an alias for int32 r := rune('龍') h = md5.New() binary.Write(h, binary.BigEndian, r) expected = h.Sum(nil) check(t, expected, r) ui64 := uint64(123456789123456789) h = md5.New() binary.Write(h, binary.BigEndian, ui64) expected = h.Sum(nil) check(t, expected, ui64) } func TestFloat(t *testing.T) { f32 := float32(123456.789) h := md5.New() binary.Write(h, binary.BigEndian, f32) expected := h.Sum(nil) check(t, expected, f32) f64 := float64(123456789.123456789) h = md5.New() binary.Write(h, binary.BigEndian, f64) expected = h.Sum(nil) check(t, expected, f64) } func TestComplex(t *testing.T) { var c64 complex64 = complex(123456.789, 654321.987) h := md5.New() binary.Write(h, binary.BigEndian, float32(real(c64))) binary.Write(h, binary.BigEndian, float32(imag(c64))) expected := h.Sum(nil) check(t, expected, c64) var c128 complex128 = complex(123456789.123456789, 987654321.987654321) h = md5.New() binary.Write(h, binary.BigEndian, float64(real(c128))) binary.Write(h, binary.BigEndian, float64(imag(c128))) expected = h.Sum(nil) check(t, expected, c128) } func TestMap(t *testing.T) { m := map[string]string{"b": "B", "a": "A"} h := md5.New() io.WriteString(h, "a") io.WriteString(h, "A") io.WriteString(h, "b") io.WriteString(h, "B") expected := h.Sum(nil) check(t, expected, m) } func TestPtr(t *testing.T) { i := int8(1) h := md5.New() binary.Write(h, binary.BigEndian, i) expected := h.Sum(nil) // Sum should be the same, whether DeepHash(value), or DeepHash(&value). check(t, expected, i) check(t, expected, &i) } func TestPtrNil(t *testing.T) { h := md5.New() h.Write([]byte{0}) expected := h.Sum(nil) check(t, expected, nil) } func TestSlice(t *testing.T) { a := []string{"abc", "def"} h := md5.New() io.WriteString(h, a[0]) io.WriteString(h, a[1]) expected := h.Sum(nil) check(t, expected, a) b := []int{1, 2, 3} h = md5.New() binary.Write(h, binary.BigEndian, b[0]) binary.Write(h, binary.BigEndian, b[1]) binary.Write(h, binary.BigEndian, b[2]) expected = h.Sum(nil) check(t, expected, b) p := []*int{} x, y, z := int(1), int(2), int(3) p = append(p, &x, &y, &z, nil) h = md5.New() binary.Write(h, binary.BigEndian, x) binary.Write(h, binary.BigEndian, y) binary.Write(h, binary.BigEndian, z) h.Write([]byte{0}) expected = h.Sum(nil) check(t, expected, p) } func TestString(t *testing.T) { s := "just a string" h := md5.New() io.WriteString(h, s) expected := h.Sum(nil) check(t, expected, s) type myString string var ms myString = myString(s) check(t, expected, ms) } cloud-print-connector-1.12/lib/job.go000066400000000000000000000010241311204274000175230ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import "github.com/google/cloud-print-connector/cdd" type Job struct { NativePrinterName string Filename string Title string User string JobID string Ticket *cdd.CloudJobTicket UpdateJob func(string, *cdd.PrintJobStateDiff) error } cloud-print-connector-1.12/lib/printer.go000066400000000000000000000212261311204274000204420ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "reflect" "regexp" "github.com/google/cloud-print-connector/cdd" ) type PrinterState uint8 // DuplexVendorMap maps a DuplexType to a CUPS key:value option string for a given printer. type DuplexVendorMap map[cdd.DuplexType]string // CUPS: cups_dest_t; GCP: /register and /update interfaces type Printer struct { GCPID string // GCP: printerid (GCP key) Name string // CUPS: cups_dest_t.name (CUPS key); GCP: name field DefaultDisplayName string // CUPS: printer-info; GCP: default_display_name field UUID string // CUPS: printer-uuid; GCP: uuid field Manufacturer string // CUPS: PPD; GCP: manufacturer field Model string // CUPS: PPD; GCP: model field GCPVersion string // GCP: gcpVersion field SetupURL string // GCP: setup_url field SupportURL string // GCP: support_url field UpdateURL string // GCP: update_url field ConnectorVersion string // GCP: firmware field State *cdd.PrinterStateSection // CUPS: various; GCP: semantic_state field Description *cdd.PrinterDescriptionSection // CUPS: translated PPD; GCP: capabilities field CapsHash string // CUPS: hash of PPD; GCP: capsHash field Tags map[string]string // CUPS: all printer attributes; GCP: repeated tag field DuplexMap DuplexVendorMap // CUPS: PPD; NativeJobSemaphore *Semaphore QuotaEnabled bool DailyQuota int } var rDeviceURIHostname *regexp.Regexp = regexp.MustCompile( "(?i)^(?:socket|http|https|ipp|ipps|lpd)://([a-z][a-z0-9.-]*)") // GetHostname gets the network hostname, parsed from Printer.Tags["device-uri"]. func (p *Printer) GetHostname() (string, bool) { deviceURI, ok := p.Tags["device-uri"] if !ok { return "", false } parts := rDeviceURIHostname.FindStringSubmatch(deviceURI) if len(parts) == 2 { return parts[1], true } return "", false } type PrinterDiffOperation int8 const ( RegisterPrinter PrinterDiffOperation = iota UpdatePrinter DeletePrinter NoChangeToPrinter ) // Describes changes to be pushed to a GCP printer. type PrinterDiff struct { Operation PrinterDiffOperation Printer Printer DefaultDisplayNameChanged bool ManufacturerChanged bool ModelChanged bool GCPVersionChanged bool SetupURLChanged bool SupportURLChanged bool UpdateURLChanged bool ConnectorVersionChanged bool StateChanged bool DescriptionChanged bool CapsHashChanged bool TagsChanged bool DuplexMapChanged bool QuotaEnabledChanged bool DailyQuotaChanged bool } func printerSliceToMapByName(s []Printer) map[string]Printer { m := make(map[string]Printer, len(s)) for i := range s { m[s[i].Name] = s[i] } return m } // DiffPrinters returns the diff between old (GCP) and new (native) printers. // Returns nil if zero printers or if all diffs are NoChangeToPrinter operation. func DiffPrinters(nativePrinters, gcpPrinters []Printer) []PrinterDiff { // So far, no changes. dirty := false diffs := make([]PrinterDiff, 0, 1) printersConsidered := make(map[string]struct{}, len(nativePrinters)) nativePrintersByName := printerSliceToMapByName(nativePrinters) for i := range gcpPrinters { if _, exists := printersConsidered[gcpPrinters[i].Name]; exists { // GCP can have multiple printers with one name. Remove dupes. diffs = append(diffs, PrinterDiff{Operation: DeletePrinter, Printer: gcpPrinters[i]}) dirty = true } else { printersConsidered[gcpPrinters[i].Name] = struct{}{} if nativePrinter, exists := nativePrintersByName[gcpPrinters[i].Name]; exists { // Native printer doesn't know about GCPID yet. nativePrinter.GCPID = gcpPrinters[i].GCPID // Don't lose track of this semaphore. nativePrinter.NativeJobSemaphore = gcpPrinters[i].NativeJobSemaphore diff := diffPrinter(&nativePrinter, &gcpPrinters[i]) diffs = append(diffs, diff) if diff.Operation != NoChangeToPrinter { dirty = true } } else { diffs = append(diffs, PrinterDiff{Operation: DeletePrinter, Printer: gcpPrinters[i]}) dirty = true } } } for i := range nativePrinters { if _, exists := printersConsidered[nativePrinters[i].Name]; !exists { diffs = append(diffs, PrinterDiff{Operation: RegisterPrinter, Printer: nativePrinters[i]}) dirty = true } } if dirty { return diffs } else { return nil } } // diffPrinter finds the difference between a native printer and the corresponding GCP printer. // // pn: printer-native; the thing that is correct // // pg: printer-GCP; the thing that will be updated func diffPrinter(pn, pg *Printer) PrinterDiff { d := PrinterDiff{ Operation: UpdatePrinter, Printer: *pn, } if pg.DefaultDisplayName != pn.DefaultDisplayName { d.DefaultDisplayNameChanged = true } if pg.Manufacturer != pn.Manufacturer { d.ManufacturerChanged = true } if pg.Model != pn.Model { d.ModelChanged = true } if pg.GCPVersion != pn.GCPVersion { if pg.GCPVersion > pn.GCPVersion { panic("GCP version cannot be downgraded; delete GCP printers") } d.GCPVersionChanged = true } if pg.SetupURL != pn.SetupURL { d.SetupURLChanged = true } if pg.SupportURL != pn.SupportURL { d.SupportURLChanged = true } if pg.UpdateURL != pn.UpdateURL { d.UpdateURLChanged = true } if pg.ConnectorVersion != pn.ConnectorVersion { d.ConnectorVersionChanged = true } if !reflect.DeepEqual(pg.State, pn.State) { d.StateChanged = true } if !reflect.DeepEqual(pg.Description, pn.Description) { d.DescriptionChanged = true } if pg.CapsHash != pn.CapsHash { d.CapsHashChanged = true } gcpTagshash, gcpHasTagshash := pg.Tags["tagshash"] nativeTagshash, nativeHasTagshash := pn.Tags["tagshash"] if !gcpHasTagshash || !nativeHasTagshash || gcpTagshash != nativeTagshash { d.TagsChanged = true } if !reflect.DeepEqual(pg.DuplexMap, pn.DuplexMap) { d.DuplexMapChanged = true } if pg.QuotaEnabled != pn.QuotaEnabled { d.QuotaEnabledChanged = true } if pg.DailyQuota != pn.DailyQuota { d.DailyQuotaChanged = true } if d.DefaultDisplayNameChanged || d.ManufacturerChanged || d.ModelChanged || d.GCPVersionChanged || d.SetupURLChanged || d.SupportURLChanged || d.UpdateURLChanged || d.ConnectorVersionChanged || d.StateChanged || d.DescriptionChanged || d.CapsHashChanged || d.TagsChanged || d.DuplexMapChanged || d.QuotaEnabledChanged || d.DailyQuotaChanged { return d } return PrinterDiff{ Operation: NoChangeToPrinter, Printer: *pg, } } func FilterBlacklistPrinters(printers []Printer, list map[string]interface{}) []Printer { return filterPrinters(printers, list, false) } func FilterWhitelistPrinters(printers []Printer, list map[string]interface{}) []Printer { if len(list) == 0 { return printers // Empty whitelist means don't use whitelist } return filterPrinters(printers, list, true) } func filterPrinters(printers []Printer, list map[string]interface{}, isWhitelist bool) []Printer { result := make([]Printer, 0, len(printers)) for i := range printers { if _, exists := list[printers[i].Name]; exists == isWhitelist { result = append(result, printers[i]) } } return result } // FilterRawPrinters splits a slice of printers into non-raw and raw. func FilterRawPrinters(printers []Printer) ([]Printer, []Printer) { notRaw, raw := make([]Printer, 0, len(printers)), make([]Printer, 0, 0) for i := range printers { if PrinterIsRaw(printers[i]) { raw = append(raw, printers[i]) } else { notRaw = append(notRaw, printers[i]) } } return notRaw, raw } func PrinterIsRaw(printer Printer) bool { if printer.Tags["printer-make-and-model"] == "Local Raw Printer" { return true } return false } func PrinterIsClass(printer Printer) bool { if printer.Tags["printer-make-and-model"] == "Local Printer Class" { return true } return false } cloud-print-connector-1.12/lib/printer_test.go000066400000000000000000000023601311204274000214770ustar00rootroot00000000000000/* Copyright 2016 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib import ( "reflect" "testing" ) func TestFilterBlacklistPrinters(t *testing.T) { printers := []Printer{ {Name: "Stay1"}, {Name: "Go1"}, {Name: "Go2"}, {Name: "Stay2"}, } blacklist := map[string]interface{}{ "Go1": "", "Go2": "", } correctFilteredPrinters := []Printer{ {Name: "Stay1"}, {Name: "Stay2"}, } filteredPrinters := FilterBlacklistPrinters(printers, blacklist) if !reflect.DeepEqual(filteredPrinters, correctFilteredPrinters) { t.Fatalf("filtering result incorrect: %v", filteredPrinters) } } func TestFilterWhitelistPrinters(t *testing.T) { printers := []Printer{ {Name: "Stay1"}, {Name: "Go1"}, {Name: "Go2"}, {Name: "Stay2"}, } whitelist := map[string]interface{}{ "Stay1": "", "Stay2": "", } correctFilteredPrinters := []Printer{ {Name: "Stay1"}, {Name: "Stay2"}, } filteredPrinters := FilterWhitelistPrinters(printers, whitelist) if !reflect.DeepEqual(filteredPrinters, correctFilteredPrinters) { t.Fatalf("filtering result incorrect: %v", filteredPrinters) } } cloud-print-connector-1.12/lib/semaphore.go000066400000000000000000000022501311204274000207360ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package lib type Semaphore struct { ch chan struct{} } func NewSemaphore(size uint) *Semaphore { s := make(chan struct{}, size) return &Semaphore{s} } // Acquire increments the semaphore, blocking if necessary. func (s *Semaphore) Acquire() { s.ch <- struct{}{} } // TryAcquire increments the semaphore without blocking. // Returns false if the semaphore was not acquired. func (s *Semaphore) TryAcquire() bool { select { case s.ch <- struct{}{}: return true default: return false } } // Release decrements the semaphore. If this operation causes // the semaphore value to be negative, then panics. func (s *Semaphore) Release() { select { case _ = <-s.ch: return default: panic("Semaphore was released without being acquired") } } // Count returns the current value of the semaphore. func (s *Semaphore) Count() uint { return uint(len(s.ch)) } // Size returns the maximum semaphore value. func (s *Semaphore) Size() uint { return uint(cap(s.ch)) } cloud-print-connector-1.12/log/000077500000000000000000000000001311204274000164405ustar00rootroot00000000000000cloud-print-connector-1.12/log/log.go000066400000000000000000000074551311204274000175630ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package log import "strings" // LogLevel represents a subset of the severity levels named by CUPS. type LogLevel uint8 const ( FATAL LogLevel = iota ERROR WARNING INFO DEBUG ) var ( stringByLevel = map[LogLevel]string{ FATAL: "FATAL", ERROR: "ERROR", WARNING: "WARNING", INFO: "INFO", DEBUG: "DEBUG", } levelByString = map[string]LogLevel{ "FATAL": FATAL, "ERROR": ERROR, "WARNING": WARNING, "INFO": INFO, "DEBUG": DEBUG, } ) func LevelFromString(level string) (LogLevel, bool) { v, ok := levelByString[strings.ToUpper(level)] if !ok { return 0, false } return v, true } func Fatal(args ...interface{}) { log(FATAL, "", "", "", args...) } func Fatalf(format string, args ...interface{}) { log(FATAL, "", "", format, args...) } func FatalJob(jobID string, args ...interface{}) { log(FATAL, "", jobID, "", args...) } func FatalJobf(jobID, format string, args ...interface{}) { log(FATAL, "", jobID, format, args...) } func FatalPrinter(printerID string, args ...interface{}) { log(FATAL, printerID, "", "", args...) } func FatalPrinterf(printerID, format string, args ...interface{}) { log(FATAL, printerID, "", format, args...) } func Error(args ...interface{}) { log(ERROR, "", "", "", args...) } func Errorf(format string, args ...interface{}) { log(ERROR, "", "", format, args...) } func ErrorJob(jobID string, args ...interface{}) { log(ERROR, "", jobID, "", args...) } func ErrorJobf(jobID, format string, args ...interface{}) { log(ERROR, "", jobID, format, args...) } func ErrorPrinter(printerID string, args ...interface{}) { log(ERROR, printerID, "", "", args...) } func ErrorPrinterf(printerID, format string, args ...interface{}) { log(ERROR, printerID, "", format, args...) } func Warning(args ...interface{}) { log(WARNING, "", "", "", args...) } func Warningf(format string, args ...interface{}) { log(WARNING, "", "", format, args...) } func WarningJob(jobID string, args ...interface{}) { log(WARNING, "", jobID, "", args...) } func WarningJobf(jobID, format string, args ...interface{}) { log(WARNING, "", jobID, format, args...) } func WarningPrinter(printerID string, args ...interface{}) { log(WARNING, printerID, "", "", args...) } func WarningPrinterf(printerID, format string, args ...interface{}) { log(WARNING, printerID, "", format, args...) } func Info(args ...interface{}) { log(INFO, "", "", "", args...) } func Infof(format string, args ...interface{}) { log(INFO, "", "", format, args...) } func InfoJob(jobID string, args ...interface{}) { log(INFO, "", jobID, "", args...) } func InfoJobf(jobID, format string, args ...interface{}) { log(INFO, "", jobID, format, args...) } func InfoPrinter(printerID string, args ...interface{}) { log(INFO, printerID, "", "", args...) } func InfoPrinterf(printerID, format string, args ...interface{}) { log(INFO, printerID, "", format, args...) } func Debug(args ...interface{}) { log(DEBUG, "", "", "", args...) } func Debugf(format string, args ...interface{}) { log(DEBUG, "", "", format, args...) } func DebugJob(jobID string, args ...interface{}) { log(DEBUG, "", jobID, "", args...) } func DebugJobf(jobID, format string, args ...interface{}) { log(DEBUG, "", jobID, format, args...) } func DebugPrinter(printerID string, args ...interface{}) { log(DEBUG, printerID, "", "", args...) } func DebugPrinterf(printerID, format string, args ...interface{}) { log(DEBUG, printerID, "", format, args...) } cloud-print-connector-1.12/log/log_unix.go000066400000000000000000000056551311204274000206260ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd // The log package logs to an io.Writer using the same log format that CUPS uses. package log import ( "fmt" "io" "os" "runtime" "strconv" "time" "github.com/coreos/go-systemd/journal" ) const ( logFormat = "%c [%s] %s\n" logJobFormat = "%c [%s] [Job %s] %s\n" logPrinterFormat = "%c [%s] [Printer %s] %s\n" dateTimeFormat = "02/Jan/2006:15:04:05 -0700" journalJobFormat = "[Job %s] %s" journalPrinterFormat = "[Printer %s] %s" ) var ( levelToInitial = map[LogLevel]rune{ FATAL: 'X', // "EMERG" in CUPS. ERROR: 'E', WARNING: 'W', INFO: 'I', DEBUG: 'D', } logger struct { writer io.Writer level LogLevel journalEnabled bool } ) func (l LogLevel) priority() journal.Priority { switch l { case FATAL: return journal.PriCrit case ERROR: return journal.PriErr case WARNING: return journal.PriWarning case INFO: return journal.PriInfo case DEBUG: return journal.PriDebug default: return journal.PriDebug } } func init() { logger.writer = os.Stderr logger.level = INFO } // SetWriter sets the io.Writer to log to. Default is os.Stderr. func SetWriter(w io.Writer) { logger.writer = w } // SetLevel sets the minimum severity level to log. Default is INFO. func SetLevel(l LogLevel) { logger.level = l } // SetJournalEnabled enables or disables writing to the systemd journal. Default is false. func SetJournalEnabled(b bool) { logger.journalEnabled = b } func log(level LogLevel, printerID, jobID, format string, args ...interface{}) { if level > logger.level { return } levelInitial := levelToInitial[level] dateTime := time.Now().Format(dateTimeFormat) var message string if format == "" { message = fmt.Sprint(args...) } else { message = fmt.Sprintf(format, args...) } journalVars := make(map[string]string) var journalMessage string if printerID != "" { fmt.Fprintf(logger.writer, logPrinterFormat, levelInitial, dateTime, printerID, message) journalVars["PRINTER_ID"] = printerID journalMessage = fmt.Sprintf(journalPrinterFormat, printerID, message) } else if jobID != "" { fmt.Fprintf(logger.writer, logJobFormat, levelInitial, dateTime, jobID, message) journalVars["JOB_ID"] = jobID journalMessage = fmt.Sprintf(journalJobFormat, jobID, message) } else { fmt.Fprintf(logger.writer, logFormat, levelInitial, dateTime, message) journalMessage = message } if logger.journalEnabled { pc := make([]uintptr, 1) runtime.Callers(3, pc) f := runtime.FuncForPC(pc[0]) journalVars["CODE_FUNC"] = f.Name() file, line := f.FileLine(pc[0]) journalVars["CODE_FILE"] = file journalVars["CODE_LINE"] = strconv.Itoa(line) journal.Send(journalMessage, level.priority(), journalVars) } } cloud-print-connector-1.12/log/log_windows.go000066400000000000000000000037041311204274000213260ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows // The log package logs to the Windows Event Log, or stdout. package log import ( "fmt" "github.com/google/cloud-print-connector/lib" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" ) const ( logJobFormat = "[Job %s] %s" logPrinterFormat = "[Printer %s] %s" dateTimeFormat = "2006-Jan-02 15:04:05" ) var logger struct { level LogLevel elog debug.Log } func init() { logger.level = INFO } // SetLevel sets the minimum severity level to log. Default is INFO. func SetLevel(l LogLevel) { logger.level = l } func Start(logToConsole bool) error { if logToConsole { logger.elog = debug.New(lib.ConnectorName) } else { l, err := eventlog.Open(lib.ConnectorName) if err != nil { return err } logger.elog = l } return nil } func Stop() { err := logger.elog.Close() if err != nil { panic("Failed to close log") } } func log(level LogLevel, printerID, jobID, format string, args ...interface{}) { if logger.elog == nil { panic("Attempted to log without first calling Start()") } if level > logger.level { return } var message string if format == "" { message = fmt.Sprint(args...) } else { message = fmt.Sprintf(format, args...) } if printerID != "" { message = fmt.Sprintf(logPrinterFormat, printerID, message) } else if jobID != "" { message = fmt.Sprintf(logJobFormat, jobID, message) } if level == DEBUG || level == FATAL { // Windows Event Log only has three levels; these two extra information prepended. message = fmt.Sprintf("%s %s", stringByLevel[level], message) } switch level { case FATAL, ERROR: logger.elog.Error(1, message) case WARNING: logger.elog.Warning(2, message) case INFO, DEBUG: logger.elog.Info(3, message) } } cloud-print-connector-1.12/log/logroller.go000066400000000000000000000072471311204274000210020ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package log import ( "fmt" "math" "os" "path/filepath" "regexp" "sort" "strconv" "sync" ) var rollPattern = regexp.MustCompile(`^\.([0-9]+)$`) const rollFormatFormat = "%s.%%0%dd" type LogRoller struct { fileName string fileMaxBytes uint maxFiles uint rollFormat string m sync.Mutex file *os.File fileSize uint } func NewLogRoller(fileName string, fileMaxBytes, maxFiles uint) (*LogRoller, error) { // How many digits to append to rolled file name? // 0 => 0 ; 1 => 1 ; 9 => 1 ; 99 => 2 ; 100 => 3 var digits int if maxFiles > 0 { digits = int(math.Log10(float64(maxFiles))) + 1 } rollFormat := fmt.Sprintf(rollFormatFormat, fileName, digits) lr := LogRoller{ fileName: fileName, fileMaxBytes: fileMaxBytes, maxFiles: maxFiles, rollFormat: rollFormat, } return &lr, nil } func (lr *LogRoller) Write(p []byte) (int, error) { lr.m.Lock() defer lr.m.Unlock() if lr.file == nil { lr.fileSize = 0 if err := lr.openFile(); err != nil { return 0, err } } written, err := lr.file.Write(p) if err != nil { return 0, err } lr.fileSize += uint(written) if lr.fileSize > lr.fileMaxBytes { lr.file.Close() lr.file = nil } return written, nil } // openFile opens a new file for logging, rolling the oldest one if needed. func (lr *LogRoller) openFile() error { if err := lr.roll(); err != nil { return err } if f, err := os.Create(lr.fileName); err != nil { return err } else { lr.file = f } return nil } type sortableNumberStrings []string func (s sortableNumberStrings) Len() int { return len(s) } func (s sortableNumberStrings) Less(i, j int) bool { ip, _ := strconv.ParseUint(s[i], 10, 16) jp, _ := strconv.ParseUint(s[j], 10, 16) return ip < jp } func (s sortableNumberStrings) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // roll deletes old log files until there are lr.maxFiles or fewer, // and renames remaining log files so that the file named lr.fileName+".3" becomes lr.fileName+".4". // The file named lr.fileName becomes lr.fileName+".0". // If lr.fileName does not exist, then this is a noop. func (lr *LogRoller) roll() error { if _, err := os.Stat(lr.fileName); os.IsNotExist(err) { // Nothing to do; the target log file name already does not exist. return nil } // Get all rolled logs, plus some. allFiles, err := filepath.Glob(lr.fileName + ".*") if err != nil { return err } // Get number suffixes from the rolled logs; ignore non-matches. numbers := make(sortableNumberStrings, 0, len(allFiles)) for _, file := range allFiles { match := rollPattern.FindStringSubmatch(file[len(lr.fileName):]) if len(match) < 2 { continue } if _, err := strconv.ParseUint(match[1], 10, 16); err == nil { // Keep the string form of the number. numbers = append(numbers, match[1]) } } // Delete old log files and rename the rest. sort.Sort(numbers) for i := len(numbers) - 1; i >= 0; i-- { oldpath := fmt.Sprintf("%s.%s", lr.fileName, numbers[i]) if uint(i+1) >= lr.maxFiles { err := os.Remove(oldpath) if err != nil { return err } } else { n, _ := strconv.ParseUint(numbers[i], 10, 16) newpath := fmt.Sprintf(lr.rollFormat, n+1) err := os.Rename(oldpath, newpath) if err != nil { return err } } } if lr.maxFiles > 0 { newpath := fmt.Sprintf(lr.rollFormat, 0) err = os.Rename(lr.fileName, newpath) if err != nil { return err } } // Else the existing file will be truncated. return nil } cloud-print-connector-1.12/log/logroller_test.go000066400000000000000000000020741311204274000220320ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package log import ( "sort" "strconv" "testing" ) func checkSort(s sortableNumberStrings) (bool, error) { for i := 0; i < s.Len()-1; i++ { a, err := strconv.Atoi(s[i]) if err != nil { return false, err } b, err := strconv.Atoi(s[i+1]) if err != nil { return false, err } if a > b { return false, nil } } return true, nil } func testSort(t *testing.T, s sortableNumberStrings) { sort.Sort(s) res, err := checkSort(s) if err != nil { t.Log(err) t.Fail() } else if !res { t.Logf("sort failed: %v", s) t.Fail() } } func TestSortableNumberStrings(t *testing.T) { s := sortableNumberStrings{"2", "1"} testSort(t, s) s = sortableNumberStrings{"100", "10", "1", "11"} testSort(t, s) s = sortableNumberStrings{"0100", "10", "01", "11"} testSort(t, s) s = sortableNumberStrings{"0100", "10", "11", "10"} testSort(t, s) } cloud-print-connector-1.12/manager/000077500000000000000000000000001311204274000172715ustar00rootroot00000000000000cloud-print-connector-1.12/manager/printermanager.go000066400000000000000000000313101311204274000226340ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package manager import ( "fmt" "hash/adler32" "os" "reflect" "strings" "sync" "time" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" "github.com/google/cloud-print-connector/privet" "github.com/google/cloud-print-connector/xmpp" ) type NativePrintSystem interface { GetPrinters() ([]lib.Printer, error) GetJobState(printerName string, jobID uint32) (*cdd.PrintJobStateDiff, error) Print(printer *lib.Printer, fileName, title, user, gcpJobID string, ticket *cdd.CloudJobTicket) (uint32, error) ReleaseJob(printerName string, jobID uint32) error RemoveCachedPPD(printerName string) } // Manages state and interactions between the native print system and Google Cloud Print. type PrinterManager struct { native NativePrintSystem gcp *gcp.GoogleCloudPrint xmpp *xmpp.XMPP privet *privet.Privet printers *lib.ConcurrentPrinterMap // Job stats are numbers reported to monitoring. jobStatsMutex sync.Mutex jobsDone uint jobsError uint // Jobs in flight are jobs that have been received, and are not // finished printing yet. Key is Job ID. jobsInFlightMutex sync.Mutex jobsInFlight map[string]struct{} nativeJobQueueSize uint jobFullUsername bool shareScope string quit chan struct{} } func NewPrinterManager(native NativePrintSystem, gcp *gcp.GoogleCloudPrint, privet *privet.Privet, printerPollInterval time.Duration, nativeJobQueueSize uint, jobFullUsername bool, shareScope string, jobs <-chan *lib.Job, xmppNotifications <-chan xmpp.PrinterNotification) (*PrinterManager, error) { var printers *lib.ConcurrentPrinterMap var queuedJobsCount map[string]uint var err error if gcp != nil { // Get all GCP printers. var gcpPrinters []lib.Printer gcpPrinters, queuedJobsCount, err = gcp.ListPrinters() if err != nil { return nil, err } // Organize the GCP printers into a map. for i := range gcpPrinters { gcpPrinters[i].NativeJobSemaphore = lib.NewSemaphore(nativeJobQueueSize) } printers = lib.NewConcurrentPrinterMap(gcpPrinters) } else { printers = lib.NewConcurrentPrinterMap(nil) } // Construct. pm := PrinterManager{ native: native, gcp: gcp, privet: privet, printers: printers, jobStatsMutex: sync.Mutex{}, jobsDone: 0, jobsError: 0, jobsInFlightMutex: sync.Mutex{}, jobsInFlight: make(map[string]struct{}), nativeJobQueueSize: nativeJobQueueSize, jobFullUsername: jobFullUsername, shareScope: shareScope, quit: make(chan struct{}), } // Sync once before returning, to make sure things are working. // Ignore privet updates this first time because Privet always starts // with zero printers. if err = pm.SyncPrinters(true); err != nil { return nil, err } // Initialize Privet printers. if privet != nil { for _, printer := range pm.printers.GetAll() { err := privet.AddPrinter(printer, pm.printers.GetByNativeName) if err != nil { log.WarningPrinterf(printer.Name, "Failed to register locally: %s", err) } else { log.InfoPrinterf(printer.Name, "Registered locally") } } } pm.syncPrintersPeriodically(printerPollInterval) pm.listenNotifications(jobs, xmppNotifications) if gcp != nil { for gcpPrinterID := range queuedJobsCount { p, _ := printers.GetByGCPID(gcpPrinterID) go gcp.HandleJobs(&p, func() { pm.incrementJobsProcessed(false) }) } } return &pm, nil } func (pm *PrinterManager) Quit() { close(pm.quit) } func (pm *PrinterManager) syncPrintersPeriodically(interval time.Duration) { go func() { t := time.NewTimer(interval) defer t.Stop() for { select { case <-t.C: if err := pm.SyncPrinters(false); err != nil { log.Error(err) } t.Reset(interval) case <-pm.quit: return } } }() } func (pm *PrinterManager) SyncPrinters(ignorePrivet bool) error { log.Info("Synchronizing printers, stand by") // Get current snapshot of native printers. nativePrinters, err := pm.native.GetPrinters() if err != nil { return fmt.Errorf("Sync failed while calling GetPrinters(): %s", err) } // Set CapsHash on all printers. for i := range nativePrinters { h := adler32.New() lib.DeepHash(nativePrinters[i].Tags, h) nativePrinters[i].Tags["tagshash"] = fmt.Sprintf("%x", h.Sum(nil)) h = adler32.New() lib.DeepHash(nativePrinters[i].Description, h) nativePrinters[i].CapsHash = fmt.Sprintf("%x", h.Sum(nil)) } // Compare the snapshot to what we know currently. diffs := lib.DiffPrinters(nativePrinters, pm.printers.GetAll()) if diffs == nil { log.Infof("Printers are already in sync; there are %d", len(nativePrinters)) return nil } // Update GCP. ch := make(chan lib.Printer, len(diffs)) for i := range diffs { go pm.applyDiff(&diffs[i], ch, ignorePrivet) } currentPrinters := make([]lib.Printer, 0, len(diffs)) for _ = range diffs { p := <-ch if p.Name != "" { currentPrinters = append(currentPrinters, p) } } // Update what we know. pm.printers.Refresh(currentPrinters) log.Infof("Finished synchronizing %d printers", len(currentPrinters)) return nil } func (pm *PrinterManager) applyDiff(diff *lib.PrinterDiff, ch chan<- lib.Printer, ignorePrivet bool) { switch diff.Operation { case lib.RegisterPrinter: if pm.gcp != nil { if err := pm.gcp.Register(&diff.Printer); err != nil { log.ErrorPrinterf(diff.Printer.Name, "Failed to register: %s", err) break } log.InfoPrinterf(diff.Printer.Name+" "+diff.Printer.GCPID, "Registered in the cloud") if pm.gcp.CanShare() { if err := pm.gcp.Share(diff.Printer.GCPID, pm.shareScope, gcp.User, true, false); err != nil { log.ErrorPrinterf(diff.Printer.Name, "Failed to share: %s", err) } else { log.InfoPrinterf(diff.Printer.Name, "Shared") } } } diff.Printer.NativeJobSemaphore = lib.NewSemaphore(pm.nativeJobQueueSize) if pm.privet != nil && !ignorePrivet { err := pm.privet.AddPrinter(diff.Printer, pm.printers.GetByNativeName) if err != nil { log.WarningPrinterf(diff.Printer.Name, "Failed to register locally: %s", err) } else { log.InfoPrinterf(diff.Printer.Name, "Registered locally") } } ch <- diff.Printer return case lib.UpdatePrinter: if pm.gcp != nil { if err := pm.gcp.Update(diff); err != nil { log.ErrorPrinterf(diff.Printer.Name+" "+diff.Printer.GCPID, "Failed to update: %s", err) } else { log.InfoPrinterf(diff.Printer.Name+" "+diff.Printer.GCPID, "Updated in the cloud") } } if pm.privet != nil && !ignorePrivet && diff.DefaultDisplayNameChanged { err := pm.privet.UpdatePrinter(diff) if err != nil { log.WarningPrinterf(diff.Printer.Name, "Failed to update locally: %s", err) } else { log.InfoPrinterf(diff.Printer.Name, "Updated locally") } } ch <- diff.Printer return case lib.DeletePrinter: pm.native.RemoveCachedPPD(diff.Printer.Name) if pm.gcp != nil { if err := pm.gcp.Delete(diff.Printer.GCPID); err != nil { log.ErrorPrinterf(diff.Printer.Name+" "+diff.Printer.GCPID, "Failed to delete from the cloud: %s", err) break } log.InfoPrinterf(diff.Printer.Name+" "+diff.Printer.GCPID, "Deleted from the cloud") } if pm.privet != nil && !ignorePrivet { err := pm.privet.DeletePrinter(diff.Printer.Name) if err != nil { log.WarningPrinterf(diff.Printer.Name, "Failed to delete: %s", err) } else { log.InfoPrinterf(diff.Printer.Name, "Deleted locally") } } case lib.NoChangeToPrinter: ch <- diff.Printer return } ch <- lib.Printer{} } // listenNotifications handles the messages found on the channels. func (pm *PrinterManager) listenNotifications(jobs <-chan *lib.Job, xmppMessages <-chan xmpp.PrinterNotification) { go func() { for { select { case <-pm.quit: return case job := <-jobs: log.DebugJobf(job.JobID, "Received job: %+v", job) go pm.printJob(job.NativePrinterName, job.Filename, job.Title, job.User, job.JobID, job.Ticket, job.UpdateJob) case notification := <-xmppMessages: log.Debugf("Received XMPP message: %+v", notification) if notification.Type == xmpp.PrinterNewJobs { if p, exists := pm.printers.GetByGCPID(notification.GCPID); exists { go pm.gcp.HandleJobs(&p, func() { pm.incrementJobsProcessed(false) }) } } } } }() } func (pm *PrinterManager) incrementJobsProcessed(success bool) { pm.jobStatsMutex.Lock() defer pm.jobStatsMutex.Unlock() if success { pm.jobsDone += 1 } else { pm.jobsError += 1 } } // addInFlightJob adds a job ID to the in flight set. // // Returns true if the job ID was added, false if it already exists. func (pm *PrinterManager) addInFlightJob(jobID string) bool { pm.jobsInFlightMutex.Lock() defer pm.jobsInFlightMutex.Unlock() if _, exists := pm.jobsInFlight[jobID]; exists { return false } pm.jobsInFlight[jobID] = struct{}{} return true } // deleteInFlightJob deletes a job from the in flight set. func (pm *PrinterManager) deleteInFlightJob(jobID string) { pm.jobsInFlightMutex.Lock() defer pm.jobsInFlightMutex.Unlock() delete(pm.jobsInFlight, jobID) } // printJob prints a new job to a native printer, then polls the native job state // and updates the GCP/Privet job state. then returns when the job state is DONE // or ABORTED. // // All errors are reported and logged from inside this function. func (pm *PrinterManager) printJob(nativePrinterName, filename, title, user, jobID string, ticket *cdd.CloudJobTicket, updateJob func(string, *cdd.PrintJobStateDiff) error) { defer os.Remove(filename) if !pm.addInFlightJob(jobID) { // This print job was already received. We probably received it // again because the first instance is still QUEUED (ie not // IN_PROGRESS). That's OK, just throw away the second instance. return } defer pm.deleteInFlightJob(jobID) if !pm.jobFullUsername { user = strings.Split(user, "@")[0] } printer, exists := pm.printers.GetByNativeName(nativePrinterName) if !exists { pm.incrementJobsProcessed(false) state := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, ServiceActionCause: &cdd.ServiceActionCause{ErrorCode: cdd.ServiceActionCausePrinterDeleted}, }, } if err := updateJob(jobID, &state); err != nil { log.ErrorJob(jobID, err) } return } nativeJobID, err := pm.native.Print(&printer, filename, title, user, jobID, ticket) if err != nil { pm.incrementJobsProcessed(false) log.ErrorJobf(jobID, "Failed to submit to native print system: %s", err) state := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCausePrintFailure}, }, } if err := updateJob(jobID, &state); err != nil { log.ErrorJob(jobID, err) } return } log.InfoJobf(jobID, "Submitted as native job %d", nativeJobID) var state cdd.PrintJobStateDiff ticker := time.NewTicker(time.Second) defer ticker.Stop() defer pm.releaseJob(printer.Name, nativeJobID, jobID) for _ = range ticker.C { nativeState, err := pm.native.GetJobState(printer.Name, nativeJobID) if err != nil { log.WarningJobf(jobID, "Failed to get state of native job %d: %s", nativeJobID, err) state = cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{ErrorCode: cdd.DeviceActionCauseOther}, }, PagesPrinted: state.PagesPrinted, } if err := updateJob(jobID, &state); err != nil { log.ErrorJob(jobID, err) } pm.incrementJobsProcessed(false) return } if !reflect.DeepEqual(*nativeState, state) { state = *nativeState if err = updateJob(jobID, &state); err != nil { log.ErrorJob(jobID, err) } log.InfoJobf(jobID, "State: %s", state.State.Type) } if state.State.Type != cdd.JobStateInProgress && state.State.Type != cdd.JobStateStopped { if state.State.Type == cdd.JobStateDone { pm.incrementJobsProcessed(true) } else { pm.incrementJobsProcessed(false) } return } } } func (pm *PrinterManager) releaseJob(printerName string, nativeJobID uint32, jobID string) { if err := pm.native.ReleaseJob(printerName, nativeJobID); err != nil { log.ErrorJob(jobID, err) } } // GetJobStats returns information that is useful for monitoring // the connector. func (pm *PrinterManager) GetJobStats() (uint, uint, uint, error) { var processing uint for _, printer := range pm.printers.GetAll() { processing += printer.NativeJobSemaphore.Count() } pm.jobStatsMutex.Lock() defer pm.jobStatsMutex.Unlock() return pm.jobsDone, pm.jobsError, processing, nil } cloud-print-connector-1.12/monitor/000077500000000000000000000000001311204274000173465ustar00rootroot00000000000000cloud-print-connector-1.12/monitor/monitor.go000066400000000000000000000060751311204274000213740ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux darwin freebsd package monitor import ( "fmt" "net" "github.com/google/cloud-print-connector/cups" "github.com/google/cloud-print-connector/gcp" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" "github.com/google/cloud-print-connector/manager" "github.com/google/cloud-print-connector/privet" ) const monitorFormat = `cups-printers=%d cups-raw-printers=%d gcp-printers=%d local-printers=%d cups-conn-qty=%d cups-conn-max-qty=%d jobs-done=%d jobs-error=%d jobs-in-progress=%d ` type Monitor struct { cups *cups.CUPS gcp *gcp.GoogleCloudPrint p *privet.Privet pm *manager.PrinterManager listenerQuit chan bool } func NewMonitor(cups *cups.CUPS, gcp *gcp.GoogleCloudPrint, p *privet.Privet, pm *manager.PrinterManager, socketFilename string) (*Monitor, error) { m := Monitor{cups, gcp, p, pm, make(chan bool)} listener, err := net.ListenUnix("unix", &net.UnixAddr{socketFilename, "unix"}) if err != nil { return nil, err } go m.listen(listener) return &m, nil } func (m *Monitor) listen(listener net.Listener) { ch := make(chan net.Conn) quitReq := make(chan bool, 1) quitAck := make(chan bool) go func() { for { conn, err := listener.Accept() if err != nil { select { case <-quitReq: quitAck <- true return } log.Errorf("Error listening to monitor socket: %s", err) } else { ch <- conn } } }() for { select { case conn := <-ch: log.Info("Received monitor request") stats, err := m.getStats() if err != nil { log.Warningf("Monitor request failed: %s", err) conn.Write([]byte("error")) } else { conn.Write([]byte(stats)) } conn.Close() case <-m.listenerQuit: quitReq <- true listener.Close() <-quitAck m.listenerQuit <- true return } } } func (m *Monitor) Quit() { m.listenerQuit <- true <-m.listenerQuit } func (m *Monitor) getStats() (string, error) { var cupsPrinterQuantity, rawPrinterQuantity, gcpPrinterQuantity, privetPrinterQuantity int if cupsPrinters, err := m.cups.GetPrinters(); err != nil { return "", err } else { cupsPrinterQuantity = len(cupsPrinters) _, rawPrinters := lib.FilterRawPrinters(cupsPrinters) rawPrinterQuantity = len(rawPrinters) } cupsConnOpen := m.cups.ConnQtyOpen() cupsConnMax := m.cups.ConnQtyMax() if m.gcp != nil { if gcpPrinters, err := m.gcp.List(); err != nil { return "", err } else { gcpPrinterQuantity = len(gcpPrinters) } } if m.p != nil { privetPrinterQuantity = m.p.Size() } jobsDone, jobsError, jobsProcessing, err := m.pm.GetJobStats() if err != nil { return "", err } stats := fmt.Sprintf( monitorFormat, cupsPrinterQuantity, rawPrinterQuantity, gcpPrinterQuantity, privetPrinterQuantity, cupsConnOpen, cupsConnMax, jobsDone, jobsError, jobsProcessing) return stats, nil } cloud-print-connector-1.12/privet/000077500000000000000000000000001311204274000171705ustar00rootroot00000000000000cloud-print-connector-1.12/privet/api-server.go000066400000000000000000000321751311204274000216040ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "encoding/json" "errors" "io" "io/ioutil" "net" "net/http" "os" "strconv" "strings" "time" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" "github.com/google/cloud-print-connector/log" ) var ( closed = errors.New("closed") supportedAPIsOnline = []string{ "/privet/accesstoken", "/privet/capabilities", "/privet/printer/createjob", "/privet/printer/submitdoc", "/privet/printer/jobstate", } supportedAPIsOffline = []string{ "/privet/capabilities", "/privet/printer/createjob", "/privet/printer/submitdoc", "/privet/printer/jobstate", } ) type privetError struct { Error string `json:"error"` Description string `json:"description,omitempty"` ServerAPI string `json:"server_api,omitempty"` ServerCode int `json:"server_code,omitempty"` ServerHTTPCode int `json:"server_http_code,omitempty"` Timeout int `json:"timeout,omitempty"` } func writeError(w http.ResponseWriter, e, description string) { pe := privetError{ Error: e, Description: description, }.json() w.Write(pe) } func (e privetError) json() []byte { marshalled, err := json.MarshalIndent(e, "", " ") if err != nil { log.Errorf("Failed to marshal Privet Error: %s", err) } return marshalled } type privetAPI struct { gcpID string name string gcpBaseURL string xsrf xsrfSecret online bool jc *jobCache jobs chan<- *lib.Job getPrinter func(string) (lib.Printer, bool) getProximityToken func(string, string) ([]byte, int, error) listener *quittableListener startTime time.Time } func newPrivetAPI(gcpID, name, gcpBaseURL string, xsrf xsrfSecret, online bool, jc *jobCache, jobs chan<- *lib.Job, getPrinter func(string) (lib.Printer, bool), getProximityToken func(string, string) ([]byte, int, error), listener *quittableListener) (*privetAPI, error) { api := &privetAPI{ gcpID: gcpID, name: name, gcpBaseURL: gcpBaseURL, xsrf: xsrf, online: online, jc: jc, jobs: jobs, getPrinter: getPrinter, getProximityToken: getProximityToken, listener: listener, startTime: time.Now(), } go api.serve() return api, nil } func (api *privetAPI) port() uint16 { return uint16(api.listener.Addr().(*net.TCPAddr).Port) } func (api *privetAPI) quit() { api.listener.quit() } func (api *privetAPI) serve() { sm := http.NewServeMux() sm.HandleFunc("/privet/info", api.info) if api.online { sm.HandleFunc("/privet/accesstoken", api.accesstoken) } sm.HandleFunc("/privet/capabilities", api.capabilities) sm.HandleFunc("/privet/printer/createjob", api.createjob) sm.HandleFunc("/privet/printer/submitdoc", api.submitdoc) sm.HandleFunc("/privet/printer/jobstate", api.jobstate) err := http.Serve(api.listener, sm) if err != nil && err != closed { log.Errorf("Privet API HTTP server failed: %s", err) } } type infoResponse struct { Version string `json:"version"` Name string `json:"name"` Description string `json:"description,omitempty"` URL string `json:"url"` Type []string `json:"type"` ID string `json:"id"` DeviceState string `json:"device_state"` ConnectionState string `json:"connection_state"` Manufacturer string `json:"manufacturer"` Model string `json:"model"` SerialNumber string `json:"serial_number,omitempty"` Firmware string `json:"firmware"` Uptime uint `json:"uptime"` SetupURL string `json:"setup_url,omitempty"` SupportURL string `json:"support_url,omitempty"` UpdateURL string `json:"update_url,omitempty"` XPrivetToken string `json:"x-privet-token"` API []string `json:"api"` SemanticState cdd.CloudDeviceState `json:"semantic_state,omitempty"` } func (api *privetAPI) info(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /info request: %+v", r) if r.Method != "GET" { w.WriteHeader(http.StatusMethodNotAllowed) return } if _, exists := r.Header["X-Privet-Token"]; !exists { w.WriteHeader(http.StatusBadRequest) writeError(w, "invalid_x_privet_token", "X-Privet-Token request header is missing or invalid") return } printer, exists := api.getPrinter(api.name) if !exists { w.WriteHeader(http.StatusInternalServerError) return } var s cdd.CloudConnectionStateType if api.online { s = cdd.CloudConnectionStateOnline } else { s = cdd.CloudConnectionStateOffline } state := cdd.CloudDeviceState{ Version: "1.0", CloudConnectionState: &s, Printer: printer.State, } var connectionState string var supportedAPIs []string if api.online { connectionState = "online" supportedAPIs = supportedAPIsOnline } else { connectionState = "offline" supportedAPIs = supportedAPIsOffline } response := infoResponse{ Version: "1.0", Name: printer.DefaultDisplayName, URL: api.gcpBaseURL, Type: []string{"printer"}, ID: printer.GCPID, DeviceState: strings.ToLower(string(printer.State.State)), ConnectionState: connectionState, Manufacturer: printer.Manufacturer, Model: printer.Model, SerialNumber: printer.UUID, Firmware: printer.ConnectorVersion, Uptime: uint(time.Since(api.startTime).Seconds()), SetupURL: printer.SetupURL, SupportURL: printer.SupportURL, UpdateURL: printer.UpdateURL, XPrivetToken: api.xsrf.newToken(), API: supportedAPIs, SemanticState: state, } j, err := json.MarshalIndent(response, "", " ") if err != nil { log.Errorf("Failed to marshal Privet info: %s", err) w.WriteHeader(http.StatusInternalServerError) return } w.Write(j) } func (api *privetAPI) checkRequest(w http.ResponseWriter, r *http.Request, method string) bool { if r.Method != method { w.WriteHeader(http.StatusMethodNotAllowed) return false } if token, exists := r.Header["X-Privet-Token"]; !exists || len(token) != 1 || !api.xsrf.isTokenValid(token[0]) { w.WriteHeader(http.StatusBadRequest) writeError(w, "invalid_x_privet_token", "X-Privet-Token request header is missing or invalid") return false } if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) return false } return true } func (api *privetAPI) accesstoken(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /accesstoken request: %+v", r) if ok := api.checkRequest(w, r, "GET"); !ok { return } user := r.Form.Get("user") if len(user) == 0 { writeError(w, "invalid_params", "user parameter expected") return } responseBody, httpStatusCode, err := api.getProximityToken(api.gcpID, user) if err != nil { log.Errorf("Failed to get proximity token: %s", err) } if responseBody == nil || len(responseBody) == 0 { log.Warning("Cloud returned empty response body") writeError(w, "server_error", "Check connector logs") return } var response struct { Success bool `json:"success"` Message string `json:"message"` ErrorCode int `json:"errorCode"` ProximityToken map[string]interface{} `json:"proximity_token"` } if err = json.Unmarshal(responseBody, &response); err != nil { log.Errorf("Failed to unmarshal ticket from cloud: %s", err) writeError(w, "server_error", "Check connector logs") return } if response.Success { token, err := json.MarshalIndent(response.ProximityToken, "", " ") if err != nil { log.Errorf("Failed to marshal something that was just unmarshalled: %s", err) writeError(w, "server_error", "Check connector logs") } else { w.Write(token) } return } if response.ErrorCode != 0 { e := privetError{ Error: "server_error", Description: response.Message, ServerAPI: "/proximitytoken", ServerCode: response.ErrorCode, ServerHTTPCode: httpStatusCode, }.json() w.Write(e) return } writeError(w, "server_error", "Check connector logs") } func (api *privetAPI) capabilities(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /capabilities request: %+v", r) if ok := api.checkRequest(w, r, "GET"); !ok { return } printer, exists := api.getPrinter(api.name) if !exists { w.WriteHeader(http.StatusInternalServerError) return } capabilities := cdd.CloudDeviceDescription{ Version: "1.0", Printer: printer.Description, } j, err := json.MarshalIndent(capabilities, "", " ") if err != nil { log.Errorf("Failed to marshal capabilities response: %s", err) w.WriteHeader(http.StatusInternalServerError) } else { w.Write(j) } } func (api *privetAPI) createjob(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /createjob request: %+v", r) if ok := api.checkRequest(w, r, "POST"); !ok { return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { log.Warningf("Failed to read request body: %s", err) writeError(w, "invalid_ticket", "Check connector logs") return } var ticket cdd.CloudJobTicket if err = json.Unmarshal(requestBody, &ticket); err != nil { log.Warningf("Failed to read request body: %s", err) writeError(w, "invalid_ticket", "Check connector logs") return } printer, exists := api.getPrinter(api.name) if !exists { w.WriteHeader(http.StatusInternalServerError) return } if printer.State.State == cdd.CloudDeviceStateStopped { writeError(w, "printer_error", "Printer is stopped") return } jobID, expiresIn := api.jc.createJob(&ticket) var response struct { JobID string `json:"job_id"` ExpiresIn int32 `json:"expires_in"` } response.JobID = jobID response.ExpiresIn = expiresIn j, err := json.MarshalIndent(response, "", " ") if err != nil { api.jc.deleteJob(jobID) log.Errorf("Failed to marrshal createJob response: %s", err) w.WriteHeader(http.StatusInternalServerError) } else { w.Write(j) } } func (api *privetAPI) submitdoc(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /submitdoc request: %+v", r) if ok := api.checkRequest(w, r, "POST"); !ok { return } file, err := ioutil.TempFile("", "cloud-print-connector-privet-") if err != nil { log.Errorf("Failed to create file for new Privet job: %s", err) w.WriteHeader(http.StatusInternalServerError) return } defer file.Close() jobSize, err := io.Copy(file, r.Body) if err != nil { log.Errorf("Failed to copy new print job file: %s", err) w.WriteHeader(http.StatusInternalServerError) os.Remove(file.Name()) return } if length, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil || length != jobSize { writeError(w, "invalid_params", "Content-Length header doesn't match length of content") os.Remove(file.Name()) return } jobType := r.Header.Get("Content-Type") if jobType == "" { writeError(w, "invalid_document_type", "Content-Type header is missing") os.Remove(file.Name()) return } printer, exists := api.getPrinter(api.name) if !exists { w.WriteHeader(http.StatusInternalServerError) os.Remove(file.Name()) return } if printer.State.State == cdd.CloudDeviceStateStopped { writeError(w, "printer_error", "Printer is stopped") os.Remove(file.Name()) return } jobName := r.Form.Get("job_name") userName := r.Form.Get("user_name") jobID := r.Form.Get("job_id") var expiresIn int32 var ticket *cdd.CloudJobTicket if jobID == "" { jobID, expiresIn = api.jc.createJob(nil) } else { var ok bool if expiresIn, ticket, ok = api.jc.getJobExpiresIn(jobID); !ok { pe := privetError{ Error: "invalid_print_job", Timeout: 5, }.json() w.Write(pe) os.Remove(file.Name()) return } } api.jobs <- &lib.Job{ NativePrinterName: api.name, Filename: file.Name(), Title: jobName, User: userName, JobID: jobID, Ticket: ticket, UpdateJob: api.jc.updateJob, } var response struct { JobID string `json:"job_id"` ExpiresIn int32 `json:"expires_in"` JobType string `json:"job_type"` JobSize int64 `json:"job_size"` JobName string `json:"job_name,omitempty"` } response.JobID = jobID response.ExpiresIn = expiresIn response.JobType = jobType response.JobSize = jobSize response.JobName = jobName j, err := json.MarshalIndent(response, "", " ") if err != nil { log.ErrorJobf(jobID, "Failed to marshal submitdoc response: %s", err) w.WriteHeader(http.StatusInternalServerError) return } w.Write(j) } func (api *privetAPI) jobstate(w http.ResponseWriter, r *http.Request) { log.Debugf("Received /jobstate request: %+v", r) if ok := api.checkRequest(w, r, "GET"); !ok { return } jobID := r.Form.Get("job_id") jobState, exists := api.jc.jobState(jobID) if !exists { writeError(w, "invalid_print_job", "") return } w.Write(jobState) } cloud-print-connector-1.12/privet/avahi.c000066400000000000000000000064151311204274000204320ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux freebsd #include "avahi.h" #include "_cgo_export.h" const char *SERVICE_TYPE = "_privet._tcp", *SERVICE_SUBTYPE = "_printer._sub._privet._tcp"; // startAvahiClient initializes a poll object, and a client. const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **client) { *threaded_poll = avahi_threaded_poll_new(); if (!*threaded_poll) { return avahi_strerror(avahi_client_errno(*client)); } int error; *client = avahi_client_new(avahi_threaded_poll_get(*threaded_poll), AVAHI_CLIENT_NO_FAIL, handleClientStateChange, NULL, &error); if (!*client) { avahi_threaded_poll_free(*threaded_poll); return avahi_strerror(error); } error = avahi_threaded_poll_start(*threaded_poll); if (AVAHI_OK != error) { avahi_client_free(*client); avahi_threaded_poll_free(*threaded_poll); return avahi_strerror(error); } return NULL; } static const char *populateGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, unsigned short port, AvahiStringList *txt) { int error = avahi_entry_group_add_service_strlst( group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, NULL, port, txt); if (AVAHI_OK != error) { avahi_entry_group_free(group); return avahi_strerror(error); } error = avahi_entry_group_add_service_subtype(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, SERVICE_SUBTYPE); if (AVAHI_OK != error) { avahi_entry_group_free(group); return avahi_strerror(error); } error = avahi_entry_group_commit(group); if (AVAHI_OK != error) { avahi_entry_group_free(group); return avahi_strerror(error); } return NULL; } const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, const char *service_name, unsigned short port, AvahiStringList *txt) { *group = avahi_entry_group_new(client, handleGroupStateChange, (void *)printer_name); if (!*group) { return avahi_strerror(avahi_client_errno(client)); } return populateGroup(client, *group, service_name, port, txt); } const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, unsigned short port, AvahiStringList *txt) { avahi_entry_group_reset(group); return populateGroup(client, group, service_name, port, txt); } const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt) { int error = avahi_entry_group_update_service_txt_strlst(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, txt); if (AVAHI_OK != error) { return avahi_strerror(error); } return NULL; } const char *removeAvahiGroup(AvahiEntryGroup *group) { int error = avahi_entry_group_free(group); if (AVAHI_OK != error) { return avahi_strerror(error); } return NULL; } void stopAvahiClient(AvahiThreadedPoll *threaded_poll, AvahiClient *client) { avahi_threaded_poll_stop(threaded_poll); avahi_client_free(client); avahi_threaded_poll_free(threaded_poll); } cloud-print-connector-1.12/privet/avahi.go000066400000000000000000000221201311204274000206040ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux freebsd package privet // #cgo linux LDFLAGS: -lavahi-client -lavahi-common // #cgo freebsd CFLAGS: -I/usr/local/include // #cgo freebsd LDFLAGS: -L/usr/local/lib -lavahi-client -lavahi-common // #include "avahi.h" import "C" import ( "errors" "fmt" "sync" "unsafe" "github.com/google/cloud-print-connector/log" ) var ( txtversKey = C.CString("txtvers") txtversValue = C.CString("1") tyKey = C.CString("ty") noteKey = C.CString("note") urlKey = C.CString("url") typeKey = C.CString("type") typeValue = C.CString("printer") idKey = C.CString("id") csKey = C.CString("cs") csValueOnline = C.CString("online") csValueOffline = C.CString("offline") ) type record struct { // printerName is the name of the printer, which must live on the heap so that the // C event handler can see it. // serviceName is the name of the service, which is the same as name except // when there is a collision. printerName *C.char serviceName *C.char port uint16 ty string note string url string id string online bool group *C.AvahiEntryGroup } type zeroconf struct { threadedPoll *C.AvahiThreadedPoll client *C.AvahiClient state C.AvahiClientState printers map[string]record spMutex sync.Mutex // Protects state and printers. restart chan struct{} q chan struct{} } // Keep the only instance in a global (package) var for C event handling. var instance *zeroconf func newZeroconf() (*zeroconf, error) { z := zeroconf{ state: C.AVAHI_CLIENT_CONNECTING, printers: make(map[string]record), restart: make(chan struct{}), q: make(chan struct{}), } instance = &z if errstr := C.startAvahiClient(&z.threadedPoll, &z.client); errstr != nil { err := fmt.Errorf("Failed to start Avahi client: %s", C.GoString(errstr)) return nil, err } go z.restartAndQuit() return &z, nil } func prepareTXT(ty, note, url, id string, online bool) *C.AvahiStringList { var txt *C.AvahiStringList txt = C.avahi_string_list_add_pair(txt, txtversKey, txtversValue) txt = C.avahi_string_list_add_pair(txt, typeKey, typeValue) tyValue := C.CString(ty) defer C.free(unsafe.Pointer(tyValue)) txt = C.avahi_string_list_add_pair(txt, tyKey, tyValue) if note != "" { noteValue := C.CString(note) defer C.free(unsafe.Pointer(noteValue)) txt = C.avahi_string_list_add_pair(txt, noteKey, noteValue) } urlValue := C.CString(url) defer C.free(unsafe.Pointer(urlValue)) txt = C.avahi_string_list_add_pair(txt, urlKey, urlValue) idValue := C.CString(id) defer C.free(unsafe.Pointer(idValue)) txt = C.avahi_string_list_add_pair(txt, idKey, idValue) if online { txt = C.avahi_string_list_add_pair(txt, csKey, csValueOnline) } else { txt = C.avahi_string_list_add_pair(txt, csKey, csValueOffline) } return txt } func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { nameValue := C.CString(name) r := record{ printerName: nameValue, serviceName: C.avahi_strdup(nameValue), port: port, ty: ty, note: note, url: url, id: id, online: online, } z.spMutex.Lock() defer z.spMutex.Unlock() if _, exists := z.printers[name]; exists { C.free(unsafe.Pointer(r.printerName)) C.avahi_free(unsafe.Pointer(r.serviceName)) return fmt.Errorf("printer %s was already added to Avahi publishing", name) } if z.state == C.AVAHI_CLIENT_S_RUNNING { txt := prepareTXT(ty, note, url, id, online) defer C.avahi_string_list_free(txt) C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) if errstr := C.addAvahiGroup(z.client, &r.group, r.printerName, r.serviceName, C.ushort(r.port), txt); errstr != nil { C.free(unsafe.Pointer(r.printerName)) C.avahi_free(unsafe.Pointer(r.serviceName)) err := fmt.Errorf("Failed to add Avahi group: %s", C.GoString(errstr)) return err } } z.printers[name] = r return nil } func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { z.spMutex.Lock() defer z.spMutex.Unlock() r, exists := z.printers[name] if !exists { return fmt.Errorf("printer %s cannot be updated for Avahi publishing; it was never added", name) } r.ty = ty r.url = url r.id = id r.online = online if z.state == C.AVAHI_CLIENT_S_RUNNING && r.group != nil { txt := prepareTXT(ty, note, url, id, online) defer C.avahi_string_list_free(txt) C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) if errstr := C.updateAvahiGroup(r.group, r.serviceName, txt); errstr != nil { err := fmt.Errorf("Failed to update Avahi group: %s", C.GoString(errstr)) return err } } z.printers[name] = r return nil } func (z *zeroconf) removePrinter(name string) error { z.spMutex.Lock() defer z.spMutex.Unlock() r, exists := z.printers[name] if !exists { return fmt.Errorf("printer %s cannot be updated for Avahi publishing; it was never added", name) } if z.state == C.AVAHI_CLIENT_S_RUNNING && r.group != nil { C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) if errstr := C.removeAvahiGroup(r.group); errstr != nil { err := fmt.Errorf("Failed to remove Avahi group: %s", C.GoString(errstr)) return err } } C.free(unsafe.Pointer(r.printerName)) C.avahi_free(unsafe.Pointer(r.serviceName)) delete(z.printers, name) return nil } func (z *zeroconf) quit() { z.q <- struct{}{} <-z.q } func (z *zeroconf) restartAndQuit() { for { select { case <-z.restart: log.Warning("Avahi client failed. Make sure that avahi-daemon is running while I restart the client.") C.stopAvahiClient(z.threadedPoll, z.client) if errstr := C.startAvahiClient(&z.threadedPoll, &z.client); errstr != nil { err := errors.New(C.GoString(errstr)) log.Errorf("Failed to restart Avahi client: %s", err) } case <-z.q: for name := range z.printers { z.removePrinter(name) } C.stopAvahiClient(z.threadedPoll, z.client) close(z.q) return } } } // handleClientStateChange makes clean transitions as the connection with // avahi-daemon changes. //export handleClientStateChange func handleClientStateChange(client *C.AvahiClient, newState C.AvahiClientState, userdata unsafe.Pointer) { z := instance z.spMutex.Lock() defer z.spMutex.Unlock() // Name conflict. if newState == C.AVAHI_CLIENT_S_COLLISION { log.Warning("Avahi reports a host name collision.") } // Transition from not connecting to connecting. Warn in logs. if newState == C.AVAHI_CLIENT_CONNECTING { log.Warning("Cannot find Avahi daemon. Is it running?") } // Transition from running to not running. Free all groups. if newState != C.AVAHI_CLIENT_S_RUNNING { log.Info("Local printing disabled (Avahi client is not running).") for name, r := range z.printers { if r.group != nil { if errstr := C.removeAvahiGroup(r.group); errstr != nil { err := errors.New(C.GoString(errstr)) log.Errorf("Failed to remove Avahi group: %s", err) } r.group = nil z.printers[name] = r } } } // Transition from not running to running. Recreate all groups. if newState == C.AVAHI_CLIENT_S_RUNNING { log.Info("Local printing enabled (Avahi client is running).") for name, r := range z.printers { txt := prepareTXT(r.ty, r.note, r.url, r.id, r.online) defer C.avahi_string_list_free(txt) if errstr := C.addAvahiGroup(z.client, &r.group, r.printerName, r.serviceName, C.ushort(r.port), txt); errstr != nil { err := errors.New(C.GoString(errstr)) log.Errorf("Failed to add Avahi group: %s", err) } z.printers[name] = r } } // Transition from not failure to failure. Recreate thread poll and client. if newState == C.AVAHI_CLIENT_FAILURE { z.restart <- struct{}{} } z.state = newState } //export handleGroupStateChange func handleGroupStateChange(group *C.AvahiEntryGroup, state C.AvahiEntryGroupState, name unsafe.Pointer) { switch state { case C.AVAHI_ENTRY_GROUP_COLLISION: z := instance z.spMutex.Lock() defer z.spMutex.Unlock() // Pick a new name. printerName := C.GoString((*C.char)(name)) r := z.printers[printerName] txt := prepareTXT(r.ty, r.note, r.url, r.id, r.online) defer C.avahi_string_list_free(txt) altName := C.avahi_alternative_service_name(r.serviceName) C.avahi_free(unsafe.Pointer(r.serviceName)) r.serviceName = altName log.Warningf("Avahi failed to register '%s' due to a naming collision, trying with '%s'", printerName, C.GoString((*C.char)(altName))) if errstr := C.resetAvahiGroup(z.client, r.group, r.serviceName, C.ushort(r.port), txt); errstr != nil { r.group = nil err := errors.New(C.GoString(errstr)) log.Errorf("Failed to reset Avahi group: %s", err) } z.printers[printerName] = r case C.AVAHI_ENTRY_GROUP_FAILURE: log.Warningf("Avahi failed to register '%s', don't know why", C.GoString((*C.char)(name))) } } cloud-print-connector-1.12/privet/avahi.h000066400000000000000000000021431311204274000204310ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build linux freebsd #include #include #include #include #include #include #include // free const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **client); const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, const char *service_name, unsigned short port, AvahiStringList *txt); const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, unsigned short port, AvahiStringList *txt); const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt); const char *removeAvahiGroup(AvahiEntryGroup *group); void stopAvahiClient(AvahiThreadedPoll *threaded_poll, AvahiClient *client); cloud-print-connector-1.12/privet/bonjour.c000066400000000000000000000125601311204274000210160ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build darwin #include "bonjour.h" #include "_cgo_export.h" // streamErrorToString converts a CFStreamError to a string. char *streamErrorToString(CFStreamError *error) { const char *errorDomain; switch (error->domain) { case kCFStreamErrorDomainCustom: errorDomain = "custom"; break; case kCFStreamErrorDomainPOSIX: errorDomain = "POSIX"; break; case kCFStreamErrorDomainMacOSStatus: errorDomain = "MacOS status"; break; default: errorDomain = "unknown"; break; } char *err = NULL; asprintf(&err, "domain %s code %d", errorDomain, error->error); return err; } void registerCallback(CFNetServiceRef service, CFStreamError *streamError, void *info) { CFStringRef printerName = (CFStringRef)info; char *printerNameC = malloc(sizeof(char) * (CFStringGetLength(printerName) + 1)); char *streamErrorC = streamErrorToString(streamError); char *error = NULL; asprintf(&error, "Error while announcing Bonjour service for printer %s: %s", printerNameC, streamErrorC); logBonjourError(error); CFRelease(printerName); free(printerNameC); free(streamErrorC); free(error); } // startBonjour starts and returns a bonjour service. // // Returns a registered service. Returns NULL and sets err on failure. CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short int port, const char *ty, const char *note, const char *url, const char *id, const char *cs, char **err) { CFStringRef nameCF = CFStringCreateWithCString(NULL, name, kCFStringEncodingASCII); CFStringRef typeCF = CFStringCreateWithCString(NULL, type, kCFStringEncodingASCII); CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); CFDictionarySetValue(dict, CFSTR("ty"), tyCF); if (CFStringGetLength(noteCF) > 0) { CFDictionarySetValue(dict, CFSTR("note"), noteCF); } CFDictionarySetValue(dict, CFSTR("url"), urlCF); CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); CFDictionarySetValue(dict, CFSTR("id"), idCF); CFDictionarySetValue(dict, CFSTR("cs"), csCF); CFDataRef txt = CFNetServiceCreateTXTDataWithDictionary(NULL, dict); CFNetServiceRef service = CFNetServiceCreate(NULL, CFSTR("local"), typeCF, nameCF, port); CFNetServiceSetTXTData(service, txt); // context now owns n, and will release n when service is released. CFNetServiceClientContext context = {0, (void *) nameCF, NULL, CFRelease, NULL}; CFNetServiceSetClient(service, registerCallback, &context); CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFOptionFlags options = kCFNetServiceFlagNoAutoRename; CFStreamError error; if (!CFNetServiceRegisterWithOptions(service, options, &error)) { char *errorString = streamErrorToString(&error); asprintf(err, "Failed to register Bonjour service: %s", errorString); free(errorString); CFRelease(service); service = NULL; } CFRelease(typeCF); CFRelease(tyCF); CFRelease(noteCF); CFRelease(urlCF); CFRelease(idCF); CFRelease(csCF); CFRelease(dict); CFRelease(txt); return service; } // updateBonjour updates the TXT record of service. void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, const char *id, const char *cs) { CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); CFDictionarySetValue(dict, CFSTR("ty"), tyCF); if (CFStringGetLength(noteCF) > 0) { CFDictionarySetValue(dict, CFSTR("note"), noteCF); } CFDictionarySetValue(dict, CFSTR("url"), urlCF); CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); CFDictionarySetValue(dict, CFSTR("id"), idCF); CFDictionarySetValue(dict, CFSTR("cs"), csCF); CFDataRef txt = CFNetServiceCreateTXTDataWithDictionary(NULL, dict); CFNetServiceSetTXTData(service, txt); CFRelease(tyCF); CFRelease(noteCF); CFRelease(urlCF); CFRelease(idCF); CFRelease(csCF); CFRelease(dict); CFRelease(txt); } // stopBonjour stops service and frees associated resources. void stopBonjour(CFNetServiceRef service) { CFNetServiceUnscheduleFromRunLoop(service, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFNetServiceSetClient(service, NULL, NULL); CFNetServiceCancel(service); CFRelease(service); } cloud-print-connector-1.12/privet/bonjour.go000066400000000000000000000064671311204274000212120ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build darwin package privet // #cgo LDFLAGS: -framework CoreServices // #include "bonjour.h" import "C" import ( "errors" "fmt" "sync" "unsafe" "github.com/google/cloud-print-connector/log" ) // TODO: How to add the _printer subtype? const serviceType = "_privet._tcp" type zeroconf struct { printers map[string]C.CFNetServiceRef pMutex sync.RWMutex // Protects printers. q chan struct{} } // NewZeroconf manages Bonjour services for printers shared via Privet. func newZeroconf() (*zeroconf, error) { z := zeroconf{ printers: make(map[string]C.CFNetServiceRef), q: make(chan struct{}), } return &z, nil } func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { z.pMutex.RLock() if _, exists := z.printers[name]; exists { z.pMutex.RUnlock() return fmt.Errorf("Bonjour already has printer %s", name) } z.pMutex.RUnlock() nameC := C.CString(name) defer C.free(unsafe.Pointer(nameC)) serviceTypeC := C.CString(serviceType) defer C.free(unsafe.Pointer(serviceTypeC)) tyC := C.CString(ty) defer C.free(unsafe.Pointer(tyC)) noteC := C.CString(note) defer C.free(unsafe.Pointer(noteC)) urlC := C.CString(url) defer C.free(unsafe.Pointer(urlC)) idC := C.CString(id) defer C.free(unsafe.Pointer(idC)) var onlineC *C.char if online { onlineC = C.CString("online") } else { onlineC = C.CString("offline") } defer C.free(unsafe.Pointer(onlineC)) var errstr *C.char = nil service := C.startBonjour(nameC, serviceTypeC, C.ushort(port), tyC, noteC, urlC, idC, onlineC, &errstr) if errstr != nil { defer C.free(unsafe.Pointer(errstr)) return errors.New(C.GoString(errstr)) } z.pMutex.Lock() defer z.pMutex.Unlock() z.printers[name] = service return nil } // updatePrinterTXT updates the advertised TXT record. func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { tyC := C.CString(ty) defer C.free(unsafe.Pointer(tyC)) noteC := C.CString(note) defer C.free(unsafe.Pointer(noteC)) urlC := C.CString(url) defer C.free(unsafe.Pointer(urlC)) idC := C.CString(id) defer C.free(unsafe.Pointer(idC)) var onlineC *C.char if online { onlineC = C.CString("online") } else { onlineC = C.CString("offline") } defer C.free(unsafe.Pointer(onlineC)) z.pMutex.RLock() defer z.pMutex.RUnlock() if service, exists := z.printers[name]; exists { C.updateBonjour(service, tyC, noteC, urlC, idC, onlineC) } else { return fmt.Errorf("Bonjour can't update printer %s that hasn't been added", name) } return nil } func (z *zeroconf) removePrinter(name string) error { z.pMutex.Lock() defer z.pMutex.Unlock() if service, exists := z.printers[name]; exists { C.stopBonjour(service) delete(z.printers, name) } else { return fmt.Errorf("Bonjour can't remove printer %s that hasn't been added", name) } return nil } func (z *zeroconf) quit() { z.pMutex.Lock() defer z.pMutex.Unlock() for name, service := range z.printers { C.stopBonjour(service) delete(z.printers, name) } } //export logBonjourError func logBonjourError(err *C.char) { log.Warningf("Bonjour: %s", C.GoString(err)) } cloud-print-connector-1.12/privet/bonjour.h000066400000000000000000000014061311204274000210200ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build darwin #include #include #include #include // asprintf #include // free CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short int port, const char *ty, const char *note, const char *url, const char *id, const char *cs, char **err); void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, const char *id, const char *cs); void stopBonjour(CFNetServiceRef service); cloud-print-connector-1.12/privet/jobcache.go000066400000000000000000000106671311204274000212670ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "encoding/json" "strconv" "sync" "time" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/log" ) // Jobs expire after this much time. const jobLifetime = time.Hour type entry struct { jobID string ticket *cdd.CloudJobTicket expiresAt time.Time state cdd.JobState pagesPrinted *int32 jobName string jobType string jobSize int64 timer *time.Timer } func newEntry(jobID string, ticket *cdd.CloudJobTicket) *entry { var state cdd.JobState if ticket == nil { state.Type = cdd.JobStateDraft } else { state.Type = cdd.JobStateQueued } entry := entry{ jobID: jobID, ticket: ticket, expiresAt: time.Now().Add(jobLifetime), state: state, } return &entry } func (e *entry) expiresIn() int32 { i := int32(e.expiresAt.Sub(time.Now()).Seconds()) if i < 0 { return 0 } return i } type jobCache struct { nextJobID int64 nextJobMutex sync.Mutex entries map[string]entry entriesMutex sync.RWMutex } func newJobCache() *jobCache { return &jobCache{ nextJobID: time.Now().UnixNano(), entries: make(map[string]entry), } } func (jc *jobCache) getNextJobID() string { jc.nextJobMutex.Lock() defer jc.nextJobMutex.Unlock() jc.nextJobID += 1 return strconv.FormatInt(jc.nextJobID, 36) } // createJob creates a new job, returns the new jobID and expires_in value. func (jc *jobCache) createJob(ticket *cdd.CloudJobTicket) (string, int32) { jobID := jc.getNextJobID() entry := newEntry(jobID, ticket) jc.entriesMutex.Lock() defer jc.entriesMutex.Unlock() entry.timer = time.AfterFunc(jobLifetime, func() { jc.deleteJob(jobID) }) jc.entries[jobID] = *entry return entry.jobID, int32(jobLifetime.Seconds()) } func (jc *jobCache) getJobExpiresIn(jobID string) (int32, *cdd.CloudJobTicket, bool) { jc.entriesMutex.RLock() defer jc.entriesMutex.RUnlock() if entry, ok := jc.entries[jobID]; !ok { return 0, nil, false } else { return entry.expiresIn(), entry.ticket, true } } func (jc *jobCache) submitJob(jobID, jobName, jobType string, jobSize int64) int32 { jc.entriesMutex.Lock() defer jc.entriesMutex.Unlock() if entry, ok := jc.entries[jobID]; ok { entry.jobName = jobName entry.jobType = jobType entry.jobSize = jobSize jc.entries[jobID] = entry return entry.expiresIn() } return 0 } func (jc *jobCache) deleteJob(jobID string) { jc.entriesMutex.Lock() defer jc.entriesMutex.Unlock() if entry, exists := jc.entries[jobID]; exists { // In case this job was deleted early, cancel the timer. entry.timer.Stop() } delete(jc.entries, jobID) } func (jc *jobCache) updateJob(jobID string, stateDiff *cdd.PrintJobStateDiff) error { jc.entriesMutex.Lock() defer jc.entriesMutex.Unlock() if entry, ok := jc.entries[jobID]; ok { if stateDiff.State != nil { entry.state = *stateDiff.State } if stateDiff.PagesPrinted != nil { entry.pagesPrinted = stateDiff.PagesPrinted } jc.entries[jobID] = entry } return nil } // jobState gets the state of the job identified by jobID as JSON-encoded response. // // Returns an empty byte array if the job doesn't exist (because it expired). func (jc *jobCache) jobState(jobID string) ([]byte, bool) { jc.entriesMutex.Lock() defer jc.entriesMutex.Unlock() entry, exists := jc.entries[jobID] if !exists { return []byte{}, false } var response struct { JobID string `json:"job_id"` State cdd.JobStateType `json:"state"` ExpiresIn int32 `json:"expires_in"` JobType string `json:"job_type,omitempty"` JobSize int64 `json:"job_size,omitempty"` JobName string `json:"job_name,omitempty"` SemanticState cdd.PrintJobState `json:"semantic_state"` } response.JobID = jobID response.State = entry.state.Type response.ExpiresIn = entry.expiresIn() response.JobType = entry.jobType response.JobSize = entry.jobSize response.JobName = entry.jobName response.SemanticState.Version = "1.0" response.SemanticState.State = entry.state response.SemanticState.PagesPrinted = entry.pagesPrinted j, err := json.MarshalIndent(response, "", " ") if err != nil { log.Errorf("Failed to marshal Privet jobState: %s", err) return []byte{}, false } return j, true } cloud-print-connector-1.12/privet/portmanager.go000066400000000000000000000060501311204274000220370ustar00rootroot00000000000000/* Copyright 2016 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "errors" "net" "os" "sync" "syscall" "time" ) var NoPortsAvailable = errors.New("No ports available") // portManager opens ports within the interval [low, high], starting with low. type portManager struct { low uint16 high uint16 // Keeping a cache of used ports improves benchmark tests by over 100x. m sync.Mutex p map[uint16]struct{} } func newPortManager(low, high uint16) *portManager { return &portManager{ low: low, high: high, p: make(map[uint16]struct{}), } } // listen finds an open port, returns an open listener on that port. // // Returns error when no ports are available. func (p *portManager) listen() (*quittableListener, error) { for port := p.nextAvailablePort(p.low); port != 0; port = p.nextAvailablePort(port) { if l, err := newQuittableListener(port, p); err == nil { return l, nil } else { if !isAddrInUse(err) { return nil, err } } } return nil, NoPortsAvailable } // nextAvailablePort checks the p map for the next port available. // p only keeps track of ports used by the connector, so the start parameter // is useful to check the port after a port that is in use by some other process. // // Returns zero when no available port can be found. func (p *portManager) nextAvailablePort(start uint16) uint16 { p.m.Lock() defer p.m.Unlock() for port := start; port <= p.high; port++ { if _, exists := p.p[port]; !exists { p.p[port] = struct{}{} return port } } return 0 } func (p *portManager) freePort(port uint16) { p.m.Lock() defer p.m.Unlock() delete(p.p, port) } func isAddrInUse(err error) bool { if err, ok := err.(*net.OpError); ok { if err, ok := err.Err.(*os.SyscallError); ok { return err.Err == syscall.EADDRINUSE } } return false } type quittableListener struct { *net.TCPListener pm *portManager // When q is closed, the listener is quitting. q chan struct{} } func newQuittableListener(port uint16, pm *portManager) (*quittableListener, error) { l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: int(port)}) if err != nil { return nil, err } return &quittableListener{l, pm, make(chan struct{}, 0)}, nil } func (l *quittableListener) Accept() (net.Conn, error) { conn, err := l.AcceptTCP() select { case <-l.q: if err == nil { conn.Close() } // The listener was closed on purpose. // Returning an error that is not a net.Error causes net.Server.Serve() to return. return nil, closed default: } // Clean up zombie connections. conn.SetKeepAlive(true) conn.SetKeepAlivePeriod(time.Minute) return conn, err } func (l *quittableListener) Close() error { err := l.TCPListener.Close() if err != nil { return err } l.pm.freePort(l.port()) return nil } func (l *quittableListener) port() uint16 { return uint16(l.Addr().(*net.TCPAddr).Port) } func (l *quittableListener) quit() { close(l.q) l.Close() } cloud-print-connector-1.12/privet/portmanager_test.go000066400000000000000000000054631311204274000231050ustar00rootroot00000000000000/* Copyright 2016 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import "testing" const portLow = 26000 func TestListen_available1(t *testing.T) { pm := newPortManager(portLow, portLow) l1, err := pm.listen() if err != nil { t.Fatal(err) } if l1.port() != portLow { t.Logf("Expected port %d, got port %d", portLow, l1.port()) l1.Close() t.FailNow() } l2, err := pm.listen() if err == nil { l1.Close() l2.Close() t.Fatal("Expected error when too many ports opened") } err = l1.Close() if err != nil { t.Fatal(err) } l3, err := pm.listen() if err != nil { t.Fatal(err) } if l3.port() != portLow { t.Logf("Expected port %d, got port %d", portLow, l3.port()) } l3.Close() } func TestListen_available2(t *testing.T) { pm := newPortManager(portLow, portLow+1) l1, err := pm.listen() if err != nil { t.Fatal(err) } if l1.port() != portLow { t.Logf("Expected port %d, got port %d", portLow, l1.port()) l1.Close() t.FailNow() } l2, err := pm.listen() if err != nil { t.Fatal(err) } if l2.port() != portLow+1 { t.Logf("Expected port %d, got port %d", portLow+1, l2.port()) l2.Close() t.FailNow() } l3, err := pm.listen() if err == nil { l1.Close() l2.Close() l3.Close() t.Fatal("Expected error when too many ports opened") } err = l2.Close() if err != nil { l1.Close() t.Fatal(err) } l4, err := pm.listen() if err != nil { t.Fatal(err) } if l4.port() != portLow+1 { t.Logf("Expected port %d, got port %d", portLow+1, l4.port()) } l5, err := pm.listen() if err == nil { l1.Close() l4.Close() l5.Close() t.Fatal("Expected error when too many ports opened") } err = l1.Close() if err != nil { l4.Close() t.Fatal(err) } l6, err := pm.listen() if err != nil { t.Fatal(err) } if l6.port() != portLow { t.Logf("Expected port %d, got port %d", portLow, l6.port()) } l4.Close() l6.Close() } // openPorts attempts to open n ports, where m are available. func openPorts(n, m uint16) { pm := newPortManager(portLow, portLow+m-1) for i := uint16(0); i < n; i++ { l, err := pm.listen() if err == nil { defer l.Close() } } } func BenchmarkListen_range1_available1(*testing.B) { openPorts(1, 1) } func BenchmarkListen_range10_available10(*testing.B) { openPorts(10, 10) } func BenchmarkListen_range100_available100(*testing.B) { openPorts(100, 100) } func BenchmarkListen_range1000_available1000(*testing.B) { openPorts(1000, 1000) } func BenchmarkListen_range10_available1(*testing.B) { openPorts(10, 1) } func BenchmarkListen_range100_available10(*testing.B) { openPorts(100, 10) } func BenchmarkListen_range1000_available100(*testing.B) { openPorts(1000, 100) } cloud-print-connector-1.12/privet/privet.go000066400000000000000000000061621311204274000210350ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "fmt" "sync" "github.com/google/cloud-print-connector/lib" ) // Privet managers local discovery and printing. type Privet struct { xsrf xsrfSecret apis map[string]*privetAPI apisMutex sync.RWMutex // Protects apis zc *zeroconf pm *portManager jobs chan<- *lib.Job jc jobCache gcpBaseURL string getProximityToken func(string, string) ([]byte, int, error) } // NewPrivet constructs a new Privet object. // // getProximityToken should be GoogleCloudPrint.ProximityToken() func NewPrivet(jobs chan<- *lib.Job, portLow, portHigh uint16, gcpBaseURL string, getProximityToken func(string, string) ([]byte, int, error)) (*Privet, error) { zc, err := newZeroconf() if err != nil { return nil, err } p := Privet{ xsrf: newXSRFSecret(), apis: make(map[string]*privetAPI), zc: zc, pm: newPortManager(portLow, portHigh), jobs: jobs, jc: *newJobCache(), gcpBaseURL: gcpBaseURL, getProximityToken: getProximityToken, } return &p, nil } // AddPrinter makes a printer available locally. func (p *Privet) AddPrinter(printer lib.Printer, getPrinter func(string) (lib.Printer, bool)) error { online := false if printer.GCPID != "" { online = true } listener, err := p.pm.listen() if err != nil { return err } api, err := newPrivetAPI(printer.GCPID, printer.Name, p.gcpBaseURL, p.xsrf, online, &p.jc, p.jobs, getPrinter, p.getProximityToken, listener) if err != nil { return err } var localDefaultDisplayName = printer.DefaultDisplayName if online { localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) } err = p.zc.addPrinter(printer.Name, api.port(), localDefaultDisplayName, "", p.gcpBaseURL, printer.GCPID, online) if err != nil { api.quit() return err } p.apisMutex.Lock() defer p.apisMutex.Unlock() p.apis[printer.Name] = api return nil } // UpdatePrinter updates a printer's TXT mDNS record. func (p *Privet) UpdatePrinter(diff *lib.PrinterDiff) error { online := false if diff.Printer.GCPID != "" { online = true } var localDefaultDisplayName = diff.Printer.DefaultDisplayName if online { localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) } return p.zc.updatePrinterTXT(diff.Printer.GCPID, localDefaultDisplayName, "", p.gcpBaseURL, diff.Printer.GCPID, online) } // DeletePrinter removes a printer from Privet. func (p *Privet) DeletePrinter(cupsPrinterName string) error { p.apisMutex.Lock() defer p.apisMutex.Unlock() err := p.zc.removePrinter(cupsPrinterName) if api, ok := p.apis[cupsPrinterName]; ok { api.quit() delete(p.apis, cupsPrinterName) } return err } func (p *Privet) Quit() { p.apisMutex.Lock() defer p.apisMutex.Unlock() p.zc.quit() for cupsPrinterName, api := range p.apis { api.quit() delete(p.apis, cupsPrinterName) } } func (p *Privet) Size() int { p.apisMutex.RLock() defer p.apisMutex.RUnlock() return len(p.apis) } cloud-print-connector-1.12/privet/windows.go000066400000000000000000000013371311204274000212150ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package privet import ( "errors" ) type zeroconf struct{} func newZeroconf() (*zeroconf, error) { return nil, errors.New("Privet has not been implemented for Windows") } func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { return nil } func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { return nil } func (z *zeroconf) removePrinter(name string) error { return nil } func (z *zeroconf) quit() {} cloud-print-connector-1.12/privet/xsrf.go000066400000000000000000000035471311204274000205120ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "bytes" "crypto/rand" "crypto/sha1" "encoding/base64" "time" ) const ( deviceSecretLength = 24 // 24 bytes == 192 bits tokenTimeout = 24 * time.Hour // Specified in GCP Privet doc. ) // xsrfSecret generates and validates XSRF tokens. type xsrfSecret []byte func newXSRFSecret() xsrfSecret { // Generate a random device secret. deviceSecret := make([]byte, deviceSecretLength) rand.Read(deviceSecret) return deviceSecret } func (x xsrfSecret) newToken() string { t := time.Now() return x.newTokenProvideTime(t) } func (x xsrfSecret) newTokenProvideTime(t time.Time) string { tb := int64ToBytes(t.Unix()) sum := sha1.Sum(append(x, tb...)) token := append(sum[:], tb...) return base64.StdEncoding.EncodeToString(token) } func (x xsrfSecret) isTokenValid(token string) bool { return x.isTokenValidProvideTime(token, time.Now()) } func (x xsrfSecret) isTokenValidProvideTime(token string, now time.Time) bool { tokenBytes, err := base64.StdEncoding.DecodeString(token) if err != nil { return false } if len(tokenBytes) != sha1.Size+8 { return false } tb := tokenBytes[sha1.Size:] t := time.Unix(bytesToInt64(tb), 0) if now.Sub(t) > tokenTimeout { return false } if t.Sub(now) > 0 { return false } sum := sha1.Sum(append(x, tb...)) if 0 != bytes.Compare(sum[:], tokenBytes[:sha1.Size]) { return false } return true } func int64ToBytes(v int64) []byte { b := make([]byte, 8) for i := range b { b[i] = byte(v >> uint(8*i)) } return b } func bytesToInt64(b []byte) int64 { if len(b) != 8 { return 0 } var v int64 for i := range b { v |= int64(b[i]) << uint(8*i) } return v } cloud-print-connector-1.12/privet/xsrf_test.go000066400000000000000000000047641311204274000215530ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package privet import ( "bytes" "testing" "time" ) var ( deviceSecret xsrfSecret = []byte("secretsecretsecretsecret") testTime = time.Unix(1234567890123456789, 0) testToken = "/8IVybcmeL9NPBBJgaZEa0r+GIkVgel99BAiEQ==" ) func TestNewToken(t *testing.T) { x := deviceSecret token := x.newTokenProvideTime(testTime) if token != testToken { t.Errorf("new token was %s should be %s", token, testToken) } } func TestIsTokenValid(t *testing.T) { x := deviceSecret if !x.isTokenValidProvideTime(testToken, testTime) { t.Errorf("valid token reported as invalid (+0ns)") } altTime := testTime.Add(time.Minute) if !x.isTokenValidProvideTime(testToken, altTime) { t.Errorf("valid token reported as invalid (+1m)") } altTime = testTime.Add(time.Hour) if !x.isTokenValidProvideTime(testToken, altTime) { t.Errorf("valid token reported as invalid (+1h)") } altTime = testTime.Add(23 * time.Hour) if !x.isTokenValidProvideTime(testToken, altTime) { t.Errorf("valid token reported as invalid (+23h)") } altTime = testTime.Add(25 * time.Hour) if x.isTokenValidProvideTime(testToken, altTime) { t.Errorf("invalid token reported as valid (+25h)") } altTime = testTime.Add(-time.Minute) if x.isTokenValidProvideTime(testToken, altTime) { t.Errorf("invalid token reported as valid (-1m)") } } func TestIsBadFormatTokenValid(t *testing.T) { x := deviceSecret if x.isTokenValidProvideTime("", testTime) { t.Errorf("empty token reported as valid") } } func TestInt64ToBytes(t *testing.T) { var v int64 = 0 b := []byte{0, 0, 0, 0, 0, 0, 0, 0} if got := int64ToBytes(v); bytes.Compare(b, got) != 0 { t.Errorf("expected %v got %v", b, got) } v = 1234567890123456789 b = []byte{21, 129, 233, 125, 244, 16, 34, 17} if got := int64ToBytes(v); bytes.Compare(b, got) != 0 { t.Errorf("expected %v got %v", b, got) } } func TestBytesToInt64(t *testing.T) { var v int64 = 0 b := []byte{0, 0, 0, 0, 0, 0, 0, 0} if got := bytesToInt64(b); v != got { t.Errorf("expected %d got %d", v, got) } v = 1234567890123456789 b = []byte{21, 129, 233, 125, 244, 16, 34, 17} if got := bytesToInt64(b); v != got { t.Errorf("expected %d got %d", v, got) } v = 0 b = []byte{1, 2, 3, 4} if got := bytesToInt64(b); v != got { t.Errorf("expected %d got %d", v, got) } } cloud-print-connector-1.12/systemd/000077500000000000000000000000001311204274000173475ustar00rootroot00000000000000cloud-print-connector-1.12/systemd/cloud-print-connector.service000066400000000000000000000012471311204274000251650ustar00rootroot00000000000000# Copyright 2016 Google Inc. All rights reserved. # # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd [Unit] Description=Google Cloud Print Connector Documentation="https://github.com/google/cloud-print-connector" After=cups.service avahi-daemon.service network-online.target Wants=cups.service avahi-daemon.service network-online.target [Service] ExecStart=/opt/cloud-print-connector/gcp-cups-connector -config-filename /opt/cloud-print-connector/gcp-cups-connector.config.json Restart=on-failure User=cloud-print-connector [Install] WantedBy=multi-user.target cloud-print-connector-1.12/winspool/000077500000000000000000000000001311204274000175315ustar00rootroot00000000000000cloud-print-connector-1.12/winspool/cairo.go000066400000000000000000000075171311204274000211670ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package winspool /* #cgo pkg-config: cairo-win32 #include */ import "C" import ( "fmt" "unsafe" ) func cairoStatusToError(status C.cairo_status_t) error { s := C.cairo_status_to_string(status) return fmt.Errorf("Cairo error: %s", C.GoString(s)) } type CairoSurface uintptr func (s CairoSurface) nativePointer() *C.struct__cairo_surface { return (*C.struct__cairo_surface)(unsafe.Pointer(s)) } func CairoWin32PrintingSurfaceCreate(hDC HDC) (CairoSurface, error) { cHDC := (*C.struct_HDC__)(unsafe.Pointer(hDC)) surface := C.cairo_win32_printing_surface_create(cHDC) s := CairoSurface(unsafe.Pointer(surface)) if err := s.status(); err != nil { return 0, err } return s, nil } func (s CairoSurface) status() error { status := C.cairo_surface_status(s.nativePointer()) if status != 0 { return cairoStatusToError(status) } return nil } func (s CairoSurface) ShowPage() error { C.cairo_surface_show_page(s.nativePointer()) return s.status() } func (s CairoSurface) Finish() error { C.cairo_surface_finish(s.nativePointer()) return s.status() } func (s *CairoSurface) Destroy() error { C.cairo_surface_destroy(s.nativePointer()) if err := s.status(); err != nil { return err } *s = 0 return nil } func (s *CairoSurface) GetDeviceOffset() (float64, float64, error) { var xOffset, yOffset float64 C.cairo_surface_get_device_offset(s.nativePointer(), (*C.double)(&xOffset), (*C.double)(&yOffset)) return xOffset, yOffset, s.status() } func (s *CairoSurface) GetDeviceScale() (float64, float64, error) { var xScale, yScale float64 C.cairo_surface_get_device_scale(s.nativePointer(), (*C.double)(&xScale), (*C.double)(&yScale)) return xScale, yScale, s.status() } func (s *CairoSurface) SetFallbackResolution(xPPI, yPPI float64) error { C.cairo_surface_set_fallback_resolution(s.nativePointer(), C.double(xPPI), C.double(yPPI)) return s.status() } type CairoContext uintptr func (c CairoContext) nativePointer() *C.struct__cairo { return (*C.struct__cairo)(unsafe.Pointer(c)) } func CairoCreateContext(surface CairoSurface) (CairoContext, error) { context := C.cairo_create(surface.nativePointer()) c := CairoContext(unsafe.Pointer(context)) if err := c.status(); err != nil { return 0, err } return c, nil } func (c CairoContext) status() error { status := C.cairo_status(c.nativePointer()) if status != 0 { return cairoStatusToError(status) } return nil } func (c *CairoContext) Destroy() error { C.cairo_destroy(c.nativePointer()) *c = 0 return nil } func (c CairoContext) Save() error { C.cairo_save(c.nativePointer()) return c.status() } func (c CairoContext) Restore() error { C.cairo_restore(c.nativePointer()) return c.status() } func (c CairoContext) IdentityMatrix() error { C.cairo_identity_matrix(c.nativePointer()) return c.status() } type CairoMatrix struct { xx float64 yx float64 xy float64 yy float64 x0 float64 y0 float64 } func (c CairoContext) GetMatrix() (*CairoMatrix, error) { var m CairoMatrix C.cairo_get_matrix(c.nativePointer(), (*C.struct__cairo_matrix)(unsafe.Pointer(&m))) return &m, c.status() } func (c CairoContext) Translate(x, y float64) error { C.cairo_translate(c.nativePointer(), C.double(x), C.double(y)) return c.status() } func (c CairoContext) Scale(x, y float64) error { C.cairo_scale(c.nativePointer(), C.double(x), C.double(y)) return c.status() } func (c CairoContext) Clip() error { C.cairo_clip(c.nativePointer()) return c.status() } func (c CairoContext) Rectangle(x, y, width, height float64) error { C.cairo_rectangle(c.nativePointer(), C.double(x), C.double(y), C.double(width), C.double(height)) return c.status() } cloud-print-connector-1.12/winspool/poppler.go000066400000000000000000000054571311204274000215540ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package winspool /* #cgo pkg-config: poppler-glib #include #include #include // free */ import "C" import ( "errors" "fmt" "path/filepath" "unsafe" ) func gErrorToGoError(gerr *C.GError) error { if gerr == nil { return errors.New("Poppler/GLib: unknown error") } defer C.g_error_free(gerr) message := C.GoString((*C.char)(gerr.message)) if message == "No error" { // Work around inconsistent error message when named file doesn't exist. quarkString := C.GoString((*C.char)(C.g_quark_to_string(gerr.domain))) if "g-file-error-quark" == quarkString { return fmt.Errorf("Poppler/GLib: file error, code %d", gerr.code) } return fmt.Errorf("Poppler/GLib: unknown error, domain %d, code %d", gerr.domain, gerr.code) } return fmt.Errorf("Poppler/GLib: %s", C.GoString((*C.char)(gerr.message))) } type PopplerDocument uintptr func (d PopplerDocument) nativePointer() *C.struct__PopplerDocument { return (*C.struct__PopplerDocument)(unsafe.Pointer(d)) } func PopplerDocumentNewFromFile(filename string) (PopplerDocument, error) { filename, err := filepath.Abs(filename) if err != nil { return 0, err } cFilename := (*C.gchar)(C.CString(filename)) defer C.free(unsafe.Pointer(cFilename)) var gerr *C.GError uri := C.g_filename_to_uri(cFilename, nil, &gerr) if uri == nil || gerr != nil { return 0, gErrorToGoError(gerr) } defer C.g_free(C.gpointer(uri)) doc := C.poppler_document_new_from_file((*C.char)(uri), nil, &gerr) if gerr != nil { return 0, gErrorToGoError(gerr) } return PopplerDocument(unsafe.Pointer(doc)), nil } func (d PopplerDocument) GetNPages() int { n := C.poppler_document_get_n_pages(d.nativePointer()) return int(n) } func (d PopplerDocument) GetPage(index int) PopplerPage { p := C.poppler_document_get_page(d.nativePointer(), C.int(index)) return PopplerPage(uintptr(unsafe.Pointer(p))) } func (d *PopplerDocument) Unref() { C.g_object_unref(C.gpointer(*d)) *d = 0 } type PopplerPage uintptr func (p PopplerPage) nativePointer() *C.struct__PopplerPage { return (*C.struct__PopplerPage)(unsafe.Pointer(p)) } // GetSize returns the width and height of the page, in points (1/72 inch). func (p PopplerPage) GetSize() (float64, float64, error) { var width, height C.double C.poppler_page_get_size(p.nativePointer(), &width, &height) return float64(width), float64(height), nil } func (p PopplerPage) RenderForPrinting(context CairoContext) { C.poppler_page_render_for_printing(p.nativePointer(), context.nativePointer()) } func (p *PopplerPage) Unref() { C.g_object_unref(C.gpointer(*p)) *p = 0 } cloud-print-connector-1.12/winspool/utf16.go000066400000000000000000000013151311204274000210250ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package winspool import ( "reflect" "syscall" "unsafe" ) const utf16StringMaxBytes = 1024 func utf16PtrToStringSize(s *uint16, bytes uint32) string { if s == nil { return "" } hdr := reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(s)), Len: int(bytes / 2), Cap: int(bytes / 2), } c := *(*[]uint16)(unsafe.Pointer(&hdr)) return syscall.UTF16ToString(c) } func utf16PtrToString(s *uint16) string { return utf16PtrToStringSize(s, utf16StringMaxBytes) } cloud-print-connector-1.12/winspool/win32.go000066400000000000000000001102501311204274000210210ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package winspool import ( "errors" "fmt" "reflect" "strings" "syscall" "unsafe" "golang.org/x/sys/windows" ) var ( gdi32 = syscall.MustLoadDLL("gdi32.dll") kernel32 = syscall.MustLoadDLL("kernel32.dll") ntoskrnl = syscall.MustLoadDLL("ntoskrnl.exe") winspool = syscall.MustLoadDLL("winspool.drv") user32 = syscall.MustLoadDLL("user32.dll") abortDocProc = gdi32.MustFindProc("AbortDoc") closePrinterProc = winspool.MustFindProc("ClosePrinter") createDCProc = gdi32.MustFindProc("CreateDCW") deleteDCProc = gdi32.MustFindProc("DeleteDC") deviceCapabilitiesProc = winspool.MustFindProc("DeviceCapabilitiesW") documentPropertiesProc = winspool.MustFindProc("DocumentPropertiesW") endDocProc = gdi32.MustFindProc("EndDoc") endPageProc = gdi32.MustFindProc("EndPage") enumPrintersProc = winspool.MustFindProc("EnumPrintersW") getDeviceCapsProc = gdi32.MustFindProc("GetDeviceCaps") getJobProc = winspool.MustFindProc("GetJobW") openPrinterProc = winspool.MustFindProc("OpenPrinterW") resetDCProc = gdi32.MustFindProc("ResetDCW") rtlGetVersionProc = ntoskrnl.MustFindProc("RtlGetVersion") setGraphicsModeProc = gdi32.MustFindProc("SetGraphicsMode") setJobProc = winspool.MustFindProc("SetJobW") setWorldTransformProc = gdi32.MustFindProc("SetWorldTransform") startDocProc = gdi32.MustFindProc("StartDocW") startPageProc = gdi32.MustFindProc("StartPage") registerDeviceNotificationProc = user32.MustFindProc("RegisterDeviceNotificationW") ) // System error codes. const ( ERROR_SUCCESS = 0 ERROR_MORE_DATA = 234 ) // Errors returned by GetLastError(). const ( NO_ERROR = syscall.Errno(0) ERROR_INVALID_PARAMETER = syscall.Errno(87) ERROR_INSUFFICIENT_BUFFER = syscall.Errno(122) ) // First parameter to EnumPrinters(). const ( PRINTER_ENUM_DEFAULT = 0x00000001 PRINTER_ENUM_LOCAL = 0x00000002 PRINTER_ENUM_CONNECTIONS = 0x00000004 PRINTER_ENUM_FAVORITE = 0x00000004 PRINTER_ENUM_NAME = 0x00000008 PRINTER_ENUM_REMOTE = 0x00000010 PRINTER_ENUM_SHARED = 0x00000020 PRINTER_ENUM_NETWORK = 0x00000040 PRINTER_ENUM_EXPAND = 0x00004000 PRINTER_ENUM_CONTAINER = 0x00008000 PRINTER_ENUM_ICONMASK = 0x00ff0000 PRINTER_ENUM_ICON1 = 0x00010000 PRINTER_ENUM_ICON2 = 0x00020000 PRINTER_ENUM_ICON3 = 0x00040000 PRINTER_ENUM_ICON4 = 0x00080000 PRINTER_ENUM_ICON5 = 0x00100000 PRINTER_ENUM_ICON6 = 0x00200000 PRINTER_ENUM_ICON7 = 0x00400000 PRINTER_ENUM_ICON8 = 0x00800000 PRINTER_ENUM_HIDE = 0x01000000 ) // Registry value types. const ( REG_NONE = 0 REG_SZ = 1 REG_EXPAND_SZ = 2 REG_BINARY = 3 REG_DWORD = 4 REG_DWORD_LITTLE_ENDIAN = 4 REG_DWORD_BIG_ENDIAN = 5 REG_LINK = 6 REG_MULTI_SZ = 7 REG_RESOURCE_LIST = 8 REG_FULL_RESOURCE_DESCRIPTOR = 9 REG_RESOURCE_REQUIREMENTS_LIST = 10 REG_QWORD = 11 REG_QWORD_LITTLE_ENDIAN = 11 ) // PRINTER_INFO_2 attribute values const ( PRINTER_ATTRIBUTE_QUEUED uint32 = 0x00000001 PRINTER_ATTRIBUTE_DIRECT uint32 = 0x00000002 PRINTER_ATTRIBUTE_DEFAULT uint32 = 0x00000004 PRINTER_ATTRIBUTE_SHARED uint32 = 0x00000008 PRINTER_ATTRIBUTE_NETWORK uint32 = 0x00000010 PRINTER_ATTRIBUTE_HIDDEN uint32 = 0x00000020 PRINTER_ATTRIBUTE_LOCAL uint32 = 0x00000040 PRINTER_ATTRIBUTE_ENABLE_DEVQ uint32 = 0x00000080 PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS uint32 = 0x00000100 PRINTER_ATTRIBUTE_DO_COMPLETE_FIRST uint32 = 0x00000200 PRINTER_ATTRIBUTE_WORK_OFFLINE uint32 = 0x00000400 PRINTER_ATTRIBUTE_ENABLE_BIDI uint32 = 0x00000800 PRINTER_ATTRIBUTE_RAW_ONLY uint32 = 0x00001000 PRINTER_ATTRIBUTE_PUBLISHED uint32 = 0x00002000 ) // PRINTER_INFO_2 status values. const ( PRINTER_STATUS_PAUSED uint32 = 0x00000001 PRINTER_STATUS_ERROR uint32 = 0x00000002 PRINTER_STATUS_PENDING_DELETION uint32 = 0x00000004 PRINTER_STATUS_PAPER_JAM uint32 = 0x00000008 PRINTER_STATUS_PAPER_OUT uint32 = 0x00000010 PRINTER_STATUS_MANUAL_FEED uint32 = 0x00000020 PRINTER_STATUS_PAPER_PROBLEM uint32 = 0x00000040 PRINTER_STATUS_OFFLINE uint32 = 0x00000080 PRINTER_STATUS_IO_ACTIVE uint32 = 0x00000100 PRINTER_STATUS_BUSY uint32 = 0x00000200 PRINTER_STATUS_PRINTING uint32 = 0x00000400 PRINTER_STATUS_OUTPUT_BIN_FULL uint32 = 0x00000800 PRINTER_STATUS_NOT_AVAILABLE uint32 = 0x00001000 PRINTER_STATUS_WAITING uint32 = 0x00002000 PRINTER_STATUS_PROCESSING uint32 = 0x00004000 PRINTER_STATUS_INITIALIZING uint32 = 0x00008000 PRINTER_STATUS_WARMING_UP uint32 = 0x00010000 PRINTER_STATUS_TONER_LOW uint32 = 0x00020000 PRINTER_STATUS_NO_TONER uint32 = 0x00040000 PRINTER_STATUS_PAGE_PUNT uint32 = 0x00080000 PRINTER_STATUS_USER_INTERVENTION uint32 = 0x00100000 PRINTER_STATUS_OUT_OF_MEMORY uint32 = 0x00200000 PRINTER_STATUS_DOOR_OPEN uint32 = 0x00400000 PRINTER_STATUS_SERVER_UNKNOWN uint32 = 0x00800000 PRINTER_STATUS_POWER_SAVE uint32 = 0x01000000 PRINTER_STATUS_SERVER_OFFLINE uint32 = 0x02000000 PRINTER_STATUS_DRIVER_UPDATE_NEEDED uint32 = 0x04000000 ) // PRINTER_INFO_2 struct. type PrinterInfo2 struct { pServerName *uint16 pPrinterName *uint16 pShareName *uint16 pPortName *uint16 pDriverName *uint16 pComment *uint16 pLocation *uint16 pDevMode *DevMode pSepFile *uint16 pPrintProcessor *uint16 pDatatype *uint16 pParameters *uint16 pSecurityDescriptor uintptr attributes uint32 priority uint32 defaultPriority uint32 startTime uint32 untilTime uint32 status uint32 cJobs uint32 averagePPM uint32 } func (pi *PrinterInfo2) GetPrinterName() string { return utf16PtrToString(pi.pPrinterName) } func (pi *PrinterInfo2) GetPortName() string { return utf16PtrToString(pi.pPortName) } func (pi *PrinterInfo2) GetDriverName() string { return utf16PtrToString(pi.pDriverName) } func (pi *PrinterInfo2) GetLocation() string { return utf16PtrToString(pi.pLocation) } func (pi *PrinterInfo2) GetDevMode() *DevMode { return pi.pDevMode } func (pi *PrinterInfo2) GetAttributes() uint32 { return pi.attributes } func (pi *PrinterInfo2) GetStatus() uint32 { return pi.status } // PRINTER_ENUM_VALUES struct. type PrinterEnumValues struct { pValueName *uint16 cbValueName uint32 dwType uint32 pData uintptr cbData uint32 } // DEVMODE constants. const ( CCHDEVICENAME = 32 CCHFORMNAME = 32 DM_SPECVERSION uint16 = 0x0401 DM_COPY uint32 = 2 DM_MODIFY uint32 = 8 DM_ORIENTATION = 0x00000001 DM_PAPERSIZE = 0x00000002 DM_PAPERLENGTH = 0x00000004 DM_PAPERWIDTH = 0x00000008 DM_SCALE = 0x00000010 DM_POSITION = 0x00000020 DM_NUP = 0x00000040 DM_DISPLAYORIENTATION = 0x00000080 DM_COPIES = 0x00000100 DM_DEFAULTSOURCE = 0x00000200 DM_PRINTQUALITY = 0x00000400 DM_COLOR = 0x00000800 DM_DUPLEX = 0x00001000 DM_YRESOLUTION = 0x00002000 DM_TTOPTION = 0x00004000 DM_COLLATE = 0x00008000 DM_FORMNAME = 0x00010000 DM_LOGPIXELS = 0x00020000 DM_BITSPERPEL = 0x00040000 DM_PELSWIDTH = 0x00080000 DM_PELSHEIGHT = 0x00100000 DM_DISPLAYFLAGS = 0x00200000 DM_DISPLAYFREQUENCY = 0x00400000 DM_ICMMETHOD = 0x00800000 DM_ICMINTENT = 0x01000000 DM_MEDIATYPE = 0x02000000 DM_DITHERTYPE = 0x04000000 DM_PANNINGWIDTH = 0x08000000 DM_PANNINGHEIGHT = 0x10000000 DM_DISPLAYFIXEDOUTPUT = 0x20000000 DMORIENT_PORTRAIT int16 = 1 DMORIENT_LANDSCAPE int16 = 2 DMCOLOR_MONOCHROME int16 = 1 DMCOLOR_COLOR int16 = 2 DMDUP_SIMPLEX int16 = 1 DMDUP_VERTICAL int16 = 2 DMDUP_HORIZONTAL int16 = 3 DMCOLLATE_FALSE int16 = 0 DMCOLLATE_TRUE int16 = 1 DMNUP_SYSTEM uint32 = 1 DMNUP_ONEUP uint32 = 2 ) // DEVMODE struct. type DevMode struct { // WCHAR dmDeviceName[CCHDEVICENAME] dmDeviceName, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ uint16 dmSpecVersion uint16 dmDriverVersion uint16 dmSize uint16 dmDriverExtra uint16 dmFields uint32 dmOrientation int16 dmPaperSize int16 dmPaperLength int16 dmPaperWidth int16 dmScale int16 dmCopies int16 dmDefaultSource int16 dmPrintQuality int16 dmColor int16 dmDuplex int16 dmYResolution int16 dmTTOption int16 dmCollate int16 // WCHAR dmFormName[CCHFORMNAME] dmFormName, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ uint16 dmLogPixels int16 dmBitsPerPel uint16 dmPelsWidth uint16 dmPelsHeight uint16 dmNup uint32 dmDisplayFrequency uint32 dmICMMethod uint32 dmICMIntent uint32 dmMediaType uint32 dmDitherType uint32 dmReserved1 uint32 dmReserved2 uint32 dmPanningWidth uint32 dmPanningHeight uint32 } func (dm *DevMode) String() string { s := []string{ fmt.Sprintf("device name: %s", dm.GetDeviceName()), fmt.Sprintf("spec version: %d", dm.dmSpecVersion), } if dm.dmFields&DM_ORIENTATION != 0 { s = append(s, fmt.Sprintf("orientation: %d", dm.dmOrientation)) } if dm.dmFields&DM_PAPERSIZE != 0 { s = append(s, fmt.Sprintf("paper size: %d", dm.dmPaperSize)) } if dm.dmFields&DM_PAPERLENGTH != 0 { s = append(s, fmt.Sprintf("paper length: %d", dm.dmPaperLength)) } if dm.dmFields&DM_PAPERWIDTH != 0 { s = append(s, fmt.Sprintf("paper width: %d", dm.dmPaperWidth)) } if dm.dmFields&DM_SCALE != 0 { s = append(s, fmt.Sprintf("scale: %d", dm.dmScale)) } if dm.dmFields&DM_COPIES != 0 { s = append(s, fmt.Sprintf("copies: %d", dm.dmCopies)) } if dm.dmFields&DM_DEFAULTSOURCE != 0 { s = append(s, fmt.Sprintf("default source: %d", dm.dmDefaultSource)) } if dm.dmFields&DM_PRINTQUALITY != 0 { s = append(s, fmt.Sprintf("print quality: %d", dm.dmPrintQuality)) } if dm.dmFields&DM_COLOR != 0 { s = append(s, fmt.Sprintf("color: %d", dm.dmColor)) } if dm.dmFields&DM_DUPLEX != 0 { s = append(s, fmt.Sprintf("duplex: %d", dm.dmDuplex)) } if dm.dmFields&DM_YRESOLUTION != 0 { s = append(s, fmt.Sprintf("y-resolution: %d", dm.dmYResolution)) } if dm.dmFields&DM_TTOPTION != 0 { s = append(s, fmt.Sprintf("TT option: %d", dm.dmTTOption)) } if dm.dmFields&DM_COLLATE != 0 { s = append(s, fmt.Sprintf("collate: %d", dm.dmCollate)) } if dm.dmFields&DM_FORMNAME != 0 { s = append(s, fmt.Sprintf("formname: %s", utf16PtrToString(&dm.dmFormName))) } if dm.dmFields&DM_LOGPIXELS != 0 { s = append(s, fmt.Sprintf("log pixels: %d", dm.dmLogPixels)) } if dm.dmFields&DM_BITSPERPEL != 0 { s = append(s, fmt.Sprintf("bits per pel: %d", dm.dmBitsPerPel)) } if dm.dmFields&DM_PELSWIDTH != 0 { s = append(s, fmt.Sprintf("pels width: %d", dm.dmPelsWidth)) } if dm.dmFields&DM_PELSHEIGHT != 0 { s = append(s, fmt.Sprintf("pels height: %d", dm.dmPelsHeight)) } if dm.dmFields&DM_NUP != 0 { s = append(s, fmt.Sprintf("display flags: %d", dm.dmNup)) } if dm.dmFields&DM_DISPLAYFREQUENCY != 0 { s = append(s, fmt.Sprintf("display frequency: %d", dm.dmDisplayFrequency)) } if dm.dmFields&DM_ICMMETHOD != 0 { s = append(s, fmt.Sprintf("ICM method: %d", dm.dmICMMethod)) } if dm.dmFields&DM_ICMINTENT != 0 { s = append(s, fmt.Sprintf("ICM intent: %d", dm.dmICMIntent)) } if dm.dmFields&DM_DITHERTYPE != 0 { s = append(s, fmt.Sprintf("dither type: %d", dm.dmDitherType)) } if dm.dmFields&DM_PANNINGWIDTH != 0 { s = append(s, fmt.Sprintf("panning width: %d", dm.dmPanningWidth)) } if dm.dmFields&DM_PANNINGHEIGHT != 0 { s = append(s, fmt.Sprintf("panning height: %d", dm.dmPanningHeight)) } return strings.Join(s, ", ") } func (dm *DevMode) GetDeviceName() string { return utf16PtrToStringSize(&dm.dmDeviceName, CCHDEVICENAME*2) } func (dm *DevMode) GetOrientation() (int16, bool) { return dm.dmOrientation, dm.dmFields&DM_ORIENTATION != 0 } func (dm *DevMode) SetOrientation(orientation int16) { dm.dmOrientation = orientation dm.dmFields |= DM_ORIENTATION } func (dm *DevMode) GetPaperSize() (int16, bool) { return dm.dmPaperSize, dm.dmFields&DM_PAPERSIZE != 0 } func (dm *DevMode) SetPaperSize(paperSize int16) { dm.dmPaperSize = paperSize dm.dmFields |= DM_PAPERSIZE } func (dm *DevMode) ClearPaperSize() { dm.dmFields &^= DM_PAPERSIZE } func (dm *DevMode) GetPaperLength() (int16, bool) { return dm.dmPaperLength, dm.dmFields&DM_PAPERLENGTH != 0 } func (dm *DevMode) SetPaperLength(length int16) { dm.dmPaperLength = length dm.dmFields |= DM_PAPERLENGTH } func (dm *DevMode) ClearPaperLength() { dm.dmFields &^= DM_PAPERLENGTH } func (dm *DevMode) GetPaperWidth() (int16, bool) { return dm.dmPaperWidth, dm.dmFields&DM_PAPERWIDTH != 0 } func (dm *DevMode) SetPaperWidth(width int16) { dm.dmPaperWidth = width dm.dmFields |= DM_PAPERWIDTH } func (dm *DevMode) ClearPaperWidth() { dm.dmFields &^= DM_PAPERWIDTH } func (dm *DevMode) GetCopies() (int16, bool) { return dm.dmCopies, dm.dmFields&DM_COPIES != 0 } func (dm *DevMode) SetCopies(copies int16) { dm.dmCopies = copies dm.dmFields |= DM_COPIES } func (dm *DevMode) GetColor() (int16, bool) { return dm.dmColor, dm.dmFields&DM_COLOR != 0 } func (dm *DevMode) SetColor(color int16) { dm.dmColor = color dm.dmFields |= DM_COLOR } func (dm *DevMode) GetDuplex() (int16, bool) { return dm.dmDuplex, dm.dmFields&DM_DUPLEX != 0 } func (dm *DevMode) SetDuplex(duplex int16) { dm.dmDuplex = duplex dm.dmFields |= DM_DUPLEX } func (dm *DevMode) GetCollate() (int16, bool) { return dm.dmCollate, dm.dmFields&DM_COLLATE != 0 } func (dm *DevMode) SetCollate(collate int16) { dm.dmCollate = collate dm.dmFields |= DM_COLLATE } // DOCINFO struct. type DocInfo struct { cbSize int32 lpszDocName *uint16 lpszOutput *uint16 lpszDatatype *uint16 fwType uint32 } // Device parameters for GetDeviceCaps(). const ( DRIVERVERSION = 0 TECHNOLOGY = 2 HORZSIZE = 4 VERTSIZE = 6 HORZRES = 8 // Printable area of paper in pixels. VERTRES = 10 BITSPIXEL = 12 PLANES = 14 NUMBRUSHES = 16 NUMPENS = 18 NUMMARKERS = 20 NUMFONTS = 22 NUMCOLORS = 24 PDEVICESIZE = 26 CURVECAPS = 28 LINECAPS = 30 POLYGONALCAPS = 32 TEXTCAPS = 34 CLIPCAPS = 36 RASTERCAPS = 38 ASPECTX = 40 ASPECTY = 42 ASPECTXY = 44 LOGPIXELSX = 88 // Pixels per inch. LOGPIXELSY = 90 SIZEPALETTE = 104 NUMRESERVED = 106 COLORRES = 108 PHYSICALWIDTH = 110 // Paper width in pixels. PHYSICALHEIGHT = 111 PHYSICALOFFSETX = 112 // Paper margin in pixels. PHYSICALOFFSETY = 113 SCALINGFACTORX = 114 SCALINGFACTORY = 115 VREFRESH = 116 DESKTOPVERTRES = 117 DESKTOPHORZRES = 118 BTLALIGNMENT = 119 SHADEBLENDCAPS = 120 COLORMGMTCAPS = 121 ) // Device capabilities for DeviceCapabilities(). const ( DC_FIELDS = 1 DC_PAPERS = 2 DC_PAPERSIZE = 3 DC_MINEXTENT = 4 DC_MAXEXTENT = 5 DC_BINS = 6 DC_DUPLEX = 7 DC_SIZE = 8 DC_EXTRA = 9 DC_VERSION = 10 DC_DRIVER = 11 DC_BINNAMES = 12 DC_ENUMRESOLUTIONS = 13 DC_FILEDEPENDENCIES = 14 DC_TRUETYPE = 15 DC_PAPERNAMES = 16 DC_ORIENTATION = 17 DC_COPIES = 18 DC_BINADJUST = 19 DC_EMF_COMPLAINT = 20 DC_DATATYPE_PRODUCED = 21 DC_COLLATE = 22 DC_MANUFACTURER = 23 DC_MODEL = 24 DC_PERSONALITY = 25 DC_PRINTRATE = 26 DC_PRINTRATEUNIT = 27 DC_PRINTERMEM = 28 DC_MEDIAREADY = 29 DC_STAPLE = 30 DC_PRINTRATEPPM = 31 DC_COLORDEVICE = 32 DC_NUP = 33 DC_MEDIATYPENAMES = 34 DC_MEDIATYPES = 35 PRINTRATEUNIT_PPM = 1 PRINTRATEUNIT_CPS = 2 PRINTRATEUNIT_LPM = 3 PRINTRATEUNIT_IPM = 4 ) func binaryRegValueToBytes(data uintptr, size uint32) []byte { hdr := reflect.SliceHeader{ Data: data, Len: int(size), Cap: int(size), } return *(*[]byte)(unsafe.Pointer(&hdr)) } func enumPrinters(level uint32) ([]byte, uint32, error) { var cbBuf, pcReturned uint32 _, _, err := enumPrintersProc.Call(PRINTER_ENUM_LOCAL, 0, uintptr(level), 0, 0, uintptr(unsafe.Pointer(&cbBuf)), uintptr(unsafe.Pointer(&pcReturned))) if err != ERROR_INSUFFICIENT_BUFFER { return nil, 0, err } var pPrinterEnum []byte = make([]byte, cbBuf) r1, _, err := enumPrintersProc.Call(PRINTER_ENUM_LOCAL, 0, uintptr(level), uintptr(unsafe.Pointer(&pPrinterEnum[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf)), uintptr(unsafe.Pointer(&pcReturned))) if r1 == 0 { return nil, 0, err } return pPrinterEnum, pcReturned, nil } func EnumPrinters2() ([]PrinterInfo2, error) { pPrinterEnum, pcReturned, err := enumPrinters(2) if err != nil { return nil, err } hdr := reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(&pPrinterEnum[0])), Len: int(pcReturned), Cap: int(pcReturned), } printers := *(*[]PrinterInfo2)(unsafe.Pointer(&hdr)) return printers, nil } type HANDLE uintptr func OpenPrinter(printerName string) (HANDLE, error) { var pPrinterName *uint16 pPrinterName, err := syscall.UTF16PtrFromString(printerName) if err != nil { return 0, err } var hPrinter HANDLE r1, _, err := openPrinterProc.Call(uintptr(unsafe.Pointer(pPrinterName)), uintptr(unsafe.Pointer(&hPrinter)), 0) if r1 == 0 { return 0, err } return hPrinter, nil } func (hPrinter *HANDLE) ClosePrinter() error { r1, _, err := closePrinterProc.Call(uintptr(*hPrinter)) if r1 == 0 { return err } *hPrinter = 0 return nil } func (hPrinter HANDLE) DocumentPropertiesGet(deviceName string) (*DevMode, error) { pDeviceName, err := syscall.UTF16PtrFromString(deviceName) if err != nil { return nil, err } r1, _, err := documentPropertiesProc.Call(0, uintptr(hPrinter), uintptr(unsafe.Pointer(pDeviceName)), 0, 0, 0) cbBuf := int32(r1) if cbBuf < 0 { return nil, err } var pDevMode []byte = make([]byte, cbBuf) devMode := (*DevMode)(unsafe.Pointer(&pDevMode[0])) devMode.dmSize = uint16(cbBuf) devMode.dmSpecVersion = DM_SPECVERSION r1, _, err = documentPropertiesProc.Call(0, uintptr(hPrinter), uintptr(unsafe.Pointer(pDeviceName)), uintptr(unsafe.Pointer(devMode)), uintptr(unsafe.Pointer(devMode)), uintptr(DM_COPY)) if int32(r1) < 0 { return nil, err } return devMode, nil } func (hPrinter HANDLE) DocumentPropertiesSet(deviceName string, devMode *DevMode) error { pDeviceName, err := syscall.UTF16PtrFromString(deviceName) if err != nil { return err } r1, _, err := documentPropertiesProc.Call(0, uintptr(hPrinter), uintptr(unsafe.Pointer(pDeviceName)), uintptr(unsafe.Pointer(devMode)), uintptr(unsafe.Pointer(devMode)), uintptr(DM_COPY|DM_MODIFY)) if int32(r1) < 0 { return err } return nil } // JOB_INFO_1 status values. const ( JOB_STATUS_PAUSED uint32 = 0x00000001 JOB_STATUS_ERROR uint32 = 0x00000002 JOB_STATUS_DELETING uint32 = 0x00000004 JOB_STATUS_SPOOLING uint32 = 0x00000008 JOB_STATUS_PRINTING uint32 = 0x00000010 JOB_STATUS_OFFLINE uint32 = 0x00000020 JOB_STATUS_PAPEROUT uint32 = 0x00000040 JOB_STATUS_PRINTED uint32 = 0x00000080 JOB_STATUS_DELETED uint32 = 0x00000100 JOB_STATUS_BLOCKED_DEVQ uint32 = 0x00000200 JOB_STATUS_USER_INTERVENTION uint32 = 0x00000400 JOB_STATUS_RESTART uint32 = 0x00000800 JOB_STATUS_COMPLETE uint32 = 0x00001000 JOB_STATUS_RETAINED uint32 = 0x00002000 JOB_STATUS_RENDERING_LOCALLY uint32 = 0x00004000 ) // JOB_INFO_1 struct. type JobInfo1 struct { jobID uint32 pPrinterName *uint16 pMachineName *uint16 pUserName *uint16 pDocument *uint16 pDatatype *uint16 pStatus *uint16 status uint32 priority uint32 position uint32 totalPages uint32 pagesPrinted uint32 // SYSTEMTIME structure, in line. wSubmittedYear uint16 wSubmittedMonth uint16 wSubmittedDayOfWeek uint16 wSubmittedDay uint16 wSubmittedHour uint16 wSubmittedMinute uint16 wSubmittedSecond uint16 wSubmittedMilliseconds uint16 } func (ji1 *JobInfo1) GetStatus() uint32 { return ji1.status } func (ji1 *JobInfo1) GetTotalPages() uint32 { return ji1.totalPages } func (ji1 *JobInfo1) GetPagesPrinted() uint32 { return ji1.pagesPrinted } func (hPrinter HANDLE) GetJob(jobID int32) (*JobInfo1, error) { var cbBuf uint32 _, _, err := getJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, 0, 0, uintptr(unsafe.Pointer(&cbBuf))) if err != ERROR_INSUFFICIENT_BUFFER { return nil, err } var pJob []byte = make([]byte, cbBuf) r1, _, err := getJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, uintptr(unsafe.Pointer(&pJob[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf))) if r1 == 0 { return nil, err } var ji1 JobInfo1 = *(*JobInfo1)(unsafe.Pointer(&pJob[0])) return &ji1, nil } // SetJob command values. const ( JOB_CONTROL_PAUSE uint32 = 1 JOB_CONTROL_RESUME uint32 = 2 JOB_CONTROL_CANCEL uint32 = 3 JOB_CONTROL_RESTART uint32 = 4 JOB_CONTROL_DELETE uint32 = 5 JOB_CONTROL_SENT_TO_PRINTER uint32 = 6 JOB_CONTROL_LAST_PAGE_EJECTED uint32 = 7 JOB_CONTROL_RETAIN uint32 = 8 JOB_CONTROL_RELEASE uint32 = 9 ) func (hPrinter HANDLE) SetJobCommand(jobID int32, command uint32) error { r1, _, err := setJobProc.Call(uintptr(hPrinter), uintptr(jobID), 0, 0, uintptr(command)) if r1 == 0 { return err } return nil } func (hPrinter HANDLE) SetJobInfo1(jobID int32, ji1 *JobInfo1) error { r1, _, err := setJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, uintptr(unsafe.Pointer(ji1)), 0) if r1 == 0 { return err } return nil } func (hPrinter HANDLE) SetJobUserName(jobID int32, userName string) error { ji1, err := hPrinter.GetJob(jobID) if err != nil { return err } pUserName, err := syscall.UTF16PtrFromString(userName) if err != nil { return err } ji1.pUserName = pUserName ji1.position = 0 // To prevent a possible access denied error (0 is JOB_POSITION_UNSPECIFIED) err = hPrinter.SetJobInfo1(jobID, ji1) if err != nil { return err } return nil } type HDC uintptr func CreateDC(deviceName string, devMode *DevMode) (HDC, error) { lpszDevice, err := syscall.UTF16PtrFromString(deviceName) if err != nil { return 0, err } r1, _, err := createDCProc.Call(0, uintptr(unsafe.Pointer(lpszDevice)), 0, uintptr(unsafe.Pointer(devMode))) if r1 == 0 { return 0, err } return HDC(r1), nil } func (hDC HDC) ResetDC(devMode *DevMode) error { r1, _, err := resetDCProc.Call(uintptr(hDC), uintptr(unsafe.Pointer(devMode))) if r1 == 0 { return err } return nil } func (hDC *HDC) DeleteDC() error { r1, _, err := deleteDCProc.Call(uintptr(*hDC)) if r1 == 0 { return err } *hDC = 0 return nil } func (hDC HDC) GetDeviceCaps(nIndex int32) int32 { // No error returned. r1 == 0 when nIndex == -1. r1, _, _ := getDeviceCapsProc.Call(uintptr(hDC), uintptr(nIndex)) return int32(r1) } func (hDC HDC) StartDoc(docName string) (int32, error) { var docInfo DocInfo var err error docInfo.cbSize = int32(unsafe.Sizeof(docInfo)) docInfo.lpszDocName, err = syscall.UTF16PtrFromString(docName) if err != nil { return 0, err } r1, _, err := startDocProc.Call(uintptr(hDC), uintptr(unsafe.Pointer(&docInfo))) if r1 <= 0 { return 0, err } return int32(r1), nil } func (hDC HDC) EndDoc() error { r1, _, err := endDocProc.Call(uintptr(hDC)) if r1 <= 0 { return err } return nil } func (hDC HDC) AbortDoc() error { r1, _, err := abortDocProc.Call(uintptr(hDC)) fmt.Println(r1, err, "using untested AbortDoc") return err } func (hDC HDC) StartPage() error { r1, _, err := startPageProc.Call(uintptr(hDC)) if r1 <= 0 { return err } return nil } func (hDC HDC) EndPage() error { r1, _, err := endPageProc.Call(uintptr(hDC)) if r1 <= 0 { return err } return nil } const ( GM_COMPATIBLE int32 = 1 GM_ADVANCED int32 = 2 ) func (hDC HDC) SetGraphicsMode(iMode int32) error { r1, _, err := setGraphicsModeProc.Call(uintptr(hDC), uintptr(iMode)) if r1 == 0 { return err } return nil } type XFORM struct { eM11 float32 // X scale. eM12 float32 // Always zero. eM21 float32 // Always zero. eM22 float32 // Y scale. eDx float32 // X offset. eDy float32 // Y offset. } func NewXFORM(xScale, yScale, xOffset, yOffset float32) *XFORM { return &XFORM{xScale, 0, 0, yScale, xOffset, yOffset} } func (hDC HDC) SetWorldTransform(xform *XFORM) error { r1, _, err := setWorldTransformProc.Call(uintptr(hDC), uintptr(unsafe.Pointer(xform))) if r1 == 0 { if err == NO_ERROR { return fmt.Errorf("SetWorldTransform call failed; return value %d", int32(r1)) } return err } return nil } func DeviceCapabilitiesInt32(device, port string, fwCapability uint16) (int32, error) { pDevice, err := syscall.UTF16PtrFromString(device) if err != nil { return 0, err } pPort, err := syscall.UTF16PtrFromString(port) if err != nil { return 0, err } r1, _, _ := deviceCapabilitiesProc.Call(uintptr(unsafe.Pointer(pDevice)), uintptr(unsafe.Pointer(pPort)), uintptr(fwCapability), 0, 0) return int32(r1), nil } func deviceCapabilities(device, port string, fwCapability uint16, pOutput []byte) (int32, error) { pDevice, err := syscall.UTF16PtrFromString(device) if err != nil { return 0, err } pPort, err := syscall.UTF16PtrFromString(port) if err != nil { return 0, err } var r1 uintptr if pOutput == nil { r1, _, _ = deviceCapabilitiesProc.Call(uintptr(unsafe.Pointer(pDevice)), uintptr(unsafe.Pointer(pPort)), uintptr(fwCapability), 0, 0) } else { r1, _, _ = deviceCapabilitiesProc.Call(uintptr(unsafe.Pointer(pDevice)), uintptr(unsafe.Pointer(pPort)), uintptr(fwCapability), uintptr(unsafe.Pointer(&pOutput[0])), 0) } if int32(r1) == -1 { return 0, errors.New("DeviceCapabilities called with unsupported capability, or there was an error") } return int32(r1), nil } func DeviceCapabilitiesStrings(device, port string, fwCapability uint16, stringLength int32) ([]string, error) { nString, err := deviceCapabilities(device, port, fwCapability, nil) if err != nil { return nil, err } if nString <= 0 { return []string{}, nil } pOutput := make([]byte, stringLength*uint16Size*nString) _, err = deviceCapabilities(device, port, fwCapability, pOutput) if err != nil { return nil, err } values := make([]string, 0, nString) for i := int32(0); i < nString; i++ { value := utf16PtrToString((*uint16)(unsafe.Pointer(&pOutput[i*stringLength]))) values = append(values, value) } return values, nil } const ( uint16Size = 2 int32Size = 4 ) func DeviceCapabilitiesUint16Array(device, port string, fwCapability uint16) ([]uint16, error) { nValue, err := deviceCapabilities(device, port, fwCapability, nil) if err != nil { return nil, err } if nValue <= 0 { return []uint16{}, nil } pOutput := make([]byte, uint16Size*nValue) _, err = deviceCapabilities(device, port, fwCapability, pOutput) if err != nil { return nil, err } values := make([]uint16, 0, nValue) for i := int32(0); i < nValue; i++ { value := *(*uint16)(unsafe.Pointer(&pOutput[i*uint16Size])) values = append(values, value) } return values, nil } // DeviceCapabilitiesInt32Pairs returns a slice of an even quantity of int32. func DeviceCapabilitiesInt32Pairs(device, port string, fwCapability uint16) ([]int32, error) { nValue, err := deviceCapabilities(device, port, fwCapability, nil) if err != nil { return nil, err } if nValue <= 0 { return []int32{}, nil } pOutput := make([]byte, int32Size*2*nValue) _, err = deviceCapabilities(device, port, fwCapability, pOutput) if err != nil { return nil, err } values := make([]int32, 0, nValue*2) for i := int32(0); i < nValue*2; i++ { value := *(*int32)(unsafe.Pointer(&pOutput[i*int32Size])) values = append(values, value) } return values, nil } // DevMode.dmPaperSize values. const ( DMPAPER_LETTER = 1 DMPAPER_LETTERSMALL = 2 DMPAPER_TABLOID = 3 DMPAPER_LEDGER = 4 DMPAPER_LEGAL = 5 DMPAPER_STATEMENT = 6 DMPAPER_EXECUTIVE = 7 DMPAPER_A3 = 8 DMPAPER_A4 = 9 DMPAPER_A4SMALL = 10 DMPAPER_A5 = 11 DMPAPER_B4 = 12 DMPAPER_B5 = 13 DMPAPER_FOLIO = 14 DMPAPER_QUARTO = 15 DMPAPER_10X14 = 16 DMPAPER_11X17 = 17 DMPAPER_NOTE = 18 DMPAPER_ENV_9 = 19 DMPAPER_ENV_10 = 20 DMPAPER_ENV_11 = 21 DMPAPER_ENV_12 = 22 DMPAPER_ENV_14 = 23 DMPAPER_CSHEET = 24 DMPAPER_DSHEET = 25 DMPAPER_ESHEET = 26 DMPAPER_ENV_DL = 27 DMPAPER_ENV_C5 = 28 DMPAPER_ENV_C3 = 29 DMPAPER_ENV_C4 = 30 DMPAPER_ENV_C6 = 31 DMPAPER_ENV_C65 = 32 DMPAPER_ENV_B4 = 33 DMPAPER_ENV_B5 = 34 DMPAPER_ENV_B6 = 35 DMPAPER_ENV_ITALY = 36 DMPAPER_ENV_MONARCH = 37 DMPAPER_ENV_PERSONAL = 38 DMPAPER_FANFOLD_US = 39 DMPAPER_FANFOLD_STD_GERMAN = 40 DMPAPER_FANFOLD_LGL_GERMAN = 41 DMPAPER_ISO_B4 = 42 DMPAPER_JAPANESE_POSTCARD = 43 DMPAPER_9X11 = 44 DMPAPER_10X11 = 45 DMPAPER_15X11 = 46 DMPAPER_ENV_INVITE = 47 DMPAPER_RESERVED_48 = 48 DMPAPER_RESERVED_49 = 49 DMPAPER_LETTER_EXTRA = 50 DMPAPER_LEGAL_EXTRA = 51 DMPAPER_TABLOID_EXTRA = 52 DMPAPER_A4_EXTRA = 53 DMPAPER_LETTER_TRANSVERSE = 54 DMPAPER_A4_TRANSVERSE = 55 DMPAPER_LETTER_EXTRA_TRANSVERSE = 56 DMPAPER_A_PLUS = 57 DMPAPER_B_PLUS = 58 DMPAPER_LETTER_PLUS = 59 DMPAPER_A4_PLUS = 60 DMPAPER_A5_TRANSVERSE = 61 DMPAPER_B5_TRANSVERSE = 62 DMPAPER_A3_EXTRA = 63 DMPAPER_A5_EXTRA = 64 DMPAPER_B5_EXTRA = 65 DMPAPER_A2 = 66 DMPAPER_A3_TRANSVERSE = 67 DMPAPER_A3_EXTRA_TRANSVERSE = 68 DMPAPER_DBL_JAPANESE_POSTCARD = 69 DMPAPER_A6 = 70 DMPAPER_JENV_KAKU2 = 71 DMPAPER_JENV_KAKU3 = 72 DMPAPER_JENV_CHOU3 = 73 DMPAPER_JENV_CHOU4 = 74 DMPAPER_LETTER_ROTATED = 75 DMPAPER_A3_ROTATED = 76 DMPAPER_A4_ROTATED = 77 DMPAPER_A5_ROTATED = 78 DMPAPER_B4_JIS_ROTATED = 79 DMPAPER_B5_JIS_ROTATED = 80 DMPAPER_JAPANESE_POSTCARD_ROTATED = 81 DMPAPER_DBL_JAPANESE_POSTCARD_ROTATED = 82 DMPAPER_A6_ROTATED = 83 DMPAPER_JENV_KAKU2_ROTATED = 84 DMPAPER_JENV_KAKU3_ROTATED = 85 DMPAPER_JENV_CHOU3_ROTATED = 86 DMPAPER_JENV_CHOU4_ROTATED = 87 DMPAPER_B6_JIS = 88 DMPAPER_B6_JIS_ROTATED = 89 DMPAPER_12X11 = 90 DMPAPER_JENV_YOU4 = 91 DMPAPER_JENV_YOU4_ROTATED = 92 DMPAPER_P16K = 93 DMPAPER_P32K = 94 DMPAPER_P32KBIG = 95 DMPAPER_PENV_1 = 96 DMPAPER_PENV_2 = 97 DMPAPER_PENV_3 = 98 DMPAPER_PENV_4 = 99 DMPAPER_PENV_5 = 100 DMPAPER_PENV_6 = 101 DMPAPER_PENV_7 = 102 DMPAPER_PENV_8 = 103 DMPAPER_PENV_9 = 104 DMPAPER_PENV_10 = 105 DMPAPER_P16K_ROTATED = 106 DMPAPER_P32K_ROTATED = 107 DMPAPER_P32KBIG_ROTATED = 108 DMPAPER_PENV_1_ROTATED = 109 DMPAPER_PENV_2_ROTATED = 110 DMPAPER_PENV_3_ROTATED = 111 DMPAPER_PENV_4_ROTATED = 112 DMPAPER_PENV_5_ROTATED = 113 DMPAPER_PENV_6_ROTATED = 114 DMPAPER_PENV_7_ROTATED = 115 DMPAPER_PENV_8_ROTATED = 116 DMPAPER_PENV_9_ROTATED = 117 DMPAPER_PENV_10_ROTATED = 118 ) type RTLOSVersionInfo struct { dwOSVersionInfoSize uint32 dwMajorVersion uint32 dwMinorVersion uint32 dwBuildNumber uint32 dwPlatformId uint32 // WCHAR szCSDVersion[128] szCSDVersion, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ uint16 } func GetWindowsVersion() string { var osVersionInfo RTLOSVersionInfo osVersionInfo.dwOSVersionInfoSize = uint32(unsafe.Sizeof(osVersionInfo)) r1, _, _ := rtlGetVersionProc.Call(uintptr(unsafe.Pointer(&osVersionInfo))) if r1 != 0 { // This is unimportant, so "unknown" is fine. return "unknown (error)" } version := fmt.Sprintf("%d.%d.%d", osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion, osVersionInfo.dwBuildNumber) servicePackVersion := utf16PtrToString((*uint16)(&osVersionInfo.szCSDVersion)) if servicePackVersion != "" { version = fmt.Sprintf("%s %s", version, servicePackVersion) } return version } type GUID struct { Data1 uint32 Data2 uint16 Data3 uint16 Data4 [8]byte } var PRINTERS_DEVICE_CLASS = GUID{ 0x4d36e979, 0xe325, 0x11ce, [8]byte{0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18}, } type DevBroadcastDevinterface struct { dwSize uint32 dwDeviceType uint32 dwReserved uint32 classGuid GUID szName uint16 } const ( DEVICE_NOTIFY_SERVICE_HANDLE = 1 DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 4 DBT_DEVTYP_DEVICEINTERFACE = 5 ) func RegisterDeviceNotification(handle windows.Handle) error { var notificationFilter DevBroadcastDevinterface notificationFilter.dwSize = uint32(unsafe.Sizeof(notificationFilter)) notificationFilter.dwDeviceType = DBT_DEVTYP_DEVICEINTERFACE notificationFilter.dwReserved = 0 // BUG(pastarmovj): This class is ignored for now. Figure out what the right GUID is. notificationFilter.classGuid = PRINTERS_DEVICE_CLASS notificationFilter.szName = 0 r1, _, err := registerDeviceNotificationProc.Call(uintptr(handle), uintptr(unsafe.Pointer(¬ificationFilter)), DEVICE_NOTIFY_SERVICE_HANDLE|DEVICE_NOTIFY_ALL_INTERFACE_CLASSES) if r1 == 0 { return err } return nil } cloud-print-connector-1.12/winspool/winspool.go000066400000000000000000000653451311204274000217470ustar00rootroot00000000000000// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // +build windows package winspool import ( "errors" "fmt" "os" "runtime" "strconv" "strings" "github.com/google/cloud-print-connector/cdd" "github.com/google/cloud-print-connector/lib" "golang.org/x/sys/windows" ) // winspoolPDS represents capabilities that WinSpool always provides. var winspoolPDS = cdd.PrinterDescriptionSection{ SupportedContentType: &[]cdd.SupportedContentType{ cdd.SupportedContentType{ContentType: "application/pdf"}, }, FitToPage: &cdd.FitToPage{ Option: []cdd.FitToPageOption{ cdd.FitToPageOption{ Type: cdd.FitToPageNoFitting, IsDefault: true, }, cdd.FitToPageOption{ Type: cdd.FitToPageFitToPage, IsDefault: false, }, }, }, } // Interface between Go and the Windows API. type WinSpool struct { prefixJobIDToJobTitle bool displayNamePrefix string systemTags map[string]string printerBlacklist map[string]interface{} printerWhitelist map[string]interface{} } func NewWinSpool(prefixJobIDToJobTitle bool, displayNamePrefix string, printerBlacklist []string, printerWhitelist []string) (*WinSpool, error) { systemTags, err := getSystemTags() if err != nil { return nil, err } pb := map[string]interface{}{} for _, p := range printerBlacklist { pb[p] = struct{}{} } pw := map[string]interface{}{} for _, p := range printerWhitelist { pw[p] = struct{}{} } ws := WinSpool{ prefixJobIDToJobTitle: prefixJobIDToJobTitle, displayNamePrefix: displayNamePrefix, systemTags: systemTags, printerBlacklist: pb, printerWhitelist: pw, } return &ws, nil } func getSystemTags() (map[string]string, error) { tags := make(map[string]string) tags["connector-version"] = lib.BuildDate hostname, err := os.Hostname() if err == nil { tags["system-hostname"] = hostname } tags["system-arch"] = runtime.GOARCH tags["system-golang-version"] = runtime.Version() tags["system-windows-version"] = GetWindowsVersion() return tags, nil } func convertPrinterState(wsStatus uint32, wsAttributes uint32) *cdd.PrinterStateSection { state := cdd.PrinterStateSection{ State: cdd.CloudDeviceStateIdle, VendorState: &cdd.VendorState{}, } if wsStatus&(PRINTER_STATUS_PRINTING|PRINTER_STATUS_PROCESSING) != 0 { state.State = cdd.CloudDeviceStateProcessing } if wsStatus&PRINTER_STATUS_PAUSED != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateWarning, DescriptionLocalized: cdd.NewLocalizedString("printer paused"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_ERROR != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("printer error"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_PENDING_DELETION != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("printer is being deleted"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_PAPER_JAM != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("paper jam"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_PAPER_OUT != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("paper out"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_MANUAL_FEED != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("manual feed mode"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_PAPER_PROBLEM != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("paper problem"), } state.VendorState.Item = append(state.VendorState.Item, vs) } // If PRINTER_ATTRIBUTE_WORK_OFFLINE is set // spooler won't despool any jobs to the printer. // At least for some USB printers, this flag is controlled // automatically by the system depending on the state of physical connection. if wsStatus&PRINTER_STATUS_OFFLINE != 0 || wsAttributes&PRINTER_ATTRIBUTE_WORK_OFFLINE != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("printer is offline"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_IO_ACTIVE != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("active I/O state"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_BUSY != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("busy"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_OUTPUT_BIN_FULL != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("output bin is full"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_NOT_AVAILABLE != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("printer not available"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_WAITING != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("waiting"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_INITIALIZING != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("intitializing"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_WARMING_UP != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("warming up"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_TONER_LOW != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateWarning, DescriptionLocalized: cdd.NewLocalizedString("toner low"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_NO_TONER != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("no toner"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_PAGE_PUNT != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("cannot print the current page"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_USER_INTERVENTION != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("user intervention required"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_OUT_OF_MEMORY != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("out of memory"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_DOOR_OPEN != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("door open"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_SERVER_UNKNOWN != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateError, DescriptionLocalized: cdd.NewLocalizedString("printer status unknown"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if wsStatus&PRINTER_STATUS_POWER_SAVE != 0 { vs := cdd.VendorStateItem{ State: cdd.VendorStateInfo, DescriptionLocalized: cdd.NewLocalizedString("power save mode"), } state.VendorState.Item = append(state.VendorState.Item, vs) } if len(state.VendorState.Item) == 0 { state.VendorState = nil } return &state } func getManModel(driverName string) (man string, model string) { man = "Google" model = "Cloud Printer" parts := strings.SplitN(driverName, " ", 2) if len(parts) > 0 && len(parts[0]) > 0 { man = parts[0] } if len(parts) > 1 && len(parts[1]) > 0 { model = parts[1] } return } // GetPrinters gets all Windows printers found on this computer. func (ws *WinSpool) GetPrinters() ([]lib.Printer, error) { pi2s, err := EnumPrinters2() if err != nil { return nil, err } printers := make([]lib.Printer, 0, len(pi2s)) for _, pi2 := range pi2s { printerName := pi2.GetPrinterName() portName := pi2.GetPortName() devMode := pi2.GetDevMode() manufacturer, model := getManModel(pi2.GetDriverName()) printer := lib.Printer{ Name: printerName, DefaultDisplayName: ws.displayNamePrefix + printerName, UUID: printerName, // TODO: Add something unique from host. Manufacturer: manufacturer, Model: model, State: convertPrinterState(pi2.GetStatus(), pi2.GetAttributes()), Description: &cdd.PrinterDescriptionSection{}, Tags: map[string]string{ "printer-location": pi2.GetLocation(), }, } // Advertise color based on default value, which should be a solid indicator // of color-ness, because the source of this devMode object is EnumPrinters. if def, ok := devMode.GetColor(); ok { if def == DMCOLOR_COLOR { printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: strconv.FormatInt(int64(DMCOLOR_COLOR), 10), Type: cdd.ColorTypeStandardColor, IsDefault: true, CustomDisplayNameLocalized: cdd.NewLocalizedString("Color"), }, cdd.ColorOption{ VendorID: strconv.FormatInt(int64(DMCOLOR_MONOCHROME), 10), Type: cdd.ColorTypeStandardMonochrome, IsDefault: false, CustomDisplayNameLocalized: cdd.NewLocalizedString("Monochrome"), }, }, } } else if def == DMCOLOR_MONOCHROME { printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ VendorID: strconv.FormatInt(int64(DMCOLOR_MONOCHROME), 10), Type: cdd.ColorTypeStandardMonochrome, IsDefault: true, CustomDisplayNameLocalized: cdd.NewLocalizedString("Monochrome"), }, }, } } } if def, ok := devMode.GetDuplex(); ok { duplex, err := DeviceCapabilitiesInt32(printerName, portName, DC_DUPLEX) if err != nil { return nil, err } if duplex == 1 { printer.Description.Duplex = &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{ Type: cdd.DuplexNoDuplex, IsDefault: def == DMDUP_SIMPLEX, }, cdd.DuplexOption{ Type: cdd.DuplexLongEdge, IsDefault: def == DMDUP_VERTICAL, }, cdd.DuplexOption{ Type: cdd.DuplexShortEdge, IsDefault: def == DMDUP_HORIZONTAL, }, }, } } } if def, ok := devMode.GetOrientation(); ok { orientation, err := DeviceCapabilitiesInt32(printerName, portName, DC_ORIENTATION) if err != nil { return nil, err } if orientation == 90 || orientation == 270 { printer.Description.PageOrientation = &cdd.PageOrientation{ Option: []cdd.PageOrientationOption{ cdd.PageOrientationOption{ Type: cdd.PageOrientationPortrait, IsDefault: def == DMORIENT_PORTRAIT, }, cdd.PageOrientationOption{ Type: cdd.PageOrientationLandscape, IsDefault: def == DMORIENT_LANDSCAPE, }, }, } } } if def, ok := devMode.GetCopies(); ok { copies, err := DeviceCapabilitiesInt32(printerName, portName, DC_COPIES) if err != nil { return nil, err } if copies > 1 { printer.Description.Copies = &cdd.Copies{ Default: int32(def), Max: copies, } } } printer.Description.MediaSize, err = convertMediaSize(printerName, portName, devMode) if err != nil { return nil, err } if def, ok := devMode.GetCollate(); ok { collate, err := DeviceCapabilitiesInt32(printerName, portName, DC_COLLATE) if err != nil { return nil, err } if collate == 1 { printer.Description.Collate = &cdd.Collate{ Default: def == DMCOLLATE_TRUE, } } } printers = append(printers, printer) } printers = lib.FilterBlacklistPrinters(printers, ws.printerBlacklist) printers = lib.FilterWhitelistPrinters(printers, ws.printerWhitelist) printers = addStaticDescriptionToPrinters(printers) printers = ws.addSystemTagsToPrinters(printers) return printers, nil } // addStaticDescriptionToPrinters adds information that is true for all // printers to printers. func addStaticDescriptionToPrinters(printers []lib.Printer) []lib.Printer { for i := range printers { printers[i].GCPVersion = lib.GCPAPIVersion printers[i].SetupURL = lib.ConnectorHomeURL printers[i].SupportURL = lib.ConnectorHomeURL printers[i].UpdateURL = lib.ConnectorHomeURL printers[i].ConnectorVersion = lib.ShortName printers[i].Description.Absorb(&winspoolPDS) } return printers } func (ws *WinSpool) addSystemTagsToPrinters(printers []lib.Printer) []lib.Printer { for i := range printers { for k, v := range ws.systemTags { printers[i].Tags[k] = v } } return printers } func convertMediaSize(printerName, portName string, devMode *DevMode) (*cdd.MediaSize, error) { defSize, defSizeOK := devMode.GetPaperSize() defLength, defLengthOK := devMode.GetPaperLength() defWidth, defWidthOK := devMode.GetPaperWidth() names, err := DeviceCapabilitiesStrings(printerName, portName, DC_PAPERNAMES, 64*2) if err != nil { return nil, err } papers, err := DeviceCapabilitiesUint16Array(printerName, portName, DC_PAPERS) if err != nil { return nil, err } sizes, err := DeviceCapabilitiesInt32Pairs(printerName, portName, DC_PAPERSIZE) if err != nil { return nil, err } if len(names) != len(papers) || len(names) != len(sizes)/2 { return nil, nil } ms := cdd.MediaSize{ Option: make([]cdd.MediaSizeOption, 0, len(names)), } var foundDef bool for i := range names { if names[i] == "" { continue } // Convert from tenths-of-mm to micrometers width, length := sizes[2*i]*100, sizes[2*i+1]*100 var def bool if !foundDef { if defSizeOK { if uint16(defSize) == papers[i] { def = true foundDef = true } } else if defLengthOK && int32(defLength) == length && defWidthOK && int32(defWidth) == width { def = true foundDef = true } } o := cdd.MediaSizeOption{ Name: cdd.MediaSizeCustom, WidthMicrons: width, HeightMicrons: length, IsDefault: def, VendorID: strconv.FormatUint(uint64(papers[i]), 10), CustomDisplayNameLocalized: cdd.NewLocalizedString(names[i]), } ms.Option = append(ms.Option, o) } if !foundDef && len(ms.Option) > 0 { ms.Option[0].IsDefault = true } return &ms, nil } func convertJobState(wsStatus uint32) *cdd.JobState { var state cdd.JobState if wsStatus&(JOB_STATUS_SPOOLING|JOB_STATUS_PRINTING) != 0 { state.Type = cdd.JobStateInProgress } else if wsStatus&(JOB_STATUS_PRINTED|JOB_STATUS_COMPLETE) != 0 { state.Type = cdd.JobStateDone } else if wsStatus&JOB_STATUS_PAUSED != 0 || wsStatus == 0 { state.Type = cdd.JobStateDone } else if wsStatus&JOB_STATUS_ERROR != 0 { state.Type = cdd.JobStateAborted state.DeviceActionCause = &cdd.DeviceActionCause{cdd.DeviceActionCausePrintFailure} } else if wsStatus&(JOB_STATUS_DELETING|JOB_STATUS_DELETED) != 0 { state.Type = cdd.JobStateAborted state.UserActionCause = &cdd.UserActionCause{cdd.UserActionCauseCanceled} } else if wsStatus&(JOB_STATUS_OFFLINE|JOB_STATUS_PAPEROUT|JOB_STATUS_BLOCKED_DEVQ|JOB_STATUS_USER_INTERVENTION) != 0 { state.Type = cdd.JobStateStopped state.DeviceStateCause = &cdd.DeviceStateCause{cdd.DeviceStateCauseOther} } else { // Don't know what is going on. Get the job out of our queue. state.Type = cdd.JobStateAborted state.DeviceActionCause = &cdd.DeviceActionCause{cdd.DeviceActionCauseOther} } return &state } // GetJobState gets the current state of the job indicated by jobID. func (ws *WinSpool) GetJobState(printerName string, jobID uint32) (*cdd.PrintJobStateDiff, error) { hPrinter, err := OpenPrinter(printerName) if err != nil { return nil, err } ji1, err := hPrinter.GetJob(int32(jobID)) if err != nil { if err == ERROR_INVALID_PARAMETER { jobState := cdd.PrintJobStateDiff{ State: &cdd.JobState{ Type: cdd.JobStateAborted, DeviceActionCause: &cdd.DeviceActionCause{cdd.DeviceActionCauseOther}, }, } return &jobState, nil } return nil, err } jobState := cdd.PrintJobStateDiff{ State: convertJobState(ji1.GetStatus()), } return &jobState, nil } type jobContext struct { jobID int32 pDoc PopplerDocument hPrinter HANDLE devMode *DevMode hDC HDC cSurface CairoSurface cContext CairoContext } func newJobContext(printerName, fileName, title, user string) (*jobContext, error) { pDoc, err := PopplerDocumentNewFromFile(fileName) if err != nil { return nil, err } hPrinter, err := OpenPrinter(printerName) if err != nil { pDoc.Unref() return nil, err } devMode, err := hPrinter.DocumentPropertiesGet(printerName) if err != nil { hPrinter.ClosePrinter() pDoc.Unref() return nil, err } err = hPrinter.DocumentPropertiesSet(printerName, devMode) if err != nil { hPrinter.ClosePrinter() pDoc.Unref() return nil, err } hDC, err := CreateDC(printerName, devMode) if err != nil { hPrinter.ClosePrinter() pDoc.Unref() return nil, err } jobID, err := hDC.StartDoc(title) if err != nil { hDC.DeleteDC() hPrinter.ClosePrinter() pDoc.Unref() return nil, err } err = hPrinter.SetJobCommand(jobID, JOB_CONTROL_RETAIN) if err != nil { hDC.EndDoc() hDC.DeleteDC() hPrinter.ClosePrinter() pDoc.Unref() return nil, err } hPrinter.SetJobUserName(jobID, user) cSurface, err := CairoWin32PrintingSurfaceCreate(hDC) if err != nil { hDC.EndDoc() hDC.DeleteDC() hPrinter.ClosePrinter() pDoc.Unref() return nil, err } cContext, err := CairoCreateContext(cSurface) if err != nil { cSurface.Destroy() hDC.EndDoc() hDC.DeleteDC() hPrinter.ClosePrinter() pDoc.Unref() return nil, err } c := jobContext{jobID, pDoc, hPrinter, devMode, hDC, cSurface, cContext} return &c, nil } func (c *jobContext) free() error { var err error err = c.cContext.Destroy() if err != nil { return err } err = c.cSurface.Destroy() if err != nil { return err } err = c.hDC.EndDoc() if err != nil { return err } err = c.hDC.DeleteDC() if err != nil { return err } err = c.hPrinter.ClosePrinter() if err != nil { return err } c.pDoc.Unref() return nil } func getScaleAndOffset(wDocPoints, hDocPoints float64, wPaperPixels, hPaperPixels, xMarginPixels, yMarginPixels, wPrintablePixels, hPrintablePixels, xDPI, yDPI int32, fitToPage bool) (scale, xOffsetPoints, yOffsetPoints float64) { wPaperPoints, hPaperPoints := float64(wPaperPixels*72)/float64(xDPI), float64(hPaperPixels*72)/float64(yDPI) var wPrintablePoints, hPrintablePoints float64 if fitToPage { wPrintablePoints, hPrintablePoints = float64(wPrintablePixels*72)/float64(xDPI), float64(hPrintablePixels*72)/float64(yDPI) } else { wPrintablePoints, hPrintablePoints = wPaperPoints, hPaperPoints } xScale, yScale := wPrintablePoints/wDocPoints, hPrintablePoints/hDocPoints if xScale < yScale { scale = xScale } else { scale = yScale } xOffsetPoints = (wPaperPoints - wDocPoints*scale) / 2 yOffsetPoints = (hPaperPoints - hDocPoints*scale) / 2 return } func printPage(printerName string, i int, c *jobContext, fitToPage bool) error { pPage := c.pDoc.GetPage(i) defer pPage.Unref() if err := c.hPrinter.DocumentPropertiesSet(printerName, c.devMode); err != nil { return err } if err := c.hDC.ResetDC(c.devMode); err != nil { return err } // Set device to zero offset, and to points scale. xDPI := c.hDC.GetDeviceCaps(LOGPIXELSX) yDPI := c.hDC.GetDeviceCaps(LOGPIXELSY) xMarginPixels := c.hDC.GetDeviceCaps(PHYSICALOFFSETX) yMarginPixels := c.hDC.GetDeviceCaps(PHYSICALOFFSETY) xform := NewXFORM(float32(xDPI)/72, float32(yDPI)/72, float32(-xMarginPixels), float32(-yMarginPixels)) if err := c.hDC.SetGraphicsMode(GM_ADVANCED); err != nil { return err } if err := c.hDC.SetWorldTransform(xform); err != nil { return err } if err := c.hDC.StartPage(); err != nil { return err } defer c.hDC.EndPage() if err := c.cContext.Save(); err != nil { return err } wPaperPixels := c.hDC.GetDeviceCaps(PHYSICALWIDTH) hPaperPixels := c.hDC.GetDeviceCaps(PHYSICALHEIGHT) wPrintablePixels := c.hDC.GetDeviceCaps(HORZRES) hPrintablePixels := c.hDC.GetDeviceCaps(VERTRES) wDocPoints, hDocPoints, err := pPage.GetSize() if err != nil { return err } scale, xOffsetPoints, yOffsetPoints := getScaleAndOffset(wDocPoints, hDocPoints, wPaperPixels, hPaperPixels, xMarginPixels, yMarginPixels, wPrintablePixels, hPrintablePixels, xDPI, yDPI, fitToPage) if err := c.cContext.IdentityMatrix(); err != nil { return err } if err := c.cContext.Translate(xOffsetPoints, yOffsetPoints); err != nil { return err } if err := c.cContext.Scale(scale, scale); err != nil { return err } pPage.RenderForPrinting(c.cContext) if err := c.cContext.Restore(); err != nil { return err } if err := c.cSurface.ShowPage(); err != nil { return err } return nil } var ( colorValueByType = map[cdd.ColorType]int16{ cdd.ColorTypeStandardColor: DMCOLOR_COLOR, cdd.ColorTypeStandardMonochrome: DMCOLOR_MONOCHROME, // Ignore the rest, since we don't advertise them. } duplexValueByType = map[cdd.DuplexType]int16{ cdd.DuplexNoDuplex: DMDUP_SIMPLEX, cdd.DuplexLongEdge: DMDUP_VERTICAL, cdd.DuplexShortEdge: DMDUP_HORIZONTAL, } pageOrientationByType = map[cdd.PageOrientationType]int16{ cdd.PageOrientationPortrait: DMORIENT_PORTRAIT, cdd.PageOrientationLandscape: DMORIENT_LANDSCAPE, // Ignore cdd.PageOrientationAuto for ticket parsing, in order to interpret "auto". } ) // Print sends a new print job to the specified printer. The job ID // is returned. func (ws *WinSpool) Print(printer *lib.Printer, fileName, title, user, gcpJobID string, ticket *cdd.CloudJobTicket) (uint32, error) { printer.NativeJobSemaphore.Acquire() defer printer.NativeJobSemaphore.Release() if ws.prefixJobIDToJobTitle { title = fmt.Sprintf("gcp:%s %s", gcpJobID, title) } if printer == nil { return 0, errors.New("Print() called with nil printer") } if ticket == nil { return 0, errors.New("Print() called with nil ticket") } jobContext, err := newJobContext(printer.Name, fileName, title, user) if err != nil { return 0, err } defer jobContext.free() if ticket.Print.Color != nil && printer.Description.Color != nil { if color, ok := colorValueByType[ticket.Print.Color.Type]; ok { jobContext.devMode.SetColor(color) } else if ticket.Print.Color.VendorID != "" { v, err := strconv.ParseInt(ticket.Print.Color.VendorID, 10, 16) if err != nil { return 0, err } jobContext.devMode.SetColor(int16(v)) } } if ticket.Print.Duplex != nil && printer.Description.Duplex != nil { if duplex, ok := duplexValueByType[ticket.Print.Duplex.Type]; ok { jobContext.devMode.SetDuplex(duplex) } } if ticket.Print.PageOrientation != nil && printer.Description.PageOrientation != nil { if pageOrientation, ok := pageOrientationByType[ticket.Print.PageOrientation.Type]; ok { jobContext.devMode.SetOrientation(pageOrientation) } } if ticket.Print.Copies != nil && printer.Description.Copies != nil { if ticket.Print.Copies.Copies > 0 { jobContext.devMode.SetCopies(int16(ticket.Print.Copies.Copies)) } } var fitToPage bool if ticket.Print.FitToPage != nil && printer.Description.FitToPage != nil { if ticket.Print.FitToPage.Type == cdd.FitToPageFitToPage { fitToPage = true } } if ticket.Print.MediaSize != nil && printer.Description.MediaSize != nil { if ticket.Print.MediaSize.VendorID != "" { v, err := strconv.ParseInt(ticket.Print.MediaSize.VendorID, 10, 16) if err != nil { return 0, err } jobContext.devMode.SetPaperSize(int16(v)) jobContext.devMode.ClearPaperLength() jobContext.devMode.ClearPaperWidth() } else { jobContext.devMode.ClearPaperSize() jobContext.devMode.SetPaperLength(int16(ticket.Print.MediaSize.HeightMicrons / 10)) jobContext.devMode.SetPaperWidth(int16(ticket.Print.MediaSize.WidthMicrons / 10)) } } if ticket.Print.Collate != nil && printer.Description.Collate != nil { if ticket.Print.Collate.Collate { jobContext.devMode.SetCollate(DMCOLLATE_TRUE) } else { jobContext.devMode.SetCollate(DMCOLLATE_FALSE) } } for i := 0; i < jobContext.pDoc.GetNPages(); i++ { if err := printPage(printer.Name, i, jobContext, fitToPage); err != nil { return 0, err } } return uint32(jobContext.jobID), nil } func (ws *WinSpool) ReleaseJob(printerName string, jobID uint32) error { hPrinter, err := OpenPrinter(printerName) if err != nil { return err } err = hPrinter.SetJobCommand(int32(jobID), JOB_CONTROL_RELEASE) if err != nil { return err } return nil } func (ws *WinSpool) StartPrinterNotifications(handle windows.Handle) error { err := RegisterDeviceNotification(handle) return err } // The following functions are not relevant to Windows printing, but are required by the NativePrintSystem interface. func (ws *WinSpool) RemoveCachedPPD(printerName string) {} cloud-print-connector-1.12/wix/000077500000000000000000000000001311204274000164665ustar00rootroot00000000000000cloud-print-connector-1.12/wix/LICENSE.rtf000066400000000000000000000033531311204274000202710ustar00rootroot00000000000000{\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Courier New;}} {\*\generator Riched20 10.0.10586}\viewkind4\uc1 \pard\f0\fs20\lang1033 Copyright 2015, Google Inc. All rights reserved.\par \par Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are\par met:\par \par * Redistributions of source code must retain the above copyright\par notice, this list of conditions and the following disclaimer.\par * Redistributions in binary form must reproduce the above\par copyright notice, this list of conditions and the following disclaimer\par in the documentation and/or other materials provided with the\par distribution.\par * Neither the name of Google Inc. nor the names of its\par contributors may be used to endorse or promote products derived from\par this software without specific prior written permission.\par \par THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\par "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\par LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\par A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\par OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\par SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\par LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\par DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\par THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\par (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\par OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\par \par } cloud-print-connector-1.12/wix/README.md000066400000000000000000000053531311204274000177530ustar00rootroot00000000000000# Windows Installer ## Build Requirements The WIX toolset is required to build the Windows Installer file. It can be downloaded from http://wixtoolset.org. ## Build Instructions Build the Cloud Print Connector binaries. See https://github.com/google/cloud-print-connector/wiki/Build-from-source Update the dependencies.wxs file by running ./generate-dependencies.sh (in mingw64 bash shell). Use the WIX tools to build the MSI. The WIX tools that are used are candle.exe and light.exe. They are installed by default to "C:\Program Files (x86)\WiX Toolset v3.10\bin" (/c/Program\ Files\ (x86)/WiX\ Toolset\ v3.10/bin/light.exe if you're using mingw bash shell). You can add this directory to your PATH to run the following two commands. Run candle.exe to build wixobj file from the wxs file: ``` candle.exe -arch x64 windows-connector.wxs dependencies.wxs ``` Expected output: > Windows Installer XML Toolset Compiler version 3.10.2.2516 > Copyright (c) Outercurve Foundation. All rights reserved. > > windows-connector.wxs > dependencies.wxs Run light.exe to build MSI file from the wixobj ``` light.exe -ext "C:\Program Files (x86)\WiX Toolset v3.10\bin\WixUIExtension.dll" windows-connector.wixobj dependencies.wixobj -o windows-connector.msi ``` Expected output: > Windows Installer XML Toolset Linker version 3.10.2.2516 > Copyright (c) Outercurve Foundation. All rights reserved. The light.exe command line requires the path of WixUIExtension.dll which provides the UI that is used by this installer. If the WIX toolset is installed to a different directory, use that directory path for the UI extension dll. If the built Windows Connector binaries are not in $GOPATH\bin, then add -dSourceDir= to the light.exe command line to specify where the files can be found. If mingw64 is not installed to C:\msys64\mingw64, then use -dDependencyDir= to specify where it is installed. ## Installation Instructions Install the MSI by any normal method of installing an MSI file (double-clicking, automated deployment, etc.) During an installation with UI, gcp-connector-util init will be run as the last step which will open a console window to initialize the connector. The following public properties may be set during install of the MSI (see https://msdn.microsoft.com/en-us/library/windows/desktop/aa370912(v=vs.85).aspx) * CONFIGFILE = Path of connector config file to use instead of running gcp-connector-util init during install ## Modifying the Config File after install The installer will create (or copy) the config file specified to the Common Application Data directory at %PROGRAMDATA%\Google\Cloud Print Connector. This is the file that is used by the connector. This file can be modified and the service restarted to change the configuration. cloud-print-connector-1.12/wix/build-msi.sh000066400000000000000000000024001311204274000207030ustar00rootroot00000000000000if [ $# -eq 0 ]; then me=$(basename $0) echo "Usage: $me " exit 1 fi CONNECTOR_VERSION=$1 LDFLAGS="github.com/google/cloud-print-connector/lib.BuildDate=$CONNECTOR_VERSION" CONNECTOR_DIR=$GOPATH/src/github.com/google/cloud-print-connector MSI_FILE="$CONNECTOR_DIR/wix/windows-connector-$CONNECTOR_VERSION.msi" echo "Running go get..." go get -ldflags -X="$LDFLAGS" -v github.com/google/cloud-print-connector/... rc=$? if [[ $rc != 0 ]]; then echo "Error $rc with go get. Exiting." exit $rc fi echo "Running generate-dependencies.sh..." $CONNECTOR_DIR/wix/generate-dependencies.sh rc=$? if [[ $rc != 0 ]]; then echo "Error $rc with generate-dependencies.sh. Exiting." exit $rc fi echo "Running WIX candle.exe..." "$WIX/bin/candle.exe" -arch x64 "$CONNECTOR_DIR/wix/windows-connector.wxs" \ "$CONNECTOR_DIR/wix/dependencies.wxs" rc=$? if [[ $rc != 0 ]]; then echo "Error $rc with WIX candle.exe. Exiting." exit $rc fi echo "Running WIX light.exe..." "$WIX/bin/light.exe" -ext "$WIX/bin/WixUIExtension.dll" \ "$CONNECTOR_DIR/wix/windows-connector.wixobj" "$CONNECTOR_DIR/wix/dependencies.wixobj" \ -o "$MSI_FILE" rc=$? if [[ $rc != 0 ]]; then echo "Error $rc with WIX light.exe. Exiting." exit $rc fi echo "Successfully generated $MSI_FILE" cloud-print-connector-1.12/wix/dependencies.wxs000066400000000000000000000106241311204274000216620ustar00rootroot00000000000000 cloud-print-connector-1.12/wix/generate-dependencies.sh000066400000000000000000000011441311204274000232400ustar00rootroot00000000000000#!/usr/bin/bash echo ''>dependencies.wxs echo ' '>>dependencies.wxs for f in `ldd ${GOPATH}/bin/gcp-windows-connector.exe | grep -i -v Windows | sed s/" =>.*"// | sed s/"\t"// | sort` do echo " ">>dependencies.wxs; done echo ' '>>dependencies.wxs cloud-print-connector-1.12/wix/windows-connector.wxs000066400000000000000000000135241311204274000227200ustar00rootroot00000000000000 STARTSERVICE="YES" NOT CONFIGFILE CONFIGFILE NOT CONFIGFILE and NOT Installed and NOT WIX_UPGRADE_DETECTED and RUNINIT="YES" DELETEPRINTERS="YES" AND $ConfigFile=2 cloud-print-connector-1.12/xmpp/000077500000000000000000000000001311204274000166435ustar00rootroot00000000000000cloud-print-connector-1.12/xmpp/internal-xmpp.go000066400000000000000000000421441311204274000217750ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ // Package xmpp is the Google Cloud Print XMPP interface. package xmpp import ( "bufio" "crypto/tls" "encoding/base64" "encoding/xml" "errors" "fmt" "io" "net" "net/http" "net/url" "strconv" "strings" "time" "github.com/google/cloud-print-connector/log" ) const ( // This is a long-lived, potentially quiet, conversation. Keep it alive! netKeepAlive = time.Second * 60 // Set our own timeout, rather than have the OS or server timeout for us. netTimeout = time.Second * 60 ) // Interface with XMPP server. type internalXMPP struct { conn *tls.Conn xmlEncoder *xml.Encoder xmlDecoder *xml.Decoder fullJID string notifications chan<- PrinterNotification pongs chan uint8 nextPingID uint8 dead chan<- struct{} } // newInternalXMPP creates a new XMPP connection. // // Received XMPP notifications are sent on the notifications channel. // // If the connection dies unexpectedly, a message is sent on dead. func newInternalXMPP(jid, accessToken, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, notifications chan<- PrinterNotification, dead chan<- struct{}) (*internalXMPP, error) { var user, domain string if parts := strings.SplitN(jid, "@", 2); len(parts) != 2 { return nil, fmt.Errorf("Tried to use invalid XMPP JID: %s", jid) } else { user = parts[0] domain = parts[1] } conn, err := dialViaHTTPProxy(server, port) if err != nil { return nil, fmt.Errorf("Failed to dial XMPP server via proxy: %s", err) } if conn == nil { conn, err = dial(server, port) if err != nil { return nil, fmt.Errorf("Failed to dial XMPP service: %s", err) } } t := &tee{conn, conn} xmlEncoder := xml.NewEncoder(t) xmlDecoder := xml.NewDecoder(t) // SASL if err = saslHandshake(xmlEncoder, xmlDecoder, domain, user, accessToken); err != nil { return nil, fmt.Errorf("Failed to perform XMPP-SASL handshake: %s", err) } // XMPP fullJID, err := xmppHandshake(xmlEncoder, xmlDecoder, domain, proxyName) if err != nil { return nil, fmt.Errorf("Failed to perform final XMPP handshake: %s", err) } // Subscribe if err = subscribe(xmlEncoder, xmlDecoder, fullJID); err != nil { return nil, fmt.Errorf("Failed to subscribe: %s", err) } x := internalXMPP{ conn: conn, xmlEncoder: xmlEncoder, xmlDecoder: xmlDecoder, fullJID: fullJID, notifications: notifications, pongs: make(chan uint8, 10), nextPingID: 0, dead: dead, } // dispatchIncoming signals pingPeriodically to return via dying. dying := make(chan struct{}) go x.dispatchIncoming(dying) // Check by ping if success, err := x.ping(pingTimeout); !success { x.Quit() <-dying return nil, fmt.Errorf("XMPP conversation started, but initial ping failed: %s", err) } go x.pingPeriodically(pingTimeout, pingInterval, dying) return &x, nil } // Quit causes the XMPP connection to close. func (x *internalXMPP) Quit() { // Trigger dispatchIncoming to return. x.conn.Close() // dispatchIncoming notifies pingPeriodically via dying channel. // pingPeriodically signals death via x.dead channel. } func (x *internalXMPP) pingPeriodically(timeout, interval time.Duration, dying <-chan struct{}) { t := time.NewTimer(interval) defer t.Stop() for { select { case <-t.C: if success, err := x.ping(timeout); success { t.Reset(interval) } else { log.Info(err) x.Quit() } case <-dying: // Signal death externally. x.dead <- struct{}{} return } } } // dispatchIncoming listens for new XMPP notifications and puts them into // separate channels, by type of message. func (x *internalXMPP) dispatchIncoming(dying chan<- struct{}) { for { // The xml.StartElement tells us what is coming up. startElement, err := readStartElement(x.xmlDecoder) if err != nil { if isXMLErrorClosedConnection(err) { break } log.Errorf("Failed to read the next start element: %s", err) break } // Parse the message. if startElement.Name.Local == "message" { var message struct { XMLName xml.Name `xml:"message"` Data string `xml:"push>data"` } if err := x.xmlDecoder.DecodeElement(&message, startElement); err != nil { if isXMLErrorClosedConnection(err) { break } log.Warningf("Error while parsing print jobs notification via XMPP: %s", err) continue } messageData, err := base64.StdEncoding.DecodeString(message.Data) if err != nil { log.Warningf("Failed to convert XMPP message data from base64: %s", err) continue } messageDataString := string(messageData) if strings.ContainsRune(messageDataString, '/') { if strings.HasSuffix(messageDataString, "/delete") { gcpID := strings.TrimSuffix(messageDataString, "/delete") x.notifications <- PrinterNotification{gcpID, PrinterDelete} } // Ignore other suffixes, like /update_settings. } else { x.notifications <- PrinterNotification{messageDataString, PrinterNewJobs} } } else if startElement.Name.Local == "iq" { var message struct { XMLName xml.Name `xml:"iq"` ID string `xml:"id,attr"` Type string `xml:"type,attr"` } if err := x.xmlDecoder.DecodeElement(&message, startElement); err != nil { if isXMLErrorClosedConnection(err) { break } log.Warningf("Error while parsing XMPP pong: %s", err) continue } pingID, err := strconv.ParseUint(message.ID, 10, 8) if err != nil { log.Warningf("Failed to convert XMPP ping ID: %s", err) continue } x.pongs <- uint8(pingID) } else { log.Warningf("Unexpected element while waiting for print message: %+v", startElement) } } dying <- struct{}{} } // ping sends a ping message and blocks until pong is received. // // Returns false if timeout time passes before pong, or on any // other error. Errors are logged but not returned. func (x *internalXMPP) ping(timeout time.Duration) (bool, error) { var ping struct { XMLName xml.Name `xml:"iq"` From string `xml:"from,attr"` To string `xml:"to,attr"` ID string `xml:"id,attr"` Type string `xml:"type,attr"` Ping struct { XMLName xml.Name `xml:"ping"` XMLNS string `xml:"xmlns,attr"` } } pingID := x.nextPingID x.nextPingID++ ping.From = x.fullJID ping.To = "cloudprint.google.com" ping.ID = fmt.Sprintf("%d", pingID) ping.Type = "get" ping.Ping.XMLNS = "urn:xmpp:ping" if err := x.xmlEncoder.Encode(&ping); err != nil { return false, fmt.Errorf("XMPP ping request failed: %s", err) } for { select { case pongID := <-x.pongs: if pongID == pingID { return true, nil } case <-time.After(timeout): return false, fmt.Errorf("Pong not received after %s", timeout.String()) } } panic("unreachable") } func dialViaHTTPProxy(server string, port uint16) (*tls.Conn, error) { xmppHost := fmt.Sprintf("%s:%d", server, port) fakeRequest := http.Request{ URL: &url.URL{ Scheme: "https", Host: xmppHost, }, } proxyURLFunc := http.ProxyFromEnvironment if tr, ok := http.DefaultTransport.(*http.Transport); ok && tr.Proxy != nil { proxyURLFunc = tr.Proxy } proxyURL, err := proxyURLFunc(&fakeRequest) if err != nil { return nil, err } if proxyURL == nil { return nil, nil } dialer := net.Dialer{ KeepAlive: netKeepAlive, Timeout: netTimeout, } conn, err := dialer.Dial("tcp", proxyURL.Host) if err != nil { return nil, fmt.Errorf("Failed to connect to HTTP proxy server: %s", err) } proxyAuth := "" if u := proxyURL.User; u != nil { username := u.Username() password, _ := u.Password() basicAuth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) proxyAuth = "Proxy-Authorization: Basic " + basicAuth + "\r\n" } fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", xmppHost, xmppHost, proxyAuth) response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) if err != nil { return nil, err } if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("Failed to connect to proxy: %s", response.Status) } return addTLS(server, conn) } func dial(server string, port uint16) (*tls.Conn, error) { dialer := net.Dialer{ KeepAlive: netKeepAlive, Timeout: netTimeout, } conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%d", server, port)) if err != nil { return nil, fmt.Errorf("Failed to connect to XMPP server: %s", err) } return addTLS(server, conn) } func addTLS(server string, conn net.Conn) (*tls.Conn, error) { tlsConfig := tls.Config{} if tr, ok := http.DefaultTransport.(*http.Transport); ok && tr.TLSClientConfig != nil { tlsConfig = *tr.TLSClientConfig } tlsConfig.ServerName = server tlsClient := tls.Client(conn, &tlsConfig) if err := tlsClient.Handshake(); err != nil { return nil, fmt.Errorf("Failed to TLS handshake with XMPP server: %s", err) } if err := tlsClient.VerifyHostname(server); err != nil { return nil, fmt.Errorf("Failed to verify hostname of XMPP server: %s", err) } return tlsClient, nil } func saslHandshake(xmlEncoder *xml.Encoder, xmlDecoder *xml.Decoder, domain, user, accessToken string) error { handshake := xml.StartElement{ Name: xml.Name{"jabber:client", "stream:stream"}, Attr: []xml.Attr{ xml.Attr{xml.Name{Local: "to"}, domain}, xml.Attr{xml.Name{Local: "xml:lang"}, "en"}, xml.Attr{xml.Name{Local: "version"}, "1.0"}, xml.Attr{xml.Name{Local: "xmlns:stream"}, "http://etherx.jabber.org/streams"}, }, } if err := xmlEncoder.EncodeToken(handshake); err != nil { return fmt.Errorf("Failed to write SASL handshake: %s", err) } if err := xmlEncoder.Flush(); err != nil { return fmt.Errorf("Failed to flush encoding stream: %s", err) } if startElement, err := readStartElement(xmlDecoder); err != nil { return err } else if startElement.Name.Space != "http://etherx.jabber.org/streams" || startElement.Name.Local != "stream" { return fmt.Errorf("Read unexpected SASL XML stanza: %s", startElement.Name.Local) } var features struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` Mechanisms *struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` } } if err := xmlDecoder.Decode(&features); err != nil { return fmt.Errorf("Read unexpected SASL XML element: %s", err) } else if features.Mechanisms == nil { return errors.New("SASL mechanisms missing from handshake") } credential := base64.StdEncoding.EncodeToString([]byte("\x00" + user + "\x00" + accessToken)) var auth struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"` Mechanism string `xml:"mechanism,attr"` Service string `xml:"auth:service,attr"` Allow string `xml:"auth:allow-generated-jid,attr"` FullBind string `xml:"auth:client-uses-full-bind-result,attr"` XMLNS string `xml:"xmlns:auth,attr"` Credential string `xml:",chardata"` } auth.Mechanism = "X-OAUTH2" auth.Service = "chromiumsync" auth.Allow = "true" auth.FullBind = "true" auth.XMLNS = "http://www.google.com/talk/protocol/auth" auth.Credential = credential if err := xmlEncoder.Encode(auth); err != nil { return fmt.Errorf("Failed to write SASL credentials: %s", err) } var success struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` } if err := xmlDecoder.Decode(&success); err != nil { return fmt.Errorf("Failed to complete SASL handshake: %s", err) } return nil } func xmppHandshake(xmlEncoder *xml.Encoder, xmlDecoder *xml.Decoder, domain, proxyName string) (string, error) { handshake := xml.StartElement{ Name: xml.Name{"jabber:client", "stream:stream"}, Attr: []xml.Attr{ xml.Attr{xml.Name{Local: "to"}, domain}, xml.Attr{xml.Name{Local: "xml:lang"}, "en"}, xml.Attr{xml.Name{Local: "version"}, "1.0"}, xml.Attr{xml.Name{Local: "xmlns:stream"}, "http://etherx.jabber.org/streams"}, }, } if err := xmlEncoder.EncodeToken(handshake); err != nil { return "", fmt.Errorf("Failed to write SASL handshake: %s", err) } if err := xmlEncoder.Flush(); err != nil { return "", fmt.Errorf("Failed to flush encoding stream: %s", err) } if startElement, err := readStartElement(xmlDecoder); err != nil { return "", err } else if startElement.Name.Space != "http://etherx.jabber.org/streams" || startElement.Name.Local != "stream" { return "", fmt.Errorf("Read unexpected XMPP XML stanza: %s", startElement.Name.Local) } var features struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` Bind *struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` } Session *struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` } } if err := xmlDecoder.Decode(&features); err != nil { return "", fmt.Errorf("Read unexpected XMPP XML element: %s", err) } else if features.Bind == nil || features.Session == nil { return "", errors.New("XMPP bind or session missing from handshake") } var resource struct { XMLName xml.Name `xml:"jabber:client iq"` Type string `xml:"type,attr"` ID string `xml:"id,attr"` Bind struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` Resource struct { XMLName xml.Name `xml:"resource"` ResourceName string `xml:",chardata"` } } } resource.Type = "set" resource.ID = "0" resource.Bind.Resource.ResourceName = proxyName if err := xmlEncoder.Encode(&resource); err != nil { return "", fmt.Errorf("Failed to set resource during XMPP handshake: %s", err) } var jid struct { XMLName xml.Name `xml:"jabber:client iq"` Bind *struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` JID string `xml:"jid"` } } if err := xmlDecoder.Decode(&jid); err != nil { return "", err } else if jid.Bind == nil || jid.Bind.JID == "" { return "", errors.New("Received unexpected XML element during XMPP handshake") } fullJID := jid.Bind.JID var session struct { XMLName xml.Name `xml:"jabber:client iq"` Type string `xml:"type,attr"` ID string `xml:"id,attr"` Session struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` } } session.Type = "set" session.ID = "1" if err := xmlEncoder.Encode(&session); err != nil { return "", fmt.Errorf("Failed to complete XMPP handshake: %s", err) } var done struct { XMLName xml.Name `xml:"jabber:client iq"` ID string `xml:"id,attr"` } if err := xmlDecoder.Decode(&done); err != nil { return "", err } else if done.ID != "1" { return "", errors.New("Received unexpected result at end of XMPP handshake") } return fullJID, nil } func subscribe(xmlEncoder *xml.Encoder, xmlDecoder *xml.Decoder, fullJID string) error { var bareJID string if barePosition := strings.Index(fullJID, "/"); barePosition < 0 { return fmt.Errorf("Can't split JID %s", fullJID) } else { bareJID = fullJID[:barePosition] } var subscribe struct { XMLName xml.Name `xml:"jabber:client iq"` Type string `xml:"type,attr"` To string `xml:"to,attr"` ID string `xml:"id,attr"` Subscribe struct { XMLName xml.Name `xml:"google:push subscribe"` Item struct { XMLName xml.Name `xml:"item"` Channel string `xml:"channel,attr"` From string `xml:"from,attr"` } } } subscribe.Type = "set" subscribe.To = bareJID subscribe.ID = "3" subscribe.Subscribe.Item.Channel = "cloudprint.google.com" subscribe.Subscribe.Item.From = "cloudprint.google.com" if err := xmlEncoder.Encode(&subscribe); err != nil { return fmt.Errorf("XMPP subscription request failed: %s", err) } var subscription struct { XMLName xml.Name `xml:"jabber:client iq"` To string `xml:"to,attr"` From string `xml:"from,attr"` } if err := xmlDecoder.Decode(&subscription); err != nil { return fmt.Errorf("XMPP subscription response invalid: %s", err) } else if fullJID != subscription.To || bareJID != subscription.From { return errors.New("XMPP subscription failed") } return nil } func readStartElement(d *xml.Decoder) (*xml.StartElement, error) { for { token, err := d.Token() if err != nil { return nil, err } if startElement, ok := token.(xml.StartElement); ok { return &startElement, nil } } panic("unreachable") } // isXMLErrorClosedConnection simplifies an xml.Decoder error. // // If the error is a variation of "connection closed" then logs a suitable error and returns true. // Otherwise, returns false. func isXMLErrorClosedConnection(err error) bool { if strings.Contains(err.Error(), "use of closed network connection") { log.Info("XMPP connection was closed") return true } else if strings.Contains(err.Error(), "connection reset by peer") { log.Info("XMPP connection was forcibly closed by server") return true } else if err == io.EOF { log.Info("XMPP connection failed") return true } return false } type tee struct { r io.Reader w io.Writer } func (t *tee) Read(p []byte) (int, error) { n, err := t.r.Read(p) log.Debugf("XMPP read %d %s", n, p[0:n]) return n, err } func (t *tee) Write(p []byte) (int, error) { n, err := t.w.Write(p) log.Debugf("XMPP wrote %d %s", n, p[0:n]) return n, err } cloud-print-connector-1.12/xmpp/xmpp.go000066400000000000000000000056261311204274000201670ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package xmpp import ( "fmt" "time" "github.com/google/cloud-print-connector/log" ) type PrinterNotificationType uint8 const ( PrinterNewJobs PrinterNotificationType = iota PrinterDelete ) type PrinterNotification struct { GCPID string Type PrinterNotificationType } type XMPP struct { jid string proxyName string server string port uint16 pingTimeout time.Duration pingInterval time.Duration getAccessToken func() (string, error) notifications chan<- PrinterNotification dead chan struct{} quit chan struct{} ix *internalXMPP } func NewXMPP(jid, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, getAccessToken func() (string, error), notifications chan<- PrinterNotification) (*XMPP, error) { x := XMPP{ jid: jid, proxyName: proxyName, server: server, port: port, pingTimeout: pingTimeout, pingInterval: pingInterval, getAccessToken: getAccessToken, notifications: notifications, dead: make(chan struct{}), quit: make(chan struct{}), } if err := x.startXMPP(); err != nil { for err != nil { log.Errorf("XMPP start failed, will try again in 10s: %s", err) time.Sleep(10 * time.Second) err = x.startXMPP() } } go x.keepXMPPAlive() return &x, nil } // Quit terminates the XMPP conversation so that new jobs stop arriving. func (x *XMPP) Quit() { // Signal to KeepXMPPAlive. close(x.quit) select { case <-x.dead: // Wait for XMPP to die. case <-time.After(3 * time.Second): // But not too long. log.Error("XMPP taking a while to close, so giving up") } } // startXMPP tries to start an XMPP conversation. func (x *XMPP) startXMPP() error { if x.ix != nil { go x.ix.Quit() x.ix = nil } password, err := x.getAccessToken() if err != nil { return fmt.Errorf("While starting XMPP, failed to get access token (password): %s", err) } // The current access token is the XMPP password. ix, err := newInternalXMPP(x.jid, password, x.proxyName, x.server, x.port, x.pingTimeout, x.pingInterval, x.notifications, x.dead) if err != nil { return fmt.Errorf("Failed to start XMPP conversation: %s", err) } x.ix = ix return nil } // keepXMPPAlive restarts XMPP when it fails. func (x *XMPP) keepXMPPAlive() { for { select { case <-x.dead: log.Error("XMPP conversation died; restarting") if err := x.startXMPP(); err != nil { for err != nil { log.Errorf("XMPP restart failed, will try again in 10s: %s", err) time.Sleep(10 * time.Second) err = x.startXMPP() } log.Error("XMPP conversation restarted successfully") } case <-x.quit: // Close XMPP. x.ix.Quit() return } } } cloud-print-connector-1.12/xmpp/xmpp_test.go000066400000000000000000000252421311204274000212220ustar00rootroot00000000000000/* Copyright 2015 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd */ package xmpp_test import ( "crypto/tls" "crypto/x509" "encoding/xml" "io" "io/ioutil" "net" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "sync" "testing" "time" "github.com/google/cloud-print-connector/xmpp" ) func TestXMPP_proxyauth(t *testing.T) { cfg := configureTLS(t) ts := httptest.NewServer(&testXMPPHandler{T: t, cfg: cfg, wantProxyAuth: "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal("failed to parse URL", ts.URL) } u.User = url.UserPassword("Aladdin", "open sesame") orig := http.DefaultTransport http.DefaultTransport = &http.Transport{ Proxy: http.ProxyURL(u), TLSClientConfig: cfg, } defer func() { http.DefaultTransport = orig }() strs := strings.Split(u.Host, ":") port, err := strconv.Atoi(strs[1]) if err != nil { t.Fatal(err) } ch := make(chan<- xmpp.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", strs[0], uint16(port), time.Minute, time.Minute, func() (string, error) { return "accessToken", nil }, ch) if err != nil { t.Fatal(err) } x.Quit() } func TestXMPP_reconnect10(t *testing.T) { // run 10 times to test concurrent condition for i := 0; i < 10; i++ { testXMPP_reconnect(t) } } func testXMPP_reconnect(t *testing.T) { cfg := configureTLS(t) waiting := make(chan struct{}) ts := &testXMPPServer{handler: &testXMPPHandler{T: t, cfg: cfg, waiting: waiting}} ts.Start() defer ts.Close() orig := http.DefaultTransport http.DefaultTransport = &http.Transport{ TLSClientConfig: cfg, } defer func() { http.DefaultTransport = orig }() ch := make(chan<- xmpp.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Minute, time.Minute, func() (string, error) { return "accessToken", nil }, ch) if err != nil { t.Fatal(err) } waiting <- struct{}{} // signal testXMPPHandler to reconnect waiting <- struct{}{} go func() { waiting <- struct{}{} }() // make crossing condition Quit and reconnect. note that this goroutine may leak x.Quit() } func TestXMPP_ping(t *testing.T) { cfg := configureTLS(t) waiting := make(chan struct{}) ts := &testXMPPServer{handler: &testXMPPHandler{T: t, cfg: cfg, waiting: waiting, wantPing: 2}} ts.Start() defer ts.Close() orig := http.DefaultTransport http.DefaultTransport = &http.Transport{ TLSClientConfig: cfg, } defer func() { http.DefaultTransport = orig }() ch := make(chan<- xmpp.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Second, time.Second, func() (string, error) { return "accessToken", nil }, ch) if err != nil { t.Fatal(err) } waiting <- struct{}{} // sync pings received x.Quit() } func TestXMPP_pingtimeout10(t *testing.T) { // run 10 times to test concurrent condition for i := 0; i < 10; i++ { testXMPP_pingtimeout(t) } } func testXMPP_pingtimeout(t *testing.T) { cfg := configureTLS(t) ts := &testXMPPServer{handler: &testXMPPHandler{T: t, cfg: cfg}} ts.Start() defer ts.Close() orig := http.DefaultTransport http.DefaultTransport = &http.Transport{ TLSClientConfig: cfg, } defer func() { http.DefaultTransport = orig }() ch := make(chan<- xmpp.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Millisecond, time.Millisecond, func() (string, error) { return "accessToken", nil }, ch) if err != nil { if strings.Contains(err.Error(), "initial ping failed") { // ignore initial ping failed due to short timeout duration t.Log(err) return } t.Fatal(err) } time.Sleep(time.Millisecond * 100) // make ping timeout x.Quit() ts.Close() if ts.count <= 1 { t.Fatal("want: multiple connection counts by reconnecting but:", ts.count) } } type testXMPPServer struct { handler *testXMPPHandler listener net.Listener port uint16 clientConnections sync.WaitGroup count int } func (t *testXMPPServer) Close() { t.listener.Close() t.clientConnections.Wait() } func (t *testXMPPServer) Start() error { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return err } t.listener = ln strs := strings.Split(ln.Addr().String(), ":") port, err := strconv.Atoi(strs[1]) if err != nil { return err } t.port = uint16(port) go func() { for { conn, err := ln.Accept() if err != nil { return } t.clientConnections.Add(1) t.count++ go func() { defer t.clientConnections.Done() defer conn.Close() tlsConn := t.handler.handshakeTLS(conn) t.handler.serveXMPP(tlsConn) }() } }() return nil } type testXMPPHandler struct { *testing.T cfg *tls.Config wantProxyAuth string wantPing int waiting chan struct{} dec *xml.Decoder } func (t testXMPPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "CONNECT" { t.Fatal("want: proxy CONNECT but:", r.Method) } if auth := r.Header.Get("Proxy-Authorization"); auth != t.wantProxyAuth { t.Fatal("want: ", t.wantProxyAuth, " but: ", auth) } w.WriteHeader(http.StatusOK) hj, ok := w.(http.Hijacker) if !ok { t.Fatal("webserver doesn't support hijacking") } conn, bufrw, err := hj.Hijack() if err != nil { t.Fatal("failed to hijack", err) } defer conn.Close() if err := bufrw.Flush(); err != nil { t.Fatal("failed to flush", err) } tlsConn := t.handshakeTLS(conn) t.serveXMPP(tlsConn) } func (t testXMPPHandler) handshakeTLS(conn net.Conn) net.Conn { cloneTLSConfig := *t.cfg tlsConn := tls.Server(conn, &cloneTLSConfig) if err := tlsConn.Handshake(); err != nil { t.Fatal("failed to handshake TLS", err) } return tlsConn } func (t testXMPPHandler) serveXMPP(conn net.Conn) { t.dec = xml.NewDecoder(conn) t.xmppHello(conn) // from https://developers.google.com/cloud-print/docs/rawxmpp t.saslHandshake(conn) t.xmppHandshake(conn) t.handleSubscribe(conn) t.handlePing(conn) for i := 0; i < t.wantPing; i++ { t.handlePing(conn) } if t.waiting == nil { ioutil.ReadAll(conn) // wait for client end } else { <-t.waiting } } func (t testXMPPHandler) xmppHello(conn net.Conn) { io.WriteString(conn, ` `) } func (t testXMPPHandler) saslHandshake(conn net.Conn) { t.readElement("stream") io.WriteString(conn, ` PLAIN X-GOOGLE-TOKEN X-OAUTH2 `) t.readElement("auth") io.WriteString(conn, ` `) } func (t testXMPPHandler) xmppHandshake(conn net.Conn) { t.readElement("stream") io.WriteString(conn, ` `) t.readElement("iq", "bind") io.WriteString(conn, ` barejid/fulljid `) t.readElement("iq", "session") io.WriteString(conn, ` `) } func (t testXMPPHandler) handleSubscribe(conn net.Conn) { t.readElement("iq", "subscribe") io.WriteString(conn, ` `) } func (t testXMPPHandler) handlePing(conn net.Conn) { iq := t.readElement("iq", "ping") id := "0" for _, attr := range iq.Attr { if attr.Name.Local == "id" { id = attr.Value break } } io.WriteString(conn, ` `) } func (t testXMPPHandler) readElement(wantName string, wantChildren ...string) *xml.StartElement { d := t.dec for { token, err := d.Token() if err != nil { t.Fatal("failed to read start element", err) } if startElement, ok := token.(xml.StartElement); ok { if actual := startElement.Name.Local; actual != wantName { continue } for _, want := range wantChildren { t.readElement(want) } return &startElement } } panic("unreachable") } func configureTLS(t *testing.T) *tls.Config { cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatal("failed to load x509 key pair", err) } cfg := tls.Config{Certificates: []tls.Certificate{cert}} x509Cert, err := x509.ParseCertificate(cfg.Certificates[0].Certificate[0]) cfg.RootCAs = x509.NewCertPool() cfg.RootCAs.AddCert(x509Cert) return &cfg } // localhostCert is a PEM-encoded TLS cert with SAN IPs borrowed from http/httptest var localhostCert = []byte(`-----BEGIN CERTIFICATE----- MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM fblo6RBxUQ== -----END CERTIFICATE-----`) // localhostKey is the private key for localhostCert. var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== -----END RSA PRIVATE KEY-----`)