### Copyright 2016 Pixar
###
### Licensed under the Apache License, Version 2.0 (the "Apache License")
### with the following modification; you may not use this file except in
### compliance with the Apache License and the following modification to it:
### Section 6. Trademarks. is deleted and replaced with:
###
### 6. Trademarks. This License does not grant permission to use the trade
### names, trademarks, service marks, or product names of the Licensor
### and its affiliates, except as required to comply with Section 4(c) of
### the License and to reproduce the content of the NOTICE file.
###
### You may obtain a copy of the Apache License at
###
### http://www.apache.org/licenses/LICENSE-2.0
###
### Unless required by applicable law or agreed to in writing, software
### distributed under the Apache License with the above modification is
### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
### KIND, either express or implied. See the Apache License for the specific
### language governing permissions and limitations under the Apache License.
###
###
###
module JSS
#####################################
### Constants
#####################################
#####################################
### Module Variables
#####################################
#####################################
### Module Methods
#####################################
#####################################
### Module Classes
#####################################
###
### An API connection to the JSS.
###
### This is a singleton class, only one can exist at a time.
### Its one instance is created automatically when the module loads, but it
### isn't connected to anything at that time.
###
### Use it via the {JSS::API} constant to call the #connect
### method, and the {#get_rsrc}, {#put_rsrc}, {#post_rsrc}, & {#delete_rsrc}
### methods, q.v. below.
###
### To access the underlying RestClient::Resource instance,
### use JSS::API.cnx
###
class APIConnection
include Singleton
#####################################
### Class Constants
#####################################
### The base API path in the jss URL
RSRC_BASE = "JSSResource"
### A url path to load to see if there's an API available at a host.
### This just loads the API resource docs page
TEST_PATH = "api"
### If the test path loads correctly from a casper server, it'll contain
### this text
TEST_CONTENT = "
JSS REST API Resource Documentation"
### The Default port
HTTP_PORT = 9006
### The SSL port
SSL_PORT = 8443
### The top line of an XML doc for submitting data via API
XML_HEADER = ''
### Default timeouts in seconds
DFT_OPEN_TIMEOUT = 60
DFT_TIMEOUT = 60
### The Default SSL Version
DFT_SSL_VERSION = 'TLSv1'
#####################################
### Attributes
#####################################
### @return [String] the username who's connected to the JSS API
attr_reader :jss_user
### @return [RestClient::Resource] the underlying connection resource
attr_reader :cnx
### @return [Boolean] are we connected right now?
attr_reader :connected
### @return [JSS::Server] the details of the JSS to which we're connected.
attr_reader :server
### @return [String] the hostname of the JSS to which we're connected.
attr_reader :server_host
#####################################
### Constructor
#####################################
###
### To connect, use JSS::APIConnection.instance.connect
### or a shortcut, JSS::API.connect
###
def initialize ()
@connected = false
end # init
#####################################
### Class Methods
#####################################
###
### Connect to the JSS API.
###
### @param args[Hash] the keyed arguments for connection.
###
### @option args :server[String] the hostname of the JSS API server, required if not defined in JSS::CONFIG
###
### @option args :port[Integer] the port number to connect with, defaults to 8443
###
### @option args :use_ssl[Boolean] should the connection be made over SSL? Defaults to true.
###
### @option args :verify_cert[Boolean] should HTTPS SSL certificates be verified. Defaults to true.
### If your connection raises RestClient::SSLCertificateNotVerified, and you don't care about the
### validity of the SSL cert. just set this explicitly to false.
###
### @option args :user[String] a JSS user who has API privs, required if not defined in JSS::CONFIG
###
### @option args :pw[String,Symbol] Required, the password for that user, or :prompt, or :stdin
### If :prompt, the user is promted on the commandline to enter the password for the :user.
### If :stdin#, the password is read from a line of std in represented by the digit at #,
### so :stdin3 reads the passwd from the third line of standard input. defaults to line 1,
### if no digit is supplied. see {JSS.stdin}
###
### @option args :open_timeout[Integer] the number of seconds to wait for an initial response, defaults to 60
###
### @option args :timeout[Integer] the number of seconds before an API call times out, defaults to 60
###
### @return [true]
###
def connect (args = {})
# the server, if not specified, might come from a couple places.
# see #hostname
args[:server] ||= hostname
# settings from config if they aren't in the args
args[:server] ||= JSS::CONFIG.api_server_name
args[:port] ||= JSS::CONFIG.api_server_port
args[:user] ||= JSS::CONFIG.api_username
args[:timeout] ||= JSS::CONFIG.api_timeout
args[:open_timeout] ||= JSS::CONFIG.api_timeout_open
args[:ssl_version] ||= JSS::CONFIG.api_ssl_version
# if verify cert given was NOT in the args....
if args[:verify_cert].nil?
# set it from the prefs
args[:verify_cert] = JSS::CONFIG.api_verify_cert
end
# settings from client jamf plist if needed
args[:port] ||= JSS::Client.jss_port
# default settings if needed
args[:port] ||= SSL_PORT
args[:timeout] ||= DFT_TIMEOUT
args[:open_timeout] ||= DFT_OPEN_TIMEOUT
# As of Casper 9.61 we can't use SSL, must use TLS, since SSLv3 was susceptible to poodles.
# NOTE - this requires rest-client v 1.7.0 or higher
# which requires mime-types 2.0 or higher, which requires ruby 1.9.2 or higher!
# That means that support for ruby 1.8.7 stops with Casper 9.6
args[:ssl_version] ||= DFT_SSL_VERSION
# must have server, user, and pw
raise JSS::MissingDataError, "No JSS :server specified, or in configuration." unless args[:server]
raise JSS::MissingDataError, "No JSS :user specified, or in configuration." unless args[:user]
raise JSS::MissingDataError, "Missing :pw for user '#{args[:user]}'" unless args[:pw]
# we're using ssl if 1) args[:use_ssl] is anything but false
# or 2) the port is the default casper ssl port.
args[:use_ssl] = (not args[:use_ssl] == false) or (args[:port] == SSL_PORT)
# and here's the URL
ssl = args[:use_ssl] ? "s" : ''
@rest_url = URI::encode "http#{ssl}://#{args[:server]}:#{args[:port]}/#{RSRC_BASE}"
# prep the args for passing to RestClient::Resource
# if verify_cert is anything but false, we will verify
args[:verify_ssl] = (args[:verify_cert] == false) ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
args[:password] = if args[:pw] == :prompt
JSS.prompt_for_password "Enter the password for JSS user #{args[:user]}@#{args[:server]}:"
elsif args[:pw].is_a?(Symbol) and args[:pw].to_s.start_with?('stdin')
args[:pw].to_s =~ /^stdin(\d+)$/
line = $1
line ||= 1
JSS.stdin line
else
args[:pw]
end
# heres our connection
@cnx = RestClient::Resource.new("#{@rest_url}", args)
@jss_user = args[:user]
@server_host = args[:server]
@connected = true
@server = JSS::Server.new
if @server.version < JSS.parse_jss_version(JSS::MINIMUM_SERVER_VERSION)[:version]
raise JSS::UnsupportedError, "Your JSS Server version, #{@server.raw_version}, is to low. Must be #{JSS::MINIMUM_SERVER_VERSION} or higher."
end
return @connected ? @server_host : nil
end # connect
###
### Reset the response timeout for the rest connection
###
### @param timeout[Integer] the new timeout in seconds
###
### @return [void]
###
def timeout= (timeout)
@cnx.options[:timeout] = timeout
end
###
### Reset the open-connection timeout for the rest connection
###
### @param timeout[Integer] the new timeout in seconds
###
### @return [void]
###
def open_timeout= (timeout)
@cnx.options[:open_timeout] = timeout
end
###
### With a REST connection, there isn't any real "connection" to disconnect from
### So to disconnect, we just unset all our credentials.
###
### @return [void]
###
def disconnect
@jss_user = nil
@rest_url = nil
@server_host = nil
@cnx = nil
@connected = false
end # disconnect
###
### Get an arbitrary JSS resource
###
### The first argument is the resource to get (the part of the API url
### after the 'JSSResource/' )
###
### By default we get the data in JSON, and parse it
### into a ruby data structure (arrays, hashes, strings, etc)
### with symbolized Hash keys.
###
### @param rsrc[String] the resource to get
### (the part of the API url after the 'JSSResource/' )
###
### @param format[Symbol] either ;json or :xml
### If the second argument is :xml, the XML data is returned as a String.
###
### @return [Hash,String] the result of the get
###
def get_rsrc (rsrc, format = :json)
raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
rsrc = URI::encode rsrc
data = @cnx[rsrc].get(:accept => format)
return JSON.parse(data, :symbolize_names => true) if format == :json
data
end
###
### Change an existing JSS resource
###
### @param rsrc[String] the API resource being changed, the URL part after 'JSSResource/'
###
### @param xml[String] the xml specifying the changes.
###
### @return [String] the xml response from the server.
###
def put_rsrc(rsrc,xml)
raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
### convert CRs & to
xml.gsub!(/\r/, '
')
### send the data
@cnx[rsrc].put(xml, :content_type => 'text/xml')
end
###
### Create a new JSS resource
###
### @param rsrc[String] the API resource being created, the URL part after 'JSSResource/'
###
### @param xml[String] the xml specifying the new object.
###
### @return [String] the xml response from the server.
###
def post_rsrc(rsrc,xml)
raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
### convert CRs & to
xml.gsub!(/\r/, '
')
### send the data
@cnx[rsrc].post xml, :content_type => 'text/xml', :accept => :json
end #post_rsrc
### Delete a resource from the JSS
###
### @param rsrc[String] the resource to create, the URL part after 'JSSResource/'
###
### @return [String] the xml response from the server.
###
def delete_rsrc(rsrc)
raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
raise MissingDataError, "Missing :rsrc" if rsrc.nil?
### delete the resource
@cnx[rsrc].delete
end #delete_rsrc
### Test that a given hostname & port is a JSS API server
###
### @param server[String] The hostname to test,
###
### @param port[Integer] The port to try connecting on
###
### @return [Boolean] does the server host a JSS API?
###
def valid_server? (server, port = SSL_PORT)
# try ssl first
begin
return true if open("https://#{server}:#{port}/#{TEST_PATH}", ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read.include? TEST_CONTENT
rescue
# then regular http
begin
return true if open("http://#{server}:#{port}/#{TEST_PATH}").read.include? TEST_CONTENT
rescue
# any errors = no API
return false
end # begin
end #begin
# if we're here, no API
return false
end
### The server to which we are connected, or will
### try connecting to if none is specified with the
### call to #connect
###
### @return [String] the hostname of the server
###
def hostname
return @server_host if @server_host
srvr = JSS::CONFIG.api_server_name
srvr ||= JSS::Client.jss_server
return srvr
end
### aliases
alias connected? connected
end # class JSSAPIConnection
### The single instance of the APIConnection
API = APIConnection.instance
end # module