├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cdd ├── cdd.go ├── cds.go ├── cjs.go └── cjt.go ├── cups ├── core.go ├── cups.c ├── cups.go ├── cups.h ├── ppdcache.go ├── translate-attrs.go ├── translate-attrs_test.go ├── translate-ppd.go ├── translate-ppd_test.go ├── translate-ticket.go └── translate-ticket_test.go ├── fcm ├── fcm.go └── fcm_test.go ├── gcp-connector-util ├── gcp-cups-connector-util.go ├── init.go ├── main_unix.go ├── main_windows.go └── monitor.go ├── gcp-cups-connector └── gcp-cups-connector.go ├── gcp-windows-connector └── gcp-windows-connector.go ├── gcp ├── gcp.go ├── http.go └── job.go ├── lib ├── backoff.go ├── backoff_test.go ├── concprintermap.go ├── config.go ├── config_unix.go ├── config_windows.go ├── deephash.go ├── deephash_test.go ├── job.go ├── printer.go └── semaphore.go ├── log ├── log.go ├── log_unix.go ├── log_windows.go ├── logroller.go └── logroller_test.go ├── manager └── printermanager.go ├── monitor └── monitor.go ├── notification └── notification.go ├── privet ├── api-server.go ├── avahi.c ├── avahi.go ├── avahi.h ├── bonjour.c ├── bonjour.go ├── bonjour.h ├── jobcache.go ├── portmanager.go ├── portmanager_test.go ├── privet.go ├── windows.go ├── xsrf.go └── xsrf_test.go ├── systemd └── cloud-print-connector.service ├── winspool ├── cairo.go ├── poppler.go ├── utf16.go ├── win32.go └── winspool.go ├── wix ├── LICENSE.rtf ├── README.md ├── build-msi.sh ├── generate-dependencies.sh ├── windows-connector-x64.wxs └── windows-connector-x86.wxs └── xmpp ├── internal-xmpp.go ├── xmpp.go └── xmpp_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | dist: xenial 3 | 4 | go: 5 | - "1.10.x" 6 | - "1.11.x" 7 | - "1.x" 8 | - master 9 | 10 | os: 11 | - linux 12 | - osx 13 | 14 | before_install: 15 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi 16 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -y libcups2-dev libavahi-client-dev ; fi 17 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 18 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install bazaar ; fi 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) 6 | (CLA), which you can do online. The CLA is necessary mainly because you own the 7 | copyright to your changes, even after your contribution becomes part of our 8 | codebase, so we need your permission to use and distribute your code. We also 9 | need to be sure of various other things—for instance that you'll tell us if you 10 | know that your code infringes on other people's patents. You don't have to sign 11 | the CLA until after you've submitted your code for review and a member has 12 | approved it, but you must do it before we can put your code into our codebase. 13 | Before you start working on a larger contribution, you should get in touch with 14 | us first through the issue tracker with your idea so that we can help out and 15 | possibly guide you. Coordinating up front makes it much easier to avoid 16 | frustration later on. 17 | 18 | ### Code reviews 19 | All submissions, including submissions by project members, require review. We 20 | use Github pull requests for this purpose. 21 | 22 | ### The small print 23 | Contributions made by corporations are covered by a different agreement than 24 | the one above, the Software Grant and Corporate Contributor License Agreement. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, Google Inc. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Print Connector 2 | 3 | **Note: After December 31, 2020, Google Cloud Print will no longer be supported. Learn more about your [migration options](https://support.google.com/chrome/?p=cloudprint).** 4 | 5 | ## Introduction 6 | 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. 7 | 8 | Lots of help can be found in [the wiki](https://github.com/google/cloud-print-connector/wiki). 9 | 10 | ## Build Status 11 | * Linux/OSX: [![Build Status](https://travis-ci.org/google/cloud-print-connector.svg?branch=master)](https://travis-ci.org/google/cloud-print-connector) 12 | 13 | * FreeBSD: [![Build Status](http://jenkins.mouf.net/job/cloud-print-connector/badge/icon)](http://jenkins.mouf.net/job/cloud-print-connector/) 14 | 15 | ## License 16 | Copyright 2015 Google Inc. All rights reserved. 17 | 18 | Use of this source code is governed by a BSD-style 19 | license that can be found in the LICENSE file or at 20 | https://developers.google.com/open-source/licenses/bsd 21 | -------------------------------------------------------------------------------- /cdd/cds.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package cdd 10 | 11 | type CloudConnectionStateType string 12 | 13 | const ( 14 | CloudConnectionStateUnknown CloudConnectionStateType = "UNKNOWN" 15 | CloudConnectionStateNotConfigured CloudConnectionStateType = "NOT_CONFIGURED" 16 | CloudConnectionStateOnline CloudConnectionStateType = "ONLINE" 17 | CloudConnectionStateOffline CloudConnectionStateType = "OFFLINE" 18 | ) 19 | 20 | type CloudDeviceState struct { 21 | Version string `json:"version"` 22 | CloudConnectionState *CloudConnectionStateType `json:"cloud_connection_state,omitempty"` 23 | Printer *PrinterStateSection `json:"printer"` 24 | } 25 | 26 | type CloudDeviceStateType string 27 | 28 | const ( 29 | CloudDeviceStateIdle CloudDeviceStateType = "IDLE" 30 | CloudDeviceStateProcessing CloudDeviceStateType = "PROCESSING" 31 | CloudDeviceStateStopped CloudDeviceStateType = "STOPPED" 32 | ) 33 | 34 | type PrinterStateSection struct { 35 | State CloudDeviceStateType `json:"state"` 36 | InputTrayState *InputTrayState `json:"input_tray_state,omitempty"` 37 | OutputBinState *OutputBinState `json:"output_bin_state,omitempty"` 38 | MarkerState *MarkerState `json:"marker_state,omitempty"` 39 | CoverState *CoverState `json:"cover_state,omitempty"` 40 | MediaPathState *MediaPathState `json:"media_path_state,omitempty"` 41 | VendorState *VendorState `json:"vendor_state,omitempty"` 42 | } 43 | 44 | type InputTrayState struct { 45 | Item []InputTrayStateItem `json:"item"` 46 | } 47 | 48 | type InputTrayStateType string 49 | 50 | const ( 51 | InputTrayStateOK InputTrayStateType = "OK" 52 | InputTrayStateEmpty InputTrayStateType = "EMPTY" 53 | InputTrayStateOpen InputTrayStateType = "OPEN" 54 | InputTrayStateOff InputTrayStateType = "OFF" 55 | InputTrayStateFailure InputTrayStateType = "FAILURE" 56 | ) 57 | 58 | type InputTrayStateItem struct { 59 | VendorID string `json:"vendor_id"` 60 | State InputTrayStateType `json:"state"` 61 | LevelPercent *int32 `json:"level_percent,omitempty"` 62 | VendorMessage string `json:"vendor_message,omitempty"` 63 | } 64 | 65 | type OutputBinState struct { 66 | Item []OutputBinStateItem `json:"item"` 67 | } 68 | 69 | type OutputBinStateType string 70 | 71 | const ( 72 | OutputBinStateOK OutputBinStateType = "OK" 73 | OutputBinStateFull OutputBinStateType = "FULL" 74 | OutputBinStateOpen OutputBinStateType = "OPEN" 75 | OutputBinStateOff OutputBinStateType = "OFF" 76 | OutputBinStateFailure OutputBinStateType = "FAILURE" 77 | ) 78 | 79 | type OutputBinStateItem struct { 80 | VendorID string `json:"vendor_id"` 81 | State OutputBinStateType `json:"state"` 82 | LevelPercent *int32 `json:"level_percent,omitempty"` 83 | VendorMessage string `json:"vendor_message,omitempty"` 84 | } 85 | 86 | type MarkerState struct { 87 | Item []MarkerStateItem `json:"item"` 88 | } 89 | 90 | type MarkerStateType string 91 | 92 | const ( 93 | MarkerStateOK MarkerStateType = "OK" 94 | MarkerStateExhausted MarkerStateType = "EXHAUSTED" 95 | MarkerStateRemoved MarkerStateType = "REMOVED" 96 | MarkerStateFailure MarkerStateType = "FAILURE" 97 | ) 98 | 99 | type MarkerStateItem struct { 100 | VendorID string `json:"vendor_id"` 101 | State MarkerStateType `json:"state"` 102 | LevelPercent *int32 `json:"level_percent,omitempty"` 103 | LevelPages *int32 `json:"level_pages,omitempty"` 104 | VendorMessage string `json:"vendor_message,omitempty"` 105 | } 106 | 107 | type CoverState struct { 108 | Item []CoverStateItem `json:"item"` 109 | } 110 | 111 | type CoverStateType string 112 | 113 | const ( 114 | CoverStateOK CoverStateType = "OK" 115 | CoverStateOpen CoverStateType = "OPEN" 116 | CoverStateFailure CoverStateType = "FAILURE" 117 | ) 118 | 119 | type CoverStateItem struct { 120 | VendorID string `json:"vendor_id"` 121 | State CoverStateType `json:"state"` 122 | VendorMessage string `json:"vendor_message,omitempty"` 123 | } 124 | 125 | type MediaPathState struct { 126 | Item []MediaPathStateItem `json:"item"` 127 | } 128 | 129 | type MediaPathStateType string 130 | 131 | const ( 132 | MediaPathStateOK MediaPathStateType = "OK" 133 | MediaPathStateMediaJam MediaPathStateType = "MEDIA_JAM" 134 | MediaPathStateFailure MediaPathStateType = "FAILURE" 135 | ) 136 | 137 | type MediaPathStateItem struct { 138 | VendorID string `json:"vendor_id"` 139 | State MediaPathStateType `json:"state"` 140 | VendorMessage string `json:"vendor_message,omitempty"` 141 | } 142 | 143 | type VendorState struct { 144 | Item []VendorStateItem `json:"item"` 145 | } 146 | 147 | type VendorStateType string 148 | 149 | const ( 150 | VendorStateError VendorStateType = "ERROR" 151 | VendorStateWarning VendorStateType = "WARNING" 152 | VendorStateInfo VendorStateType = "INFO" 153 | ) 154 | 155 | type VendorStateItem struct { 156 | State VendorStateType `json:"state"` 157 | Description string `json:"description,omitempty"` 158 | DescriptionLocalized *[]LocalizedString `json:"description_localized,omitempty"` 159 | } 160 | -------------------------------------------------------------------------------- /cdd/cjs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package cdd 10 | 11 | type PrintJobState struct { 12 | Version string `json:"version"` 13 | State JobState `json:"state"` 14 | PagesPrinted *int32 `json:"pages_printed,omitempty"` 15 | DeliveryAttempts *int32 `json:"delivery_attempts,omitempty"` 16 | } 17 | 18 | type PrintJobStateDiff struct { 19 | State *JobState `json:"state,omitempty"` 20 | PagesPrinted *int32 `json:"pages_printed,omitempty"` 21 | } 22 | 23 | type JobStateType string 24 | 25 | const ( 26 | JobStateDraft JobStateType = "DRAFT" 27 | JobStateHeld JobStateType = "HELD" 28 | JobStateQueued JobStateType = "QUEUED" 29 | JobStateInProgress JobStateType = "IN_PROGRESS" 30 | JobStateStopped JobStateType = "STOPPED" 31 | JobStateDone JobStateType = "DONE" 32 | JobStateAborted JobStateType = "ABORTED" 33 | ) 34 | 35 | type JobState struct { 36 | Type JobStateType `json:"type"` 37 | UserActionCause *UserActionCause `json:"user_action_cause,omitempty"` 38 | DeviceStateCause *DeviceStateCause `json:"device_state_cause,omitempty"` 39 | DeviceActionCause *DeviceActionCause `json:"device_action_cause,omitempty"` 40 | ServiceActionCause *ServiceActionCause `json:"service_action_cause,omitempty"` 41 | } 42 | 43 | type UserActionCauseCode string 44 | 45 | const ( 46 | UserActionCauseCanceled UserActionCauseCode = "CANCELLED" // Two L's 47 | UserActionCausePaused UserActionCauseCode = "PAUSED" 48 | UserActionCauseOther UserActionCauseCode = "OTHER" 49 | ) 50 | 51 | type UserActionCause struct { 52 | ActionCode UserActionCauseCode `json:"action_code"` 53 | } 54 | 55 | type DeviceStateCauseCode string 56 | 57 | const ( 58 | DeviceStateCauseInputTray DeviceStateCauseCode = "INPUT_TRAY" 59 | DeviceStateCauseMarker DeviceStateCauseCode = "MARKER" 60 | DeviceStateCauseMediaPath DeviceStateCauseCode = "MEDIA_PATH" 61 | DeviceStateCauseMediaSize DeviceStateCauseCode = "MEDIA_SIZE" 62 | DeviceStateCauseMediaType DeviceStateCauseCode = "MEDIA_TYPE" 63 | DeviceStateCauseOther DeviceStateCauseCode = "OTHER" 64 | ) 65 | 66 | type DeviceStateCause struct { 67 | ErrorCode DeviceStateCauseCode `json:"error_code"` 68 | } 69 | 70 | type DeviceActionCauseCode string 71 | 72 | const ( 73 | DeviceActionCauseDownloadFailure DeviceActionCauseCode = "DOWNLOAD_FAILURE" 74 | DeviceActionCauseInvalidTicket DeviceActionCauseCode = "INVALID_TICKET" 75 | DeviceActionCausePrintFailure DeviceActionCauseCode = "PRINT_FAILURE" 76 | DeviceActionCauseOther DeviceActionCauseCode = "OTHER" 77 | ) 78 | 79 | type DeviceActionCause struct { 80 | ErrorCode DeviceActionCauseCode `json:"error_code"` 81 | } 82 | 83 | type ServiceActionCauseCode string 84 | 85 | const ( 86 | ServiceActionCauseCommunication ServiceActionCauseCode = "COMMUNICATION_WITH_DEVICE_ERROR" 87 | ServiceActionCauseConversionError ServiceActionCauseCode = "CONVERSION_ERROR" 88 | ServiceActionCauseConversionFileTooBig ServiceActionCauseCode = "CONVERSION_FILE_TOO_BIG" 89 | ServiceActionCauseConversionType ServiceActionCauseCode = "CONVERSION_UNSUPPORTED_CONTENT_TYPE" 90 | ServiceActionCauseDeliveryFailure ServiceActionCauseCode = "DELIVERY_FAILURE" 91 | ServiceActionCauseExpiration ServiceActionCauseCode = "EXPIRATION" 92 | ServiceActionCauseFetchForbidden ServiceActionCauseCode = "FETCH_DOCUMENT_FORBIDDEN" 93 | ServiceActionCauseFetchNotFound ServiceActionCauseCode = "FETCH_DOCUMENT_NOT_FOUND" 94 | ServiceActionCauseDriveQuota ServiceActionCauseCode = "GOOGLE_DRIVE_QUOTA" 95 | ServiceActionCauseInconsistentJob ServiceActionCauseCode = "INCONSISTENT_JOB" 96 | ServiceActionCauseInconsistentPrinter ServiceActionCauseCode = "INCONSISTENT_PRINTER" 97 | ServiceActionCausePrinterDeleted ServiceActionCauseCode = "PRINTER_DELETED" 98 | ServiceActionCauseRemoteJobNoExist ServiceActionCauseCode = "REMOTE_JOB_NO_LONGER_EXISTS" 99 | ServiceActionCauseRemoteJobError ServiceActionCauseCode = "REMOTE_JOB_ERROR" 100 | ServiceActionCauseRemoteJobTimeout ServiceActionCauseCode = "REMOTE_JOB_TIMEOUT" 101 | ServiceActionCauseRemoteJobAborted ServiceActionCauseCode = "REMOTE_JOB_ABORTED" 102 | ServiceActionCauseOther ServiceActionCauseCode = "OTHER" 103 | ) 104 | 105 | type ServiceActionCause struct { 106 | ErrorCode ServiceActionCauseCode `json:"error_code"` 107 | } 108 | -------------------------------------------------------------------------------- /cdd/cjt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package cdd 10 | 11 | type CloudJobTicket struct { 12 | Version string `json:"version"` 13 | Print PrintTicketSection `json:"print"` 14 | } 15 | 16 | type PrintTicketSection struct { 17 | VendorTicketItem []VendorTicketItem `json:"vendor_ticket_item,omitempty"` 18 | Color *ColorTicketItem `json:"color,omitempty"` 19 | Duplex *DuplexTicketItem `json:"duplex,omitempty"` 20 | PageOrientation *PageOrientationTicketItem `json:"page_orientation,omitempty"` 21 | Copies *CopiesTicketItem `json:"copies,omitempty"` 22 | Margins *MarginsTicketItem `json:"margins,omitempty"` 23 | DPI *DPITicketItem `json:"dpi,omitempty"` 24 | FitToPage *FitToPageTicketItem `json:"fit_to_page,omitempty"` 25 | PageRange *PageRangeTicketItem `json:"page_range,omitempty"` 26 | MediaSize *MediaSizeTicketItem `json:"media_size,omitempty"` 27 | Collate *CollateTicketItem `json:"collate,omitempty"` 28 | ReverseOrder *ReverseOrderTicketItem `json:"reverse_order,omitempty"` 29 | } 30 | 31 | type VendorTicketItem struct { 32 | ID string `json:"id"` 33 | Value string `json:"value"` 34 | } 35 | 36 | type ColorTicketItem struct { 37 | VendorID string `json:"vendor_id"` 38 | Type ColorType `json:"type"` 39 | } 40 | 41 | type DuplexTicketItem struct { 42 | Type DuplexType `json:"type"` 43 | } 44 | 45 | type PageOrientationTicketItem struct { 46 | Type PageOrientationType `json:"type"` 47 | } 48 | 49 | type CopiesTicketItem struct { 50 | Copies int32 `json:"copies"` 51 | } 52 | 53 | type MarginsTicketItem struct { 54 | TopMicrons int32 `json:"top_microns"` 55 | RightMicrons int32 `json:"right_microns"` 56 | BottomMicrons int32 `json:"bottom_microns"` 57 | LeftMicrons int32 `json:"left_microns"` 58 | } 59 | 60 | type DPITicketItem struct { 61 | HorizontalDPI int32 `json:"horizontal_dpi"` 62 | VerticalDPI int32 `json:"vertical_dpi"` 63 | VendorID string `json:"vendor_id"` 64 | } 65 | 66 | type FitToPageTicketItem struct { 67 | Type FitToPageType `json:"type"` 68 | } 69 | 70 | type PageRangeTicketItem struct { 71 | Interval []PageRangeInterval `json:"interval"` 72 | } 73 | 74 | type MediaSizeTicketItem struct { 75 | WidthMicrons int32 `json:"width_microns"` 76 | HeightMicrons int32 `json:"height_microns"` 77 | IsContinuousFeed bool `json:"is_continuous_feed"` // default = false 78 | VendorID string `json:"vendor_id"` 79 | } 80 | 81 | type CollateTicketItem struct { 82 | Collate bool `json:"collate"` 83 | } 84 | 85 | type ReverseOrderTicketItem struct { 86 | ReverseOrder bool `json:"reverse_order"` 87 | } 88 | -------------------------------------------------------------------------------- /cups/cups.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | #include "cups.h" 10 | 11 | const char 12 | *JOB_STATE = "job-state", 13 | *JOB_MEDIA_SHEETS_COMPLETED = "job-media-sheets-completed", 14 | *POST_RESOURCE = "/", 15 | *REQUESTED_ATTRIBUTES = "requested-attributes", 16 | *JOB_URI_ATTRIBUTE = "job-uri", 17 | *IPP = "ipp"; 18 | 19 | // Allocates a new char**, initializes the values to NULL. 20 | char **newArrayOfStrings(int size) { 21 | return calloc(size, sizeof(char *)); 22 | } 23 | 24 | // Sets one value in a char**. 25 | void setStringArrayValue(char **stringArray, int index, char *value) { 26 | stringArray[index] = value; 27 | } 28 | 29 | // Frees a char** and associated non-NULL char*. 30 | void freeStringArrayAndStrings(char **stringArray, int size) { 31 | int i; 32 | for (i = 0; i < size; i++) { 33 | if (stringArray[i] != NULL) 34 | free(stringArray[i]); 35 | } 36 | free(stringArray); 37 | } 38 | 39 | // getIPPRequestStatusCode gets the status_code field. 40 | // This field is not visible to cgo (don't know why). 41 | ipp_status_t getIPPRequestStatusCode(ipp_t *ipp) { 42 | return ipp->request.status.status_code; 43 | } 44 | 45 | // getAttributeDateValue gets the ith date value from attr. 46 | const ipp_uchar_t *getAttributeDateValue(ipp_attribute_t *attr, int i) { 47 | return attr->values[i].date; 48 | } 49 | 50 | // getAttributeIntegerValue gets the ith integer value from attr. 51 | int getAttributeIntegerValue(ipp_attribute_t *attr, int i) { 52 | return attr->values[i].integer; 53 | } 54 | 55 | // getAttributeStringValue gets the ith string value from attr. 56 | const char *getAttributeStringValue(ipp_attribute_t *attr, int i) { 57 | return attr->values[i].string.text; 58 | } 59 | 60 | // getAttributeValueRange gets the ith range value from attr. 61 | void getAttributeValueRange(ipp_attribute_t *attr, int i, int *lower, 62 | int *upper) { 63 | *lower = attr->values[i].range.lower; 64 | *upper = attr->values[i].range.upper; 65 | } 66 | 67 | // getAttributeValueResolution gets the ith resolution value from attr. 68 | // The values returned are always "per inch" not "per centimeter". 69 | void getAttributeValueResolution(ipp_attribute_t *attr, int i, int *xres, 70 | int *yres) { 71 | if (IPP_RES_PER_CM == attr->values[i].resolution.units) { 72 | *xres = attr->values[i].resolution.xres * 2.54; 73 | *yres = attr->values[i].resolution.yres * 2.54; 74 | } else { 75 | *xres = attr->values[i].resolution.xres; 76 | *yres = attr->values[i].resolution.yres; 77 | } 78 | } 79 | 80 | #ifndef _CUPS_API_1_7 81 | // Skip attribute validation with older clients. 82 | int ippValidateAttributes(ipp_t *ipp) { 83 | return 1; 84 | } 85 | 86 | // Ignore some fields with older clients. 87 | // The connector doesn't use addrlist anyways. 88 | // Older clients use msec = 30000. 89 | http_t *httpConnect2(const char *host, int port, http_addrlist_t *addrlist, int family, 90 | http_encryption_t encryption, int blocking, int msec, int *cancel) { 91 | return httpConnectEncrypt(host, port, encryption); 92 | } 93 | #endif 94 | -------------------------------------------------------------------------------- /cups/cups.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | // Since CUPS 1.6, the ipp struct properties have been private, with accessor 10 | // functions added (STR #3928). This line makes the properties not private, so 11 | // that the connector can be compiled against pre and post 1.6 libraries. 12 | #define _IPP_PRIVATE_STRUCTURES 1 13 | 14 | #include 15 | #include 16 | #include // size_t 17 | #include // free, calloc, malloc 18 | #include // AF_UNSPEC 19 | #include // uname 20 | #include // time_t 21 | 22 | extern const char 23 | *JOB_STATE, 24 | *JOB_MEDIA_SHEETS_COMPLETED, 25 | *POST_RESOURCE, 26 | *REQUESTED_ATTRIBUTES, 27 | *JOB_URI_ATTRIBUTE, 28 | *IPP; 29 | 30 | char **newArrayOfStrings(int size); 31 | void setStringArrayValue(char **stringArray, int index, char *value); 32 | void freeStringArrayAndStrings(char **stringArray, int size); 33 | 34 | ipp_status_t getIPPRequestStatusCode(ipp_t *ipp); 35 | const ipp_uchar_t *getAttributeDateValue(ipp_attribute_t *attr, int i); 36 | int getAttributeIntegerValue(ipp_attribute_t *attr, int i); 37 | const char *getAttributeStringValue(ipp_attribute_t *attr, int i); 38 | void getAttributeValueRange(ipp_attribute_t *attr, int i, int *lower, int *upper); 39 | void getAttributeValueResolution(ipp_attribute_t *attr, int i, int *xres, int *yres); 40 | 41 | #ifndef _CUPS_API_1_7 42 | int ippValidateAttributes(ipp_t *ipp); 43 | http_t *httpConnect2(const char *host, int port, http_addrlist_t *addrlist, int family, 44 | http_encryption_t encryption, int blocking, int msec, int *cancel); 45 | 46 | # define HTTP_ENCRYPTION_IF_REQUESTED HTTP_ENCRYPT_IF_REQUESTED 47 | # define HTTP_ENCRYPTION_NEVER HTTP_ENCRYPT_NEVER 48 | # define HTTP_ENCRYPTION_REQUIRED HTTP_ENCRYPT_REQUIRED 49 | # define HTTP_ENCRYPTION_ALWAYS HTTP_ENCRYPT_ALWAYS 50 | # define HTTP_STATUS_OK HTTP_OK 51 | # define HTTP_STATUS_NOT_MODIFIED HTTP_NOT_MODIFIED 52 | # define IPP_OP_CUPS_GET_PRINTERS CUPS_GET_PRINTERS 53 | # define IPP_OP_GET_JOB_ATTRIBUTES IPP_GET_JOB_ATTRIBUTES 54 | # define IPP_STATUS_OK IPP_OK 55 | # define IPP_STATUS_ERROR_NOT_FOUND IPP_NOT_FOUND 56 | #endif 57 | -------------------------------------------------------------------------------- /cups/ppdcache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package cups 10 | 11 | /* 12 | #include "cups.h" 13 | */ 14 | import "C" 15 | import ( 16 | "bytes" 17 | "errors" 18 | "os" 19 | "sync" 20 | "unsafe" 21 | 22 | "github.com/google/cloud-print-connector/cdd" 23 | "github.com/google/cloud-print-connector/lib" 24 | ) 25 | 26 | // This isn't really a cache, but an interface to CUPS' quirky PPD interface. 27 | // The connector needs to know when a PPD changes, but the CUPS API can only: 28 | // (1) fetch a PPD to a file 29 | // (2) indicate whether a PPD file is up-to-date. 30 | // So, this "cache": 31 | // (1) maintains temporary file copies of PPDs for each printer 32 | // (2) updates those PPD files as necessary 33 | type ppdCache struct { 34 | cc *cupsCore 35 | vendorPPDOptions []string 36 | cache map[string]*ppdCacheEntry 37 | cacheMutex sync.RWMutex 38 | } 39 | 40 | func newPPDCache(cc *cupsCore, vendorPPDOptions []string) *ppdCache { 41 | cache := make(map[string]*ppdCacheEntry) 42 | pc := ppdCache{ 43 | cc: cc, 44 | vendorPPDOptions: vendorPPDOptions, 45 | cache: cache, 46 | } 47 | return &pc 48 | } 49 | 50 | func (pc *ppdCache) quit() { 51 | pc.cacheMutex.Lock() 52 | defer pc.cacheMutex.Unlock() 53 | 54 | for printername, pce := range pc.cache { 55 | pce.free() 56 | delete(pc.cache, printername) 57 | } 58 | } 59 | 60 | // removePPD removes a cache entry from the cache. 61 | func (pc *ppdCache) removePPD(printername string) { 62 | pc.cacheMutex.Lock() 63 | defer pc.cacheMutex.Unlock() 64 | 65 | if pce, exists := pc.cache[printername]; exists { 66 | pce.free() 67 | delete(pc.cache, printername) 68 | } 69 | } 70 | 71 | func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap, error) { 72 | pc.cacheMutex.RLock() 73 | pce, exists := pc.cache[printername] 74 | pc.cacheMutex.RUnlock() 75 | 76 | if !exists { 77 | pce, err := createPPDCacheEntry(printername) 78 | if err != nil { 79 | return nil, "", "", nil, err 80 | } 81 | if err = pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { 82 | pce.free() 83 | return nil, "", "", nil, err 84 | } 85 | 86 | pc.cacheMutex.Lock() 87 | defer pc.cacheMutex.Unlock() 88 | 89 | if firstPCE, exists := pc.cache[printername]; exists { 90 | // Two entries were created at the same time. Remove the older one. 91 | delete(pc.cache, printername) 92 | go firstPCE.free() 93 | } 94 | pc.cache[printername] = pce 95 | description, manufacturer, model, duplexMap := pce.getFields() 96 | return &description, manufacturer, model, duplexMap, nil 97 | 98 | } else { 99 | if err := pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { 100 | delete(pc.cache, printername) 101 | pce.free() 102 | return nil, "", "", nil, err 103 | } 104 | description, manufacturer, model, duplexMap := pce.getFields() 105 | return &description, manufacturer, model, duplexMap, nil 106 | } 107 | } 108 | 109 | // Holds persistent data needed for calling C.cupsGetPPD3. 110 | type ppdCacheEntry struct { 111 | printername *C.char 112 | modtime C.time_t 113 | description cdd.PrinterDescriptionSection 114 | manufacturer string 115 | model string 116 | duplexMap lib.DuplexVendorMap 117 | mutex sync.Mutex 118 | } 119 | 120 | // createPPDCacheEntry creates an instance of ppdCache with the name field set, 121 | // all else empty. The caller must free the name and buffer fields with 122 | // ppdCacheEntry.free() 123 | func createPPDCacheEntry(name string) (*ppdCacheEntry, error) { 124 | pce := &ppdCacheEntry{ 125 | printername: C.CString(name), 126 | modtime: C.time_t(0), 127 | } 128 | 129 | return pce, nil 130 | } 131 | 132 | // getFields gets externally-interesting fields from this ppdCacheEntry under 133 | // a lock. The description is passed as a value (copy), to protect the cached copy. 134 | func (pce *ppdCacheEntry) getFields() (cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap) { 135 | pce.mutex.Lock() 136 | defer pce.mutex.Unlock() 137 | return pce.description, pce.manufacturer, pce.model, pce.duplexMap 138 | } 139 | 140 | // free frees the memory that stores the name and buffer fields, and deletes 141 | // the file named by the buffer field. If the file doesn't exist, no error is 142 | // returned. 143 | func (pce *ppdCacheEntry) free() { 144 | pce.mutex.Lock() 145 | defer pce.mutex.Unlock() 146 | 147 | C.free(unsafe.Pointer(pce.printername)) 148 | } 149 | 150 | // refresh calls cupsGetPPD3() to refresh this PPD information, in 151 | // case CUPS has a new PPD for the printer. 152 | func (pce *ppdCacheEntry) refresh(cc *cupsCore, vendorPPDOptions []string) error { 153 | pce.mutex.Lock() 154 | defer pce.mutex.Unlock() 155 | 156 | ppdFilename, err := cc.getPPD(pce.printername, &pce.modtime) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if ppdFilename == nil { 162 | // Cache hit. 163 | return nil 164 | } 165 | 166 | // (else) Cache miss. 167 | defer C.free(unsafe.Pointer(ppdFilename)) 168 | defer os.Remove(C.GoString(ppdFilename)) 169 | 170 | // Read from CUPS temporary file. 171 | r, err := os.Open(C.GoString(ppdFilename)) 172 | if err != nil { 173 | return err 174 | } 175 | defer r.Close() 176 | 177 | // Write to a buffer string for translation. 178 | var w bytes.Buffer 179 | if _, err := w.ReadFrom(r); err != nil { 180 | return err 181 | } 182 | 183 | description, manufacturer, model, duplexMap := translatePPD(w.String(), vendorPPDOptions) 184 | if description == nil || manufacturer == "" || model == "" { 185 | return errors.New("Failed to parse PPD") 186 | } 187 | 188 | pce.description = *description 189 | pce.manufacturer = manufacturer 190 | pce.model = model 191 | pce.duplexMap = duplexMap 192 | 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /cups/translate-ticket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package cups 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/google/cloud-print-connector/cdd" 19 | "github.com/google/cloud-print-connector/lib" 20 | ) 21 | 22 | var rVendorIDKeyValue = regexp.MustCompile( 23 | `^([^\` + internalKeySeparator + `]+)(?:` + internalKeySeparator + `(.+))?$`) 24 | 25 | // translateTicket converts a CloudJobTicket to a map of options, suitable for a new CUPS print job. 26 | func translateTicket(printer *lib.Printer, ticket *cdd.CloudJobTicket) (map[string]string, error) { 27 | if printer == nil || ticket == nil { 28 | return map[string]string{}, nil 29 | } 30 | 31 | m := map[string]string{} 32 | for _, vti := range ticket.Print.VendorTicketItem { 33 | if vti.ID == ricohPasswordVendorID { 34 | if vti.Value == "" { 35 | // do not add specific map of options for Ricoh vendor like ppdLockedPrintPassword or ppdJobType when password is empty 36 | continue; 37 | } 38 | if !rRicohPasswordFormat.MatchString(vti.Value) { 39 | return map[string]string{}, errors.New("Invalid password format") 40 | } 41 | } 42 | 43 | for _, option := range strings.Split(vti.ID, internalValueSeparator) { 44 | var key, value string 45 | parts := rVendorIDKeyValue.FindStringSubmatch(option) 46 | if parts == nil || parts[2] == "" { 47 | key, value = option, vti.Value 48 | } else { 49 | key, value = parts[1], parts[2] 50 | } 51 | m[key] = value 52 | } 53 | } 54 | if ticket.Print.Color != nil && printer.Description.Color != nil { 55 | var colorString string 56 | if ticket.Print.Color.VendorID != "" { 57 | colorString = ticket.Print.Color.VendorID 58 | } else { 59 | // The ticket doesn't provide the VendorID. Let's find it by Type. 60 | for _, colorOption := range printer.Description.Color.Option { 61 | if ticket.Print.Color.Type == colorOption.Type { 62 | colorString = colorOption.VendorID 63 | break 64 | } 65 | } 66 | } 67 | parts := rVendorIDKeyValue.FindStringSubmatch(colorString) 68 | if parts != nil && parts[2] != "" { 69 | m[parts[1]] = parts[2] 70 | } 71 | } 72 | if ticket.Print.Duplex != nil && printer.Description.Duplex != nil { 73 | duplexString := printer.DuplexMap[ticket.Print.Duplex.Type] 74 | parts := rVendorIDKeyValue.FindStringSubmatch(duplexString) 75 | if parts != nil && parts[2] != "" { 76 | m[parts[1]] = parts[2] 77 | } 78 | } 79 | if ticket.Print.PageOrientation != nil && printer.Description.PageOrientation != nil { 80 | if orientation, exists := orientationValueByType[ticket.Print.PageOrientation.Type]; exists { 81 | m[attrOrientationRequested] = orientation 82 | } 83 | } 84 | if ticket.Print.Copies != nil && printer.Description.Copies != nil { 85 | m[attrCopies] = strconv.FormatInt(int64(ticket.Print.Copies.Copies), 10) 86 | } 87 | if ticket.Print.Margins != nil && printer.Description.Margins != nil { 88 | m[attrMediaLeftMargin] = micronsToPoints(ticket.Print.Margins.LeftMicrons) 89 | m[attrMediaRightMargin] = micronsToPoints(ticket.Print.Margins.RightMicrons) 90 | m[attrMediaTopMargin] = micronsToPoints(ticket.Print.Margins.TopMicrons) 91 | m[attrMediaBottomMargin] = micronsToPoints(ticket.Print.Margins.BottomMicrons) 92 | } 93 | if ticket.Print.DPI != nil && printer.Description.DPI != nil { 94 | if ticket.Print.DPI.VendorID != "" { 95 | m[ppdResolution] = ticket.Print.DPI.VendorID 96 | } else { 97 | for _, dpiOption := range printer.Description.DPI.Option { 98 | if ticket.Print.DPI.HorizontalDPI == dpiOption.HorizontalDPI && 99 | ticket.Print.DPI.VerticalDPI == dpiOption.VerticalDPI { 100 | m[ppdResolution] = dpiOption.VendorID 101 | } 102 | } 103 | } 104 | } 105 | if ticket.Print.FitToPage != nil && printer.Description.FitToPage != nil { 106 | switch ticket.Print.FitToPage.Type { 107 | case cdd.FitToPageFitToPage: 108 | m[attrFitToPage] = attrTrue 109 | case cdd.FitToPageNoFitting: 110 | m[attrFitToPage] = attrFalse 111 | } 112 | } 113 | if ticket.Print.MediaSize != nil && printer.Description.MediaSize != nil { 114 | if ticket.Print.MediaSize.VendorID != "" { 115 | m[ppdPageSize] = ticket.Print.MediaSize.VendorID 116 | } else { 117 | widthPoints := micronsToPoints(ticket.Print.MediaSize.WidthMicrons) 118 | heightPoints := micronsToPoints(ticket.Print.MediaSize.HeightMicrons) 119 | m[ppdPageSize] = fmt.Sprintf("Custom.%sx%s", widthPoints, heightPoints) 120 | } 121 | } 122 | if ticket.Print.Collate != nil && printer.Description.Collate != nil { 123 | if ticket.Print.Collate.Collate { 124 | m[attrCollate] = attrTrue 125 | } else { 126 | m[attrCollate] = attrFalse 127 | } 128 | } 129 | if ticket.Print.ReverseOrder != nil && printer.Description.ReverseOrder != nil { 130 | if ticket.Print.ReverseOrder.ReverseOrder { 131 | m[attrOutputOrder] = "reverse" 132 | } else { 133 | m[attrOutputOrder] = "normal" 134 | } 135 | } 136 | 137 | return m, nil 138 | } 139 | 140 | func micronsToPoints(microns int32) string { 141 | return strconv.Itoa(int(float32(microns)*72/25400 + 0.5)) 142 | } 143 | -------------------------------------------------------------------------------- /fcm/fcm.go: -------------------------------------------------------------------------------- 1 | package fcm 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | import ( 13 | "github.com/google/cloud-print-connector/log" 14 | "github.com/google/cloud-print-connector/notification" 15 | ) 16 | 17 | const ( 18 | gcpFcmSubscribePath = "fcm/subscribe" 19 | ) 20 | 21 | type FCM struct { 22 | fcmServerBindURL string 23 | cachedToken string 24 | tokenRefreshTime time.Time 25 | clientID string 26 | proxyName string 27 | fcmTTLSecs float64 28 | FcmSubscribe func(string) (interface{}, error) 29 | 30 | notifications chan<- notification.PrinterNotification 31 | dead chan struct{} 32 | 33 | quit chan struct{} 34 | backoff backoff 35 | } 36 | 37 | type FCMMessage []struct { 38 | From string `json:"from"` 39 | Category string `json:"category"` 40 | CollapseKey string `json:"collapse_key"` 41 | Data struct { 42 | Notification string `json:"notification"` 43 | Subtype string `json:"subtype"` 44 | } `json:"data"` 45 | MessageID string `json:"message_id"` 46 | TimeToLive int `json:"time_to_live"` 47 | } 48 | 49 | func NewFCM(clientID string, proxyName string, fcmServerBindURL string, FcmSubscribe func(string) (interface{}, error), notifications chan<- notification.PrinterNotification) (*FCM, error) { 50 | f := FCM{ 51 | fcmServerBindURL, 52 | "", 53 | time.Time{}, 54 | clientID, 55 | proxyName, 56 | 0, 57 | FcmSubscribe, 58 | notifications, 59 | make(chan struct{}), 60 | make(chan struct{}), 61 | backoff{0, time.Second * 5, time.Minute * 5}, 62 | } 63 | return &f, nil 64 | } 65 | 66 | // get token from GCP and connect to FCM. 67 | func (f *FCM) Init() { 68 | iidToken := f.GetTokenWithRetry() 69 | if err := f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit); err != nil { 70 | for err != nil { 71 | f.backoff.addError() 72 | log.Errorf("FCM restart failed, will try again in %4.0f s: %s", 73 | f.backoff.delay().Seconds(), err) 74 | time.Sleep(f.backoff.delay()) 75 | err = f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit) 76 | } 77 | f.backoff.reset() 78 | log.Info("FCM conversation restarted successfully") 79 | } 80 | 81 | go f.KeepFcmAlive() 82 | } 83 | 84 | // Quit terminates the FCM conversation so that new jobs stop arriving. 85 | func (f *FCM) Quit() { 86 | // Signal to KeepFCMAlive. 87 | close(f.quit) 88 | } 89 | 90 | // Fcm notification listener 91 | func (f *FCM) ConnectToFcm(fcmNotifications chan<- notification.PrinterNotification, iidToken string, dead chan<- struct{}, quit chan<- struct{}) error { 92 | log.Debugf("Connecting to %s?token=%s", f.fcmServerBindURL, iidToken) 93 | resp, err := http.Get(fmt.Sprintf("%s?token=%s", f.fcmServerBindURL, iidToken)) 94 | if err != nil { 95 | // retrying exponentially 96 | log.Errorf("%v", err) 97 | return err 98 | } 99 | if resp.StatusCode == 200 { 100 | reader := bufio.NewReader(resp.Body) 101 | go func() { 102 | for { 103 | printerId, err := GetPrinterID(reader) 104 | if len(printerId) > 0 { 105 | pn := notification.PrinterNotification{printerId, notification.PrinterNewJobs} 106 | fcmNotifications <- pn 107 | } 108 | if err != nil { 109 | log.Info("DRAIN message received, client reconnecting.") 110 | dead <- struct{}{} 111 | break 112 | } 113 | } 114 | }() 115 | } 116 | return nil 117 | } 118 | 119 | func GetPrinterID(reader *bufio.Reader) (string, error) { 120 | raw_input, err := reader.ReadBytes('\n') 121 | if err == nil { 122 | // Trim last \n char 123 | raw_input = raw_input[:len(raw_input) - 1] 124 | buffer_size, _ := strconv.Atoi(string(raw_input)) 125 | notification_buffer := make([]byte, buffer_size) 126 | var sofar, sz int 127 | for err == nil && sofar < buffer_size { 128 | sz, err = reader.Read(notification_buffer[sofar:]) 129 | sofar += sz 130 | } 131 | 132 | if sofar == buffer_size { 133 | var d [][]interface{} 134 | var f FCMMessage 135 | json.Unmarshal([]byte(string(notification_buffer)), &d) 136 | s, _ := json.Marshal(d[0][1]) 137 | json.Unmarshal(s, &f) 138 | return f[0].Data.Notification, err 139 | } 140 | } 141 | return "", err 142 | } 143 | 144 | type backoff struct { 145 | // The number of consecutive connection errors. 146 | numErrors uint 147 | // The minimum amount of time to backoff. 148 | minBackoff time.Duration 149 | // The maximum amount of time to backoff. 150 | maxBackoff time.Duration 151 | } 152 | 153 | // Computes the amount of time to delay based on the number of errors. 154 | func (b *backoff) delay() time.Duration { 155 | if b.numErrors == 0 { 156 | // Never delay when there are no errors. 157 | return 0 158 | } 159 | curDelay := b.minBackoff 160 | for i := uint(1); i < b.numErrors; i++ { 161 | curDelay = curDelay * 2 162 | } 163 | if curDelay > b.maxBackoff { 164 | return b.maxBackoff 165 | } 166 | return curDelay 167 | } 168 | 169 | // Adds an observed error to inform the backoff delay decision. 170 | func (b *backoff) addError() { 171 | log.Info("err count") 172 | b.numErrors++ 173 | } 174 | 175 | // Resets the backoff back to having no errors. 176 | func (b *backoff) reset() { 177 | b.numErrors = 0 178 | } 179 | 180 | // Restart FCM connection when lost. 181 | func (f *FCM) KeepFcmAlive() { 182 | for { 183 | select { 184 | case <-f.dead: 185 | iidToken := f.GetTokenWithRetry() 186 | log.Error("FCM conversation died; restarting") 187 | if err := f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit); err != nil { 188 | for err != nil { 189 | f.backoff.addError() 190 | log.Errorf("FCM connection restart failed, will try again in %4.0f s: %s", 191 | f.backoff.delay().Seconds(), err) 192 | time.Sleep(f.backoff.delay()) 193 | err = f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit) 194 | } 195 | f.backoff.reset() 196 | log.Info("FCM conversation restarted successfully") 197 | } 198 | 199 | case <-f.quit: 200 | log.Info("Fcm client Quitting ...") 201 | // quitting keeping alive 202 | return 203 | } 204 | } 205 | } 206 | 207 | func (f *FCM) GetTokenWithRetry() string { 208 | retryCount := 3 209 | iidToken, err := f.GetToken() 210 | for err != nil && retryCount < 3 { 211 | retryCount -= 1 212 | log.Errorf("unable to get FCM token from GCP server, will try again in 10s: %s", err) 213 | time.Sleep(10 * time.Second) 214 | iidToken, err = f.GetToken() 215 | } 216 | if err != nil { 217 | log.Errorf("unable to get FCM token from GCP server.") 218 | panic(err) 219 | } 220 | return iidToken 221 | } 222 | 223 | // Returns cached token and Refresh token if needed. 224 | func (f *FCM) GetToken() (string, error) { 225 | if f.tokenRefreshTime == (time.Time{}) || time.Now().UTC().Sub(f.tokenRefreshTime).Seconds() > f.fcmTTLSecs { 226 | result, err := f.FcmSubscribe(fmt.Sprintf("%s?client=%s&proxy=%s", gcpFcmSubscribePath, f.clientID, f.proxyName)) 227 | if err != nil { 228 | log.Errorf("Unable to subscribe to FCM : %s", err) 229 | return "", err 230 | } 231 | token := result.(map[string]interface{})["token"] 232 | ttlSeconds, err := strconv.ParseFloat(result.(map[string]interface{})["fcmttl"].(string), 64) 233 | if err != nil { 234 | log.Errorf("Failed to parse FCM ttl : %s", err) 235 | return "", err 236 | } 237 | f.fcmTTLSecs = ttlSeconds 238 | log.Info("Updated FCM token.") 239 | f.cachedToken = token.(string) 240 | f.tokenRefreshTime = time.Now().UTC() 241 | } 242 | return f.cachedToken, nil 243 | } -------------------------------------------------------------------------------- /fcm/fcm_test.go: -------------------------------------------------------------------------------- 1 | package fcm_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | import ( 13 | "github.com/google/cloud-print-connector/fcm" 14 | "github.com/google/cloud-print-connector/notification" 15 | ) 16 | 17 | func TestFCM_ReceiveNotification(t *testing.T) { 18 | 19 | // test FCM server 20 | handler := func(w http.ResponseWriter, r *http.Request) { 21 | printerNotificationStr := 22 | `148 23 | [[4,[{"from":"xyz","category":"js","collapse_key":"xyz","data":{"notification":"printerId","subtype":"xyz"},"message_id":"xyz","time_to_live":60}]]]` 24 | fmt.Fprint(w, printerNotificationStr) 25 | } 26 | 27 | ts := httptest.NewServer(http.HandlerFunc(handler)) 28 | defer ts.Close() 29 | 30 | var f *fcm.FCM 31 | notifications := make(chan notification.PrinterNotification, 5) 32 | 33 | // sample notification 34 | var fcmToken map[string]interface{} 35 | fcmTokenStr := `{"fcmttl":"2419200","request":{"params":{"client":["xyz"],"proxy":["xyz"]},"time":"0","user":"xyz","users":["xyz"]},"success":true,"token":"token","xsrf_token":"xyz"}` 36 | json.Unmarshal([]byte(fcmTokenStr), &fcmToken) 37 | 38 | f, err := fcm.NewFCM("clientid", "", ts.URL, func(input string) (interface{}, error) { return fcmToken, nil }, notifications) 39 | defer f.Quit() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | // This method is stubbed. 45 | result, err := f.FcmSubscribe("SubscribeUrl") 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | token := result.(map[string]interface{})["token"].(string) 51 | dead := make(chan struct{}) 52 | quit := make(chan struct{}) 53 | 54 | // bind to FCM to receive notifications 55 | f.ConnectToFcm(notifications, token, dead, quit) 56 | go func() { 57 | time.Sleep(4 * time.Second) 58 | // time out 59 | notifications <- notification.PrinterNotification{"dummy", notification.PrinterNewJobs} 60 | }() 61 | message := <-notifications 62 | 63 | // verify if right message received. 64 | if message.GCPID != "printerId" { 65 | t.Fatal("Did not receive right printer notification") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gcp-connector-util/main_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package main 10 | 11 | import ( 12 | "os" 13 | "time" 14 | 15 | "github.com/google/cloud-print-connector/lib" 16 | "github.com/urfave/cli" 17 | ) 18 | 19 | var unixInitFlags = []cli.Flag{ 20 | &cli.StringFlag{ 21 | Name: "log-file-name", 22 | Usage: "Log file name, full path", 23 | Value: lib.DefaultConfig.LogFileName, 24 | }, 25 | &cli.IntFlag{ 26 | Name: "log-file-max-megabytes", 27 | Usage: "Log file max size, in megabytes", 28 | Value: int(lib.DefaultConfig.LogFileMaxMegabytes), 29 | }, 30 | &cli.IntFlag{ 31 | Name: "log-max-files", 32 | Usage: "Maximum log file quantity before rollover", 33 | Value: int(lib.DefaultConfig.LogMaxFiles), 34 | }, 35 | &cli.BoolFlag{ 36 | Name: "log-to-journal", 37 | Usage: "Log to the systemd journal (if available) instead of to log-file-name", 38 | }, 39 | &cli.StringFlag{ 40 | Name: "monitor-socket-filename", 41 | Usage: "Filename of unix socket for connector-check to talk to connector", 42 | Value: lib.DefaultConfig.MonitorSocketFilename, 43 | }, 44 | &cli.IntFlag{ 45 | Name: "cups-max-connections", 46 | Usage: "Max connections to CUPS server", 47 | Value: int(lib.DefaultConfig.CUPSMaxConnections), 48 | }, 49 | &cli.StringFlag{ 50 | Name: "cups-connect-timeout", 51 | Usage: "CUPS timeout for opening a new connection", 52 | Value: lib.DefaultConfig.CUPSConnectTimeout, 53 | }, 54 | &cli.BoolFlag{ 55 | Name: "cups-job-full-username", 56 | Usage: "Whether to use the full username (joe@example.com) in CUPS jobs", 57 | }, 58 | &cli.BoolFlag{ 59 | Name: "cups-ignore-raw-printers", 60 | Usage: "Whether to ignore CUPS raw printers", 61 | DefaultText: "1", 62 | }, 63 | &cli.BoolFlag{ 64 | Name: "cups-ignore-class-printers", 65 | Usage: "Whether to ignore CUPS class printers", 66 | DefaultText: "1", 67 | }, 68 | &cli.BoolFlag{ 69 | Name: "copy-printer-info-to-display-name", 70 | Usage: "Whether to copy the CUPS printer's printer-info attribute to the GCP printer's defaultDisplayName", 71 | DefaultText: "1", 72 | }, 73 | } 74 | 75 | var unixCommands = []*cli.Command{ 76 | &cli.Command{ 77 | Name: "init", 78 | Aliases: []string{"i"}, 79 | Usage: "Creates a config file", 80 | Action: initConfigFile, 81 | Flags: append(commonInitFlags, unixInitFlags...), 82 | }, 83 | &cli.Command{ 84 | Name: "monitor", 85 | Aliases: []string{"m"}, 86 | Usage: "Read stats from a running connector", 87 | Action: monitorConnector, 88 | Flags: []cli.Flag{ 89 | &cli.DurationFlag{ 90 | Name: "monitor-timeout", 91 | Usage: "wait for a monitor response no more than this long", 92 | Value: 10 * time.Second, 93 | }, 94 | }, 95 | }, 96 | } 97 | 98 | func main() { 99 | app := cli.NewApp() 100 | app.Name = "gcp-connector-util" 101 | app.Usage = lib.ConnectorName + " for CUPS utility tools" 102 | app.Version = lib.BuildDate 103 | app.Flags = []cli.Flag{ 104 | &lib.ConfigFilenameFlag, 105 | } 106 | app.Commands = append(unixCommands, commonCommands...) 107 | 108 | app.Run(os.Args) 109 | } 110 | 111 | // createCloudConfig creates a config object that supports cloud and (optionally) local mode. 112 | func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { 113 | return &lib.Config{ 114 | LocalPrintingEnable: localEnable, 115 | CloudPrintingEnable: true, 116 | 117 | XMPPJID: xmppJID, 118 | RobotRefreshToken: robotRefreshToken, 119 | UserRefreshToken: userRefreshToken, 120 | ShareScope: shareScope, 121 | ProxyName: proxyName, 122 | FcmServerBindUrl: context.String("fcm-server-bind-url"), 123 | XMPPServer: lib.DefaultConfig.XMPPServer, 124 | XMPPPort: uint16(context.Int("xmpp-port")), 125 | XMPPPingTimeout: context.String("xmpp-ping-timeout"), 126 | XMPPPingInterval: context.String("xmpp-ping-interval"), 127 | GCPBaseURL: context.String("gcp-base-url"), 128 | GCPOAuthClientID: context.String("gcp-oauth-client-id"), 129 | GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), 130 | GCPOAuthAuthURL: context.String("gcp-oauth-auth-url"), 131 | GCPOAuthTokenURL: context.String("gcp-oauth-token-url"), 132 | GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), 133 | 134 | NativeJobQueueSize: uint(context.Int("native-job-queue-size")), 135 | NativePrinterPollInterval: context.String("native-printer-poll-interval"), 136 | PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), 137 | DisplayNamePrefix: context.String("display-name-prefix"), 138 | PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, 139 | PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, 140 | LogLevel: context.String("log-level"), 141 | 142 | LocalPortLow: uint16(context.Int("local-port-low")), 143 | LocalPortHigh: uint16(context.Int("local-port-high")), 144 | 145 | LogFileName: context.String("log-file-name"), 146 | LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), 147 | LogMaxFiles: uint(context.Int("log-max-files")), 148 | LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), 149 | MonitorSocketFilename: context.String("monitor-socket-filename"), 150 | CUPSMaxConnections: uint(context.Int("cups-max-connections")), 151 | CUPSConnectTimeout: context.String("cups-connect-timeout"), 152 | CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, 153 | CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), 154 | CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), 155 | CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), 156 | CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), 157 | } 158 | } 159 | 160 | // createLocalConfig creates a config object that supports local mode. 161 | func createLocalConfig(context *cli.Context) *lib.Config { 162 | return &lib.Config{ 163 | LocalPrintingEnable: true, 164 | CloudPrintingEnable: false, 165 | 166 | NativeJobQueueSize: uint(context.Int("native-job-queue-size")), 167 | NativePrinterPollInterval: context.String("native-printer-poll-interval"), 168 | PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), 169 | DisplayNamePrefix: context.String("display-name-prefix"), 170 | PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, 171 | PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, 172 | LogLevel: context.String("log-level"), 173 | 174 | LocalPortLow: uint16(context.Int("local-port-low")), 175 | LocalPortHigh: uint16(context.Int("local-port-high")), 176 | 177 | LogFileName: context.String("log-file-name"), 178 | LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), 179 | LogMaxFiles: uint(context.Int("log-max-files")), 180 | LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), 181 | MonitorSocketFilename: context.String("monitor-socket-filename"), 182 | CUPSMaxConnections: uint(context.Int("cups-max-connections")), 183 | CUPSConnectTimeout: context.String("cups-connect-timeout"), 184 | CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, 185 | CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), 186 | CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), 187 | CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), 188 | CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /gcp-connector-util/main_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/google/cloud-print-connector/lib" 17 | "github.com/urfave/cli" 18 | "golang.org/x/sys/windows/svc" 19 | "golang.org/x/sys/windows/svc/eventlog" 20 | "golang.org/x/sys/windows/svc/mgr" 21 | ) 22 | 23 | var windowsCommands = []*cli.Command{ 24 | &cli.Command{ 25 | Name: "init", 26 | Aliases: []string{"i"}, 27 | Usage: "Create a config file", 28 | Action: initConfigFile, 29 | Flags: commonInitFlags, 30 | }, 31 | &cli.Command{ 32 | Name: "install-event-log", 33 | Usage: "Install registry entries for the event log", 34 | Action: installEventLog, 35 | }, 36 | &cli.Command{ 37 | Name: "remove-event-log", 38 | Usage: "Remove registry entries for the event log", 39 | Action: removeEventLog, 40 | }, 41 | &cli.Command{ 42 | Name: "create-service", 43 | Usage: "Create a service in the local service control manager", 44 | Action: createService, 45 | }, 46 | &cli.Command{ 47 | Name: "delete-service", 48 | Usage: "Delete an existing service in the local service control manager", 49 | Action: deleteService, 50 | }, 51 | &cli.Command{ 52 | Name: "start-service", 53 | Usage: "Start the service in the local service control manager", 54 | Action: startService, 55 | }, 56 | &cli.Command{ 57 | Name: "stop-service", 58 | Usage: "Stop the service in the local service control manager", 59 | Action: stopService, 60 | }, 61 | } 62 | 63 | func installEventLog(c *cli.Context) error { 64 | err := eventlog.InstallAsEventCreate(lib.ConnectorName, eventlog.Error|eventlog.Warning|eventlog.Info) 65 | if err != nil { 66 | return fmt.Errorf("Failed to install event log registry entries: %s", err) 67 | } 68 | fmt.Println("Event log registry entries installed successfully") 69 | return nil 70 | } 71 | 72 | func removeEventLog(c *cli.Context) error { 73 | err := eventlog.Remove(lib.ConnectorName) 74 | if err != nil { 75 | return fmt.Errorf("Failed to remove event log registry entries: %s\n", err) 76 | } 77 | fmt.Println("Event log registry entries removed successfully") 78 | return nil 79 | } 80 | 81 | func createService(c *cli.Context) error { 82 | exePath, err := filepath.Abs("gcp-windows-connector.exe") 83 | if err != nil { 84 | return fmt.Errorf("Failed to find the connector executable: %s\n", err) 85 | } 86 | 87 | m, err := mgr.Connect() 88 | if err != nil { 89 | return fmt.Errorf("Failed to connect to service control manager: %s\n", err) 90 | } 91 | defer m.Disconnect() 92 | 93 | config := mgr.Config{ 94 | DisplayName: lib.ConnectorName, 95 | Description: "Shares printers with Google Cloud Print", 96 | Dependencies: []string{"Spooler"}, 97 | StartType: mgr.StartAutomatic, 98 | } 99 | service, err := m.CreateService(lib.ConnectorName, exePath, config) 100 | if err != nil { 101 | return fmt.Errorf("Failed to create service: %s\n", err) 102 | } 103 | defer service.Close() 104 | 105 | fmt.Println("Service created successfully") 106 | return nil 107 | } 108 | 109 | func deleteService(c *cli.Context) error { 110 | m, err := mgr.Connect() 111 | if err != nil { 112 | return fmt.Errorf("Failed to connect to service control manager: %s\n", err) 113 | } 114 | defer m.Disconnect() 115 | 116 | service, err := m.OpenService(lib.ConnectorName) 117 | if err != nil { 118 | return fmt.Errorf("Failed to open service: %s\n", err) 119 | } 120 | defer service.Close() 121 | 122 | err = service.Delete() 123 | if err != nil { 124 | return fmt.Errorf("Failed to delete service: %s\n", err) 125 | } 126 | 127 | fmt.Println("Service deleted successfully") 128 | return nil 129 | } 130 | 131 | func startService(c *cli.Context) error { 132 | m, err := mgr.Connect() 133 | if err != nil { 134 | return fmt.Errorf("Failed to connect to service control manager: %s\n", err) 135 | } 136 | defer m.Disconnect() 137 | 138 | service, err := m.OpenService(lib.ConnectorName) 139 | if err != nil { 140 | return fmt.Errorf("Failed to open service: %s\n", err) 141 | } 142 | defer service.Close() 143 | 144 | err = service.Start() 145 | if err != nil { 146 | return fmt.Errorf("Failed to start service: %s\n", err) 147 | } 148 | 149 | fmt.Println("Service started successfully") 150 | return nil 151 | } 152 | 153 | func stopService(c *cli.Context) error { 154 | m, err := mgr.Connect() 155 | if err != nil { 156 | return fmt.Errorf("Failed to connect to service control manager: %s\n", err) 157 | } 158 | defer m.Disconnect() 159 | 160 | service, err := m.OpenService(lib.ConnectorName) 161 | if err != nil { 162 | return fmt.Errorf("Failed to open service: %s\n", err) 163 | } 164 | defer service.Close() 165 | 166 | _, err = service.Control(svc.Stop) 167 | if err != nil { 168 | return fmt.Errorf("Failed to stop service: %s\n", err) 169 | } 170 | 171 | fmt.Printf("Service stopped successfully") 172 | return nil 173 | } 174 | 175 | func main() { 176 | app := cli.NewApp() 177 | app.Name = "gcp-connector-util" 178 | app.Usage = lib.ConnectorName + " for Windows utility tools" 179 | app.Version = lib.BuildDate 180 | app.Flags = []cli.Flag{ 181 | &lib.ConfigFilenameFlag, 182 | } 183 | app.Commands = append(windowsCommands, commonCommands...) 184 | 185 | app.Run(os.Args) 186 | } 187 | 188 | // createCloudConfig creates a config object that supports cloud and (optionally) local mode. 189 | func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { 190 | return &lib.Config{ 191 | LocalPrintingEnable: localEnable, 192 | CloudPrintingEnable: true, 193 | 194 | XMPPJID: xmppJID, 195 | RobotRefreshToken: robotRefreshToken, 196 | UserRefreshToken: userRefreshToken, 197 | ShareScope: shareScope, 198 | ProxyName: proxyName, 199 | FcmServerBindUrl: context.String("fcm-server-bind-url"), 200 | XMPPServer: lib.DefaultConfig.XMPPServer, 201 | XMPPPort: uint16(context.Int("xmpp-port")), 202 | XMPPPingTimeout: context.String("xmpp-ping-timeout"), 203 | XMPPPingInterval: context.String("xmpp-ping-interval"), 204 | GCPBaseURL: lib.DefaultConfig.GCPBaseURL, 205 | GCPOAuthClientID: context.String("gcp-oauth-client-id"), 206 | GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), 207 | GCPOAuthAuthURL: lib.DefaultConfig.GCPOAuthAuthURL, 208 | GCPOAuthTokenURL: lib.DefaultConfig.GCPOAuthTokenURL, 209 | GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), 210 | 211 | NativeJobQueueSize: uint(context.Int("native-job-queue-size")), 212 | NativePrinterPollInterval: context.String("native-printer-poll-interval"), 213 | CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), 214 | PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), 215 | DisplayNamePrefix: context.String("display-name-prefix"), 216 | PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, 217 | PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, 218 | LogLevel: context.String("log-level"), 219 | 220 | LocalPortLow: uint16(context.Int("local-port-low")), 221 | LocalPortHigh: uint16(context.Int("local-port-high")), 222 | } 223 | } 224 | 225 | // createLocalConfig creates a config object that supports local mode. 226 | func createLocalConfig(context *cli.Context) *lib.Config { 227 | return &lib.Config{ 228 | LocalPrintingEnable: true, 229 | CloudPrintingEnable: false, 230 | 231 | NativeJobQueueSize: uint(context.Int("native-job-queue-size")), 232 | NativePrinterPollInterval: context.String("native-printer-poll-interval"), 233 | CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), 234 | PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), 235 | DisplayNamePrefix: context.String("display-name-prefix"), 236 | PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, 237 | PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, 238 | LogLevel: context.String("log-level"), 239 | 240 | LocalPortLow: uint16(context.Int("local-port-low")), 241 | LocalPortHigh: uint16(context.Int("local-port-high")), 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /gcp-connector-util/monitor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "io/ioutil" 14 | "net" 15 | "os" 16 | "time" 17 | 18 | "github.com/google/cloud-print-connector/lib" 19 | "github.com/urfave/cli" 20 | ) 21 | 22 | func monitorConnector(context *cli.Context) error { 23 | config, filename, err := lib.GetConfig(context) 24 | if err != nil { 25 | return fmt.Errorf("Failed to read config file: %s", err) 26 | } 27 | if filename == "" { 28 | fmt.Println("No config file was found, so using defaults") 29 | } 30 | 31 | if _, err := os.Stat(config.MonitorSocketFilename); err != nil { 32 | if !os.IsNotExist(err) { 33 | return err 34 | } 35 | return fmt.Errorf( 36 | "No connector is running, or the monitoring socket %s is mis-configured", 37 | config.MonitorSocketFilename) 38 | } 39 | 40 | timer := time.AfterFunc(context.Duration("monitor-timeout"), func() { 41 | fmt.Fprintf(os.Stderr, "Monitor check timed out after %s", context.Duration("monitor-timeout").String()) 42 | os.Exit(1) 43 | }) 44 | 45 | conn, err := net.DialTimeout("unix", config.MonitorSocketFilename, time.Second) 46 | if err != nil { 47 | return fmt.Errorf( 48 | "No connector is running, or it is not listening to socket %s", 49 | config.MonitorSocketFilename) 50 | } 51 | defer conn.Close() 52 | 53 | buf, err := ioutil.ReadAll(conn) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | timer.Stop() 59 | 60 | fmt.Printf(string(buf)) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /gcp-cups-connector/gcp-cups-connector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package main 10 | 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "os" 17 | "os/signal" 18 | "syscall" 19 | "time" 20 | 21 | "github.com/coreos/go-systemd/journal" 22 | "github.com/google/cloud-print-connector/cups" 23 | "github.com/google/cloud-print-connector/fcm" 24 | "github.com/google/cloud-print-connector/gcp" 25 | "github.com/google/cloud-print-connector/lib" 26 | "github.com/google/cloud-print-connector/log" 27 | "github.com/google/cloud-print-connector/manager" 28 | "github.com/google/cloud-print-connector/monitor" 29 | "github.com/google/cloud-print-connector/notification" 30 | "github.com/google/cloud-print-connector/privet" 31 | "github.com/google/cloud-print-connector/xmpp" 32 | "github.com/urfave/cli" 33 | ) 34 | 35 | func main() { 36 | app := cli.NewApp() 37 | app.Name = "gcp-cups-connector" 38 | app.Usage = lib.ConnectorName + " for CUPS" 39 | app.Version = lib.BuildDate 40 | app.Flags = []cli.Flag{ 41 | &lib.ConfigFilenameFlag, 42 | &cli.BoolFlag{ 43 | Name: "log-to-console", 44 | Usage: "Log to STDERR, in addition to configured logging", 45 | }, 46 | } 47 | app.Action = connector 48 | app.Run(os.Args) 49 | } 50 | 51 | func connector(context *cli.Context) error { 52 | config, configFilename, err := lib.GetConfig(context) 53 | if err != nil { 54 | return cli.NewExitError(fmt.Sprintf("Failed to read config file: %s", err), 1) 55 | } 56 | 57 | logToJournal := *config.LogToJournal && journal.Enabled() 58 | logToConsole := context.Bool("log-to-console") 59 | useFcm := config.FcmNotificationsEnable 60 | 61 | if logToJournal { 62 | log.SetJournalEnabled(true) 63 | if logToConsole { 64 | log.SetWriter(os.Stderr) 65 | } else { 66 | log.SetWriter(ioutil.Discard) 67 | } 68 | } else { 69 | logFileMaxBytes := config.LogFileMaxMegabytes * 1024 * 1024 70 | var logWriter io.Writer 71 | logWriter, err = log.NewLogRoller(config.LogFileName, logFileMaxBytes, config.LogMaxFiles) 72 | if err != nil { 73 | return cli.NewExitError(fmt.Sprintf("Failed to start log roller: %s", err), 1) 74 | } 75 | 76 | if logToConsole { 77 | logWriter = io.MultiWriter(logWriter, os.Stderr) 78 | } 79 | log.SetWriter(logWriter) 80 | } 81 | 82 | logLevel, ok := log.LevelFromString(config.LogLevel) 83 | if !ok { 84 | return cli.NewExitError(fmt.Sprintf("Log level %s is not recognized", config.LogLevel), 1) 85 | } 86 | log.SetLevel(logLevel) 87 | 88 | if configFilename == "" { 89 | log.Info("No config file was found, so using defaults") 90 | } else { 91 | log.Infof("Using config file %s", configFilename) 92 | } 93 | completeConfig, _ := json.MarshalIndent(config, "", " ") 94 | log.Debugf("Config: %s", string(completeConfig)) 95 | 96 | log.Info(lib.FullName) 97 | fmt.Println(lib.FullName) 98 | 99 | if !config.CloudPrintingEnable && !config.LocalPrintingEnable { 100 | errStr := "Cannot run connector with both local_printing_enable and cloud_printing_enable set to false" 101 | log.Fatal(errStr) 102 | return cli.NewExitError(errStr, 1) 103 | } 104 | 105 | if _, err := os.Stat(config.MonitorSocketFilename); !os.IsNotExist(err) { 106 | var errStr string 107 | if err != nil { 108 | errStr = fmt.Sprintf("Failed to stat monitor socket: %s", err) 109 | } else { 110 | errStr = fmt.Sprintf( 111 | "A connector is already running, or the monitoring socket %s wasn't cleaned up properly", 112 | config.MonitorSocketFilename) 113 | } 114 | log.Fatal(errStr) 115 | return cli.NewExitError(errStr, 1) 116 | } 117 | 118 | jobs := make(chan *lib.Job, 10) 119 | notifications := make(chan notification.PrinterNotification, 5) 120 | 121 | var g *gcp.GoogleCloudPrint 122 | var x *xmpp.XMPP 123 | var f *fcm.FCM 124 | if config.CloudPrintingEnable { 125 | xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) 126 | if err != nil { 127 | errStr := fmt.Sprintf("Failed to parse xmpp ping timeout: %s", err) 128 | log.Fatal(errStr) 129 | return cli.NewExitError(errStr, 1) 130 | } 131 | xmppPingInterval, err := time.ParseDuration(config.XMPPPingInterval) 132 | if err != nil { 133 | errStr := fmt.Sprintf("Failed to parse xmpp ping interval default: %s", err) 134 | log.Fatalf(errStr) 135 | return cli.NewExitError(errStr, 1) 136 | } 137 | 138 | g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, 139 | config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, 140 | config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, 141 | config.GCPMaxConcurrentDownloads, jobs, useFcm) 142 | if err != nil { 143 | log.Fatal(err) 144 | return cli.NewExitError(err.Error(), 1) 145 | } 146 | if useFcm { 147 | f, err = fcm.NewFCM(config.GCPOAuthClientID, config.ProxyName, config.FcmServerBindUrl, g.FcmSubscribe, notifications) 148 | if err != nil { 149 | log.Fatal(err) 150 | return cli.NewExitError(err.Error(), 1) 151 | } 152 | defer f.Quit() 153 | } else { 154 | x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, 155 | xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, notifications) 156 | if err != nil { 157 | log.Fatal(err) 158 | return cli.NewExitError(err.Error(), 1) 159 | } 160 | defer x.Quit() 161 | } 162 | } 163 | 164 | cupsConnectTimeout, err := time.ParseDuration(config.CUPSConnectTimeout) 165 | if err != nil { 166 | errStr := fmt.Sprintf("Failed to parse CUPS connect timeout: %s", err) 167 | log.Fatalf(errStr) 168 | return cli.NewExitError(errStr, 1) 169 | } 170 | c, err := cups.NewCUPS(*config.CUPSCopyPrinterInfoToDisplayName, *config.PrefixJobIDToJobTitle, 171 | config.DisplayNamePrefix, config.CUPSPrinterAttributes, config.CUPSVendorPPDOptions, config.CUPSMaxConnections, 172 | cupsConnectTimeout, config.PrinterBlacklist, config.PrinterWhitelist, *config.CUPSIgnoreRawPrinters, 173 | *config.CUPSIgnoreClassPrinters, useFcm) 174 | if err != nil { 175 | log.Fatal(err) 176 | return cli.NewExitError(err.Error(), 1) 177 | } 178 | defer c.Quit() 179 | 180 | var priv *privet.Privet 181 | if config.LocalPrintingEnable { 182 | if g == nil { 183 | priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, nil) 184 | } else { 185 | priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, g.ProximityToken) 186 | } 187 | if err != nil { 188 | log.Fatal(err) 189 | return cli.NewExitError(err.Error(), 1) 190 | } 191 | defer priv.Quit() 192 | } 193 | 194 | nativePrinterPollInterval, err := time.ParseDuration(config.NativePrinterPollInterval) 195 | if err != nil { 196 | errStr := fmt.Sprintf("Failed to parse CUPS printer poll interval: %s", err) 197 | log.Fatal(errStr) 198 | return cli.NewExitError(errStr, 1) 199 | } 200 | pm, err := manager.NewPrinterManager(c, g, priv, nativePrinterPollInterval, 201 | config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, 202 | jobs, notifications, useFcm) 203 | if err != nil { 204 | log.Fatal(err) 205 | return cli.NewExitError(err.Error(), 1) 206 | } 207 | defer pm.Quit() 208 | 209 | // Init FCM client after printers are registered 210 | if useFcm && config.CloudPrintingEnable { 211 | f.Init() 212 | } 213 | m, err := monitor.NewMonitor(c, g, priv, pm, config.MonitorSocketFilename) 214 | if err != nil { 215 | log.Fatal(err) 216 | return cli.NewExitError(err.Error(), 1) 217 | } 218 | defer m.Quit() 219 | 220 | if config.CloudPrintingEnable { 221 | if config.LocalPrintingEnable { 222 | log.Infof("Ready to rock as proxy '%s' and in local mode", config.ProxyName) 223 | fmt.Printf("Ready to rock as proxy '%s' and in local mode\n", config.ProxyName) 224 | } else { 225 | log.Infof("Ready to rock as proxy '%s'", config.ProxyName) 226 | fmt.Printf("Ready to rock as proxy '%s'\n", config.ProxyName) 227 | } 228 | } else { 229 | log.Info("Ready to rock in local-only mode") 230 | fmt.Println("Ready to rock in local-only mode") 231 | } 232 | 233 | waitIndefinitely() 234 | 235 | log.Info("Shutting down") 236 | fmt.Println("") 237 | fmt.Println("Shutting down") 238 | 239 | return nil 240 | } 241 | 242 | // Blocks until Ctrl-C or SIGTERM. 243 | func waitIndefinitely() { 244 | ch := make(chan os.Signal) 245 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 246 | <-ch 247 | 248 | go func() { 249 | // In case the process doesn't die quickly, wait for a second termination request. 250 | <-ch 251 | fmt.Println("Second termination request received") 252 | os.Exit(1) 253 | }() 254 | } 255 | -------------------------------------------------------------------------------- /gcp-windows-connector/gcp-windows-connector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package main 10 | 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | "os" 15 | "time" 16 | 17 | "github.com/google/cloud-print-connector/fcm" 18 | "github.com/google/cloud-print-connector/gcp" 19 | "github.com/google/cloud-print-connector/lib" 20 | "github.com/google/cloud-print-connector/log" 21 | "github.com/google/cloud-print-connector/manager" 22 | "github.com/google/cloud-print-connector/notification" 23 | "github.com/google/cloud-print-connector/winspool" 24 | "github.com/google/cloud-print-connector/xmpp" 25 | "github.com/urfave/cli" 26 | "golang.org/x/sys/windows/svc" 27 | "golang.org/x/sys/windows/svc/debug" 28 | ) 29 | 30 | func main() { 31 | app := cli.NewApp() 32 | app.Name = "gcp-windows-connector" 33 | app.Usage = lib.ConnectorName + " for Windows" 34 | app.Version = lib.BuildDate 35 | app.Flags = []cli.Flag{ 36 | &lib.ConfigFilenameFlag, 37 | } 38 | app.Action = runService 39 | app.Run(os.Args) 40 | } 41 | 42 | var ( 43 | runningStatus = svc.Status{ 44 | State: svc.Running, 45 | Accepts: svc.AcceptStop, 46 | } 47 | stoppingStatus = svc.Status{ 48 | State: svc.StopPending, 49 | Accepts: svc.AcceptStop, 50 | } 51 | ) 52 | 53 | type service struct { 54 | context *cli.Context 55 | interactive bool 56 | } 57 | 58 | func runService(context *cli.Context) error { 59 | interactive, err := svc.IsAnInteractiveSession() 60 | if err != nil { 61 | return cli.NewExitError(fmt.Sprintf("Failed to detect interactive session: %s", err), 1) 62 | } 63 | 64 | s := service{context, interactive} 65 | 66 | if interactive { 67 | err = debug.Run(lib.ConnectorName, &s) 68 | } else { 69 | err = svc.Run(lib.ConnectorName, &s) 70 | } 71 | if err != nil { 72 | err = cli.NewExitError(err.Error(), 1) 73 | } 74 | return err 75 | } 76 | 77 | func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { 78 | if service.interactive { 79 | if err := log.Start(true); err != nil { 80 | fmt.Fprintf(os.Stderr, "Failed to start event log: %s\n", err) 81 | return false, 1 82 | } 83 | } else { 84 | if err := log.Start(false); err != nil { 85 | fmt.Fprintf(os.Stderr, "Failed to start event log: %s\n", err) 86 | return false, 1 87 | } 88 | } 89 | defer log.Stop() 90 | 91 | config, configFilename, err := lib.GetConfig(service.context) 92 | if err != nil { 93 | fmt.Fprintf(os.Stderr, "Failed to read config file: %s\n", err) 94 | return false, 1 95 | } 96 | 97 | logLevel, ok := log.LevelFromString(config.LogLevel) 98 | if !ok { 99 | fmt.Fprintf(os.Stderr, "Log level %s is not recognized\n", config.LogLevel) 100 | return false, 1 101 | } 102 | log.SetLevel(logLevel) 103 | 104 | if configFilename == "" { 105 | log.Info("No config file was found, so using defaults") 106 | } else { 107 | log.Infof("Using config file %s", configFilename) 108 | } 109 | completeConfig, _ := json.MarshalIndent(config, "", " ") 110 | log.Debugf("Config: %s", string(completeConfig)) 111 | 112 | log.Info(lib.FullName) 113 | 114 | if !config.CloudPrintingEnable && !config.LocalPrintingEnable { 115 | log.Fatal("Cannot run connector with both local_printing_enable and cloud_printing_enable set to false") 116 | return false, 1 117 | } else if config.LocalPrintingEnable { 118 | log.Fatal("Local printing has not been implemented in this version of the Windows connector.") 119 | return false, 1 120 | } 121 | 122 | jobs := make(chan *lib.Job, 10) 123 | notifications := make(chan notification.PrinterNotification, 5) 124 | 125 | var g *gcp.GoogleCloudPrint 126 | var x *xmpp.XMPP 127 | var f *fcm.FCM 128 | if config.CloudPrintingEnable { 129 | xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) 130 | if err != nil { 131 | log.Fatalf("Failed to parse xmpp ping timeout: %s", err) 132 | return false, 1 133 | } 134 | xmppPingInterval, err := time.ParseDuration(config.XMPPPingInterval) 135 | if err != nil { 136 | log.Fatalf("Failed to parse xmpp ping interval default: %s", err) 137 | return false, 1 138 | } 139 | 140 | g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, 141 | config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, 142 | config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, 143 | config.GCPMaxConcurrentDownloads, jobs, config.FcmNotificationsEnable) 144 | if err != nil { 145 | log.Fatal(err) 146 | return false, 1 147 | } 148 | if config.FcmNotificationsEnable { 149 | f, err = fcm.NewFCM(config.GCPOAuthClientID, config.ProxyName, config.FcmServerBindUrl, g.FcmSubscribe, notifications) 150 | if err != nil { 151 | log.Fatal(err) 152 | return false, 1 153 | } 154 | defer f.Quit() 155 | } else { 156 | x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, 157 | xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, notifications) 158 | if err != nil { 159 | log.Fatal(err) 160 | return false, 1 161 | } 162 | defer x.Quit() 163 | } 164 | } 165 | 166 | ws, err := winspool.NewWinSpool(*config.PrefixJobIDToJobTitle, config.DisplayNamePrefix, config.PrinterBlacklist, config.PrinterWhitelist, config.FcmNotificationsEnable) 167 | if err != nil { 168 | log.Fatal(err) 169 | return false, 1 170 | } 171 | 172 | nativePrinterPollInterval, err := time.ParseDuration(config.NativePrinterPollInterval) 173 | if err != nil { 174 | log.Fatalf("Failed to parse printer poll interval: %s", err) 175 | return false, 1 176 | } 177 | pm, err := manager.NewPrinterManager(ws, g, nil, nativePrinterPollInterval, 178 | config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, jobs, notifications, 179 | config.FcmNotificationsEnable) 180 | if err != nil { 181 | log.Fatal(err) 182 | return false, 1 183 | } 184 | defer pm.Quit() 185 | 186 | // Init FCM client after printers are registered 187 | if config.FcmNotificationsEnable && config.CloudPrintingEnable { 188 | f.Init() 189 | } 190 | statusHandle := svc.StatusHandle() 191 | if statusHandle != 0 { 192 | err = ws.StartPrinterNotifications(statusHandle) 193 | if err != nil { 194 | log.Error(err) 195 | } else { 196 | log.Info("Successfully registered for device notifications.") 197 | } 198 | } 199 | 200 | if config.CloudPrintingEnable { 201 | if config.LocalPrintingEnable { 202 | log.Infof("Ready to rock as proxy '%s' and in local mode", config.ProxyName) 203 | } else { 204 | log.Infof("Ready to rock as proxy '%s'", config.ProxyName) 205 | } 206 | } else { 207 | log.Info("Ready to rock in local-only mode") 208 | } 209 | 210 | s <- runningStatus 211 | for { 212 | request := <-r 213 | switch request.Cmd { 214 | case svc.Interrogate: 215 | s <- runningStatus 216 | 217 | case svc.Stop: 218 | s <- stoppingStatus 219 | log.Info("Shutting down") 220 | time.AfterFunc(time.Second*30, func() { 221 | log.Fatal("Failed to stop quickly; stopping forcefully") 222 | os.Exit(1) 223 | }) 224 | 225 | return false, 0 226 | 227 | case svc.DeviceEvent: 228 | log.Infof("Printers change notification received %d.", request.EventType) 229 | // Delay the action to let the OS finish the process or we might 230 | // not see the new printer. Even if we miss it eventually the timed updates 231 | // will pick it up. 232 | time.AfterFunc(time.Second*5, func() { 233 | pm.SyncPrinters(false) 234 | }) 235 | 236 | default: 237 | log.Errorf("Received unsupported service command from service control manager: %d", request.Cmd) 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /gcp/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package gcp 10 | 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | "io/ioutil" 15 | "net/http" 16 | "net/url" 17 | "strings" 18 | "time" 19 | 20 | "github.com/google/cloud-print-connector/lib" 21 | "github.com/google/cloud-print-connector/log" 22 | 23 | "golang.org/x/oauth2" 24 | ) 25 | 26 | /* 27 | glibc < 2.20 and OSX 10.10 have problems when C.getaddrinfo is called many 28 | times concurrently. When the connector shares more than about 230 printers, and 29 | GCP is called once per printer in concurrent goroutines, http.Client.Do starts 30 | to fail with a lookup error. 31 | 32 | This solution, a semaphore, limits the quantity of concurrent HTTP requests, 33 | which also limits the quantity of concurrent calls to net.LookupHost (which 34 | calls C.getaddrinfo()). 35 | 36 | I would rather wait for the Go compiler to solve this problem than make this a 37 | configurable option, hence this long-winded comment. 38 | 39 | https://github.com/golang/go/issues/3575 40 | https://github.com/golang/go/issues/6336 41 | */ 42 | var lock *lib.Semaphore = lib.NewSemaphore(100) 43 | 44 | // newClient creates an instance of http.Client, wrapped with OAuth credentials. 45 | func newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, refreshToken string, scopes ...string) (*http.Client, error) { 46 | config := oauth2.Config{ 47 | ClientID: oauthClientID, 48 | ClientSecret: oauthClientSecret, 49 | Endpoint: oauth2.Endpoint{ 50 | AuthURL: oauthAuthURL, 51 | TokenURL: oauthTokenURL, 52 | }, 53 | RedirectURL: RedirectURL, 54 | Scopes: scopes, 55 | } 56 | 57 | token := oauth2.Token{RefreshToken: refreshToken} 58 | client := config.Client(oauth2.NoContext, &token) 59 | 60 | return client, nil 61 | } 62 | 63 | // getWithRetry calls get() and retries on HTTP temp failure 64 | // (response code 500-599). 65 | func getWithRetry(hc *http.Client, url string) (*http.Response, error) { 66 | backoff := lib.Backoff{} 67 | for { 68 | response, err := get(hc, url) 69 | if response != nil && response.StatusCode == http.StatusOK { 70 | return response, err 71 | } else if response != nil && response.StatusCode >= 500 && response.StatusCode <= 599 { 72 | p, retryAgain := backoff.Pause() 73 | if !retryAgain { 74 | log.Debugf("HTTP error %s, retry timeout hit", err) 75 | return response, err 76 | } 77 | log.Debugf("HTTP error %s, retrying after %s", err, p) 78 | time.Sleep(p) 79 | } else { 80 | log.Debugf("Permanent HTTP error %s, will not retry", err) 81 | return response, err 82 | } 83 | } 84 | } 85 | 86 | // get GETs a URL. Returns the response object (not body), in case the body 87 | // is very large. 88 | // 89 | // The caller must close the returned Response.Body object if err == nil. 90 | func get(hc *http.Client, url string) (*http.Response, error) { 91 | request, err := http.NewRequest("GET", url, nil) 92 | if err != nil { 93 | return nil, err 94 | } 95 | request.Header.Set("X-CloudPrint-Proxy", lib.ShortName) 96 | 97 | lock.Acquire() 98 | response, err := hc.Do(request) 99 | lock.Release() 100 | if err != nil { 101 | return response, fmt.Errorf("GET failure: %s", err) 102 | } 103 | if response.StatusCode != http.StatusOK { 104 | return response, fmt.Errorf("GET HTTP-level failure: %s %s", url, response.Status) 105 | } 106 | 107 | return response, nil 108 | } 109 | 110 | // postWithRetry calls post() and retries on HTTP temp failure 111 | // (response code 500-599). 112 | func postWithRetry(hc *http.Client, url string, form url.Values) ([]byte, uint, int, error) { 113 | backoff := lib.Backoff{} 114 | for { 115 | responseBody, gcpErrorCode, httpStatusCode, err := post(hc, url, form) 116 | if responseBody != nil && httpStatusCode == http.StatusOK { 117 | return responseBody, gcpErrorCode, httpStatusCode, err 118 | } else if responseBody != nil && httpStatusCode >= 500 && httpStatusCode <= 599 { 119 | p, retryAgain := backoff.Pause() 120 | if !retryAgain { 121 | log.Debugf("HTTP error %s, retry timeout hit", err) 122 | return responseBody, gcpErrorCode, httpStatusCode, err 123 | } 124 | log.Debugf("HTTP error %s, retrying after %s", err, p) 125 | time.Sleep(p) 126 | } else { 127 | log.Debugf("Permanent HTTP error %s, will not retry", err) 128 | return responseBody, gcpErrorCode, httpStatusCode, err 129 | } 130 | } 131 | } 132 | 133 | // post POSTs to a URL. Returns the body of the response. 134 | // 135 | // Returns the response body, GCP error code, HTTP status, and error. 136 | // None of the returned fields is guaranteed to be non-zero. 137 | func post(hc *http.Client, url string, form url.Values) ([]byte, uint, int, error) { 138 | requestBody := strings.NewReader(form.Encode()) 139 | request, err := http.NewRequest("POST", url, requestBody) 140 | if err != nil { 141 | return nil, 0, 0, err 142 | } 143 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 144 | request.Header.Set("X-CloudPrint-Proxy", lib.ShortName) 145 | 146 | lock.Acquire() 147 | response, err := hc.Do(request) 148 | lock.Release() 149 | if err != nil { 150 | return nil, 0, 0, fmt.Errorf("POST failure: %s", err) 151 | } 152 | 153 | defer response.Body.Close() 154 | responseBody, err := ioutil.ReadAll(response.Body) 155 | if err != nil { 156 | return nil, 0, response.StatusCode, err 157 | } 158 | 159 | if response.StatusCode != http.StatusOK { 160 | return responseBody, 0, response.StatusCode, fmt.Errorf("/%s POST HTTP-level failure: %s", url, response.Status) 161 | } 162 | 163 | var responseStatus struct { 164 | Success bool 165 | Message string 166 | ErrorCode uint 167 | } 168 | if err = json.Unmarshal(responseBody, &responseStatus); err != nil { 169 | return responseBody, 0, response.StatusCode, err 170 | } 171 | if !responseStatus.Success { 172 | return responseBody, responseStatus.ErrorCode, response.StatusCode, fmt.Errorf( 173 | "%s call failed: %s", url, responseStatus.Message) 174 | } 175 | 176 | return responseBody, responseStatus.ErrorCode, response.StatusCode, nil 177 | } 178 | -------------------------------------------------------------------------------- /gcp/job.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package gcp 10 | 11 | import "github.com/google/cloud-print-connector/cdd" 12 | 13 | type Job struct { 14 | GCPPrinterID string 15 | GCPJobID string 16 | FileURL string 17 | OwnerID string 18 | Title string 19 | SemanticState *cdd.PrintJobState 20 | } 21 | -------------------------------------------------------------------------------- /lib/backoff.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "math/rand" 13 | "time" 14 | ) 15 | 16 | const ( 17 | initialRetryInterval = 500 * time.Millisecond 18 | maxInterval = 1 * time.Minute 19 | maxElapsedTime = 15 * time.Minute 20 | multiplier = 1.5 21 | randomizationFactor = 0.5 22 | ) 23 | 24 | // Backoff provides a mechanism for determining a good amount of time before 25 | // retrying an operation. 26 | type Backoff struct { 27 | interval time.Duration 28 | elapsedTime time.Duration 29 | } 30 | 31 | // Pause returns the amount of time to wait before retrying an operation and true if 32 | // it is ok to try again or false if the operation should be abandoned. 33 | func (b *Backoff) Pause() (time.Duration, bool) { 34 | if b.interval == 0 { 35 | // first time 36 | b.interval = initialRetryInterval 37 | b.elapsedTime = 0 38 | } 39 | 40 | // interval from [1 - randomizationFactor, 1 + randomizationFactor) 41 | randomizedInterval := time.Duration((rand.Float64()*(2*randomizationFactor) + (1 - randomizationFactor)) * float64(b.interval)) 42 | b.elapsedTime += randomizedInterval 43 | 44 | if b.elapsedTime > maxElapsedTime { 45 | return 0, false 46 | } 47 | 48 | // Increase interval up to the interval cap 49 | b.interval = time.Duration(float64(b.interval) * multiplier) 50 | if b.interval > maxInterval { 51 | b.interval = maxInterval 52 | } 53 | 54 | return randomizedInterval, true 55 | } 56 | -------------------------------------------------------------------------------- /lib/backoff_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestBackoffMultiple(t *testing.T) { 17 | b := &Backoff{} 18 | // with the current parameters, we will be able to wait at least 19 times before hitting the max 19 | for i := 0; i < 19; i++ { 20 | p, ok := b.Pause() 21 | t.Logf("iteration %d pausing for %s", i, p) 22 | if !ok { 23 | t.Fatalf("hit the pause timeout after %d pauses", i) 24 | } 25 | } 26 | } 27 | 28 | func TestBackoffTimeout(t *testing.T) { 29 | var elapsed time.Duration 30 | b := &Backoff{} 31 | // with the current parameters, we will hit the timeout at or before 40 pauses 32 | for i := 0; i < 40; i++ { 33 | p, ok := b.Pause() 34 | elapsed += p 35 | t.Logf("iteration %d pausing for %s (total %s)", i, p, elapsed) 36 | if !ok { 37 | break 38 | } 39 | } 40 | if _, ok := b.Pause(); ok { 41 | t.Fatalf("did not hit the pause timeout") 42 | } 43 | 44 | if elapsed > maxElapsedTime { 45 | t.Fatalf("waited too long: %s > %s", elapsed, maxElapsedTime) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/concprintermap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import "sync" 12 | 13 | // ConcurrentPrinterMap is a map-like data structure that is also 14 | // thread-safe. Printers are keyed by Printer.Name and Printer.GCPID. 15 | type ConcurrentPrinterMap struct { 16 | byNativeName map[string]Printer 17 | byGCPID map[string]Printer 18 | mutex sync.RWMutex 19 | } 20 | 21 | // NewConcurrentPrinterMap initializes an empty ConcurrentPrinterMap. 22 | func NewConcurrentPrinterMap(printers []Printer) *ConcurrentPrinterMap { 23 | cpm := ConcurrentPrinterMap{} 24 | // TODO will this fail on nil? 25 | cpm.Refresh(printers) 26 | return &cpm 27 | } 28 | 29 | // Refresh replaces the internal (non-concurrent) map with newPrinters. 30 | func (cpm *ConcurrentPrinterMap) Refresh(newPrinters []Printer) { 31 | c := make(map[string]Printer, len(newPrinters)) 32 | for _, printer := range newPrinters { 33 | c[printer.Name] = printer 34 | } 35 | 36 | g := make(map[string]Printer, len(newPrinters)) 37 | for _, printer := range newPrinters { 38 | if len(printer.GCPID) > 0 { 39 | g[printer.GCPID] = printer 40 | } 41 | } 42 | 43 | cpm.mutex.Lock() 44 | defer cpm.mutex.Unlock() 45 | 46 | cpm.byNativeName = c 47 | cpm.byGCPID = g 48 | } 49 | 50 | // Get gets a printer, using the native name as key. 51 | // 52 | // The second return value is true if the entry exists. 53 | func (cpm *ConcurrentPrinterMap) GetByNativeName(name string) (Printer, bool) { 54 | cpm.mutex.RLock() 55 | defer cpm.mutex.RUnlock() 56 | 57 | if p, exists := cpm.byNativeName[name]; exists { 58 | return p, true 59 | } 60 | return Printer{}, false 61 | } 62 | 63 | // Get gets a printer, using the GCP ID as key. 64 | // 65 | // The second return value is true if the entry exists. 66 | func (cpm *ConcurrentPrinterMap) GetByGCPID(gcpID string) (Printer, bool) { 67 | cpm.mutex.RLock() 68 | defer cpm.mutex.RUnlock() 69 | 70 | if p, exists := cpm.byGCPID[gcpID]; exists { 71 | return p, true 72 | } 73 | return Printer{}, false 74 | } 75 | 76 | // GetAll returns a slice of all printers. 77 | func (cpm *ConcurrentPrinterMap) GetAll() []Printer { 78 | cpm.mutex.RLock() 79 | defer cpm.mutex.RUnlock() 80 | 81 | printers := make([]Printer, len(cpm.byNativeName)) 82 | i := 0 83 | for _, printer := range cpm.byNativeName { 84 | printers[i] = printer 85 | i++ 86 | } 87 | 88 | return printers 89 | } 90 | -------------------------------------------------------------------------------- /lib/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "encoding/json" 13 | "io/ioutil" 14 | "reflect" 15 | "runtime" 16 | 17 | "github.com/urfave/cli" 18 | ) 19 | 20 | const ( 21 | ConnectorName = "Google Cloud Print Connector" 22 | 23 | // A website with user-friendly information. 24 | ConnectorHomeURL = "https://github.com/google/cloud-print-connector" 25 | 26 | GCPAPIVersion = "2.0" 27 | ) 28 | 29 | var ( 30 | ConfigFilenameFlag = cli.StringFlag{ 31 | Name: "config-filename", 32 | Usage: "Connector config filename", 33 | Value: defaultConfigFilename, 34 | } 35 | 36 | // To be populated by something like: 37 | // go install -ldflags "-X github.com/google/cloud-print-connector/lib.BuildDate=`date +%Y.%m.%d`" 38 | BuildDate = "DEV" 39 | 40 | ShortName = platformName + " Connector " + BuildDate + "-" + runtime.GOOS 41 | 42 | FullName = ConnectorName + " for " + platformName + " version " + BuildDate + "-" + runtime.GOOS 43 | ) 44 | 45 | // PointerToBool converts a boolean value (constant) to a pointer-to-bool. 46 | func PointerToBool(b bool) *bool { 47 | return &b 48 | } 49 | 50 | // GetConfig reads a Config object from the config file indicated by the config 51 | // filename flag. If no such file exists, then DefaultConfig is returned. 52 | func GetConfig(context *cli.Context) (*Config, string, error) { 53 | cf, exists := getConfigFilename(context) 54 | if !exists { 55 | return &DefaultConfig, "", nil 56 | } 57 | 58 | configRaw, err := ioutil.ReadFile(cf) 59 | if err != nil { 60 | return nil, "", err 61 | } 62 | 63 | config := new(Config) 64 | if err = json.Unmarshal(configRaw, config); err != nil { 65 | return nil, "", err 66 | } 67 | 68 | // Same config as a map so that we can detect missing keys. 69 | var configMap map[string]interface{} 70 | if err = json.Unmarshal(configRaw, &configMap); err != nil { 71 | return nil, "", err 72 | } 73 | 74 | b := config.Backfill(configMap) 75 | 76 | return b, cf, nil 77 | } 78 | 79 | // ToFile writes this Config object to the config file indicated by ConfigFile. 80 | func (c *Config) ToFile(context *cli.Context) (string, error) { 81 | b, err := json.MarshalIndent(c, "", " ") 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | cf, _ := getConfigFilename(context) 87 | if err = ioutil.WriteFile(cf, b, 0600); err != nil { 88 | return "", err 89 | } 90 | return cf, nil 91 | } 92 | 93 | func (c *Config) commonSparse(context *cli.Context) *Config { 94 | s := *c 95 | 96 | if s.XMPPServer == DefaultConfig.XMPPServer { 97 | s.XMPPServer = "" 98 | } 99 | if !context.IsSet("xmpp-port") && 100 | s.XMPPPort == DefaultConfig.XMPPPort { 101 | s.XMPPPort = 0 102 | } 103 | if !context.IsSet("xmpp-ping-timeout") && 104 | s.XMPPPingTimeout == DefaultConfig.XMPPPingTimeout { 105 | s.XMPPPingTimeout = "" 106 | } 107 | if !context.IsSet("xmpp-ping-interval") && 108 | s.XMPPPingInterval == DefaultConfig.XMPPPingInterval { 109 | s.XMPPPingInterval = "" 110 | } 111 | if s.GCPBaseURL == DefaultConfig.GCPBaseURL { 112 | s.GCPBaseURL = "" 113 | } 114 | if s.FcmServerBindUrl == DefaultConfig.FcmServerBindUrl { 115 | s.FcmServerBindUrl = "" 116 | } 117 | if s.GCPOAuthClientID == DefaultConfig.GCPOAuthClientID { 118 | s.GCPOAuthClientID = "" 119 | } 120 | if s.GCPOAuthClientSecret == DefaultConfig.GCPOAuthClientSecret { 121 | s.GCPOAuthClientSecret = "" 122 | } 123 | if s.GCPOAuthAuthURL == DefaultConfig.GCPOAuthAuthURL { 124 | s.GCPOAuthAuthURL = "" 125 | } 126 | if s.GCPOAuthTokenURL == DefaultConfig.GCPOAuthTokenURL { 127 | s.GCPOAuthTokenURL = "" 128 | } 129 | if !context.IsSet("gcp-max-concurrent-downloads") && 130 | s.GCPMaxConcurrentDownloads == DefaultConfig.GCPMaxConcurrentDownloads { 131 | s.GCPMaxConcurrentDownloads = 0 132 | } 133 | if !context.IsSet("native-job-queue-size") && 134 | s.NativeJobQueueSize == DefaultConfig.NativeJobQueueSize { 135 | s.NativeJobQueueSize = 0 136 | } 137 | if !context.IsSet("native-printer-poll-interval") && 138 | s.NativePrinterPollInterval == DefaultConfig.NativePrinterPollInterval { 139 | s.NativePrinterPollInterval = "" 140 | } 141 | if !context.IsSet("cups-job-full-username") && 142 | reflect.DeepEqual(s.CUPSJobFullUsername, DefaultConfig.CUPSJobFullUsername) { 143 | s.CUPSJobFullUsername = nil 144 | } 145 | if !context.IsSet("prefix-job-id-to-job-title") && 146 | reflect.DeepEqual(s.PrefixJobIDToJobTitle, DefaultConfig.PrefixJobIDToJobTitle) { 147 | s.PrefixJobIDToJobTitle = nil 148 | } 149 | if !context.IsSet("display-name-prefix") && 150 | s.DisplayNamePrefix == DefaultConfig.DisplayNamePrefix { 151 | s.DisplayNamePrefix = "" 152 | } 153 | if !context.IsSet("local-port-low") && 154 | s.LocalPortLow == DefaultConfig.LocalPortLow { 155 | s.LocalPortLow = 0 156 | } 157 | if !context.IsSet("local-port-high") && 158 | s.LocalPortHigh == DefaultConfig.LocalPortHigh { 159 | s.LocalPortHigh = 0 160 | } 161 | 162 | return &s 163 | } 164 | 165 | func (c *Config) commonBackfill(configMap map[string]interface{}) *Config { 166 | b := *c 167 | 168 | if _, exists := configMap["xmpp_server"]; !exists { 169 | b.XMPPServer = DefaultConfig.XMPPServer 170 | } 171 | if _, exists := configMap["xmpp_port"]; !exists { 172 | b.XMPPPort = DefaultConfig.XMPPPort 173 | } 174 | if _, exists := configMap["gcp_xmpp_ping_timeout"]; !exists { 175 | b.XMPPPingTimeout = DefaultConfig.XMPPPingTimeout 176 | } 177 | if _, exists := configMap["gcp_xmpp_ping_interval_default"]; !exists { 178 | b.XMPPPingInterval = DefaultConfig.XMPPPingInterval 179 | } 180 | if _, exists := configMap["gcp_base_url"]; !exists { 181 | b.GCPBaseURL = DefaultConfig.GCPBaseURL 182 | } 183 | if _, exists := configMap["fcm_server_bind_url"]; !exists { 184 | b.FcmServerBindUrl = DefaultConfig.FcmServerBindUrl 185 | } 186 | if _, exists := configMap["gcp_oauth_client_id"]; !exists { 187 | b.GCPOAuthClientID = DefaultConfig.GCPOAuthClientID 188 | } 189 | if _, exists := configMap["gcp_oauth_client_secret"]; !exists { 190 | b.GCPOAuthClientSecret = DefaultConfig.GCPOAuthClientSecret 191 | } 192 | if _, exists := configMap["gcp_oauth_auth_url"]; !exists { 193 | b.GCPOAuthAuthURL = DefaultConfig.GCPOAuthAuthURL 194 | } 195 | if _, exists := configMap["gcp_oauth_token_url"]; !exists { 196 | b.GCPOAuthTokenURL = DefaultConfig.GCPOAuthTokenURL 197 | } 198 | if _, exists := configMap["gcp_max_concurrent_downloads"]; !exists { 199 | b.GCPMaxConcurrentDownloads = DefaultConfig.GCPMaxConcurrentDownloads 200 | } 201 | if _, exists := configMap["cups_job_queue_size"]; !exists { 202 | b.NativeJobQueueSize = DefaultConfig.NativeJobQueueSize 203 | } 204 | if _, exists := configMap["cups_printer_poll_interval"]; !exists { 205 | b.NativePrinterPollInterval = DefaultConfig.NativePrinterPollInterval 206 | } 207 | if _, exists := configMap["cups_job_full_username"]; !exists { 208 | b.CUPSJobFullUsername = DefaultConfig.CUPSJobFullUsername 209 | } 210 | if _, exists := configMap["prefix_job_id_to_job_title"]; !exists { 211 | b.PrefixJobIDToJobTitle = DefaultConfig.PrefixJobIDToJobTitle 212 | } 213 | if _, exists := configMap["display_name_prefix"]; !exists { 214 | b.DisplayNamePrefix = DefaultConfig.DisplayNamePrefix 215 | } 216 | if _, exists := configMap["printer_blacklist"]; !exists { 217 | b.PrinterBlacklist = DefaultConfig.PrinterBlacklist 218 | } 219 | if _, exists := configMap["printer_whitelist"]; !exists { 220 | b.PrinterWhitelist = DefaultConfig.PrinterWhitelist 221 | } 222 | if _, exists := configMap["local_printing_enable"]; !exists { 223 | b.LocalPrintingEnable = DefaultConfig.LocalPrintingEnable 224 | } 225 | if _, exists := configMap["cloud_printing_enable"]; !exists { 226 | b.CloudPrintingEnable = DefaultConfig.CloudPrintingEnable 227 | } 228 | if _, exists := configMap["fcm_notifications_enable"]; !exists { 229 | b.FcmNotificationsEnable = DefaultConfig.FcmNotificationsEnable 230 | } 231 | if _, exists := configMap["log_level"]; !exists { 232 | b.LogLevel = DefaultConfig.LogLevel 233 | } 234 | if _, exists := configMap["local_port_low"]; !exists { 235 | b.LocalPortLow = DefaultConfig.LocalPortLow 236 | } 237 | if _, exists := configMap["local_port_high"]; !exists { 238 | b.LocalPortHigh = DefaultConfig.LocalPortHigh 239 | } 240 | 241 | return &b 242 | } 243 | -------------------------------------------------------------------------------- /lib/config_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package lib 10 | 11 | import ( 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/urfave/cli" 16 | ) 17 | 18 | const ( 19 | platformName = "Windows" 20 | 21 | defaultConfigFilename = "gcp-windows-connector.config.json" 22 | ) 23 | 24 | type Config struct { 25 | // Enable local discovery and printing. 26 | LocalPrintingEnable bool `json:"local_printing_enable"` 27 | 28 | // Enable cloud discovery and printing. 29 | CloudPrintingEnable bool `json:"cloud_printing_enable"` 30 | 31 | // Enable fcm notifications instead of xmpp notifications. 32 | FcmNotificationsEnable bool `json:"fcm_notifications_enable"` 33 | 34 | // Associated with root account. XMPP credential. 35 | XMPPJID string `json:"xmpp_jid,omitempty"` 36 | 37 | // Associated with robot account. Used for acquiring OAuth access tokens. 38 | RobotRefreshToken string `json:"robot_refresh_token,omitempty"` 39 | 40 | // Associated with user account. Used for sharing GCP printers; may be omitted. 41 | UserRefreshToken string `json:"user_refresh_token,omitempty"` 42 | 43 | // Scope (user, group, domain) to share printers with. 44 | ShareScope string `json:"share_scope,omitempty"` 45 | 46 | // User-chosen name of this proxy. Should be unique per Google user account. 47 | ProxyName string `json:"proxy_name,omitempty"` 48 | 49 | // FCM url client should listen on. 50 | FcmServerBindUrl string `json:"fcm_server_bind_url,omitempty"` 51 | 52 | // XMPP server FQDN. 53 | XMPPServer string `json:"xmpp_server,omitempty"` 54 | 55 | // XMPP server port number. 56 | XMPPPort uint16 `json:"xmpp_port,omitempty"` 57 | 58 | // XMPP ping timeout (give up waiting after this time). 59 | // TODO: Rename with "gcp_" removed. 60 | XMPPPingTimeout string `json:"gcp_xmpp_ping_timeout,omitempty"` 61 | 62 | // XMPP ping interval (time between ping attempts). 63 | // TODO: Rename with "gcp_" removed. 64 | // TODO: Rename with "_default" removed. 65 | XMPPPingInterval string `json:"gcp_xmpp_ping_interval_default,omitempty"` 66 | 67 | // GCP API URL prefix. 68 | GCPBaseURL string `json:"gcp_base_url,omitempty"` 69 | 70 | // OAuth2 client ID (not unique per client). 71 | GCPOAuthClientID string `json:"gcp_oauth_client_id,omitempty"` 72 | 73 | // OAuth2 client secret (not unique per client). 74 | GCPOAuthClientSecret string `json:"gcp_oauth_client_secret,omitempty"` 75 | 76 | // OAuth2 auth URL. 77 | GCPOAuthAuthURL string `json:"gcp_oauth_auth_url,omitempty"` 78 | 79 | // OAuth2 token URL. 80 | GCPOAuthTokenURL string `json:"gcp_oauth_token_url,omitempty"` 81 | 82 | // Maximum quantity of jobs (data) to download concurrently. 83 | GCPMaxConcurrentDownloads uint `json:"gcp_max_concurrent_downloads,omitempty"` 84 | 85 | // Windows Spooler job queue size, must be greater than zero. 86 | // TODO: rename without cups_ prefix 87 | NativeJobQueueSize uint `json:"cups_job_queue_size,omitempty"` 88 | 89 | // Interval (eg 10s, 1m) between Windows Spooler printer state polls. 90 | // TODO: rename without cups_ prefix 91 | NativePrinterPollInterval string `json:"cups_printer_poll_interval,omitempty"` 92 | 93 | // Use the full username (joe@example.com) in job. 94 | // TODO: rename without cups_ prefix 95 | CUPSJobFullUsername *bool `json:"cups_job_full_username,omitempty"` 96 | 97 | // Add the job ID to the beginning of the job title. Useful for debugging. 98 | PrefixJobIDToJobTitle *bool `json:"prefix_job_id_to_job_title,omitempty"` 99 | 100 | // Prefix for all GCP printers hosted by this connector. 101 | DisplayNamePrefix string `json:"display_name_prefix,omitempty"` 102 | 103 | // Ignore printers with native names. 104 | PrinterBlacklist []string `json:"printer_blacklist,omitempty"` 105 | 106 | // Allow printers with native names. 107 | PrinterWhitelist []string `json:"printer_whitelist,omitempty"` 108 | 109 | // Least severity to log. 110 | LogLevel string `json:"log_level"` 111 | 112 | // Local only: HTTP API port range, low. 113 | LocalPortLow uint16 `json:"local_port_low,omitempty"` 114 | 115 | // Local only: HTTP API port range, high. 116 | LocalPortHigh uint16 `json:"local_port_high,omitempty"` 117 | } 118 | 119 | // DefaultConfig represents reasonable default values for Config fields. 120 | // Omitted Config fields are omitted on purpose; they are unique per 121 | // connector instance. 122 | var DefaultConfig = Config{ 123 | XMPPServer: "talk.google.com", 124 | XMPPPort: 443, 125 | XMPPPingTimeout: "5s", 126 | XMPPPingInterval: "2m", 127 | FcmServerBindUrl: "https://fcm-stream.googleapis.com/fcm/connect/bind", 128 | GCPBaseURL: "https://www.google.com/cloudprint/", 129 | GCPOAuthClientID: "539833558011-35iq8btpgas80nrs3o7mv99hm95d4dv6.apps.googleusercontent.com", 130 | GCPOAuthClientSecret: "V9BfPOvdiYuw12hDx5Y5nR0a", 131 | GCPOAuthAuthURL: "https://accounts.google.com/o/oauth2/auth", 132 | GCPOAuthTokenURL: "https://accounts.google.com/o/oauth2/token", 133 | GCPMaxConcurrentDownloads: 5, 134 | 135 | NativeJobQueueSize: 3, 136 | NativePrinterPollInterval: "1m", 137 | CUPSJobFullUsername: PointerToBool(false), 138 | PrefixJobIDToJobTitle: PointerToBool(false), 139 | DisplayNamePrefix: "", 140 | PrinterBlacklist: []string{ 141 | "Fax", 142 | "CutePDF Writer", 143 | "Microsoft XPS Document Writer", 144 | "Google Cloud Printer", 145 | }, 146 | PrinterWhitelist: []string{}, 147 | LocalPrintingEnable: true, 148 | CloudPrintingEnable: false, 149 | FcmNotificationsEnable: false, 150 | LogLevel: "INFO", 151 | 152 | LocalPortLow: 26000, 153 | LocalPortHigh: 26999, 154 | } 155 | 156 | // getConfigFilename gets the absolute filename of the config file specified by 157 | // the ConfigFilename flag, and whether it exists. 158 | // 159 | // If the ConfigFilename exists, then it is returned as an absolute path. 160 | // If neither of those exist, the absolute ConfigFilename is returned. 161 | func getConfigFilename(context *cli.Context) (string, bool) { 162 | cf := context.String("config-filename") 163 | 164 | if filepath.IsAbs(cf) { 165 | // Absolute path specified; user knows what they want. 166 | _, err := os.Stat(cf) 167 | return cf, err == nil 168 | } 169 | 170 | absCF, err := filepath.Abs(cf) 171 | if err != nil { 172 | // syscall failure; treat as if file doesn't exist. 173 | return cf, false 174 | } 175 | if _, err := os.Stat(absCF); err == nil { 176 | // File exists on relative path. 177 | return absCF, true 178 | } 179 | 180 | // Check for config file on path relative to executable. 181 | exeFile := os.Args[0] 182 | exeDir := filepath.Dir(exeFile) 183 | absCF = filepath.Join(exeDir, cf) 184 | if _, err := os.Stat(absCF); err == nil { 185 | return absCF, true 186 | } 187 | 188 | // This is probably what the user expects if it wasn't found anywhere else. 189 | return absCF, false 190 | } 191 | 192 | // Backfill returns a copy of this config with all missing keys set to default values. 193 | func (c *Config) Backfill(configMap map[string]interface{}) *Config { 194 | return c.commonBackfill(configMap) 195 | } 196 | 197 | // Sparse returns a copy of this config with obvious values removed. 198 | func (c *Config) Sparse(context *cli.Context) *Config { 199 | return c.commonSparse(context) 200 | } 201 | -------------------------------------------------------------------------------- /lib/deephash.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "encoding/binary" 13 | "fmt" 14 | "hash" 15 | "io" 16 | "reflect" 17 | "sort" 18 | ) 19 | 20 | // DeepHash writes an object's values to h. 21 | // Struct member names are ignored, values are written to h. 22 | // Map keys and values are written to h. 23 | // Slice inde and values are written to h. 24 | // Pointers are followed once. 25 | // Recursive pointer references cause panic. 26 | func DeepHash(data interface{}, h hash.Hash) { 27 | visited := map[uintptr]struct{}{} 28 | deepHash(h, reflect.ValueOf(data), visited) 29 | } 30 | 31 | func binWrite(h hash.Hash, d interface{}) { 32 | binary.Write(h, binary.BigEndian, d) 33 | } 34 | 35 | type sortableValues []reflect.Value 36 | 37 | func (sv sortableValues) Len() int { return len(sv) } 38 | func (sv sortableValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] } 39 | func (sv sortableValues) Less(i, j int) bool { 40 | switch sv[i].Kind() { 41 | case reflect.Bool: 42 | return sv[i].Bool() == false && sv[j].Bool() == true 43 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 44 | return sv[i].Int() < sv[i].Int() 45 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 46 | return sv[i].Uint() < sv[i].Uint() 47 | case reflect.Float32, reflect.Float64: 48 | return sv[i].Float() < sv[i].Float() 49 | case reflect.String: 50 | return sv[i].String() < sv[j].String() 51 | case reflect.Ptr: 52 | return sv[i].Pointer() < sv[i].Pointer() 53 | default: 54 | panic(fmt.Sprintf("Cannot compare type %s", sv[i].Kind().String())) 55 | } 56 | } 57 | 58 | func deepHash(h hash.Hash, v reflect.Value, visited map[uintptr]struct{}) { 59 | switch v.Kind() { 60 | case reflect.Invalid: 61 | h.Write([]byte{0}) 62 | case reflect.Bool: 63 | if v.Bool() { 64 | binWrite(h, uint8(1)) 65 | } else { 66 | binWrite(h, uint8(0)) 67 | } 68 | case reflect.Int: 69 | binWrite(h, int(v.Int())) 70 | case reflect.Int8: 71 | binWrite(h, int8(v.Int())) 72 | case reflect.Int16: 73 | binWrite(h, int16(v.Int())) 74 | case reflect.Int32: 75 | binWrite(h, int32(v.Int())) 76 | case reflect.Int64: 77 | binWrite(h, int64(v.Int())) 78 | case reflect.Uint: 79 | binWrite(h, uint(v.Uint())) 80 | case reflect.Uint8: 81 | binWrite(h, uint8(v.Uint())) 82 | case reflect.Uint16: 83 | binWrite(h, uint16(v.Uint())) 84 | case reflect.Uint32: 85 | binWrite(h, uint32(v.Uint())) 86 | case reflect.Uint64: 87 | binWrite(h, uint64(v.Uint())) 88 | case reflect.Float32: 89 | binWrite(h, float32(v.Float())) 90 | case reflect.Float64: 91 | binWrite(h, float64(v.Float())) 92 | case reflect.Complex64: 93 | binWrite(h, float32(real(v.Complex()))) 94 | binWrite(h, float32(imag(v.Complex()))) 95 | case reflect.Complex128: 96 | binWrite(h, float64(real(v.Complex()))) 97 | binWrite(h, float64(imag(v.Complex()))) 98 | case reflect.Map: 99 | keys := make(sortableValues, 0, v.Len()) 100 | for _, key := range v.MapKeys() { 101 | keys = append(keys, key) 102 | } 103 | sort.Sort(keys) 104 | for _, key := range keys { 105 | io.WriteString(h, key.String()) 106 | deepHash(h, v.MapIndex(key), visited) 107 | } 108 | case reflect.Ptr: 109 | if _, exists := visited[v.Pointer()]; exists { 110 | panic("Cannot hash recursive structure") 111 | } else { 112 | visited[v.Pointer()] = struct{}{} 113 | deepHash(h, v.Elem(), visited) 114 | delete(visited, v.Pointer()) 115 | } 116 | case reflect.Slice, reflect.Array: 117 | for i, l := 0, v.Len(); i < l; i++ { 118 | binWrite(h, i) 119 | deepHash(h, v.Index(i), visited) 120 | } 121 | case reflect.String: 122 | io.WriteString(h, v.String()) 123 | case reflect.Struct: 124 | for i, n := 0, v.NumField(); i < n; i++ { 125 | io.WriteString(h, v.Type().Field(i).Name) 126 | deepHash(h, v.Field(i), visited) 127 | } 128 | default: 129 | message := fmt.Sprintf("DeepHash not implemented for '%s' type", v.Kind().String()) 130 | panic(message) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/deephash_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "bytes" 13 | "crypto/md5" 14 | "encoding/binary" 15 | "io" 16 | "testing" 17 | ) 18 | 19 | func check(t *testing.T, expected []byte, data interface{}) { 20 | h := md5.New() 21 | DeepHash(data, h) 22 | got := h.Sum(nil) 23 | 24 | if bytes.Compare(expected, got) != 0 { 25 | t.Logf("expected %x got %x", expected, got) 26 | t.Fail() 27 | } 28 | } 29 | 30 | func TestBool(t *testing.T) { 31 | h := md5.New() 32 | binary.Write(h, binary.BigEndian, uint8(0)) 33 | expected := h.Sum(nil) 34 | check(t, expected, false) 35 | 36 | h = md5.New() 37 | binary.Write(h, binary.BigEndian, uint8(1)) 38 | expected2 := h.Sum(nil) 39 | check(t, expected2, true) 40 | } 41 | 42 | func TestInt(t *testing.T) { 43 | i := int(123456789) 44 | h := md5.New() 45 | binary.Write(h, binary.BigEndian, i) 46 | expected := h.Sum(nil) 47 | check(t, expected, i) 48 | 49 | i8 := int8(123) 50 | h = md5.New() 51 | binary.Write(h, binary.BigEndian, i8) 52 | expected = h.Sum(nil) 53 | check(t, expected, i8) 54 | 55 | // byte is an alias for uint8 56 | b := byte('Q') 57 | h = md5.New() 58 | binary.Write(h, binary.BigEndian, b) 59 | expected = h.Sum(nil) 60 | check(t, expected, b) 61 | 62 | // rune is an alias for int32 63 | r := rune('龍') 64 | h = md5.New() 65 | binary.Write(h, binary.BigEndian, r) 66 | expected = h.Sum(nil) 67 | check(t, expected, r) 68 | 69 | ui64 := uint64(123456789123456789) 70 | h = md5.New() 71 | binary.Write(h, binary.BigEndian, ui64) 72 | expected = h.Sum(nil) 73 | check(t, expected, ui64) 74 | } 75 | 76 | func TestFloat(t *testing.T) { 77 | f32 := float32(123456.789) 78 | h := md5.New() 79 | binary.Write(h, binary.BigEndian, f32) 80 | expected := h.Sum(nil) 81 | check(t, expected, f32) 82 | 83 | f64 := float64(123456789.123456789) 84 | h = md5.New() 85 | binary.Write(h, binary.BigEndian, f64) 86 | expected = h.Sum(nil) 87 | check(t, expected, f64) 88 | } 89 | 90 | func TestComplex(t *testing.T) { 91 | var c64 complex64 = complex(123456.789, 654321.987) 92 | h := md5.New() 93 | binary.Write(h, binary.BigEndian, float32(real(c64))) 94 | binary.Write(h, binary.BigEndian, float32(imag(c64))) 95 | expected := h.Sum(nil) 96 | check(t, expected, c64) 97 | 98 | var c128 complex128 = complex(123456789.123456789, 987654321.987654321) 99 | h = md5.New() 100 | binary.Write(h, binary.BigEndian, float64(real(c128))) 101 | binary.Write(h, binary.BigEndian, float64(imag(c128))) 102 | expected = h.Sum(nil) 103 | check(t, expected, c128) 104 | } 105 | 106 | func TestMap(t *testing.T) { 107 | m := map[string]string{"b": "B", "a": "A"} 108 | h := md5.New() 109 | io.WriteString(h, "a") 110 | io.WriteString(h, "A") 111 | io.WriteString(h, "b") 112 | io.WriteString(h, "B") 113 | expected := h.Sum(nil) 114 | check(t, expected, m) 115 | } 116 | 117 | func TestPtr(t *testing.T) { 118 | i := int8(1) 119 | h := md5.New() 120 | binary.Write(h, binary.BigEndian, i) 121 | expected := h.Sum(nil) 122 | 123 | // Sum should be the same, whether DeepHash(value), or DeepHash(&value). 124 | check(t, expected, i) 125 | check(t, expected, &i) 126 | } 127 | 128 | func TestPtrNil(t *testing.T) { 129 | h := md5.New() 130 | h.Write([]byte{0}) 131 | expected := h.Sum(nil) 132 | check(t, expected, nil) 133 | } 134 | 135 | func TestSlice(t *testing.T) { 136 | a := []string{"abc", "def"} 137 | h := md5.New() 138 | io.WriteString(h, a[0]) 139 | io.WriteString(h, a[1]) 140 | expected := h.Sum(nil) 141 | check(t, expected, a) 142 | 143 | b := []int{1, 2, 3} 144 | h = md5.New() 145 | binary.Write(h, binary.BigEndian, b[0]) 146 | binary.Write(h, binary.BigEndian, b[1]) 147 | binary.Write(h, binary.BigEndian, b[2]) 148 | expected = h.Sum(nil) 149 | check(t, expected, b) 150 | 151 | p := []*int{} 152 | x, y, z := int(1), int(2), int(3) 153 | p = append(p, &x, &y, &z, nil) 154 | h = md5.New() 155 | binary.Write(h, binary.BigEndian, x) 156 | binary.Write(h, binary.BigEndian, y) 157 | binary.Write(h, binary.BigEndian, z) 158 | h.Write([]byte{0}) 159 | expected = h.Sum(nil) 160 | check(t, expected, p) 161 | } 162 | 163 | func TestString(t *testing.T) { 164 | s := "just a string" 165 | h := md5.New() 166 | io.WriteString(h, s) 167 | expected := h.Sum(nil) 168 | check(t, expected, s) 169 | 170 | type myString string 171 | var ms myString = myString(s) 172 | check(t, expected, ms) 173 | } 174 | -------------------------------------------------------------------------------- /lib/job.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import "github.com/google/cloud-print-connector/cdd" 12 | 13 | type Job struct { 14 | NativePrinterName string 15 | Filename string 16 | Title string 17 | User string 18 | JobID string 19 | Ticket *cdd.CloudJobTicket 20 | UpdateJob func(string, *cdd.PrintJobStateDiff) error 21 | } 22 | -------------------------------------------------------------------------------- /lib/printer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | import ( 12 | "reflect" 13 | "regexp" 14 | 15 | "github.com/google/cloud-print-connector/cdd" 16 | ) 17 | 18 | type PrinterState uint8 19 | 20 | // DuplexVendorMap maps a DuplexType to a CUPS key:value option string for a given printer. 21 | type DuplexVendorMap map[cdd.DuplexType]string 22 | 23 | // CUPS: cups_dest_t; GCP: /register and /update interfaces 24 | type Printer struct { 25 | GCPID string // GCP: printerid (GCP key) 26 | Name string // CUPS: cups_dest_t.name (CUPS key); GCP: name field 27 | DefaultDisplayName string // CUPS: printer-info; GCP: default_display_name field 28 | UUID string // CUPS: printer-uuid; GCP: uuid field 29 | Manufacturer string // CUPS: PPD; GCP: manufacturer field 30 | Model string // CUPS: PPD; GCP: model field 31 | GCPVersion string // GCP: gcpVersion field 32 | SetupURL string // GCP: setup_url field 33 | SupportURL string // GCP: support_url field 34 | UpdateURL string // GCP: update_url field 35 | ConnectorVersion string // GCP: firmware field 36 | State *cdd.PrinterStateSection // CUPS: various; GCP: semantic_state field 37 | Description *cdd.PrinterDescriptionSection // CUPS: translated PPD; GCP: capabilities field 38 | CapsHash string // CUPS: hash of PPD; GCP: capsHash field 39 | Tags map[string]string // CUPS: all printer attributes; GCP: repeated tag field 40 | DuplexMap DuplexVendorMap // CUPS: PPD; 41 | NativeJobSemaphore *Semaphore 42 | QuotaEnabled bool 43 | DailyQuota int 44 | NotificationChannel string 45 | } 46 | 47 | var rDeviceURIHostname *regexp.Regexp = regexp.MustCompile( 48 | "(?i)^(?:socket|http|https|ipp|ipps|lpd)://([a-z][a-z0-9.-]*)") 49 | 50 | // GetHostname gets the network hostname, parsed from Printer.Tags["device-uri"]. 51 | func (p *Printer) GetHostname() (string, bool) { 52 | deviceURI, ok := p.Tags["device-uri"] 53 | if !ok { 54 | return "", false 55 | } 56 | 57 | parts := rDeviceURIHostname.FindStringSubmatch(deviceURI) 58 | if len(parts) == 2 { 59 | return parts[1], true 60 | } 61 | 62 | return "", false 63 | } 64 | 65 | type PrinterDiffOperation int8 66 | 67 | const ( 68 | RegisterPrinter PrinterDiffOperation = iota 69 | UpdatePrinter 70 | DeletePrinter 71 | NoChangeToPrinter 72 | ) 73 | 74 | // Describes changes to be pushed to a GCP printer. 75 | type PrinterDiff struct { 76 | Operation PrinterDiffOperation 77 | Printer Printer 78 | 79 | DefaultDisplayNameChanged bool 80 | ManufacturerChanged bool 81 | ModelChanged bool 82 | GCPVersionChanged bool 83 | SetupURLChanged bool 84 | SupportURLChanged bool 85 | UpdateURLChanged bool 86 | ConnectorVersionChanged bool 87 | StateChanged bool 88 | DescriptionChanged bool 89 | CapsHashChanged bool 90 | TagsChanged bool 91 | DuplexMapChanged bool 92 | QuotaEnabledChanged bool 93 | DailyQuotaChanged bool 94 | NotificationChannelChanged bool 95 | } 96 | 97 | func printerSliceToMapByName(s []Printer) map[string]Printer { 98 | m := make(map[string]Printer, len(s)) 99 | for i := range s { 100 | m[s[i].Name] = s[i] 101 | } 102 | return m 103 | } 104 | 105 | // DiffPrinters returns the diff between old (GCP) and new (native) printers. 106 | // Returns nil if zero printers or if all diffs are NoChangeToPrinter operation. 107 | func DiffPrinters(nativePrinters, gcpPrinters []Printer) []PrinterDiff { 108 | // So far, no changes. 109 | dirty := false 110 | 111 | diffs := make([]PrinterDiff, 0, 1) 112 | printersConsidered := make(map[string]struct{}, len(nativePrinters)) 113 | nativePrintersByName := printerSliceToMapByName(nativePrinters) 114 | 115 | for i := range gcpPrinters { 116 | if _, exists := printersConsidered[gcpPrinters[i].Name]; exists { 117 | // GCP can have multiple printers with one name. Remove dupes. 118 | diffs = append(diffs, PrinterDiff{Operation: DeletePrinter, Printer: gcpPrinters[i]}) 119 | dirty = true 120 | 121 | } else { 122 | printersConsidered[gcpPrinters[i].Name] = struct{}{} 123 | 124 | if nativePrinter, exists := nativePrintersByName[gcpPrinters[i].Name]; exists { 125 | // Native printer doesn't know about GCPID yet. 126 | nativePrinter.GCPID = gcpPrinters[i].GCPID 127 | // Don't lose track of this semaphore. 128 | nativePrinter.NativeJobSemaphore = gcpPrinters[i].NativeJobSemaphore 129 | 130 | diff := diffPrinter(&nativePrinter, &gcpPrinters[i]) 131 | diffs = append(diffs, diff) 132 | 133 | if diff.Operation != NoChangeToPrinter { 134 | dirty = true 135 | } 136 | 137 | } else { 138 | diffs = append(diffs, PrinterDiff{Operation: DeletePrinter, Printer: gcpPrinters[i]}) 139 | dirty = true 140 | } 141 | } 142 | } 143 | 144 | for i := range nativePrinters { 145 | if _, exists := printersConsidered[nativePrinters[i].Name]; !exists { 146 | diffs = append(diffs, PrinterDiff{Operation: RegisterPrinter, Printer: nativePrinters[i]}) 147 | dirty = true 148 | } 149 | } 150 | 151 | if dirty { 152 | return diffs 153 | } else { 154 | return nil 155 | } 156 | } 157 | 158 | // diffPrinter finds the difference between a native printer and the corresponding GCP printer. 159 | // 160 | // pn: printer-native; the thing that is correct 161 | // 162 | // pg: printer-GCP; the thing that will be updated 163 | func diffPrinter(pn, pg *Printer) PrinterDiff { 164 | d := PrinterDiff{ 165 | Operation: UpdatePrinter, 166 | Printer: *pn, 167 | } 168 | 169 | if pg.DefaultDisplayName != pn.DefaultDisplayName { 170 | d.DefaultDisplayNameChanged = true 171 | } 172 | if pg.Manufacturer != pn.Manufacturer { 173 | d.ManufacturerChanged = true 174 | } 175 | if pg.Model != pn.Model { 176 | d.ModelChanged = true 177 | } 178 | if pg.GCPVersion != pn.GCPVersion { 179 | if pg.GCPVersion > pn.GCPVersion { 180 | panic("GCP version cannot be downgraded; delete GCP printers") 181 | } 182 | d.GCPVersionChanged = true 183 | } 184 | if pg.SetupURL != pn.SetupURL { 185 | d.SetupURLChanged = true 186 | } 187 | if pg.SupportURL != pn.SupportURL { 188 | d.SupportURLChanged = true 189 | } 190 | if pg.UpdateURL != pn.UpdateURL { 191 | d.UpdateURLChanged = true 192 | } 193 | if pg.ConnectorVersion != pn.ConnectorVersion { 194 | d.ConnectorVersionChanged = true 195 | } 196 | if !reflect.DeepEqual(pg.State, pn.State) { 197 | d.StateChanged = true 198 | } 199 | if !reflect.DeepEqual(pg.Description, pn.Description) { 200 | d.DescriptionChanged = true 201 | } 202 | if pg.CapsHash != pn.CapsHash { 203 | d.CapsHashChanged = true 204 | } 205 | 206 | gcpTagshash, gcpHasTagshash := pg.Tags["tagshash"] 207 | nativeTagshash, nativeHasTagshash := pn.Tags["tagshash"] 208 | if !gcpHasTagshash || !nativeHasTagshash || gcpTagshash != nativeTagshash { 209 | d.TagsChanged = true 210 | } 211 | 212 | if !reflect.DeepEqual(pg.DuplexMap, pn.DuplexMap) { 213 | d.DuplexMapChanged = true 214 | } 215 | 216 | if pg.QuotaEnabled != pn.QuotaEnabled { 217 | d.QuotaEnabledChanged = true 218 | } 219 | 220 | if pg.DailyQuota != pn.DailyQuota { 221 | d.DailyQuotaChanged = true 222 | } 223 | 224 | if pg.NotificationChannel != pn.NotificationChannel { 225 | d.NotificationChannelChanged = true 226 | } 227 | 228 | if d.DefaultDisplayNameChanged || d.ManufacturerChanged || d.ModelChanged || 229 | d.GCPVersionChanged || d.SetupURLChanged || d.SupportURLChanged || 230 | d.UpdateURLChanged || d.ConnectorVersionChanged || d.StateChanged || 231 | d.DescriptionChanged || d.CapsHashChanged || d.TagsChanged || 232 | d.DuplexMapChanged || d.QuotaEnabledChanged || d.DailyQuotaChanged || 233 | d.NotificationChannelChanged { 234 | return d 235 | } 236 | 237 | return PrinterDiff{ 238 | Operation: NoChangeToPrinter, 239 | Printer: *pg, 240 | } 241 | } 242 | 243 | // FilterRawPrinters splits a slice of printers into non-raw and raw. 244 | func FilterRawPrinters(printers []Printer) ([]Printer, []Printer) { 245 | notRaw, raw := make([]Printer, 0, len(printers)), make([]Printer, 0, 0) 246 | for i := range printers { 247 | if PrinterIsRaw(printers[i]) { 248 | raw = append(raw, printers[i]) 249 | } else { 250 | notRaw = append(notRaw, printers[i]) 251 | } 252 | } 253 | return notRaw, raw 254 | } 255 | 256 | func PrinterIsRaw(printer Printer) bool { 257 | if printer.Tags["printer-make-and-model"] == "Local Raw Printer" { 258 | return true 259 | } 260 | return false 261 | } 262 | 263 | func PrinterIsClass(printer Printer) bool { 264 | if printer.Tags["printer-make-and-model"] == "Local Printer Class" { 265 | return true 266 | } 267 | return false 268 | } 269 | -------------------------------------------------------------------------------- /lib/semaphore.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package lib 10 | 11 | type Semaphore struct { 12 | ch chan struct{} 13 | } 14 | 15 | func NewSemaphore(size uint) *Semaphore { 16 | s := make(chan struct{}, size) 17 | return &Semaphore{s} 18 | } 19 | 20 | // Acquire increments the semaphore, blocking if necessary. 21 | func (s *Semaphore) Acquire() { 22 | s.ch <- struct{}{} 23 | } 24 | 25 | // TryAcquire increments the semaphore without blocking. 26 | // Returns false if the semaphore was not acquired. 27 | func (s *Semaphore) TryAcquire() bool { 28 | select { 29 | case s.ch <- struct{}{}: 30 | return true 31 | default: 32 | return false 33 | } 34 | } 35 | 36 | // Release decrements the semaphore. If this operation causes 37 | // the semaphore value to be negative, then panics. 38 | func (s *Semaphore) Release() { 39 | select { 40 | case _ = <-s.ch: 41 | return 42 | default: 43 | panic("Semaphore was released without being acquired") 44 | } 45 | } 46 | 47 | // Count returns the current value of the semaphore. 48 | func (s *Semaphore) Count() uint { 49 | return uint(len(s.ch)) 50 | } 51 | 52 | // Size returns the maximum semaphore value. 53 | func (s *Semaphore) Size() uint { 54 | return uint(cap(s.ch)) 55 | } 56 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package log 10 | 11 | import "strings" 12 | 13 | // LogLevel represents a subset of the severity levels named by CUPS. 14 | type LogLevel uint8 15 | 16 | const ( 17 | FATAL LogLevel = iota 18 | ERROR 19 | WARNING 20 | INFO 21 | DEBUG 22 | ) 23 | 24 | var ( 25 | stringByLevel = map[LogLevel]string{ 26 | FATAL: "FATAL", 27 | ERROR: "ERROR", 28 | WARNING: "WARNING", 29 | INFO: "INFO", 30 | DEBUG: "DEBUG", 31 | } 32 | levelByString = map[string]LogLevel{ 33 | "FATAL": FATAL, 34 | "ERROR": ERROR, 35 | "WARNING": WARNING, 36 | "INFO": INFO, 37 | "DEBUG": DEBUG, 38 | } 39 | ) 40 | 41 | func LevelFromString(level string) (LogLevel, bool) { 42 | v, ok := levelByString[strings.ToUpper(level)] 43 | if !ok { 44 | return 0, false 45 | } 46 | return v, true 47 | } 48 | 49 | func Fatal(args ...interface{}) { log(FATAL, "", "", "", args...) } 50 | func Fatalf(format string, args ...interface{}) { log(FATAL, "", "", format, args...) } 51 | func FatalJob(jobID string, args ...interface{}) { log(FATAL, "", jobID, "", args...) } 52 | func FatalJobf(jobID, format string, args ...interface{}) { log(FATAL, "", jobID, format, args...) } 53 | func FatalPrinter(printerID string, args ...interface{}) { log(FATAL, printerID, "", "", args...) } 54 | func FatalPrinterf(printerID, format string, args ...interface{}) { 55 | log(FATAL, printerID, "", format, args...) 56 | } 57 | 58 | func Error(args ...interface{}) { log(ERROR, "", "", "", args...) } 59 | func Errorf(format string, args ...interface{}) { log(ERROR, "", "", format, args...) } 60 | func ErrorJob(jobID string, args ...interface{}) { log(ERROR, "", jobID, "", args...) } 61 | func ErrorJobf(jobID, format string, args ...interface{}) { log(ERROR, "", jobID, format, args...) } 62 | func ErrorPrinter(printerID string, args ...interface{}) { log(ERROR, printerID, "", "", args...) } 63 | func ErrorPrinterf(printerID, format string, args ...interface{}) { 64 | log(ERROR, printerID, "", format, args...) 65 | } 66 | 67 | func Warning(args ...interface{}) { log(WARNING, "", "", "", args...) } 68 | func Warningf(format string, args ...interface{}) { log(WARNING, "", "", format, args...) } 69 | func WarningJob(jobID string, args ...interface{}) { log(WARNING, "", jobID, "", args...) } 70 | func WarningJobf(jobID, format string, args ...interface{}) { log(WARNING, "", jobID, format, args...) } 71 | func WarningPrinter(printerID string, args ...interface{}) { log(WARNING, printerID, "", "", args...) } 72 | func WarningPrinterf(printerID, format string, args ...interface{}) { 73 | log(WARNING, printerID, "", format, args...) 74 | } 75 | 76 | func Info(args ...interface{}) { log(INFO, "", "", "", args...) } 77 | func Infof(format string, args ...interface{}) { log(INFO, "", "", format, args...) } 78 | func InfoJob(jobID string, args ...interface{}) { log(INFO, "", jobID, "", args...) } 79 | func InfoJobf(jobID, format string, args ...interface{}) { log(INFO, "", jobID, format, args...) } 80 | func InfoPrinter(printerID string, args ...interface{}) { log(INFO, printerID, "", "", args...) } 81 | func InfoPrinterf(printerID, format string, args ...interface{}) { 82 | log(INFO, printerID, "", format, args...) 83 | } 84 | 85 | func Debug(args ...interface{}) { log(DEBUG, "", "", "", args...) } 86 | func Debugf(format string, args ...interface{}) { log(DEBUG, "", "", format, args...) } 87 | func DebugJob(jobID string, args ...interface{}) { log(DEBUG, "", jobID, "", args...) } 88 | func DebugJobf(jobID, format string, args ...interface{}) { log(DEBUG, "", jobID, format, args...) } 89 | func DebugPrinter(printerID string, args ...interface{}) { log(DEBUG, printerID, "", "", args...) } 90 | func DebugPrinterf(printerID, format string, args ...interface{}) { 91 | log(DEBUG, printerID, "", format, args...) 92 | } 93 | -------------------------------------------------------------------------------- /log/log_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | // The log package logs to an io.Writer using the same log format that CUPS uses. 10 | package log 11 | 12 | import ( 13 | "fmt" 14 | "io" 15 | "os" 16 | "runtime" 17 | "strconv" 18 | "time" 19 | 20 | "github.com/coreos/go-systemd/journal" 21 | ) 22 | 23 | const ( 24 | logFormat = "%c [%s] %s\n" 25 | logJobFormat = "%c [%s] [Job %s] %s\n" 26 | logPrinterFormat = "%c [%s] [Printer %s] %s\n" 27 | 28 | dateTimeFormat = "02/Jan/2006:15:04:05 -0700" 29 | 30 | journalJobFormat = "[Job %s] %s" 31 | journalPrinterFormat = "[Printer %s] %s" 32 | ) 33 | 34 | var ( 35 | levelToInitial = map[LogLevel]rune{ 36 | FATAL: 'X', // "EMERG" in CUPS. 37 | ERROR: 'E', 38 | WARNING: 'W', 39 | INFO: 'I', 40 | DEBUG: 'D', 41 | } 42 | 43 | logger struct { 44 | writer io.Writer 45 | level LogLevel 46 | journalEnabled bool 47 | } 48 | ) 49 | 50 | func (l LogLevel) priority() journal.Priority { 51 | switch l { 52 | case FATAL: 53 | return journal.PriCrit 54 | case ERROR: 55 | return journal.PriErr 56 | case WARNING: 57 | return journal.PriWarning 58 | case INFO: 59 | return journal.PriInfo 60 | case DEBUG: 61 | return journal.PriDebug 62 | default: 63 | return journal.PriDebug 64 | } 65 | } 66 | 67 | func init() { 68 | logger.writer = os.Stderr 69 | logger.level = INFO 70 | } 71 | 72 | // SetWriter sets the io.Writer to log to. Default is os.Stderr. 73 | func SetWriter(w io.Writer) { 74 | logger.writer = w 75 | } 76 | 77 | // SetLevel sets the minimum severity level to log. Default is INFO. 78 | func SetLevel(l LogLevel) { 79 | logger.level = l 80 | } 81 | 82 | // SetJournalEnabled enables or disables writing to the systemd journal. Default is false. 83 | func SetJournalEnabled(b bool) { 84 | logger.journalEnabled = b 85 | } 86 | 87 | func log(level LogLevel, printerID, jobID, format string, args ...interface{}) { 88 | if level > logger.level { 89 | return 90 | } 91 | 92 | levelInitial := levelToInitial[level] 93 | dateTime := time.Now().Format(dateTimeFormat) 94 | var message string 95 | if format == "" { 96 | message = fmt.Sprint(args...) 97 | } else { 98 | message = fmt.Sprintf(format, args...) 99 | } 100 | 101 | journalVars := make(map[string]string) 102 | var journalMessage string 103 | if printerID != "" { 104 | fmt.Fprintf(logger.writer, logPrinterFormat, levelInitial, dateTime, printerID, message) 105 | journalVars["PRINTER_ID"] = printerID 106 | journalMessage = fmt.Sprintf(journalPrinterFormat, printerID, message) 107 | } else if jobID != "" { 108 | fmt.Fprintf(logger.writer, logJobFormat, levelInitial, dateTime, jobID, message) 109 | journalVars["JOB_ID"] = jobID 110 | journalMessage = fmt.Sprintf(journalJobFormat, jobID, message) 111 | } else { 112 | fmt.Fprintf(logger.writer, logFormat, levelInitial, dateTime, message) 113 | journalMessage = message 114 | } 115 | 116 | if logger.journalEnabled { 117 | pc := make([]uintptr, 1) 118 | runtime.Callers(3, pc) 119 | f := runtime.FuncForPC(pc[0]) 120 | journalVars["CODE_FUNC"] = f.Name() 121 | file, line := f.FileLine(pc[0]) 122 | journalVars["CODE_FILE"] = file 123 | journalVars["CODE_LINE"] = strconv.Itoa(line) 124 | journal.Send(journalMessage, level.priority(), journalVars) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /log/log_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | // The log package logs to the Windows Event Log, or stdout. 10 | package log 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/google/cloud-print-connector/lib" 16 | "golang.org/x/sys/windows/svc/debug" 17 | "golang.org/x/sys/windows/svc/eventlog" 18 | ) 19 | 20 | const ( 21 | logJobFormat = "[Job %s] %s" 22 | logPrinterFormat = "[Printer %s] %s" 23 | 24 | dateTimeFormat = "2006-Jan-02 15:04:05" 25 | ) 26 | 27 | var logger struct { 28 | level LogLevel 29 | elog debug.Log 30 | } 31 | 32 | func init() { 33 | logger.level = INFO 34 | } 35 | 36 | // SetLevel sets the minimum severity level to log. Default is INFO. 37 | func SetLevel(l LogLevel) { 38 | logger.level = l 39 | } 40 | 41 | func Start(logToConsole bool) error { 42 | if logToConsole { 43 | logger.elog = debug.New(lib.ConnectorName) 44 | 45 | } else { 46 | l, err := eventlog.Open(lib.ConnectorName) 47 | if err != nil { 48 | return err 49 | } 50 | logger.elog = l 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func Stop() { 57 | err := logger.elog.Close() 58 | if err != nil { 59 | panic("Failed to close log") 60 | } 61 | } 62 | 63 | func log(level LogLevel, printerID, jobID, format string, args ...interface{}) { 64 | if logger.elog == nil { 65 | panic("Attempted to log without first calling Start()") 66 | } 67 | 68 | if level > logger.level { 69 | return 70 | } 71 | 72 | var message string 73 | if format == "" { 74 | message = fmt.Sprint(args...) 75 | } else { 76 | message = fmt.Sprintf(format, args...) 77 | } 78 | 79 | if printerID != "" { 80 | message = fmt.Sprintf(logPrinterFormat, printerID, message) 81 | } else if jobID != "" { 82 | message = fmt.Sprintf(logJobFormat, jobID, message) 83 | } 84 | 85 | if level == DEBUG || level == FATAL { 86 | // Windows Event Log only has three levels; these two extra information prepended. 87 | message = fmt.Sprintf("%s %s", stringByLevel[level], message) 88 | } 89 | 90 | switch level { 91 | case FATAL, ERROR: 92 | logger.elog.Error(1, message) 93 | case WARNING: 94 | logger.elog.Warning(2, message) 95 | case INFO, DEBUG: 96 | logger.elog.Info(3, message) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /log/logroller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package log 10 | 11 | import ( 12 | "fmt" 13 | "math" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "sort" 18 | "strconv" 19 | "sync" 20 | ) 21 | 22 | var rollPattern = regexp.MustCompile(`^\.([0-9]+)$`) 23 | 24 | const rollFormatFormat = "%s.%%0%dd" 25 | 26 | type LogRoller struct { 27 | fileName string 28 | fileMaxBytes uint 29 | maxFiles uint 30 | 31 | rollFormat string 32 | 33 | m sync.Mutex 34 | file *os.File 35 | fileSize uint 36 | } 37 | 38 | func NewLogRoller(fileName string, fileMaxBytes, maxFiles uint) (*LogRoller, error) { 39 | // How many digits to append to rolled file name? 40 | // 0 => 0 ; 1 => 1 ; 9 => 1 ; 99 => 2 ; 100 => 3 41 | var digits int 42 | if maxFiles > 0 { 43 | digits = int(math.Log10(float64(maxFiles))) + 1 44 | } 45 | rollFormat := fmt.Sprintf(rollFormatFormat, fileName, digits) 46 | 47 | lr := LogRoller{ 48 | fileName: fileName, 49 | fileMaxBytes: fileMaxBytes, 50 | maxFiles: maxFiles, 51 | rollFormat: rollFormat, 52 | } 53 | return &lr, nil 54 | } 55 | 56 | func (lr *LogRoller) Write(p []byte) (int, error) { 57 | lr.m.Lock() 58 | defer lr.m.Unlock() 59 | 60 | if lr.file == nil { 61 | lr.fileSize = 0 62 | if err := lr.openFile(); err != nil { 63 | return 0, err 64 | } 65 | } 66 | 67 | written, err := lr.file.Write(p) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | lr.fileSize += uint(written) 73 | if lr.fileSize > lr.fileMaxBytes { 74 | lr.file.Close() 75 | lr.file = nil 76 | } 77 | 78 | return written, nil 79 | } 80 | 81 | // openFile opens a new file for logging, rolling the oldest one if needed. 82 | func (lr *LogRoller) openFile() error { 83 | if err := lr.roll(); err != nil { 84 | return err 85 | } 86 | 87 | if f, err := os.Create(lr.fileName); err != nil { 88 | return err 89 | } else { 90 | lr.file = f 91 | } 92 | 93 | return nil 94 | } 95 | 96 | type sortableNumberStrings []string 97 | 98 | func (s sortableNumberStrings) Len() int { 99 | return len(s) 100 | } 101 | 102 | func (s sortableNumberStrings) Less(i, j int) bool { 103 | ip, _ := strconv.ParseUint(s[i], 10, 16) 104 | jp, _ := strconv.ParseUint(s[j], 10, 16) 105 | return ip < jp 106 | } 107 | 108 | func (s sortableNumberStrings) Swap(i, j int) { 109 | s[i], s[j] = s[j], s[i] 110 | } 111 | 112 | // roll deletes old log files until there are lr.maxFiles or fewer, 113 | // and renames remaining log files so that the file named lr.fileName+".3" becomes lr.fileName+".4". 114 | // The file named lr.fileName becomes lr.fileName+".0". 115 | // If lr.fileName does not exist, then this is a noop. 116 | func (lr *LogRoller) roll() error { 117 | if _, err := os.Stat(lr.fileName); os.IsNotExist(err) { 118 | // Nothing to do; the target log file name already does not exist. 119 | return nil 120 | } 121 | 122 | // Get all rolled logs, plus some. 123 | allFiles, err := filepath.Glob(lr.fileName + ".*") 124 | if err != nil { 125 | return err 126 | } 127 | 128 | // Get number suffixes from the rolled logs; ignore non-matches. 129 | numbers := make(sortableNumberStrings, 0, len(allFiles)) 130 | for _, file := range allFiles { 131 | match := rollPattern.FindStringSubmatch(file[len(lr.fileName):]) 132 | if len(match) < 2 { 133 | continue 134 | } 135 | if _, err := strconv.ParseUint(match[1], 10, 16); err == nil { 136 | // Keep the string form of the number. 137 | numbers = append(numbers, match[1]) 138 | } 139 | } 140 | 141 | // Delete old log files and rename the rest. 142 | sort.Sort(numbers) 143 | for i := len(numbers) - 1; i >= 0; i-- { 144 | oldpath := fmt.Sprintf("%s.%s", lr.fileName, numbers[i]) 145 | if uint(i+1) >= lr.maxFiles { 146 | err := os.Remove(oldpath) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | } else { 152 | n, _ := strconv.ParseUint(numbers[i], 10, 16) 153 | newpath := fmt.Sprintf(lr.rollFormat, n+1) 154 | err := os.Rename(oldpath, newpath) 155 | if err != nil { 156 | return err 157 | } 158 | } 159 | } 160 | 161 | if lr.maxFiles > 0 { 162 | newpath := fmt.Sprintf(lr.rollFormat, 0) 163 | err = os.Rename(lr.fileName, newpath) 164 | if err != nil { 165 | return err 166 | } 167 | } // Else the existing file will be truncated. 168 | 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /log/logroller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package log 10 | 11 | import ( 12 | "sort" 13 | "strconv" 14 | "testing" 15 | ) 16 | 17 | func checkSort(s sortableNumberStrings) (bool, error) { 18 | for i := 0; i < s.Len()-1; i++ { 19 | a, err := strconv.Atoi(s[i]) 20 | if err != nil { 21 | return false, err 22 | } 23 | b, err := strconv.Atoi(s[i+1]) 24 | if err != nil { 25 | return false, err 26 | } 27 | if a > b { 28 | return false, nil 29 | } 30 | } 31 | return true, nil 32 | } 33 | 34 | func testSort(t *testing.T, s sortableNumberStrings) { 35 | sort.Sort(s) 36 | res, err := checkSort(s) 37 | if err != nil { 38 | t.Log(err) 39 | t.Fail() 40 | } else if !res { 41 | t.Logf("sort failed: %v", s) 42 | t.Fail() 43 | } 44 | } 45 | 46 | func TestSortableNumberStrings(t *testing.T) { 47 | s := sortableNumberStrings{"2", "1"} 48 | testSort(t, s) 49 | 50 | s = sortableNumberStrings{"100", "10", "1", "11"} 51 | testSort(t, s) 52 | 53 | s = sortableNumberStrings{"0100", "10", "01", "11"} 54 | testSort(t, s) 55 | 56 | s = sortableNumberStrings{"0100", "10", "11", "10"} 57 | testSort(t, s) 58 | } 59 | -------------------------------------------------------------------------------- /monitor/monitor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux darwin freebsd 8 | 9 | package monitor 10 | 11 | import ( 12 | "fmt" 13 | "net" 14 | 15 | "github.com/google/cloud-print-connector/cups" 16 | "github.com/google/cloud-print-connector/gcp" 17 | "github.com/google/cloud-print-connector/lib" 18 | "github.com/google/cloud-print-connector/log" 19 | "github.com/google/cloud-print-connector/manager" 20 | "github.com/google/cloud-print-connector/privet" 21 | ) 22 | 23 | const monitorFormat = `cups-printers=%d 24 | cups-raw-printers=%d 25 | gcp-printers=%d 26 | local-printers=%d 27 | cups-conn-qty=%d 28 | cups-conn-max-qty=%d 29 | jobs-done=%d 30 | jobs-error=%d 31 | jobs-in-progress=%d 32 | ` 33 | 34 | type Monitor struct { 35 | cups *cups.CUPS 36 | gcp *gcp.GoogleCloudPrint 37 | p *privet.Privet 38 | pm *manager.PrinterManager 39 | listenerQuit chan bool 40 | } 41 | 42 | func NewMonitor(cups *cups.CUPS, gcp *gcp.GoogleCloudPrint, p *privet.Privet, pm *manager.PrinterManager, socketFilename string) (*Monitor, error) { 43 | m := Monitor{cups, gcp, p, pm, make(chan bool)} 44 | 45 | listener, err := net.ListenUnix("unix", &net.UnixAddr{socketFilename, "unix"}) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | go m.listen(listener) 51 | 52 | return &m, nil 53 | } 54 | 55 | func (m *Monitor) listen(listener net.Listener) { 56 | ch := make(chan net.Conn) 57 | quitReq := make(chan bool, 1) 58 | quitAck := make(chan bool) 59 | 60 | go func() { 61 | for { 62 | conn, err := listener.Accept() 63 | if err != nil { 64 | select { 65 | case <-quitReq: 66 | quitAck <- true 67 | return 68 | } 69 | log.Errorf("Error listening to monitor socket: %s", err) 70 | } else { 71 | ch <- conn 72 | } 73 | } 74 | }() 75 | 76 | for { 77 | select { 78 | case conn := <-ch: 79 | log.Info("Received monitor request") 80 | stats, err := m.getStats() 81 | if err != nil { 82 | log.Warningf("Monitor request failed: %s", err) 83 | conn.Write([]byte("error")) 84 | } else { 85 | conn.Write([]byte(stats)) 86 | } 87 | conn.Close() 88 | 89 | case <-m.listenerQuit: 90 | quitReq <- true 91 | listener.Close() 92 | <-quitAck 93 | m.listenerQuit <- true 94 | return 95 | } 96 | } 97 | } 98 | 99 | func (m *Monitor) Quit() { 100 | m.listenerQuit <- true 101 | <-m.listenerQuit 102 | } 103 | 104 | func (m *Monitor) getStats() (string, error) { 105 | var cupsPrinterQuantity, rawPrinterQuantity, gcpPrinterQuantity, privetPrinterQuantity int 106 | 107 | if cupsPrinters, err := m.cups.GetPrinters(); err != nil { 108 | return "", err 109 | } else { 110 | cupsPrinterQuantity = len(cupsPrinters) 111 | _, rawPrinters := lib.FilterRawPrinters(cupsPrinters) 112 | rawPrinterQuantity = len(rawPrinters) 113 | } 114 | 115 | cupsConnOpen := m.cups.ConnQtyOpen() 116 | cupsConnMax := m.cups.ConnQtyMax() 117 | 118 | if m.gcp != nil { 119 | if gcpPrinters, err := m.gcp.List(); err != nil { 120 | return "", err 121 | } else { 122 | gcpPrinterQuantity = len(gcpPrinters) 123 | } 124 | } 125 | 126 | if m.p != nil { 127 | privetPrinterQuantity = m.p.Size() 128 | } 129 | 130 | jobsDone, jobsError, jobsProcessing, err := m.pm.GetJobStats() 131 | if err != nil { 132 | return "", err 133 | } 134 | 135 | stats := fmt.Sprintf( 136 | monitorFormat, 137 | cupsPrinterQuantity, rawPrinterQuantity, gcpPrinterQuantity, privetPrinterQuantity, 138 | cupsConnOpen, cupsConnMax, 139 | jobsDone, jobsError, jobsProcessing) 140 | 141 | return stats, nil 142 | } 143 | -------------------------------------------------------------------------------- /notification/notification.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | type PrinterNotificationType uint8 4 | type PrinterNotification struct { 5 | GCPID string 6 | Type PrinterNotificationType 7 | } 8 | 9 | const ( 10 | PrinterNewJobs PrinterNotificationType = iota 11 | PrinterDelete 12 | ) 13 | -------------------------------------------------------------------------------- /privet/avahi.c: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux freebsd 8 | 9 | #include "avahi.h" 10 | #include "_cgo_export.h" 11 | 12 | const char *SERVICE_TYPE = "_privet._tcp", 13 | *SERVICE_SUBTYPE = "_printer._sub._privet._tcp"; 14 | 15 | // startAvahiClient initializes a poll object, and a client. 16 | const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **client) { 17 | *threaded_poll = avahi_threaded_poll_new(); 18 | if (!*threaded_poll) { 19 | return avahi_strerror(avahi_client_errno(*client)); 20 | } 21 | 22 | int error; 23 | *client = avahi_client_new(avahi_threaded_poll_get(*threaded_poll), 24 | AVAHI_CLIENT_NO_FAIL, handleClientStateChange, NULL, &error); 25 | if (!*client) { 26 | avahi_threaded_poll_free(*threaded_poll); 27 | return avahi_strerror(error); 28 | } 29 | 30 | error = avahi_threaded_poll_start(*threaded_poll); 31 | if (AVAHI_OK != error) { 32 | avahi_client_free(*client); 33 | avahi_threaded_poll_free(*threaded_poll); 34 | return avahi_strerror(error); 35 | } 36 | return NULL; 37 | } 38 | 39 | static const char *populateGroup(AvahiClient *client, AvahiEntryGroup *group, 40 | const char *service_name, unsigned short port, AvahiStringList *txt) { 41 | int error = avahi_entry_group_add_service_strlst( 42 | group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, 43 | SERVICE_TYPE, NULL, NULL, port, txt); 44 | if (AVAHI_OK != error) { 45 | avahi_entry_group_free(group); 46 | return avahi_strerror(error); 47 | } 48 | 49 | error = avahi_entry_group_add_service_subtype(group, AVAHI_IF_UNSPEC, 50 | AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, SERVICE_SUBTYPE); 51 | if (AVAHI_OK != error) { 52 | avahi_entry_group_free(group); 53 | return avahi_strerror(error); 54 | } 55 | 56 | error = avahi_entry_group_commit(group); 57 | if (AVAHI_OK != error) { 58 | avahi_entry_group_free(group); 59 | return avahi_strerror(error); 60 | } 61 | return NULL; 62 | } 63 | 64 | const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, 65 | const char *service_name, unsigned short port, AvahiStringList *txt) { 66 | *group = avahi_entry_group_new(client, handleGroupStateChange, (void *)printer_name); 67 | if (!*group) { 68 | return avahi_strerror(avahi_client_errno(client)); 69 | } 70 | return populateGroup(client, *group, service_name, port, txt); 71 | } 72 | 73 | const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, 74 | unsigned short port, AvahiStringList *txt) { 75 | avahi_entry_group_reset(group); 76 | return populateGroup(client, group, service_name, port, txt); 77 | } 78 | 79 | const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt) { 80 | int error = avahi_entry_group_update_service_txt_strlst(group, AVAHI_IF_UNSPEC, 81 | AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, txt); 82 | if (AVAHI_OK != error) { 83 | return avahi_strerror(error); 84 | } 85 | return NULL; 86 | } 87 | 88 | const char *removeAvahiGroup(AvahiEntryGroup *group) { 89 | int error = avahi_entry_group_free(group); 90 | if (AVAHI_OK != error) { 91 | return avahi_strerror(error); 92 | } 93 | return NULL; 94 | } 95 | 96 | void stopAvahiClient(AvahiThreadedPoll *threaded_poll, AvahiClient *client) { 97 | avahi_threaded_poll_stop(threaded_poll); 98 | avahi_client_free(client); 99 | avahi_threaded_poll_free(threaded_poll); 100 | } 101 | -------------------------------------------------------------------------------- /privet/avahi.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build linux freebsd 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include // free 17 | 18 | const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **client); 19 | const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, 20 | const char *service_name, unsigned short port, AvahiStringList *txt); 21 | const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, 22 | unsigned short port, AvahiStringList *txt); 23 | const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt); 24 | const char *removeAvahiGroup(AvahiEntryGroup *group); 25 | void stopAvahiClient(AvahiThreadedPoll *threaded_poll, AvahiClient *client); 26 | -------------------------------------------------------------------------------- /privet/bonjour.c: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build darwin 8 | 9 | #include "bonjour.h" 10 | #include "_cgo_export.h" 11 | 12 | // streamErrorToString converts a CFStreamError to a string. 13 | char *streamErrorToString(CFStreamError *error) { 14 | const char *errorDomain; 15 | switch (error->domain) { 16 | case kCFStreamErrorDomainCustom: 17 | errorDomain = "custom"; 18 | break; 19 | case kCFStreamErrorDomainPOSIX: 20 | errorDomain = "POSIX"; 21 | break; 22 | case kCFStreamErrorDomainMacOSStatus: 23 | errorDomain = "MacOS status"; 24 | break; 25 | default: 26 | errorDomain = "unknown"; 27 | break; 28 | } 29 | 30 | char *err = NULL; 31 | asprintf(&err, "domain %s code %d", errorDomain, error->error); 32 | return err; 33 | } 34 | 35 | void registerCallback(CFNetServiceRef service, CFStreamError *streamError, void *info) { 36 | CFStringRef printerName = (CFStringRef)info; 37 | char *printerNameC = malloc(sizeof(char) * (CFStringGetLength(printerName) + 1)); 38 | char *streamErrorC = streamErrorToString(streamError); 39 | char *error = NULL; 40 | asprintf(&error, "Error while announcing Bonjour service for printer %s: %s", 41 | printerNameC, streamErrorC); 42 | 43 | logBonjourError(error); 44 | 45 | CFRelease(printerName); 46 | free(printerNameC); 47 | free(streamErrorC); 48 | free(error); 49 | } 50 | 51 | // startBonjour starts and returns a bonjour service. 52 | // 53 | // Returns a registered service. Returns NULL and sets err on failure. 54 | 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) { 55 | CFStringRef nameCF = CFStringCreateWithCString(NULL, name, kCFStringEncodingASCII); 56 | CFStringRef typeCF = CFStringCreateWithCString(NULL, type, kCFStringEncodingASCII); 57 | CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); 58 | CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); 59 | CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); 60 | CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); 61 | CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); 62 | 63 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 0, 64 | &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 65 | CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); 66 | CFDictionarySetValue(dict, CFSTR("ty"), tyCF); 67 | if (CFStringGetLength(noteCF) > 0) { 68 | CFDictionarySetValue(dict, CFSTR("note"), noteCF); 69 | } 70 | CFDictionarySetValue(dict, CFSTR("url"), urlCF); 71 | CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); 72 | CFDictionarySetValue(dict, CFSTR("id"), idCF); 73 | CFDictionarySetValue(dict, CFSTR("cs"), csCF); 74 | CFDataRef txt = CFNetServiceCreateTXTDataWithDictionary(NULL, dict); 75 | 76 | CFNetServiceRef service = CFNetServiceCreate(NULL, CFSTR("local"), typeCF, nameCF, port); 77 | CFNetServiceSetTXTData(service, txt); 78 | // context now owns n, and will release n when service is released. 79 | CFNetServiceClientContext context = {0, (void *) nameCF, NULL, CFRelease, NULL}; 80 | CFNetServiceSetClient(service, registerCallback, &context); 81 | CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); 82 | 83 | CFOptionFlags options = kCFNetServiceFlagNoAutoRename; 84 | CFStreamError error; 85 | 86 | if (!CFNetServiceRegisterWithOptions(service, options, &error)) { 87 | char *errorString = streamErrorToString(&error); 88 | asprintf(err, "Failed to register Bonjour service: %s", errorString); 89 | free(errorString); 90 | CFRelease(service); 91 | service = NULL; 92 | } 93 | 94 | CFRelease(typeCF); 95 | CFRelease(tyCF); 96 | CFRelease(noteCF); 97 | CFRelease(urlCF); 98 | CFRelease(idCF); 99 | CFRelease(csCF); 100 | CFRelease(dict); 101 | CFRelease(txt); 102 | 103 | return service; 104 | } 105 | 106 | // updateBonjour updates the TXT record of service. 107 | void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, const char *id, const char *cs) { 108 | CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); 109 | CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); 110 | CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); 111 | CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); 112 | CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); 113 | 114 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 0, 115 | &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 116 | CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); 117 | CFDictionarySetValue(dict, CFSTR("ty"), tyCF); 118 | if (CFStringGetLength(noteCF) > 0) { 119 | CFDictionarySetValue(dict, CFSTR("note"), noteCF); 120 | } 121 | CFDictionarySetValue(dict, CFSTR("url"), urlCF); 122 | CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); 123 | CFDictionarySetValue(dict, CFSTR("id"), idCF); 124 | CFDictionarySetValue(dict, CFSTR("cs"), csCF); 125 | CFDataRef txt = CFNetServiceCreateTXTDataWithDictionary(NULL, dict); 126 | 127 | CFNetServiceSetTXTData(service, txt); 128 | 129 | CFRelease(tyCF); 130 | CFRelease(noteCF); 131 | CFRelease(urlCF); 132 | CFRelease(idCF); 133 | CFRelease(csCF); 134 | CFRelease(dict); 135 | CFRelease(txt); 136 | } 137 | 138 | // stopBonjour stops service and frees associated resources. 139 | void stopBonjour(CFNetServiceRef service) { 140 | CFNetServiceUnscheduleFromRunLoop(service, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); 141 | CFNetServiceSetClient(service, NULL, NULL); 142 | CFNetServiceCancel(service); 143 | CFRelease(service); 144 | } 145 | -------------------------------------------------------------------------------- /privet/bonjour.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build darwin 8 | 9 | package privet 10 | 11 | // #cgo LDFLAGS: -framework CoreServices 12 | // #include "bonjour.h" 13 | import "C" 14 | import ( 15 | "errors" 16 | "fmt" 17 | "sync" 18 | "unsafe" 19 | 20 | "github.com/google/cloud-print-connector/log" 21 | ) 22 | 23 | // TODO: How to add the _printer subtype? 24 | const serviceType = "_privet._tcp" 25 | 26 | type zeroconf struct { 27 | printers map[string]C.CFNetServiceRef 28 | pMutex sync.RWMutex // Protects printers. 29 | q chan struct{} 30 | } 31 | 32 | // NewZeroconf manages Bonjour services for printers shared via Privet. 33 | func newZeroconf() (*zeroconf, error) { 34 | z := zeroconf{ 35 | printers: make(map[string]C.CFNetServiceRef), 36 | q: make(chan struct{}), 37 | } 38 | return &z, nil 39 | } 40 | 41 | func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { 42 | z.pMutex.RLock() 43 | if _, exists := z.printers[name]; exists { 44 | z.pMutex.RUnlock() 45 | return fmt.Errorf("Bonjour already has printer %s", name) 46 | } 47 | z.pMutex.RUnlock() 48 | 49 | nameC := C.CString(name) 50 | defer C.free(unsafe.Pointer(nameC)) 51 | serviceTypeC := C.CString(serviceType) 52 | defer C.free(unsafe.Pointer(serviceTypeC)) 53 | tyC := C.CString(ty) 54 | defer C.free(unsafe.Pointer(tyC)) 55 | noteC := C.CString(note) 56 | defer C.free(unsafe.Pointer(noteC)) 57 | urlC := C.CString(url) 58 | defer C.free(unsafe.Pointer(urlC)) 59 | idC := C.CString(id) 60 | defer C.free(unsafe.Pointer(idC)) 61 | var onlineC *C.char 62 | if online { 63 | onlineC = C.CString("online") 64 | } else { 65 | onlineC = C.CString("offline") 66 | } 67 | defer C.free(unsafe.Pointer(onlineC)) 68 | 69 | var errstr *C.char = nil 70 | service := C.startBonjour(nameC, serviceTypeC, C.ushort(port), tyC, noteC, urlC, idC, onlineC, &errstr) 71 | if errstr != nil { 72 | defer C.free(unsafe.Pointer(errstr)) 73 | return errors.New(C.GoString(errstr)) 74 | } 75 | 76 | z.pMutex.Lock() 77 | defer z.pMutex.Unlock() 78 | 79 | z.printers[name] = service 80 | return nil 81 | } 82 | 83 | // updatePrinterTXT updates the advertised TXT record. 84 | func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { 85 | tyC := C.CString(ty) 86 | defer C.free(unsafe.Pointer(tyC)) 87 | noteC := C.CString(note) 88 | defer C.free(unsafe.Pointer(noteC)) 89 | urlC := C.CString(url) 90 | defer C.free(unsafe.Pointer(urlC)) 91 | idC := C.CString(id) 92 | defer C.free(unsafe.Pointer(idC)) 93 | var onlineC *C.char 94 | if online { 95 | onlineC = C.CString("online") 96 | } else { 97 | onlineC = C.CString("offline") 98 | } 99 | defer C.free(unsafe.Pointer(onlineC)) 100 | 101 | z.pMutex.RLock() 102 | defer z.pMutex.RUnlock() 103 | 104 | if service, exists := z.printers[name]; exists { 105 | C.updateBonjour(service, tyC, noteC, urlC, idC, onlineC) 106 | } else { 107 | return fmt.Errorf("Bonjour can't update printer %s that hasn't been added", name) 108 | } 109 | return nil 110 | } 111 | 112 | func (z *zeroconf) removePrinter(name string) error { 113 | z.pMutex.Lock() 114 | defer z.pMutex.Unlock() 115 | 116 | if service, exists := z.printers[name]; exists { 117 | C.stopBonjour(service) 118 | delete(z.printers, name) 119 | } else { 120 | return fmt.Errorf("Bonjour can't remove printer %s that hasn't been added", name) 121 | } 122 | return nil 123 | } 124 | 125 | func (z *zeroconf) quit() { 126 | z.pMutex.Lock() 127 | defer z.pMutex.Unlock() 128 | 129 | for name, service := range z.printers { 130 | C.stopBonjour(service) 131 | delete(z.printers, name) 132 | } 133 | } 134 | 135 | //export logBonjourError 136 | func logBonjourError(err *C.char) { 137 | log.Warningf("Bonjour: %s", C.GoString(err)) 138 | } 139 | -------------------------------------------------------------------------------- /privet/bonjour.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build darwin 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include // asprintf 14 | #include // free 15 | 16 | CFNetServiceRef startBonjour(const char *name, const char *type, 17 | unsigned short int port, const char *ty, const char *note, const char *url, 18 | const char *id, const char *cs, char **err); 19 | void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, 20 | const char *id, const char *cs); 21 | void stopBonjour(CFNetServiceRef service); 22 | -------------------------------------------------------------------------------- /privet/jobcache.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import ( 12 | "encoding/json" 13 | "strconv" 14 | "sync" 15 | "time" 16 | 17 | "github.com/google/cloud-print-connector/cdd" 18 | "github.com/google/cloud-print-connector/log" 19 | ) 20 | 21 | // Jobs expire after this much time. 22 | const jobLifetime = time.Hour 23 | 24 | type entry struct { 25 | jobID string 26 | ticket *cdd.CloudJobTicket 27 | expiresAt time.Time 28 | 29 | state cdd.JobState 30 | pagesPrinted *int32 31 | 32 | jobName string 33 | jobType string 34 | jobSize int64 35 | 36 | timer *time.Timer 37 | } 38 | 39 | func newEntry(jobID string, ticket *cdd.CloudJobTicket) *entry { 40 | var state cdd.JobState 41 | if ticket == nil { 42 | state.Type = cdd.JobStateDraft 43 | } else { 44 | state.Type = cdd.JobStateQueued 45 | } 46 | entry := entry{ 47 | jobID: jobID, 48 | ticket: ticket, 49 | expiresAt: time.Now().Add(jobLifetime), 50 | state: state, 51 | } 52 | 53 | return &entry 54 | } 55 | 56 | func (e *entry) expiresIn() int32 { 57 | i := int32(e.expiresAt.Sub(time.Now()).Seconds()) 58 | if i < 0 { 59 | return 0 60 | } 61 | return i 62 | } 63 | 64 | type jobCache struct { 65 | nextJobID int64 66 | nextJobMutex sync.Mutex 67 | entries map[string]entry 68 | entriesMutex sync.RWMutex 69 | } 70 | 71 | func newJobCache() *jobCache { 72 | return &jobCache{ 73 | nextJobID: time.Now().UnixNano(), 74 | entries: make(map[string]entry), 75 | } 76 | } 77 | 78 | func (jc *jobCache) getNextJobID() string { 79 | jc.nextJobMutex.Lock() 80 | defer jc.nextJobMutex.Unlock() 81 | 82 | jc.nextJobID += 1 83 | return strconv.FormatInt(jc.nextJobID, 36) 84 | } 85 | 86 | // createJob creates a new job, returns the new jobID and expires_in value. 87 | func (jc *jobCache) createJob(ticket *cdd.CloudJobTicket) (string, int32) { 88 | jobID := jc.getNextJobID() 89 | entry := newEntry(jobID, ticket) 90 | 91 | jc.entriesMutex.Lock() 92 | defer jc.entriesMutex.Unlock() 93 | 94 | entry.timer = time.AfterFunc(jobLifetime, func() { 95 | jc.deleteJob(jobID) 96 | }) 97 | jc.entries[jobID] = *entry 98 | 99 | return entry.jobID, int32(jobLifetime.Seconds()) 100 | } 101 | 102 | func (jc *jobCache) getJobExpiresIn(jobID string) (int32, *cdd.CloudJobTicket, bool) { 103 | jc.entriesMutex.RLock() 104 | defer jc.entriesMutex.RUnlock() 105 | 106 | if entry, ok := jc.entries[jobID]; !ok { 107 | return 0, nil, false 108 | } else { 109 | return entry.expiresIn(), entry.ticket, true 110 | } 111 | } 112 | 113 | func (jc *jobCache) submitJob(jobID, jobName, jobType string, jobSize int64) int32 { 114 | jc.entriesMutex.Lock() 115 | defer jc.entriesMutex.Unlock() 116 | 117 | if entry, ok := jc.entries[jobID]; ok { 118 | entry.jobName = jobName 119 | entry.jobType = jobType 120 | entry.jobSize = jobSize 121 | jc.entries[jobID] = entry 122 | return entry.expiresIn() 123 | } 124 | 125 | return 0 126 | } 127 | 128 | func (jc *jobCache) deleteJob(jobID string) { 129 | jc.entriesMutex.Lock() 130 | defer jc.entriesMutex.Unlock() 131 | 132 | if entry, exists := jc.entries[jobID]; exists { 133 | // In case this job was deleted early, cancel the timer. 134 | entry.timer.Stop() 135 | } 136 | 137 | delete(jc.entries, jobID) 138 | } 139 | 140 | func (jc *jobCache) updateJob(jobID string, stateDiff *cdd.PrintJobStateDiff) error { 141 | jc.entriesMutex.Lock() 142 | defer jc.entriesMutex.Unlock() 143 | 144 | if entry, ok := jc.entries[jobID]; ok { 145 | if stateDiff.State != nil { 146 | entry.state = *stateDiff.State 147 | } 148 | if stateDiff.PagesPrinted != nil { 149 | entry.pagesPrinted = stateDiff.PagesPrinted 150 | } 151 | jc.entries[jobID] = entry 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // jobState gets the state of the job identified by jobID as JSON-encoded response. 158 | // 159 | // Returns an empty byte array if the job doesn't exist (because it expired). 160 | func (jc *jobCache) jobState(jobID string) ([]byte, bool) { 161 | jc.entriesMutex.Lock() 162 | defer jc.entriesMutex.Unlock() 163 | 164 | entry, exists := jc.entries[jobID] 165 | if !exists { 166 | return []byte{}, false 167 | } 168 | 169 | var response struct { 170 | JobID string `json:"job_id"` 171 | State cdd.JobStateType `json:"state"` 172 | ExpiresIn int32 `json:"expires_in"` 173 | JobType string `json:"job_type,omitempty"` 174 | JobSize int64 `json:"job_size,omitempty"` 175 | JobName string `json:"job_name,omitempty"` 176 | SemanticState cdd.PrintJobState `json:"semantic_state"` 177 | } 178 | 179 | response.JobID = jobID 180 | response.State = entry.state.Type 181 | response.ExpiresIn = entry.expiresIn() 182 | response.JobType = entry.jobType 183 | response.JobSize = entry.jobSize 184 | response.JobName = entry.jobName 185 | response.SemanticState.Version = "1.0" 186 | response.SemanticState.State = entry.state 187 | response.SemanticState.PagesPrinted = entry.pagesPrinted 188 | 189 | j, err := json.MarshalIndent(response, "", " ") 190 | if err != nil { 191 | log.Errorf("Failed to marshal Privet jobState: %s", err) 192 | return []byte{}, false 193 | } 194 | 195 | return j, true 196 | } 197 | -------------------------------------------------------------------------------- /privet/portmanager.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import ( 12 | "errors" 13 | "net" 14 | "os" 15 | "sync" 16 | "syscall" 17 | "time" 18 | ) 19 | 20 | var NoPortsAvailable = errors.New("No ports available") 21 | 22 | // portManager opens ports within the interval [low, high], starting with low. 23 | type portManager struct { 24 | low uint16 25 | high uint16 26 | 27 | // Keeping a cache of used ports improves benchmark tests by over 100x. 28 | m sync.Mutex 29 | p map[uint16]struct{} 30 | } 31 | 32 | func newPortManager(low, high uint16) *portManager { 33 | return &portManager{ 34 | low: low, 35 | high: high, 36 | p: make(map[uint16]struct{}), 37 | } 38 | } 39 | 40 | // listen finds an open port, returns an open listener on that port. 41 | // 42 | // Returns error when no ports are available. 43 | func (p *portManager) listen() (*quittableListener, error) { 44 | for port := p.nextAvailablePort(p.low); port != 0; port = p.nextAvailablePort(port) { 45 | if l, err := newQuittableListener(port, p); err == nil { 46 | return l, nil 47 | } else { 48 | if !isAddrInUse(err) { 49 | return nil, err 50 | } 51 | } 52 | } 53 | 54 | return nil, NoPortsAvailable 55 | } 56 | 57 | // nextAvailablePort checks the p map for the next port available. 58 | // p only keeps track of ports used by the connector, so the start parameter 59 | // is useful to check the port after a port that is in use by some other process. 60 | // 61 | // Returns zero when no available port can be found. 62 | func (p *portManager) nextAvailablePort(start uint16) uint16 { 63 | p.m.Lock() 64 | defer p.m.Unlock() 65 | 66 | for port := start; port <= p.high; port++ { 67 | if _, exists := p.p[port]; !exists { 68 | p.p[port] = struct{}{} 69 | return port 70 | } 71 | } 72 | 73 | return 0 74 | } 75 | 76 | func (p *portManager) freePort(port uint16) { 77 | p.m.Lock() 78 | defer p.m.Unlock() 79 | 80 | delete(p.p, port) 81 | } 82 | 83 | func isAddrInUse(err error) bool { 84 | if err, ok := err.(*net.OpError); ok { 85 | if err, ok := err.Err.(*os.SyscallError); ok { 86 | return err.Err == syscall.EADDRINUSE 87 | } 88 | } 89 | return false 90 | } 91 | 92 | type quittableListener struct { 93 | *net.TCPListener 94 | 95 | pm *portManager 96 | 97 | // When q is closed, the listener is quitting. 98 | q chan struct{} 99 | } 100 | 101 | func newQuittableListener(port uint16, pm *portManager) (*quittableListener, error) { 102 | l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: int(port)}) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return &quittableListener{l, pm, make(chan struct{}, 0)}, nil 107 | } 108 | 109 | func (l *quittableListener) Accept() (net.Conn, error) { 110 | conn, err := l.AcceptTCP() 111 | 112 | select { 113 | case <-l.q: 114 | if err == nil { 115 | conn.Close() 116 | } 117 | // The listener was closed on purpose. 118 | // Returning an error that is not a net.Error causes net.Server.Serve() to return. 119 | return nil, closed 120 | default: 121 | } 122 | 123 | // Clean up zombie connections. 124 | conn.SetKeepAlive(true) 125 | conn.SetKeepAlivePeriod(time.Minute) 126 | 127 | return conn, err 128 | } 129 | 130 | func (l *quittableListener) Close() error { 131 | err := l.TCPListener.Close() 132 | if err != nil { 133 | return err 134 | } 135 | l.pm.freePort(l.port()) 136 | return nil 137 | } 138 | 139 | func (l *quittableListener) port() uint16 { 140 | return uint16(l.Addr().(*net.TCPAddr).Port) 141 | } 142 | 143 | func (l *quittableListener) quit() { 144 | close(l.q) 145 | l.Close() 146 | } 147 | -------------------------------------------------------------------------------- /privet/portmanager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import "testing" 12 | 13 | const portLow = 26000 14 | 15 | func TestListen_available1(t *testing.T) { 16 | pm := newPortManager(portLow, portLow) 17 | 18 | l1, err := pm.listen() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if l1.port() != portLow { 23 | t.Logf("Expected port %d, got port %d", portLow, l1.port()) 24 | l1.Close() 25 | t.FailNow() 26 | } 27 | 28 | l2, err := pm.listen() 29 | if err == nil { 30 | l1.Close() 31 | l2.Close() 32 | t.Fatal("Expected error when too many ports opened") 33 | } 34 | 35 | err = l1.Close() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | l3, err := pm.listen() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if l3.port() != portLow { 45 | t.Logf("Expected port %d, got port %d", portLow, l3.port()) 46 | } 47 | l3.Close() 48 | } 49 | 50 | func TestListen_available2(t *testing.T) { 51 | pm := newPortManager(portLow, portLow+1) 52 | 53 | l1, err := pm.listen() 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if l1.port() != portLow { 58 | t.Logf("Expected port %d, got port %d", portLow, l1.port()) 59 | l1.Close() 60 | t.FailNow() 61 | } 62 | 63 | l2, err := pm.listen() 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | if l2.port() != portLow+1 { 68 | t.Logf("Expected port %d, got port %d", portLow+1, l2.port()) 69 | l2.Close() 70 | t.FailNow() 71 | } 72 | 73 | l3, err := pm.listen() 74 | if err == nil { 75 | l1.Close() 76 | l2.Close() 77 | l3.Close() 78 | t.Fatal("Expected error when too many ports opened") 79 | } 80 | 81 | err = l2.Close() 82 | if err != nil { 83 | l1.Close() 84 | t.Fatal(err) 85 | } 86 | 87 | l4, err := pm.listen() 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if l4.port() != portLow+1 { 92 | t.Logf("Expected port %d, got port %d", portLow+1, l4.port()) 93 | } 94 | 95 | l5, err := pm.listen() 96 | if err == nil { 97 | l1.Close() 98 | l4.Close() 99 | l5.Close() 100 | t.Fatal("Expected error when too many ports opened") 101 | } 102 | 103 | err = l1.Close() 104 | if err != nil { 105 | l4.Close() 106 | t.Fatal(err) 107 | } 108 | 109 | l6, err := pm.listen() 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | if l6.port() != portLow { 114 | t.Logf("Expected port %d, got port %d", portLow, l6.port()) 115 | } 116 | l4.Close() 117 | l6.Close() 118 | } 119 | 120 | // openPorts attempts to open n ports, where m are available. 121 | func openPorts(n, m uint16) { 122 | pm := newPortManager(portLow, portLow+m-1) 123 | for i := uint16(0); i < n; i++ { 124 | l, err := pm.listen() 125 | if err == nil { 126 | defer l.Close() 127 | } 128 | } 129 | } 130 | 131 | func BenchmarkListen_range1_available1(*testing.B) { 132 | openPorts(1, 1) 133 | } 134 | 135 | func BenchmarkListen_range10_available10(*testing.B) { 136 | openPorts(10, 10) 137 | } 138 | 139 | func BenchmarkListen_range100_available100(*testing.B) { 140 | openPorts(100, 100) 141 | } 142 | 143 | func BenchmarkListen_range1000_available1000(*testing.B) { 144 | openPorts(1000, 1000) 145 | } 146 | 147 | func BenchmarkListen_range10_available1(*testing.B) { 148 | openPorts(10, 1) 149 | } 150 | 151 | func BenchmarkListen_range100_available10(*testing.B) { 152 | openPorts(100, 10) 153 | } 154 | 155 | func BenchmarkListen_range1000_available100(*testing.B) { 156 | openPorts(1000, 100) 157 | } 158 | -------------------------------------------------------------------------------- /privet/privet.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import ( 12 | "fmt" 13 | "sync" 14 | 15 | "github.com/google/cloud-print-connector/lib" 16 | ) 17 | 18 | // Privet managers local discovery and printing. 19 | type Privet struct { 20 | xsrf xsrfSecret 21 | apis map[string]*privetAPI 22 | apisMutex sync.RWMutex // Protects apis 23 | zc *zeroconf 24 | pm *portManager 25 | 26 | jobs chan<- *lib.Job 27 | jc jobCache 28 | 29 | gcpBaseURL string 30 | getProximityToken func(string, string) ([]byte, int, error) 31 | } 32 | 33 | // NewPrivet constructs a new Privet object. 34 | // 35 | // getProximityToken should be GoogleCloudPrint.ProximityToken() 36 | func NewPrivet(jobs chan<- *lib.Job, portLow, portHigh uint16, gcpBaseURL string, getProximityToken func(string, string) ([]byte, int, error)) (*Privet, error) { 37 | zc, err := newZeroconf() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | p := Privet{ 43 | xsrf: newXSRFSecret(), 44 | apis: make(map[string]*privetAPI), 45 | zc: zc, 46 | pm: newPortManager(portLow, portHigh), 47 | 48 | jobs: jobs, 49 | jc: *newJobCache(), 50 | 51 | gcpBaseURL: gcpBaseURL, 52 | getProximityToken: getProximityToken, 53 | } 54 | 55 | return &p, nil 56 | } 57 | 58 | // AddPrinter makes a printer available locally. 59 | func (p *Privet) AddPrinter(printer lib.Printer, getPrinter func(string) (lib.Printer, bool)) error { 60 | online := false 61 | if printer.GCPID != "" { 62 | online = true 63 | } 64 | 65 | listener, err := p.pm.listen() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | api, err := newPrivetAPI(printer.GCPID, printer.Name, p.gcpBaseURL, p.xsrf, online, &p.jc, p.jobs, getPrinter, p.getProximityToken, listener) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | var localDefaultDisplayName = printer.DefaultDisplayName 76 | if online { 77 | localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) 78 | } 79 | err = p.zc.addPrinter(printer.Name, api.port(), localDefaultDisplayName, "", p.gcpBaseURL, printer.GCPID, online) 80 | if err != nil { 81 | api.quit() 82 | return err 83 | } 84 | 85 | p.apisMutex.Lock() 86 | defer p.apisMutex.Unlock() 87 | 88 | p.apis[printer.Name] = api 89 | 90 | return nil 91 | } 92 | 93 | // UpdatePrinter updates a printer's TXT mDNS record. 94 | func (p *Privet) UpdatePrinter(diff *lib.PrinterDiff) error { 95 | online := false 96 | if diff.Printer.GCPID != "" { 97 | online = true 98 | } 99 | 100 | var localDefaultDisplayName = diff.Printer.DefaultDisplayName 101 | if online { 102 | localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) 103 | } 104 | 105 | return p.zc.updatePrinterTXT(diff.Printer.GCPID, localDefaultDisplayName, "", p.gcpBaseURL, diff.Printer.GCPID, online) 106 | } 107 | 108 | // DeletePrinter removes a printer from Privet. 109 | func (p *Privet) DeletePrinter(cupsPrinterName string) error { 110 | p.apisMutex.Lock() 111 | defer p.apisMutex.Unlock() 112 | 113 | err := p.zc.removePrinter(cupsPrinterName) 114 | if api, ok := p.apis[cupsPrinterName]; ok { 115 | api.quit() 116 | delete(p.apis, cupsPrinterName) 117 | } 118 | return err 119 | } 120 | 121 | func (p *Privet) Quit() { 122 | p.apisMutex.Lock() 123 | defer p.apisMutex.Unlock() 124 | 125 | p.zc.quit() 126 | for cupsPrinterName, api := range p.apis { 127 | api.quit() 128 | delete(p.apis, cupsPrinterName) 129 | } 130 | } 131 | 132 | func (p *Privet) Size() int { 133 | p.apisMutex.RLock() 134 | defer p.apisMutex.RUnlock() 135 | 136 | return len(p.apis) 137 | } 138 | -------------------------------------------------------------------------------- /privet/windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package privet 10 | 11 | import ( 12 | "errors" 13 | ) 14 | 15 | type zeroconf struct{} 16 | 17 | func newZeroconf() (*zeroconf, error) { 18 | return nil, errors.New("Privet has not been implemented for Windows") 19 | } 20 | 21 | func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { 22 | return nil 23 | } 24 | 25 | func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { 26 | return nil 27 | } 28 | 29 | func (z *zeroconf) removePrinter(name string) error { 30 | return nil 31 | } 32 | 33 | func (z *zeroconf) quit() {} 34 | -------------------------------------------------------------------------------- /privet/xsrf.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import ( 12 | "bytes" 13 | "crypto/rand" 14 | "crypto/sha1" 15 | "encoding/base64" 16 | "time" 17 | ) 18 | 19 | const ( 20 | deviceSecretLength = 24 // 24 bytes == 192 bits 21 | tokenTimeout = 24 * time.Hour // Specified in GCP Privet doc. 22 | ) 23 | 24 | // xsrfSecret generates and validates XSRF tokens. 25 | type xsrfSecret []byte 26 | 27 | func newXSRFSecret() xsrfSecret { 28 | // Generate a random device secret. 29 | deviceSecret := make([]byte, deviceSecretLength) 30 | rand.Read(deviceSecret) 31 | return deviceSecret 32 | } 33 | 34 | func (x xsrfSecret) newToken() string { 35 | t := time.Now() 36 | return x.newTokenProvideTime(t) 37 | } 38 | 39 | func (x xsrfSecret) newTokenProvideTime(t time.Time) string { 40 | tb := int64ToBytes(t.Unix()) 41 | sum := sha1.Sum(append(x, tb...)) 42 | token := append(sum[:], tb...) 43 | return base64.StdEncoding.EncodeToString(token) 44 | } 45 | 46 | func (x xsrfSecret) isTokenValid(token string) bool { 47 | return x.isTokenValidProvideTime(token, time.Now()) 48 | } 49 | 50 | func (x xsrfSecret) isTokenValidProvideTime(token string, now time.Time) bool { 51 | tokenBytes, err := base64.StdEncoding.DecodeString(token) 52 | if err != nil { 53 | return false 54 | } 55 | 56 | if len(tokenBytes) != sha1.Size+8 { 57 | return false 58 | } 59 | 60 | tb := tokenBytes[sha1.Size:] 61 | t := time.Unix(bytesToInt64(tb), 0) 62 | if now.Sub(t) > tokenTimeout { 63 | return false 64 | } 65 | if t.Sub(now) > 0 { 66 | return false 67 | } 68 | 69 | sum := sha1.Sum(append(x, tb...)) 70 | if 0 != bytes.Compare(sum[:], tokenBytes[:sha1.Size]) { 71 | return false 72 | } 73 | 74 | return true 75 | } 76 | 77 | func int64ToBytes(v int64) []byte { 78 | b := make([]byte, 8) 79 | for i := range b { 80 | b[i] = byte(v >> uint(8*i)) 81 | } 82 | return b 83 | } 84 | 85 | func bytesToInt64(b []byte) int64 { 86 | if len(b) != 8 { 87 | return 0 88 | } 89 | var v int64 90 | for i := range b { 91 | v |= int64(b[i]) << uint(8*i) 92 | } 93 | return v 94 | } 95 | -------------------------------------------------------------------------------- /privet/xsrf_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package privet 10 | 11 | import ( 12 | "bytes" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | var ( 18 | deviceSecret xsrfSecret = []byte("secretsecretsecretsecret") 19 | testTime = time.Unix(1234567890123456789, 0) 20 | testToken = "/8IVybcmeL9NPBBJgaZEa0r+GIkVgel99BAiEQ==" 21 | ) 22 | 23 | func TestNewToken(t *testing.T) { 24 | x := deviceSecret 25 | token := x.newTokenProvideTime(testTime) 26 | if token != testToken { 27 | t.Errorf("new token was %s should be %s", token, testToken) 28 | } 29 | } 30 | 31 | func TestIsTokenValid(t *testing.T) { 32 | x := deviceSecret 33 | if !x.isTokenValidProvideTime(testToken, testTime) { 34 | t.Errorf("valid token reported as invalid (+0ns)") 35 | } 36 | 37 | altTime := testTime.Add(time.Minute) 38 | if !x.isTokenValidProvideTime(testToken, altTime) { 39 | t.Errorf("valid token reported as invalid (+1m)") 40 | } 41 | 42 | altTime = testTime.Add(time.Hour) 43 | if !x.isTokenValidProvideTime(testToken, altTime) { 44 | t.Errorf("valid token reported as invalid (+1h)") 45 | } 46 | 47 | altTime = testTime.Add(23 * time.Hour) 48 | if !x.isTokenValidProvideTime(testToken, altTime) { 49 | t.Errorf("valid token reported as invalid (+23h)") 50 | } 51 | 52 | altTime = testTime.Add(25 * time.Hour) 53 | if x.isTokenValidProvideTime(testToken, altTime) { 54 | t.Errorf("invalid token reported as valid (+25h)") 55 | } 56 | 57 | altTime = testTime.Add(-time.Minute) 58 | if x.isTokenValidProvideTime(testToken, altTime) { 59 | t.Errorf("invalid token reported as valid (-1m)") 60 | } 61 | } 62 | 63 | func TestIsBadFormatTokenValid(t *testing.T) { 64 | x := deviceSecret 65 | if x.isTokenValidProvideTime("", testTime) { 66 | t.Errorf("empty token reported as valid") 67 | } 68 | } 69 | 70 | func TestInt64ToBytes(t *testing.T) { 71 | var v int64 = 0 72 | b := []byte{0, 0, 0, 0, 0, 0, 0, 0} 73 | if got := int64ToBytes(v); bytes.Compare(b, got) != 0 { 74 | t.Errorf("expected %v got %v", b, got) 75 | } 76 | 77 | v = 1234567890123456789 78 | b = []byte{21, 129, 233, 125, 244, 16, 34, 17} 79 | if got := int64ToBytes(v); bytes.Compare(b, got) != 0 { 80 | t.Errorf("expected %v got %v", b, got) 81 | } 82 | } 83 | 84 | func TestBytesToInt64(t *testing.T) { 85 | var v int64 = 0 86 | b := []byte{0, 0, 0, 0, 0, 0, 0, 0} 87 | if got := bytesToInt64(b); v != got { 88 | t.Errorf("expected %d got %d", v, got) 89 | } 90 | 91 | v = 1234567890123456789 92 | b = []byte{21, 129, 233, 125, 244, 16, 34, 17} 93 | if got := bytesToInt64(b); v != got { 94 | t.Errorf("expected %d got %d", v, got) 95 | } 96 | 97 | v = 0 98 | b = []byte{1, 2, 3, 4} 99 | if got := bytesToInt64(b); v != got { 100 | t.Errorf("expected %d got %d", v, got) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /systemd/cloud-print-connector.service: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by a BSD-style 4 | # license that can be found in the LICENSE file or at 5 | # https://developers.google.com/open-source/licenses/bsd 6 | 7 | [Unit] 8 | Description=Google Cloud Print Connector 9 | Documentation="https://github.com/google/cloud-print-connector" 10 | After=cups.service avahi-daemon.service network-online.target 11 | Wants=cups.service avahi-daemon.service network-online.target 12 | 13 | [Service] 14 | ExecStart=/opt/cloud-print-connector/gcp-cups-connector -config-filename /opt/cloud-print-connector/gcp-cups-connector.config.json 15 | Restart=on-failure 16 | User=cloud-print-connector 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /winspool/cairo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package winspool 10 | 11 | /* 12 | #cgo pkg-config: cairo-win32 13 | #include 14 | */ 15 | import "C" 16 | import ( 17 | "fmt" 18 | "unsafe" 19 | ) 20 | 21 | func cairoStatusToError(status C.cairo_status_t) error { 22 | s := C.cairo_status_to_string(status) 23 | return fmt.Errorf("Cairo error: %s", C.GoString(s)) 24 | } 25 | 26 | type CairoSurface uintptr 27 | 28 | func (s CairoSurface) nativePointer() *C.struct__cairo_surface { 29 | return (*C.struct__cairo_surface)(unsafe.Pointer(s)) 30 | } 31 | 32 | func CairoWin32PrintingSurfaceCreate(hDC HDC) (CairoSurface, error) { 33 | cHDC := (*C.struct_HDC__)(unsafe.Pointer(hDC)) 34 | surface := C.cairo_win32_printing_surface_create(cHDC) 35 | s := CairoSurface(unsafe.Pointer(surface)) 36 | if err := s.status(); err != nil { 37 | return 0, err 38 | } 39 | return s, nil 40 | } 41 | 42 | func (s CairoSurface) status() error { 43 | status := C.cairo_surface_status(s.nativePointer()) 44 | if status != 0 { 45 | return cairoStatusToError(status) 46 | } 47 | return nil 48 | } 49 | 50 | func (s CairoSurface) ShowPage() error { 51 | C.cairo_surface_show_page(s.nativePointer()) 52 | return s.status() 53 | } 54 | 55 | func (s CairoSurface) Finish() error { 56 | C.cairo_surface_finish(s.nativePointer()) 57 | return s.status() 58 | } 59 | 60 | func (s *CairoSurface) Destroy() error { 61 | C.cairo_surface_destroy(s.nativePointer()) 62 | if err := s.status(); err != nil { 63 | return err 64 | } 65 | 66 | *s = 0 67 | return nil 68 | } 69 | 70 | func (s *CairoSurface) GetDeviceOffset() (float64, float64, error) { 71 | var xOffset, yOffset float64 72 | C.cairo_surface_get_device_offset(s.nativePointer(), (*C.double)(&xOffset), (*C.double)(&yOffset)) 73 | return xOffset, yOffset, s.status() 74 | } 75 | 76 | func (s *CairoSurface) GetDeviceScale() (float64, float64, error) { 77 | var xScale, yScale float64 78 | C.cairo_surface_get_device_scale(s.nativePointer(), (*C.double)(&xScale), (*C.double)(&yScale)) 79 | return xScale, yScale, s.status() 80 | } 81 | 82 | func (s *CairoSurface) SetFallbackResolution(xPPI, yPPI float64) error { 83 | C.cairo_surface_set_fallback_resolution(s.nativePointer(), C.double(xPPI), C.double(yPPI)) 84 | return s.status() 85 | } 86 | 87 | type CairoContext uintptr 88 | 89 | func (c CairoContext) nativePointer() *C.struct__cairo { 90 | return (*C.struct__cairo)(unsafe.Pointer(c)) 91 | } 92 | 93 | func CairoCreateContext(surface CairoSurface) (CairoContext, error) { 94 | context := C.cairo_create(surface.nativePointer()) 95 | c := CairoContext(unsafe.Pointer(context)) 96 | if err := c.status(); err != nil { 97 | return 0, err 98 | } 99 | return c, nil 100 | } 101 | 102 | func (c CairoContext) status() error { 103 | status := C.cairo_status(c.nativePointer()) 104 | if status != 0 { 105 | return cairoStatusToError(status) 106 | } 107 | return nil 108 | } 109 | 110 | func (c *CairoContext) Destroy() error { 111 | C.cairo_destroy(c.nativePointer()) 112 | 113 | *c = 0 114 | return nil 115 | } 116 | 117 | func (c CairoContext) Save() error { 118 | C.cairo_save(c.nativePointer()) 119 | return c.status() 120 | } 121 | 122 | func (c CairoContext) Restore() error { 123 | C.cairo_restore(c.nativePointer()) 124 | return c.status() 125 | } 126 | 127 | func (c CairoContext) IdentityMatrix() error { 128 | C.cairo_identity_matrix(c.nativePointer()) 129 | return c.status() 130 | } 131 | 132 | type CairoMatrix struct { 133 | xx float64 134 | yx float64 135 | xy float64 136 | yy float64 137 | x0 float64 138 | y0 float64 139 | } 140 | 141 | func (c CairoContext) GetMatrix() (*CairoMatrix, error) { 142 | var m CairoMatrix 143 | C.cairo_get_matrix(c.nativePointer(), (*C.struct__cairo_matrix)(unsafe.Pointer(&m))) 144 | return &m, c.status() 145 | } 146 | 147 | func (c CairoContext) Translate(x, y float64) error { 148 | C.cairo_translate(c.nativePointer(), C.double(x), C.double(y)) 149 | return c.status() 150 | } 151 | 152 | func (c CairoContext) Scale(x, y float64) error { 153 | C.cairo_scale(c.nativePointer(), C.double(x), C.double(y)) 154 | return c.status() 155 | } 156 | 157 | func (c CairoContext) Clip() error { 158 | C.cairo_clip(c.nativePointer()) 159 | return c.status() 160 | } 161 | 162 | func (c CairoContext) Rectangle(x, y, width, height float64) error { 163 | C.cairo_rectangle(c.nativePointer(), C.double(x), C.double(y), C.double(width), C.double(height)) 164 | return c.status() 165 | } 166 | -------------------------------------------------------------------------------- /winspool/poppler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package winspool 10 | 11 | /* 12 | #cgo pkg-config: poppler-glib 13 | 14 | #include 15 | #include 16 | 17 | #include // free 18 | */ 19 | import "C" 20 | import ( 21 | "errors" 22 | "fmt" 23 | "path/filepath" 24 | "unsafe" 25 | ) 26 | 27 | func gErrorToGoError(gerr *C.GError) error { 28 | if gerr == nil { 29 | return errors.New("Poppler/GLib: unknown error") 30 | } 31 | 32 | defer C.g_error_free(gerr) 33 | 34 | message := C.GoString((*C.char)(gerr.message)) 35 | if message == "No error" { 36 | // Work around inconsistent error message when named file doesn't exist. 37 | quarkString := C.GoString((*C.char)(C.g_quark_to_string(gerr.domain))) 38 | if "g-file-error-quark" == quarkString { 39 | return fmt.Errorf("Poppler/GLib: file error, code %d", gerr.code) 40 | } 41 | return fmt.Errorf("Poppler/GLib: unknown error, domain %d, code %d", gerr.domain, gerr.code) 42 | } 43 | 44 | return fmt.Errorf("Poppler/GLib: %s", C.GoString((*C.char)(gerr.message))) 45 | } 46 | 47 | type PopplerDocument uintptr 48 | 49 | func (d PopplerDocument) nativePointer() *C.struct__PopplerDocument { 50 | return (*C.struct__PopplerDocument)(unsafe.Pointer(d)) 51 | } 52 | 53 | func PopplerDocumentNewFromFile(filename string) (PopplerDocument, error) { 54 | filename, err := filepath.Abs(filename) 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | cFilename := (*C.gchar)(C.CString(filename)) 60 | defer C.free(unsafe.Pointer(cFilename)) 61 | 62 | var gerr *C.GError 63 | uri := C.g_filename_to_uri(cFilename, nil, &gerr) 64 | if uri == nil || gerr != nil { 65 | return 0, gErrorToGoError(gerr) 66 | } 67 | defer C.g_free(C.gpointer(uri)) 68 | 69 | doc := C.poppler_document_new_from_file((*C.char)(uri), nil, &gerr) 70 | if gerr != nil { 71 | return 0, gErrorToGoError(gerr) 72 | } 73 | 74 | return PopplerDocument(unsafe.Pointer(doc)), nil 75 | } 76 | 77 | func (d PopplerDocument) GetNPages() int { 78 | n := C.poppler_document_get_n_pages(d.nativePointer()) 79 | return int(n) 80 | } 81 | 82 | func (d PopplerDocument) GetPage(index int) PopplerPage { 83 | p := C.poppler_document_get_page(d.nativePointer(), C.int(index)) 84 | return PopplerPage(uintptr(unsafe.Pointer(p))) 85 | } 86 | 87 | func (d *PopplerDocument) Unref() { 88 | C.g_object_unref(C.gpointer(*d)) 89 | *d = 0 90 | } 91 | 92 | type PopplerPage uintptr 93 | 94 | func (p PopplerPage) nativePointer() *C.struct__PopplerPage { 95 | return (*C.struct__PopplerPage)(unsafe.Pointer(p)) 96 | } 97 | 98 | // GetSize returns the width and height of the page, in points (1/72 inch). 99 | func (p PopplerPage) GetSize() (float64, float64, error) { 100 | var width, height C.double 101 | C.poppler_page_get_size(p.nativePointer(), &width, &height) 102 | return float64(width), float64(height), nil 103 | } 104 | 105 | func (p PopplerPage) RenderForPrinting(context CairoContext) { 106 | C.poppler_page_render_for_printing(p.nativePointer(), context.nativePointer()) 107 | } 108 | 109 | func (p *PopplerPage) Unref() { 110 | C.g_object_unref(C.gpointer(*p)) 111 | *p = 0 112 | } 113 | -------------------------------------------------------------------------------- /winspool/utf16.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // +build windows 8 | 9 | package winspool 10 | 11 | import ( 12 | "reflect" 13 | "syscall" 14 | "unsafe" 15 | ) 16 | 17 | const utf16StringMaxBytes = 1024 18 | 19 | func utf16PtrToStringSize(s *uint16, bytes uint32) string { 20 | if s == nil { 21 | return "" 22 | } 23 | 24 | hdr := reflect.SliceHeader{ 25 | Data: uintptr(unsafe.Pointer(s)), 26 | Len: int(bytes / 2), 27 | Cap: int(bytes / 2), 28 | } 29 | c := *(*[]uint16)(unsafe.Pointer(&hdr)) 30 | 31 | return syscall.UTF16ToString(c) 32 | } 33 | 34 | func utf16PtrToString(s *uint16) string { 35 | return utf16PtrToStringSize(s, utf16StringMaxBytes) 36 | } 37 | -------------------------------------------------------------------------------- /wix/LICENSE.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Courier New;}} 2 | {\*\generator Riched20 10.0.10586}\viewkind4\uc1 3 | \pard\f0\fs20\lang1033 Copyright 2015, Google Inc. All rights reserved.\par 4 | \par 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are\par 6 | met:\par 7 | \par 8 | * Redistributions of source code must retain the above copyright\par 9 | notice, this list of conditions and the following disclaimer.\par 10 | * Redistributions in binary form must reproduce the above\par 11 | copyright notice, this list of conditions and the following disclaimer\par 12 | in the documentation and/or other materials provided with the\par 13 | distribution.\par 14 | * Neither the name of Google Inc. nor the names of its\par 15 | contributors may be used to endorse or promote products derived from\par 16 | this software without specific prior written permission.\par 17 | \par 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\par 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\par 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\par 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\par 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\par 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\par 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\par 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\par 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\par 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\par 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\par 29 | \par 30 | } 31 | -------------------------------------------------------------------------------- /wix/README.md: -------------------------------------------------------------------------------- 1 | # Windows Installer 2 | 3 | ## Build Requirements 4 | The WIX toolset is required to build the Windows Installer file. 5 | It can be downloaded from http://wixtoolset.org. 6 | 7 | ## Build Instructions 8 | Build the Cloud Print Connector binaries. See https://github.com/google/cloud-print-connector/wiki/Build-from-source 9 | 10 | Update the dependencies.wxs file by running ./generate-dependencies.sh (in mingw64 bash shell). 11 | 12 | Use the WIX tools to build the MSI. The WIX tools that are used are candle.exe 13 | and light.exe. They are installed by default to 14 | "C:\Program Files (x86)\WiX Toolset v3.10\bin" 15 | (/c/Program\ Files\ (x86)/WiX\ Toolset\ v3.10/bin/light.exe if you're using 16 | mingw bash shell). You can add this directory to your PATH to run the following 17 | two commands. 18 | 19 | Run candle.exe to build wixobj file from the wxs file: 20 | ``` 21 | candle.exe -arch x64 windows-connector.wxs dependencies.wxs 22 | ``` 23 | 24 | Expected output: 25 | > Windows Installer XML Toolset Compiler version 3.10.2.2516 26 | > Copyright (c) Outercurve Foundation. All rights reserved. 27 | > 28 | > windows-connector.wxs 29 | > dependencies.wxs 30 | 31 | 32 | Run light.exe to build MSI file from the wixobj 33 | ``` 34 | light.exe -ext "C:\Program Files (x86)\WiX Toolset v3.10\bin\WixUIExtension.dll" windows-connector.wixobj dependencies.wixobj -o windows-connector.msi 35 | ``` 36 | 37 | Expected output: 38 | > Windows Installer XML Toolset Linker version 3.10.2.2516 39 | > Copyright (c) Outercurve Foundation. All rights reserved. 40 | 41 | The light.exe command line requires the path of WixUIExtension.dll which 42 | provides the UI that is used by this installer. If the WIX toolset is installed 43 | to a different directory, use that directory path for the UI extension dll. 44 | 45 | If the built Windows Connector binaries are not in $GOPATH\bin, then add -dSourceDir= 46 | to the light.exe command line to specify where the files can be found. 47 | 48 | If mingw64 is not installed to C:\msys64\mingw64, then use -dDependencyDir= 49 | to specify where it is installed. 50 | 51 | ## Installation Instructions 52 | Install the MSI by any normal method of installing an MSI file (double-clicking, automated deployment, etc.) 53 | 54 | During an installation with UI, gcp-connector-util init will be run as the last step which 55 | will open a console window to initialize the connector. 56 | 57 | The following public properties may be set during install of the MSI 58 | (see https://msdn.microsoft.com/en-us/library/windows/desktop/aa370912(v=vs.85).aspx) 59 | * CONFIGFILE = Path of connector config file to use instead of running gcp-connector-util init during install 60 | 61 | ## Modifying the Config File after install 62 | The installer will create (or copy) the config file specified to the Common 63 | Application Data directory at %PROGRAMDATA%\Google\Cloud Print Connector. 64 | This is the file that is used by the connector. This file can be modified 65 | and the service restarted to change the configuration. 66 | -------------------------------------------------------------------------------- /wix/build-msi.sh: -------------------------------------------------------------------------------- 1 | if [ $# -eq 0 ]; then 2 | me=$(basename $0) 3 | echo "Usage: $me " 4 | exit 1 5 | fi 6 | export CONNECTOR_VERSION=$1 7 | LDFLAGS="github.com/google/cloud-print-connector/lib.BuildDate=$CONNECTOR_VERSION" 8 | CONNECTOR_DIR=$GOPATH/src/github.com/google/cloud-print-connector 9 | 10 | arch=$(arch) 11 | if [[ "$arch" == "i686" ]]; then 12 | wixarch="x86" 13 | elif [[ "$arch" == "x86_64" ]]; then 14 | wixarch="x64" 15 | fi 16 | 17 | MSI_FILE="$CONNECTOR_DIR/wix/windows-connector-$CONNECTOR_VERSION-$arch.msi" 18 | 19 | echo "Running go get..." 20 | go get -ldflags -X="$LDFLAGS" -v github.com/google/cloud-print-connector/... 21 | rc=$? 22 | if [[ $rc != 0 ]]; then 23 | echo "Error $rc with go get. Exiting." 24 | exit $rc 25 | fi 26 | 27 | echo "Running generate-dependencies.sh..." 28 | $CONNECTOR_DIR/wix/generate-dependencies.sh 29 | rc=$? 30 | if [[ $rc != 0 ]]; then 31 | echo "Error $rc with generate-dependencies.sh. Exiting." 32 | exit $rc 33 | fi 34 | 35 | echo "Running WIX candle.exe..." 36 | "$WIX/bin/candle.exe" -arch $wixarch "$CONNECTOR_DIR/wix/windows-connector-$wixarch.wxs" \ 37 | "$CONNECTOR_DIR/wix/dependencies.wxs" 38 | rc=$? 39 | if [[ $rc != 0 ]]; then 40 | echo "Error $rc with WIX candle.exe. Exiting." 41 | exit $rc 42 | fi 43 | 44 | echo "Running WIX light.exe..." 45 | "$WIX/bin/light.exe" -ext "$WIX/bin/WixUIExtension.dll" \ 46 | "$CONNECTOR_DIR/wix/windows-connector.wixobj" "$CONNECTOR_DIR/wix/dependencies.wixobj" \ 47 | -o "$MSI_FILE" 48 | rc=$? 49 | if [[ $rc != 0 ]]; then 50 | echo "Error $rc with WIX light.exe. Exiting." 51 | exit $rc 52 | fi 53 | 54 | rm $CONNECTOR_DIR/wix/dependencies.wxs 55 | 56 | echo "Successfully generated $MSI_FILE" 57 | -------------------------------------------------------------------------------- /wix/generate-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | echo ''>dependencies.wxs 3 | echo ' 4 | 5 | '>>dependencies.wxs 9 | for f in `ldd ${GOPATH}/bin/gcp-windows-connector.exe | grep -i -v Windows | sed s/" =>.*"// | sed s/"\t"// | sort` 10 | do echo " 11 | 12 | ">>dependencies.wxs; done 13 | echo ' 14 | 15 | '>>dependencies.wxs 16 | 17 | -------------------------------------------------------------------------------- /wix/windows-connector-x64.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | 91 | STARTSERVICE="YES" 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 101 | 103 | 109 | 114 | 115 | 116 | 122 | NOT CONFIGFILE 123 | 124 | 130 | CONFIGFILE 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | NOT CONFIGFILE and NOT Installed and NOT WIX_UPGRADE_DETECTED and RUNINIT="YES" 141 | 142 | 143 | 144 | 145 | 146 | 147 | DELETEPRINTERS="YES" AND $ConfigFile=2 148 | 149 | 150 | 151 | 152 | 174 | 175 | -------------------------------------------------------------------------------- /wix/windows-connector-x86.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | 91 | STARTSERVICE="YES" 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 101 | 103 | 109 | 114 | 115 | 116 | 122 | NOT CONFIGFILE 123 | 124 | 130 | CONFIGFILE 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | NOT CONFIGFILE and NOT Installed and NOT WIX_UPGRADE_DETECTED and RUNINIT="YES" 141 | 142 | 143 | 144 | 145 | 146 | 147 | DELETEPRINTERS="YES" AND $ConfigFile=2 148 | 149 | 150 | 151 | 152 | 174 | 175 | -------------------------------------------------------------------------------- /xmpp/xmpp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by a BSD-style 5 | license that can be found in the LICENSE file or at 6 | https://developers.google.com/open-source/licenses/bsd 7 | */ 8 | 9 | package xmpp 10 | 11 | import ( 12 | "fmt" 13 | "time" 14 | 15 | "github.com/google/cloud-print-connector/log" 16 | "github.com/google/cloud-print-connector/notification" 17 | ) 18 | 19 | type PrinterNotificationType uint8 20 | 21 | type XMPP struct { 22 | jid string 23 | proxyName string 24 | server string 25 | port uint16 26 | pingTimeout time.Duration 27 | pingInterval time.Duration 28 | getAccessToken func() (string, error) 29 | 30 | notifications chan<- notification.PrinterNotification 31 | dead chan struct{} 32 | 33 | quit chan struct{} 34 | 35 | ix *internalXMPP 36 | } 37 | 38 | func NewXMPP(jid, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, getAccessToken func() (string, error), notifications chan<- notification.PrinterNotification) (*XMPP, error) { 39 | x := XMPP{ 40 | jid: jid, 41 | proxyName: proxyName, 42 | server: server, 43 | port: port, 44 | pingTimeout: pingTimeout, 45 | pingInterval: pingInterval, 46 | getAccessToken: getAccessToken, 47 | notifications: notifications, 48 | dead: make(chan struct{}), 49 | quit: make(chan struct{}), 50 | } 51 | 52 | if err := x.startXMPP(); err != nil { 53 | for err != nil { 54 | log.Errorf("XMPP start failed, will try again in 10s: %s", err) 55 | time.Sleep(10 * time.Second) 56 | err = x.startXMPP() 57 | } 58 | } 59 | go x.keepXMPPAlive() 60 | 61 | return &x, nil 62 | } 63 | 64 | // Quit terminates the XMPP conversation so that new jobs stop arriving. 65 | func (x *XMPP) Quit() { 66 | // Signal to KeepXMPPAlive. 67 | close(x.quit) 68 | select { 69 | case <-x.dead: 70 | // Wait for XMPP to die. 71 | case <-time.After(3 * time.Second): 72 | // But not too long. 73 | log.Error("XMPP taking a while to close, so giving up") 74 | } 75 | } 76 | 77 | // startXMPP tries to start an XMPP conversation. 78 | func (x *XMPP) startXMPP() error { 79 | if x.ix != nil { 80 | go x.ix.Quit() 81 | x.ix = nil 82 | } 83 | 84 | password, err := x.getAccessToken() 85 | if err != nil { 86 | return fmt.Errorf("While starting XMPP, failed to get access token (password): %s", err) 87 | } 88 | 89 | // The current access token is the XMPP password. 90 | ix, err := newInternalXMPP(x.jid, password, x.proxyName, x.server, x.port, x.pingTimeout, x.pingInterval, x.notifications, x.dead) 91 | if err != nil { 92 | return fmt.Errorf("Failed to start XMPP conversation: %s", err) 93 | } 94 | 95 | x.ix = ix 96 | return nil 97 | } 98 | 99 | // keepXMPPAlive restarts XMPP when it fails. 100 | func (x *XMPP) keepXMPPAlive() { 101 | for { 102 | select { 103 | case <-x.dead: 104 | log.Error("XMPP conversation died; restarting") 105 | if err := x.startXMPP(); err != nil { 106 | for err != nil { 107 | log.Errorf("XMPP restart failed, will try again in 10s: %s", err) 108 | time.Sleep(10 * time.Second) 109 | err = x.startXMPP() 110 | } 111 | log.Error("XMPP conversation restarted successfully") 112 | } 113 | 114 | case <-x.quit: 115 | // Close XMPP. 116 | x.ix.Quit() 117 | return 118 | } 119 | } 120 | } 121 | --------------------------------------------------------------------------------